0%

Effective C++之构造、析构、赋值运算

2章:构造、析构、赋值运算

条款05:了解C++默默编写并调用哪些函数
  • 编译器会暗自为class创建default构造函数、copy构造函数、copy assignment操作符以及析构函数

  • 对于class内含reference成员或const成员,编译器拒绝为其生成copy构造函数和copy assignment操作符

    因为C++不允许reference改指向不同的对象以及更改const成员。

条款06:若不想使用编译器自动生成的函数,就该明确拒绝
  • 为驳回编译器自动提供的函数,可将相应的成员函数声明为private并且不予实现。

  • 掌握Uncopyable类的实现机制

    • 将构造函数和析构函数设置为protected

    • 将拷贝构造函数和拷贝赋值运算符设置为private

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      class Uncopyable
      {
      protected:
      Uncopyable() {}
      ~Uncopyable() {}

      private:
      Uncopyable(const Uncopyable &);
      const Uncopyable &operator=(const Uncopyable &);
      };
条款07:为多态基类声明virtual析构函数
  • 带多态性质的base class应该声明一个virtual析构函数

  • class内至少含有一个virtual函数,才为它声明virtual析构函数

  • class的设计目的如果不是作为base class使用,或不是为了具备多态性,就不该声明virtual析构函数

  • 然而,有时候你希望拥有一个抽象类,但没有任何需要的pure virtual方法,怎么办?

    由于abstract class(不能实例化)总是被期望当作多态基类,多态基类又需要virtual析构函数,而pure virtual函数会导致abstract class,因此可将析构函数声明为pure virtual并且给出默认实现。

    1
    2
    3
    4
    5
    6
    7
    8
    // 小技巧:pure virtual 析构函数
    class AWOV
    {
    public:
    virtual ~AWOV() = 0;
    };

    AWOV::~AWOV() {/* default */}
条款08:别让异常逃离析构函数
  • 析构函数绝对不要吐出异常

假设有一个类负责数据库的连接:

1
2
3
4
5
6
7
8
class DBConnection
{
public:
...
static DBConnection create();

void close(); // 关闭联机, 失败则抛出异常
};

为了确保客户不忘记在DBConnection对象上调用close函数,一个合理的想法是创建一个用来管理DBConnection资源的类,并在析构函数中调用close

1
2
3
4
5
6
7
8
9
10
11
12
class DBConn
{
public:
...
~DBConn()
{
db.close();
}

private:
DBConnection db;
};

用户可以写出这样的代码

1
2
3
4
{
DBConn dbc(DBConnection::create());
...
} // 区块作用域结束,调用析构函数销毁对象

如果被析构函数调用的函数close可能抛出异常,析构函数应该捕获异常然后吞下它们或者结束程序。

一个好的策略是,开放一个close接口供用户调用,把调用close的责任从DBConn析构函数手上移到用户手上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class DBConn
{
public:
...
void close()
{
db.close();
closed = true;
}
~DBConn()
{
if(!closed)
{
try
{
db.close(); // 关闭连接(如果客户没做的话)
}
catch(...)
{
日志记录下对 close 调用的失败;
...
}
}
}

private:
DBConnection db;
bool closed;
};

因此,如果客户需要对某个操作函数运行期间的异常作出反应,那么class应该提供一个接口执行该操作。如果close的确发生了异常,而客户没有调用close接口进行处理,DBConn只能吞下或结束程序。

条款09:绝不在析构和构造函数中调用virtual函数
  • 派生类对象内的基类成分会在派生类自身成分被构造之前先被构造
  • 基类构造期间,虚函数绝不会下降到派生类层

需要注意的是,有时类有多个构造函数,每个都需要执行某些相同的工作,那么避免代码重复时会把相同的初始化代码放到一个init函数中实现,如果这时在init函数中同样调用了虚函数,情况是一样的但比较隐秘

条款10:令赋值操作符operator=返回一个reference to *this
  • 为了实现连续赋值
条款11:在operator=中处理自我赋值
  • 有些自我赋值并不明显,如通过指针或引用

假设你建立一个class来保存一个指针指向一块动态分配的位图(bitmap):

1
2
3
4
5
6
7
class Bitmap {...};
class Widget
{
...
private:
Bitmap* pb; // 指向一个从 heap 分配而得的对象
};

错误的operator=实现为:

1
2
3
4
5
6
7
// 自我赋值不安全
Widget& Widget::operator=(const Widget& rhs)
{
delete pb;
pb = new Bitmap(*rhs,pb);
return *this;
}

可通过一个“证同测试”来检验:

1
2
3
4
5
6
7
8
9
10
// 自我赋值安全, 但不具备异常安全
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs)
return *this;

delete pb;
pb = new Bitmap(*rhs,pb);
return *this;
}

所谓的异常安全指的是,如果new Bitmap发生异常,会导致Widget最终会持有一个指针指向一块被删除的Bitmap

1
2
3
4
5
6
7
8
9
10
// 具备异常安全, 则自动具备自我赋值安全
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* oldPb = pb; // 记住之前的 pb

pb = new Bitmap(*rhs,pb);
delete oldPb;

return *this;
}

使用更好的copy and swap技术:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Bitmap {...};
class Widget
{
...
void swap(Widget& rhs)
{
... // 交换 *this 和 rhs 的数据, 见条款 25
}
...
};

Widget& Widget::operator=(const Widget& rhs)
{
// 提升点效率 ?
// if(this == &rhs)
// return *this;

Widget temp(rhs); // copy

swap(temp); // swap

return *this;
}
条款12:复制对象时勿忘每一个成分
  • 每一个成分包括对象内所有成员变量以及所继承的基类成分
    • 在拷贝构造函数中的初始化列表中调用所继承的类的拷贝构造函数
    • 在拷贝赋值操作符函数中调用所继承的类的拷贝赋值操作符函数
  • 不要尝试让拷贝构造函数和拷贝赋值操作符函数互相调用