item05 了解C++默默编写并调用哪些函数

对于一个空类,编译器可能默默生成下面四个函数。

  1. class A {};
  2. class A {
  3. public:
  4. A() {...}
  5. A(const A& rhs) {...}
  6. ~A() {...}
  7. A& operator=(const A& rhs) {...}
  8. };

:::info

  • 构造函数
  • 拷贝函数
  • 析构函数
  • copy assignment

这些函数都是public且inline ::: :::tips 编译器生成的copy函数只是简单的将源对象的每一个non-static成员拷贝到目标对象

  • 若是成员自己没有拷贝运算符,那么就会报错
  • 因此对于引用和const成员,应该定义自己的copy :::

    item06 若不想使用编译器自动生成的函数,就应该明确拒绝

  • 编译器生成的函数时public,若是想阻止生成,最简单的方式就是将函数显示地声明为private

  • 但是若是在成员函数中或者是友元函数中调用拷贝,链接器会报错而非编译器报错; :::tips 把报错转移到编译时期的方式:
    利用一个专门为了阻止copy的base class ::: ```cpp class Uncopyable { protected: Uncopyable(); ~Uncopyable(); private: Uncopyable(const Uncopyable&); Uncopyable& operator=(const Uncopyable&); };

class A : private Uncopyable { … };

  1. :::info
  2. 任何人企图拷贝该对象时,编译器会尝试自动生成相关拷贝函数,而默认生成的拷贝构造函数会调用基类的版本,因此编译器会拒绝
  3. :::
  4. <a name="HeD51"></a>
  5. # item07 为多态基类声明virtual析构函数
  6. 若是base class的析构函数不是虚函数
  7. :::danger
  8. 若是一个derived class对象经由一个base class的指针删除,那么只能删除base class的部分。
  9. :::
  10. - 消除方法是给base class一个virtual的析构函数。或者说如果class如果带有一个virtual函数,那么他也需要一个virtual的析构函数
  11. - class如果不是作为base class使用或者不是为了实现多态性,就不应该声明virtual的虚构函数。
  12. <a name="P5Jaq"></a>
  13. # item08 别让异常逃离析构函数
  14. 如果在析构过程中抛出异常,可能会导致资源释放不完全。
  15. <a name="WxCcF"></a>
  16. ## 方法一:明确终止或吞掉异常
  17. ```cpp
  18. ~DBManager() {
  19. try { db.close(); }
  20. catch (...) {
  21. log();
  22. std::abort(); // 明确终止
  23. }
  24. }
  25. ~DBManager() {
  26. try { db.close(); }
  27. catch (...) {
  28. log(); // 直接吞掉异常
  29. }
  30. }

方法二:将析构函数中可能吐出异常的操作提取到普通函数,给用户自行处理异常的机会

  1. class DBManager {
  2. public:
  3. ...
  4. ~DBManager() {
  5. if (!closed) {
  6. try {
  7. db.close();
  8. }
  9. catch (...) {
  10. log();
  11. }
  12. }
  13. }
  14. void close() // 由客户显式调用,自行处理异常
  15. {
  16. db.close();
  17. closed = true;
  18. }
  19. private:
  20. DBConnection db;
  21. bool closed;
  22. }

item09 绝不在构造和析构过程中调用virtual函数

在构造和析构过程中调用virutal函数会带来意想不到的效果。

  1. class Transaction{
  2. public:
  3. Transaction();
  4. virtual void log() const=0;
  5. };
  6. Transaction::Transaction(){
  7. //...
  8. log();
  9. }
  10. //------------------------------
  11. class BuyTransaction:public Transaction{
  12. public:
  13. virtual void log() const;
  14. };
  15. //------------------------------
  16. class SellTransaction:public Transaction{
  17. public:
  18. virtual void log() const;
  19. };

若是执行:BuyTransaction b

  • 首先执行基类的构造函数
  • 构造函数最后会调用log;
  • 但是这时派生类还没构造,因此调用的会是基类的log :::danger 在构造或是析构中调用虚函数,此时虚函数并不是虚函数! ::: 析构函数也类似,一旦开始执行析构函数derived class会先被释放,因此虚函数会无效

item10 令operator=返回一个reference to *this

为了实现“连锁赋值”,需要令赋值操作符返回一个操作符左侧实参的引用

item11 在operator=中实现“自我赋值”

  1. class Widget { ... };
  2. Widget w;
  3. ...
  4. w = w; // 赋值给自己
  5. // 潜在的自我赋值
  6. a[i] = a[j];
  7. // 潜在的自我赋值
  8. *px = *py;
  9. // 潜在的自我赋值
  10. class Base { ... };
  11. class Derived : public Base { ... };
  12. void doSomething (const Base* rb, Derived* pd); // rb 和 pd 可能指向同一对象

问题一 自我赋值安全性

源和目的可能是同一对象,此时 delete pb 将会销毁当前对象的 bitmap,导致返回后该指针未定义,一种修改方式如下

  1. Widget& Widget::operator=(const Widget& rhs)
  2. {
  3. if (rhs == *this) return *this; // 证同测试
  4. delete pb;
  5. pb = new Bitmap(*rhs.pb);
  6. return *this;
  7. }
  8. //添加“证同测试”使得其具备“自我赋值”安全性,但是仍然存在问题二。

问题二 异常安全性

即使按照问题一的修改方式,仍可能存在问题,如果 new Bitmap 时发生异常,将会导致 pb 失效,一种简单的修改方式如下

  1. Widget& Widget::operator=(const Widget& rhs)
  2. {
  3. Bitmap* pOrig = pb;
  4. pb = new Bitmap(*rhs.pb);
  5. delete pOrg;
  6. return *this;
  7. //复制pb所指的东西前不要删除pb
  8. }

即使没有“证同测试”,这种修改方式也已经同事解决了问题一。如果比较关心效率,可以在加上“证同测试”,此时需要从效率上衡量必要性
另一种修改方式是 copy and swap 技术

  1. class Widget
  2. {
  3. ...
  4. void swap(Widget* rhs);
  5. ...
  6. };
  7. Widget& Widget::operator=(const Widget& rhs)
  8. {
  9. Widget temp(rhs); // 为 rhs 制作一份副本
  10. swap(temp); // 将 *this 与上述副本交换
  11. return *this;
  12. }

item12 复制对象时勿忘每一个成分

如果不使用编译器生成的copy函数,那么会出现的问题是: :::danger 如果遗漏了某个成员的复制,编译器不会报错! :::

  • 复制所有局部成员变量
  • 调用所有base class的copy函数 :::tips 如果发现copy函数和copy assignment有相似的代码,消除重复代码的方式是建立一个新的成员函数给二者使用。 :::