ToB企服应用市场:ToB评测及商务社交产业平台

标题: 《Effective C++》第三版-5. 实现(Implementations) [打印本页]

作者: 飞不高    时间: 2024-5-20 08:04
标题: 《Effective C++》第三版-5. 实现(Implementations)
目录

条款26:尽可能延后变量定义式的出现时间(Postpone variable definitions as long as possible)

应延后变量的定义,知道不得不使用该变量的前一刻为止,甚至直到能够给他初值实参为止
当程序的控制流达到变量的定义式时,会有构造成本;当脱离变量的作用域时,会有析构成本
  1. std::string encryptPassword(const std::string& password)
  2. {
  3.         ...
  4.         std::string encrypted(password);  //通过copy构造函数定义并初始化
  5.         encrypt(encrypted);
  6.         return encrypted;
  7. }
复制代码
考虑在循环中使用的变量:
  1. //定义于循环外:1个构造函数+1个析构函数+n个赋值操作
  2. Widget w;
  3. for (int i = 0; i < n; ++i) {
  4.         w = 取决于i的某个值;
  5.         ...
  6. }
  7. //定义于循环内:n个构造函数+n个析构函数
  8. for (int i = 0; i < n; ++i) {
  9.         Widget w(取决于i的某个值);
  10.         ...
  11. }
复制代码
Tips:
条款27:尽量少做转型动作(Minimize casting)

C++支持的转型动作通常有三种情势:
  1. //旧式转型
  2. (T)expression;  //C风格的转型
  3. expression(T);  //函数风格的转型
  4. //新式转型
  5. const_cast<T>( expression );
  6. dynamic_cast<T>( expression );
  7. reinterpret_cast<T>( expression );
  8. static_cast<T>( expression );
复制代码
新式转换相对旧式转换有两个优点:
使用旧式转型的时机:当要调用explicit构造函数将一个对象通报给一个函数
  1. class Widget {
  2. public:
  3.         explicit WIdget(int size);
  4.         ...
  5. };
  6. doSomeWork(Wistaticdget(15));  //函数风格转型
  7. doSomeWork(static_cast<Widget>(15));  //C++风格转型
复制代码
任何一类转换往往令编译器编译出运行期间执行的码。
下例中pb和&d可能不相同,此时会有偏移量在运行期被施行于Derived指针上,以取得Base的指针值。此事在多重继承中几乎一直发生,在单一继承中也可能发生,且偏移量可能编译器的不同而不同,故应避免这种用法
  1. class Base { ... };
  2. class Derived: public Base { ... };
  3. Derived d;
  4. Base* pb = & &d;  //把Derived*隐式转换为Base*
复制代码
考虑下例:很多应用框架(application frameworks)都要求派生类内的虚函数代码的第一个动作就先调用基类的对应函数,此处假设SpecialWindow的onResize函数要首先调用Window的onResize函数
  1. class Window {
  2. public:
  3.         virtual void onResize() { ... }
  4.         ...
  5. };
  6. class SpecialWindow: public WIndow {
  7. public:
  8.         virtual void onResize() {
  9.                 static_cast<WIndow>(*this).onResize();  //不可行!将*this
  10.                 ...  //这里进行SpecialWindow专属行为
  11.         }
  12.         ...
  13. };
复制代码
上述代码调用的不是当前对象上的函数,而是转型动作所建立的*this对象的基类因素的副本的onResize。
若Window:nResize修改了对象内容,则改动的是副本而非当前对象;若SpecialWIndow:nResize也修改对象内容,则当前对象会被改动
正确的写法如下:
  1. class SpecialWindow: public Window {
  2. public:
  3.         virtual void onResize() {
  4.                 Window::onResize();  //调用Window::onResize作用于*this
  5.                 ...
  6.         }
  7.         ...
  8. };
复制代码
dynamic_cast的注意事项:
  1. //使用容器并在其中存储直接指向派生类对象的指针
  2. class Window { ... };
  3. class SpecialWindow: public Window {
  4. public:
  5.         void blink();  //闪烁效果
  6.         ...
  7. };
  8. //容器内是派生类而非基类,免去在循环中使用dynamic_cast把积累转换为派生类的步骤
  9. typedef std::vector<std::tr1::shared_ptr<SpecialWindow>> VPSW;
  10. VPSW winPtrs;
  11. ...
  12. for (VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
  13.         (*iter)->blink();  //这样写比较好,不使用dynamic_cast
  14. //通过基类接口处理所有派生类
  15. class Window {
  16. public:
  17.         virtual void blink() { }
  18.                 ...
  19. };
  20. class SpecialWindow: public Window {
  21. public:
  22.         virtual void blink() { ... }
  23.         ...
  24. };
  25. typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
  26. VPW winPtrs;
  27. ...
  28. for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
  29.         (*iter)->blink();
  30.        
  31. //连串dynamic_cast
  32. for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
  33. {
  34.         if (SpecialWindow1 * psw1 = dynamic_cast<SpecialWindow1*>(iter->get()) { ... };
  35.         else if (SpecialWindow2 * psw2 = dynamic_cast<SpecialWindow2*>(iter->get()) { ... };
  36.         else if (SpecialWindow3 * psw3 = dynamic_cast<SpecialWindow3*>(iter->get()) { ... };
  37. }
复制代码
Tips:
条款28:避免返回handles指向对象内部因素(Avoid returning “handles” to object internals)

考虑涉及矩形的例子:
引用、指针、迭代器都是所谓的handles(号码牌,用来取得某个对象)
  1. class Point {
  2. public:
  3.         Point(int x, int y);
  4.         ...
  5.         void setX(int newVal);
  6.         void setY(int newVal);
  7.         ...
  8. };
  9. struct RectData {
  10.         Point ulhc;
  11.         Point lrhc;
  12. };
  13. class Rectangle {
  14. public:
  15.         ...
  16.         // 如果没有cosnt Point&,则引用指向的内容可能变化
  17.         // Point& upperLeft() const { return pData->ulhc; }
  18.         // Point& lowerRight() const { return pData->lrhc; }
  19.         // 采取这种方式可保证handle指向的数据不变
  20.         const Point& upperLeft() const { return pData->ulhc; }
  21.         const Point& lowerRight() const { return pData->lrhc; }
  22.         ...
  23. private:
  24.         std::tr1::shared_ptr<RectData> pData;
  25. };
  26. class Rectangle {
  27. };
复制代码
即使指向的内容不变,返回handle照旧可能导致dangling handle(悬空的号码牌),即所指的东西不复存在:
  1. class GUIObject { ... };  //考虑GUI对象的矩形外框
  2. const Rectangle boundingBox(const GUIObject& obj);  //以by value返回矩形
  3. GUIObject* pgo;
  4. ...
  5. const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());  //可能悬空!
复制代码
Tips:
条款29:为“异常安全”而努力是值得的(Strive for exception-safe code)

异常不安全的案例
  1. class PrettyMenu {
  2. public:
  3.         ...
  4.   void changeBackground(std::istream& imgSrc) // 改变背景图像
  5.         ...
  6. private:
  7.     Mutex mutex;  //互斥器
  8.     Image* bgImage;  //目前使用的背景图片
  9.     int imageChanges;  //图片被修改的次数
  10. };
  11. void PrettyMenu::changeBackground(std::istream& imgSrc) {
  12.         lock(&mutex);  //取得互斥器
  13.         delete bgImage;  //删除旧图片
  14.         ++imageChanges;  //修改图像更改次数
  15.         bgImage = new Image(imgSrc);  //安装新的背景图片
  16.         unlock(&mutex);  //释放互斥器
  17. }
复制代码
当异常被抛出时,异常安全函数会(上述代码均不满足):
异常安全函数的保证

异常安全函数提供以下三个保证之一:
让changeBackground提供接近但非完全的强烈的异常安全保证可考虑以下两点:
  1. class PrettyMenu {
  2.         ...
  3.         std::tr1::shared_ptr<Image> bgImage;
  4.         ...
  5. };
  6. void PrettyMenu::changeBackground(std::istream& imgSrc) {
  7.     Lock ml(&mutex);  //将互斥器封装在类中进行管理
  8.     bgImage.reset(new Image(imgSrc));  //以new Image的执行结果设定bgImage内部指针
  9.     ++imageChanges;
  10. }
复制代码
上述代码删除动作只发生在新图像创建乐成之后,shared_tpr::reset函数只有在其参数(即new Image(imgSrc))乐成之后才会被调用。delete在reset内调用,则未进入reset就不会delete。
但是如果Image构造函数抛出异常,则可能输入流(input stream)的读取记号(read marker)已被移走,这对程序别的部门是一种可见的状态改变。
强烈的异常安全

copy and swap可以提供强烈的保证
实际上通常是将所有从属对象的数据从原对象放进另一个对象内,然后赋予原对象一个指针,指向谁人所谓的实现对象(即副本),其被称为pimpl idiom:
  1. struct PMImpl {//将bgImage和imageChanges从PrettyMenu独立出来,封装成一个结构体
  2.   std::tr1::shared_ptr<Image> bgImage;
  3.   int imageChanges
  4. };
  5. class PrettyMenu {
  6.         ...
  7. private:
  8.     std::tr1::shared_ptr<PMImpl> pImpl; //创建一个该结构
  9. };
  10. void PrettyMenu::changeBackground(std::istream& imgSrc) {
  11.     using std::swap;  //见条款25
  12.     Lock ml(&mutex);  //获得mutex的副本数据
  13.     std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
  14.     pNew->bgImage.reset(new Image(imgSrc));  //修改副本
  15.     pNew->imageChanges++;
  16.     swap(pImpl, pNew);  //置换数据,释放mutex
  17. }
复制代码
上述代码将PMImpl定义为一个结构体而不是类
copy and swap不保证整个函数有强烈的异常安全性:
  1. void someFunc()
  2. {
  3.         ...  //对local状态做副本
  4.         f1();
  5.         f2();
  6.         ...  //置换修改后的状态
  7. }
复制代码
Tips:
条款30:透彻了解inlining的里里外外(Understand the ins and outs of inlining)

过分使用inline的问题:
申请inline的方式:
  1. class Person {
  2. public:
  3.   ...
  4.   int age() const { return theAge; }    // 隐喻的inline申请
  5.   ...                                   
  6. private:
  7.   int theAge;
  8. };
  9. template<typename T>                               // 明确申请inline
  10. inline const T& std::max(const T& a, const T& b)   
  11. { return a < b ? b : a; }                          
复制代码
inline函数一般必须在头文件内
templates一般在头文件内
template实例化与inlining无关
inline是一个编译器可能忽略的请求
一个inline函数是否能真是inline,取决于使用的构建环境,主要是编译器
inline函数无法随着程序库的升级而升级
大部门调试器无法有用调试inline函数
Tips:
条款31:将文件间的编译依存关系降至最低(Minimize compilation dependencies between files)

编译依靠的来源
  1. //编译器需要取得其实现代码所用到的classes string、Date、Address的定义
  2. #include <string>
  3. #include "date.h"
  4. #include "address.h"
  5. class Person {
  6. public:
  7.         Person(const std::string& name, const Date& birthday, const Address& addr);
  8.         std::string name() const;
  9.         std::string birthDate() const;
  10.         std::string address() const;
  11.         ...
  12. private:
  13.         std::string theName;  // 实现细节
  14.         Date theBirthDate;  // 实现细节
  15.         Address theAddress;   // 实现细节
  16. };
复制代码
Person定义文件和其包含的头文件之间形成了一种编译依存关系(compilation dependency):
分离类的实现
  1. namespace std {
  2.         class string;  // 前置声明(错误!详情见下面叙述)
  3. }
  4. class Date;  // 前置声明
  5. class Address;  // 前置声明
  6. class Person {
  7. public:
  8.         Person(const std::string& name, const Date& birthday, const Address& addr);
  9.         std::string name() const;
  10.         std::string birthDate() const;
  11.         std::string address() const;
  12.         ...
  13. };
复制代码
上述分离方法存在两个问题:
  1. int main()
  2. {
  3.         int x;  //定义x,编译器知道为int分配多大的空间足够
  4.         Person *p;  //定义Person,编译器需要询问Person定义式来确定多大的空间足够
  5.         ...
  6. }
复制代码
对于Person来说,一种实现方式就是将其分成两个类:
  1. #include <string>  //标准程序库组件不该被前置声明
  2. #include <memory>  //此乃为了tr1::shared_ptr而含入,详后
  3. class PersonImpl;  //Person实现类的前置声明
  4. class Date;  //Person接口用到的类的前置声明
  5. class Address;  
  6.                                                                   
  7. class Person {
  8. public:
  9.         Person(const std::string& name, const Date& birthday, const Address& addr);
  10.         std::string name() const;
  11.         std::string birthDate() const;
  12.         std::string address() const;
  13.         ...
  14. private:                                                                              
  15.         std::tr1::shared_ptr<PersonImpl> pImpl;  //指向实现物的指针
  16. };
复制代码
上述代码中:
这实现和接口的真正分离:
分离的关键与编译依存性最小化

这个分离的关键在于将定义的依存性更换为对声明的依存性。这是编译依存性最小化的本质:现实中让你的头文件能够自给自足,如果达不到这个要求,依靠其他文件中的声明而不是定义。其他的设计都来自于这个简单的设计谋略。因此:
  1. //使用按值传递参数或者按值返回(一般这样写不好)也不需要
  2. class Date;  //声明式
  3. Date today();  
  4. void clearAppointments(Date d);  //无需Date的定义式
复制代码
  1. #include "datefwd.h"  //头文件内声明但未定义Date类
  2. Date today();
  3. void clearAppointments(Date d);
复制代码
头文件datefwd.h只包含声明,它的定名是基于尺度C++库的头文件内的iostream组件声明:
C++中同样提供了export关键字,使模板声明从模板定义中分离出来,但支持export的编译器很少
句柄类和接口类

制作句柄类的方法有二:
  1. //将所有的函数调用转移到对应的实现类中
  2. #include "Person.h"
  3. #include "PersonImpl.h"
  4. Person::Person(const std::string& name, const Date& birthday, const Address& addr)
  5.         : pImpl(new PersonImpl(name, birthday, addr))
  6. {}
  7. std::string Person::name() const
  8. {
  9.         return pImpl->name();
  10. }
  11. //将Person定义成特殊的抽象基类,也就是接口类
  12. class Person {
  13. public:
  14.         virtual ~Person();
  15.         virtual std::string name() const = 0;
  16.         virtual std::string birthDate() const = 0;
  17.         virtual std::string address() const = 0;
  18.         ...
  19. };
复制代码
这个类的客户必须以Person指针大概引用来进行编程,因为不可能实例化包含纯虚函数的类。(然而实例化Person的派生类却是可能的)。
接口类只有在其接口发生变化的情况下才需要重新编译,其它情况都不需要
接口类的客户为这种类创建新对象的方法:
[code]class Person {public:        ...        static std::tr1::shared_ptr                 create(const std::string& name, const Date& birthday, const Address& addr);         ...};//客户这样使用std::string name;Date dateOfBirth;Address address;...//创建一个对象,支持Person接口std::tr1::shared_ptr pp(Person::create(name, dateOfBirth, address));...std::cout name()




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4