《Effective C++》第二章笔记
第二章 构造、析构、复制运算符
条款5:C++编译器为类生成的函数
- default 构造函数:主要调用基类的构造函数和非静态成员的构造函数。
- 析构函数:默认为非virtual(除非这个类的基类有虚析构函数),调用基类和非静态成员的析构函数。
- copy 构造函数:将源对象的每一个成员拷贝到新对象中。
- copy assignment 运算符:基本表现与copy构造函数一致。
- move 构造函数:C++11
- move copy assignme 运算符:C++11
编译器生成的copy assignment运算符不适用的情况:
- 成员中存在引用类型:由于引用类型不允许更改赋值,所以编译器不允许进行赋值;
- 成员中存在const:同样不允许更改赋值;
- 如果基类的copy assignment 运算符被设声明为private,派生类无法生成默认的赋值运算符,因为派生类对象会尝试调用基类的copy assignment运算符。
条款6:如果不想使用编译器自动生成的函数,应该明确拒绝
(PS:C++11可以在相应的成员函数的参数列表后面加上=delete 表示删除禁用相应函数。所以不用采用下面的方法)
1.将相应的函数声明为private并且不予实现:如果非成员函数或者非友元函数想要使用就会编译报错;如果成员函数或者友元函数想要使用就会链接出错,因为函数定义并不存在;
2.声明一个基类,基类中把相应的函数定义为private,然后让实际类继承这个基类:这样当实际类的成员函数或者友元函数尝试使用时就会报编译错误而不是链接错误。值得注意的是:这个做法很微妙,比如有可能导致多重继承等,又比如不一定非要public继承,也不一定需要把析构函数声明为virtual等。
条款7:为多态基类声明virtual析构函数
(多态:当派生类中某些方法与基类中的同名方法不同时,即一个方法(函数)具有多种形态,其行为随上下文而异)
1.对于polymorphic(多态性质的)的base class:应该把析构函数声明为virtual。如果class有任何的虚函数,则就应该有一个虚析构函数。(因为存在虚函数就意味着这个这个类需要作为多态的基类。)
2.如果类的设计目的不是作为base class使用或者不是为了多态性:不该声明虚析构函数。比如STL中的容器和标准string。
(值得一提的是:如果声明一个虚基类(即存在纯虚函数的类),但是又没有合适的成员函数可以被声明为纯虚函数,则可以把析构函数声明为纯虚函数,即前面加上virtual,后面加上=0。同时注意必须为纯虚析构函数提供定义。)
条款8:析构函数不要吐出异常
- 为什么C++不鼓励析构函数抛出异常?一个简单的例子:假设在一个区块中一个容器装有n个类对象,当离开这个块时,需要对对象进行析构操作,如果第一个析构失败并抛出异常,还是需要对第二个对象执行析构操作,但是如果第二个也异常,就会导致C++处理两个异常,这种行为是不明确的。
- 析构函数不吐出异常:1.出现异常就调用abort()函数终止程序;2.析构函数内catch这个异常,可以选择记录在日志中,但是不要向析构函数外传播。
- 在class中提供一个普通的函数(非析构函数)对于某个操作的异常作出反应,让用户决定是否调用:通常是某个操作的异常会改变状态成员,用户可以根据状态成员是否改变来选择是否调用普通函数进行处理这个异常。
条款9:不要在构造函数和析构函数中调用virtual函数
- 不仅不要在构造和析构函数中调用virtual函数,而且构造函数和析构函数中如果存在一般函数调用,也要保证调用链中不存在virtual函数:虚函数存在的逻辑意义是不同继承层次的对象会采用不同的行为。这就导致了问题:派生类构造函数在调用基类构造函数会使用基类的虚函数。(两种理解方式:1.基类构造函数执行派生类成员并未创建,如果此时虚函数的行为下降到派生类层次,即调用派生类成员,那么逻辑上是错误的;2.执行基类构造函数期间,对象的类型是基类而不是派生类,这意味着不仅虚函数会被编译器解析至基类,运行期类型信息(Runtime Type Identification)比如dynamic_cast和typeid也会把对象视作基类类型。所以此时不存在派生类的虚函数,只存在基类的虚函数。)析构函数同理。
- 一种解决方案是:派生类构造函数将所需要的信息通过基类构造函数调用的参数传递给基类构造函数。这里一般用派生类的静态成员函数的返回值,这样能够保证传递的已经初始化。因为最开始的问题也是由于派生类未初始化的成员(指的是派生类的虚函数)导致的,但是如果使用静态成员函数,能够保证先于对象初始化,所以不会出现错误。(原文:由于无法使用virtual 函数从base classes 向下调用,在构造期间,可以藉由“令derived classes 将必要的构造信息向上传递至base class 构造函数”替换之而加以弥补)
条款10:赋值运算符operator=返回类型是一个(*this)的引用
即对于一个类的赋值运算符来说,形式最好如下:
1 | class Wedget { |
其实为了连续赋值的话需要返回值类型是一个左值实参,所以返回值是对象或者对象的引用都可以。但是在效率的角度,返回引用更好。
其他赋值相关的运算也遵守,比如+=、=等:
1 | class Wedget { |
条款11:在operator中处理自我赋值
采用“证同测试”解决自我复制安全性:就首先判断传入的参数是否为this指向的对象,如果是则直接返回。但是要注意,增加判断分支也会影响程序效率,所以需要判断是否会常有这种自身复制的情况出现,值不值得增加分支。
下面是一个影响自我赋值安全性的例子:(如果传入的参数是自身,那么就是删除原有的空间,但是又重新指向这块删除的空间)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Bitmap { ... };
class Wifget {
...
private:
Bitmap *pb; // 指向一个Bitmap对象
}
// 错误例子
Widget& Widget::operator=(const Widget &other) {
delete pb;
pb = new Bitmap(*other.pb);
return *this;
}
// 正确示范:解决自我赋值安全性
Widget& Widget::operator=(const Widget &other) {
if (other == *this) return *this; // 增加证同测试
delete pb;
pb = new Bitmap(*other.pb);
return *this;
}一种解决异常安全性和自我复制安全性的方法:(影响异常安全性指的是:如果上面错误例子中申请空间失败,那么指针会指向一个未知空间)1.首先记住this对象的指针指向的位置;2.为指针申请新的空间并将rhs对象的内容赋值进去;3.释放原先的指针指向;4.保留这个新的指针。这种方法不是最高效的,但是一定可行。
1
2
3
4
5
6Widget& Widget::operator=(const Widget &other) {
Bitmap *pOrg = pb; // 记住原来的指针
pb = new Bitmap(*other.pb); // 新的指针指向原指针的一个副本
delete pOrg; // 删除原来的指针
return *this;
}(条款29)手工排列语句:1.定义一个交换this和rhs数据的函数;2.在operator=函数保留一份rhs数据的副本;3.在operator=函数内调用交换函数把rhs副本与this内容互换。另一个变种:当按值传递实参(注意传统的赋值运算符一般都是使用引用传递)时,由于按值传递会自动产生一个副本,所以可以省去创建副本的操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Widget {
void swap(Widget &other); // 交换*this和other的数据
}
// swap and copy
Widget& Widget::operator=(const Widget &rhs) {
Widget temp(rhs);
swap(temp);
return *this;
}
// 变种
Widget& Widget::operator=(const Widget rhs) {
swap(temp);
return *this;
}
ps:确定任何函数如果操作一个以上的对象,而多个对象有可能是同一个对象时,其行为仍然正确。
条款12:复制对象时不要忘记每一个成分
1.要保证复制每一个成员(即本类的成员);2.要保证复制所有的base class成分(即基类成员)
copy构造函数和copy assignment运算符不能相互调用(作者文中说不要问为什么);但是为了减少重复代码,可以定义一个private的复制函数,让这两个函数去调用它。
补充:对于复制构造函数,在初始化成员列表中显式调用基类的复制构造函数,否则将调用基类的默认复制构造函数;对于赋值运算符,使用作用域解析运算符显式调用基类赋值运算符。