第二章、构造、析构和赋值
五、了解C++默默编写并调用了哪些函数
empty class(空类),编译器会为他声明一个copy构造函数、copy assignment操作符和一个析构函数。此外如果没有声明任何构造函数,编译器会声明一个default构造函数。所有这些函数都是public且inline。
class Empty{};
// 跟如下代码一样
class Empty{
public:
Empty() { ... }; // default构造函数
Empty(const Empty& rhs) { ... }; // copy构造函数
~Empty() { ... }; // 析构函数,是否该声明为virtual后面见
Empty& operator=(const Empty& rhs) { ... } // copy assignment操作符
}
只有但这些函数被调用,它们才会被编译器创建出来。下面代码将会将上面的函数创建出来
Empty e1; // default构造函数和析构函数
Empty e2(e1); // copy构造函数
e2 = e1; // copy assignment操作符
如果父类将copy assignment操作符声明为private,编译器将拒绝为其子类生成一个copy assignment 操作符。
注意:
编译器可以为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
六、若不想使用编译器自动生成的函数,就应该明确拒绝。
将不想使用的copy函数和copy assignment函数声明为private而且没有定义。或者使用一个基类阻止copying动作。
问:为什么不使用delete。
七、为多态基类声明virtual析构函数
当derived class对象经由一个base class指针被删除,而该base class呆着一个Non-virtual析构函数,其结果未有定义--实际执行时通常发生的是对象的derived成分没有被销毁。于是造成一个局部销毁现象,形成资源泄漏。
消除这个问题:给base class一个virtual析构函数。任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。
如果class不含virtual函数,如果将其析构函数生命为virtual。会导致占用更多的内存。因为要实现virtual函数,对象会产生一个vptr(virtual table pointer)指针。vptr指向一个由函数指针构造成的数组,成为vtbl(virtual table pointer),每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一个virtual函数。实际被调用的函数取决于对象vptr所指向的那个vtbl。
如果class内含virtual函数,其对象的体积会增加。
析构函数的运作方式:最深层派生的那个class其析构函数最先被调用,然后是其每一个base class的析构函数被调用。
注意:
polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数
classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphically),就不应该声明virtual析构函数
八、别让异常逃离析构函数
在析构函数中突出异常,可能导致不明确行为。
1、如果close抛出异常就结束程序,通常通过调用abort完成。
2、吞下因调用close而发生的异常
注意:
1、析构函数绝对不要突出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们或结束程序
2、如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
九、绝不在构造和行析构过程中调用virtual函数
derived class对象内的base class成分会在derived class自身成分被构造前先构造出来。base class构造期间virtual函数绝不会下降到derived classes阶层。在base class构造期间,virtual函数不是virtual函数。
因为base class构造函数的执行更早于derived class构造函数,当base构造函数执行时,derived classes的成员变量尚未初始化,如果此期间调用virtual函数下降至derived classes阶层,因为这些Local成员变量尚未初始化,这回导致对象内部尚未初始化的风险。
在derived class对象的base class构造期间,对象的类型是Base class而不是Derived class。不只virtual函数会被编译器解析至base class,若使用运行期类型信息,也会把对象视为base class类型。对象在derived class构造函数开始执行前不会成为一个derived class对象。
避免代码重复,将共同的初始化代码放进一个初始化函数init内。
因为无法使用virtual函数从base classes向下调用,在构造期间,你可以藉由令derived classes将必要的构造信息向上传递至Base class构造函数。替换之而加以弥补。
注意:在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class
十、令operator=返回一个reference to *this
在赋值过程中,可以将它携程连锁形式:
int x, y, z;
x = y = z = 15; // 赋值连锁形式
// 它被解析为 x = (y = (z = 15));
这里15先被赋值给z,然后其结果(更新后的z)再被赋值给y,然后再被赋值给x。
为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。
class Wsdget {
public:
Widget& operator=(const Widget &rhs) // 返回类型是一个reference
{ // 指向当先对象
...
return *this; // 返回左侧对象
}
}
这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算。如+=,-=,*=等等。
注意:令赋值(assignment)操作符返回一个reference to *this。
十一、在operator=中处理自我赋值
别名:有一个以上的方法指称某对象,一般而言如果某段代码操作pointers或references而它们被用来指向多个相同类型的对象,就要考虑这些对象是否为同一个。实际上两个对象只要来自同一个继承体系,它们甚至不需要声明为相同类型就可能造成别名。因为一个Base class的ference或Pointer可以指向一个derived class对象。
如果阐释自行管理资源(打算写一个用于资源管理的class)。可能掉进“在停止使用资源之前意外释放它”的陷阱。
传统做法:在operator=最前面做认同测试,达到自我赋值的检验目的。
Widget& Widget::operator=(const Widget& rhs)
{
if (this == *rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
但是如果new Bitmap导致异常,Wdiget最终会吃持有一个指针指向一块被删除的Bitmap。其实精心安排语句也可以做到。只需要注意复制pb所指东西之前别删除pb就好。
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig ;
return *this;
}
替代方案:使用copy and swap技术。
class Widget {
...
void swap(Widget& rhs); // 交换*this和rhs的数据;
...
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); // 为rhs数据制作一份副本
swap(temp); // 将*this数据和上述附件的数据交换
return *this;
}
注意:
1、确保当对象自我赋值时Operator=有良好欣慰。其中技术包括比较来源对象和目标对象的地址、精心周到的语句顺序,以及copy and swap。
2、确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
十二、赋值对象时勿忘其每一个成分
当编写一个copying函数请确保:
1、复制所有Local成员变量
2、调用所有base classes内适当的copying函数。
不应使用copy assignment操作符调用copy构造函数,同样也不许copy构造函数调用copy assignment操作符。如果copying函数中有相近的代码,建立一个新的成员函数给两者调用。这个函数通常为private并被命名为init。
注意:
1、copying函数应确保赋值对象内的“所有成员变量”即所有“base class成分”
2、不要尝试以某个copying函数实现另一个copying函数。应该将共同技能放进第三个函数中,并由两个copying函数共同调用。