目录
- 条款5:了解C++默默编写并调用那些函数
- 条款6:如果不想使用编译器自动生成的函数,就应该明确拒绝
- 条款7:为多态基类声明virtual析构函数
- 条款8:别让异常逃离析构函数
- 条款9:绝不在构造和析构过程中调用virtual函数
- 条款10:令operate=返回一个reference to *this
- 条款11:在operate=中处理“自我赋值”
- 条款12:复制对象时勿忘其每一个成分
条款5:了解C++默默编写并调用那些函数
-
- 如果类中没有声明构造函数,编译器会帮你生出一个default构造函数、copy构造函数、copy assignment操作符以及析构函数。其中,default构造函数和析构函数会分别自动调用基类(父类)成员变量的构造函数和析构函数,而copy构造函数和copy assignment操作符只是简单地将每一个非static成员变量拷贝到目标对象(浅拷贝)。
-
- 有两种情况下编译器拒绝自动为class生成operator=:
(1)类内含引用&成员变量和const成员变量,这两者一经初始化就不允许再赋值,但可自定义copy操作符支持赋值;
(2)如果某个基类将copy assignment操作符设为private,以至于派生类无法访问。
- 有两种情况下编译器拒绝自动为class生成operator=:
条款6:如果不想使用编译器自动生成的函数,就应该明确拒绝
-
- 如果想要阻止编译器暗自创建copy构造函数、copy assignment操作符,同时阻止其他人调用它们,有以下两种实现方法:
(1)将这两个成员函数声明为private并且故意不实现它们,形参参数名也可以不写,因为根本不会实现它们:
(2)(将链接器错误转移至编译器)继承一个包含private声明的copy构造函数、copy assignment操作符但不实现的基类,然后继承它。因为编译器生成的copy会尝试调用基类对应的函数,由于private,则会被拒绝。
- 如果想要阻止编译器暗自创建copy构造函数、copy assignment操作符,同时阻止其他人调用它们,有以下两种实现方法:
条款7:为多态基类声明virtual析构函数
-
- 当derived class对象经由一个base class(带着非virtual析构函数)指针删除时,其结果未定义—通常是derived class自己的成员没有被删除,造成对象“局部销毁”。
(1)解决方法:给基类一个虚析构函数
(2)底层原理:本质上是多态的体现。基类一个虚析构函数,其派生类的析构函数将继承虚函性质,也是一个虚函数。当一个类有虚函数时,实例化对象中会有一个虚函数表指针vptr,指向该对象的虚函数表。虚函数表中存放了该对象的所有虚函数。虚析构函数也会放到虚函数表中。当基类指针指向派生类对象时,虚函数表中的析构函数是派生类自己的析构函数。因此,当delete此基类指针时,vptr在虚函数表中查询并执行的析构函数是派生类自定义的析构函数,而非基类的析构函数。与此同时,派生类析构函数被调用后,基类析构函数自动调用。也就是说,派生类对象拥有的基类和自己的成员均被析构。
- 当derived class对象经由一个base class(带着非virtual析构函数)指针删除时,其结果未定义—通常是derived class自己的成员没有被删除,造成对象“局部销毁”。
-
- 带有多态性质基类应该声明一个虚析构函数。如果类带有任何虚析构函数,它就应该拥有一个虚析构函数。
-
- 不要把标准库的类当作基类,可能它们不含任何虚函数。因此有可能发生上述“局部销毁”的情况。
-
- 另一种解决对象“局部销毁”做法是为抽象类添加纯虚析构函数,让其他类继承它。需要为其声明,并且定义(因为派生类执行析构函数时要调用基类析构函数)。
virtual ~ABC() = 0;//声明
virtual ABC::~ABC(){};//定义
条款8:别让异常逃离析构函数
-
- 当存在多个对象同时销毁时,可能存在多个对象在析构部分抛出异常。程序将结束执行或者导致不明确行为。
-
- 析构函数中发生异常的两种处理方式:
(1)结束程序:可以阻止异常从析构函数传播出去(下例中为析构函数中调用的close()可能出现异常)
- 析构函数中发生异常的两种处理方式:
(2)吞下异常:有时候希望程序即使遇到异常以后也要让程序继续执行一段时间(例如数据保存工作等等)。
-
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
-
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而且在析构函数中)执行该操作。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而且在析构函数中)执行该操作。
条款9:绝不在构造和析构过程中调用virtual函数
-
- 构造和析构函数中使用虚函数可能不会产生多态行为,与预期不同。base class构造期间virtual函数绝不会下降到derived class阶层。
原因:在派生类对象的基类构造期间,对象的类型是基类而不是派生类,此时执行的虚函数是基类的虚函数而不是派生类的虚函数。对象在派生类构造函数开始执行之前不会成为一个派生类对象。析构函数同理。
- 构造和析构函数中使用虚函数可能不会产生多态行为,与预期不同。base class构造期间virtual函数绝不会下降到derived class阶层。
-
- 有一种情况下很难侦测到:基类构造函数没有直接调用虚函数,而是调用了将虚函数封装到一个非virtual的函数中,此时依然会发生构造基类部分调用基类的虚函数的情况,但是难以检测。因此,在构造和析构期间不要调用虚函数,包括它们所调用的函数也不要存在调用虚函数,因为这类调用从不下降至派生类。
-
- 如果需要不同派生类在构造时,实现相对应的功能。解决办法就是让基类实现对应功能的函数为非virtual,派生类在构造中传递实现对应功能的信息给基类构造函数,基类构造函数将信息给事项对应功能的非virtual函数。通常这些携带传递信息的变量或者函数被设为static ,保证使用的变量已经初始化。由于派生类成员构造在基类成员构造之后,因此如果不用static成员传递信息,派生类其他成员可能是未初始化的。
- 如果需要不同派生类在构造时,实现相对应的功能。解决办法就是让基类实现对应功能的函数为非virtual,派生类在构造中传递实现对应功能的信息给基类构造函数,基类构造函数将信息给事项对应功能的非virtual函数。通常这些携带传递信息的变量或者函数被设为static ,保证使用的变量已经初始化。由于派生类成员构造在基类成员构造之后,因此如果不用static成员传递信息,派生类其他成员可能是未初始化的。
条款10:令operate=返回一个reference to *this
-
- 为了实现“连锁赋值”,赋值操作符必须返回一个引用指向操作符左侧实参。此条款也适用于其他赋值相关运算,例如+=,-=,*=等等。
X = Y = C = 15;//连锁赋值
条款11:在operate=中处理“自我赋值”
-
- 自我赋值类似如下:
- 自我赋值类似如下:
-
- 自我赋值可能会产生不安全的情况。如下例所示,如果是同一对象,delete销毁是this->pb所指向的区域,同时是rhs.pb所指向的区域,那么自我赋值后返回的对象中的pb指针指向的是一个已删除的区域。
解决方法:
(1)正同测试:
(2)创建副本或者以by value方式接收传入的实参(也是一份副本),这样赋值操作的两个对象/成员变量本质上是两个数据内容可能一样但实际是两个单独的对象/成员变量,copy and swap方式:
- 自我赋值可能会产生不安全的情况。如下例所示,如果是同一对象,delete销毁是this->pb所指向的区域,同时是rhs.pb所指向的区域,那么自我赋值后返回的对象中的pb指针指向的是一个已删除的区域。
-
- 确保当对象自我赋值时operator有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序,以及copy and swap。确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12:复制对象时勿忘其每一个成分
-
- 当为class添加一个成员变量,你必须同时修改copying函数(所有的拷贝构造以及operator=函数)。
-
- 如果要为derived class 撰写copying函数,必须小心复制其base class成分。如果是private成员,应该让derived class 的copying函数调用相应的base class函数。确保**(1)复制所有local成员变量;(2)调用所有基类内的适当的copying函数**。
- 如果要为derived class 撰写copying函数,必须小心复制其base class成分。如果是private成员,应该让derived class 的copying函数调用相应的base class函数。确保**(1)复制所有local成员变量;(2)调用所有基类内的适当的copying函数**。
-
- 不要尝试以某一个copying函数实现另一个copying函数。应该将共同重复机能放进第三个函数中,并由两个copying函数共同调用。