2. 构造/析构/赋值运算(Constructors,Destructors,and Assignment Operators)
条款05: 了解C++ 默默编写并调用哪些函数(Knowwhat functions C++ silently writes and calls)
本条款讲了编译器自动为你创建了哪些函数。
用户定义一个empty class,当C++ 处理过它之后,如果你自己没声明,编译器就会为它声明一个copy 构造函数、一个copy assignment操作符和一个析构函数。此外,如果你没有声明任何构造函数,编译器也会为你声明一个default 构造函数。所有这些函数都是public 且inline。惟有当这些函数被需要(被调用),它们才会被编译器创建出来。即有需求,编译器才会创建它们。
1、默认构造函数:所有的非内置类型成员不初始化,所有类类型成员用其默认构造函数初始化
2、析构函数:默认是非虚的,除非这个类的基类自身声明有virtual析构函数。
3、拷贝构造函数:Empty(constEmpty & rhs) {...} 这是单纯将来源对象的每一个non-static成员变量拷贝到目标对象
4、拷贝赋值函数:与拷贝构造函数基本一致,只是类中的引用和常量不能赋值(不然就违反了这两个类型的意义了)。如果这个类要调用基类的拷贝赋值函数,但是那个东西就是private,也编译器也不会给派生类生成copyassignment。编译器生成的拷贝赋值操作符:对于成员变量中有指针,引用,常量类型,我们都应考虑建立自己“合适”的拷贝赋值操作符。因为指向同块内存的指针是个潜在危险,引用不可改变,常量不可改变。
条款06: 若不想使用编译器自动生成的函数,就该明确拒绝(Explicitlydisallow the use of compiler-generated functions you do not want)
本条款告诉程序员,如果某些对象是独一无二的(比如房子),你应该禁用copy 构造函数或copy assignment 操作符,可选的方案有两种:
(1)将copy构造函数和copy assignment操作符声明为private,并不予实现;
(2)方法一中,当member函数或friend函数调用copy构造函数(或copy assignment操作符),会有链接错误。用下面的方法可以将连接期间的错误移到编译期间。定义一个Uncopyable公共基类,让所有独一无二的对象继承它。
class Uncopyable {
protected: //允许derived对象构造和析构
Uncopyable() {}
-Uncopyable(){}
private:
Uncopyable(const Uncopyable&}; //但阻止copying
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale: private Uncopyable{// 注意这个地方不用public 继承了
…
};
一个实际应用场景:在含有指针成员的类的拷贝中:
class Array{
int *data;
};
Array a;
Array b;
b = a;
编译器生成的缺省的构造拷贝函数和拷贝运算符的重载函数,对指针实行的是按位拷贝,仅仅只是拷贝指针的地址,而不会拷贝指针的内容。因此在执行完上面的代码之后,a.data和b.data指向的同一地址。当a或者b中任意一个结束其生命周期调用析构函数时,会删除data。由于他们的data指向的是同一个地方,两个实例的data都被删除了。但另外一个实例并不知道它的data已经被删除了,当企图再次用它的data的时候,程序就会不可避免地崩溃。解决方法除了上面的两条外,还可以重载copy构造函数和copy assignment操作符,让它们拷贝的不是地址,而是数据。
条款07: 为多态基类声明virtual 析构函数(Declaredestructors virtual in polymorphic base classes)
由来:当derivedclass 对象经由一个baseclass 指针被删除,而baseclass的析构函数不是虚的,其结果未定义,通常是base被销毁了,derived的非base部分没有被销毁。我们用virtual的目的,是为了子类实现个性化,因此,虚的目的是确有其用的,不能瞎虚,那样会影响移植性和代码性能
本条款阐述了一个程序员易犯的可能导致内存泄漏的错误,总结了两个程序员应遵守的编程原则:
(1) polymorphic (带多态性质的) base classes 应该声明一个virtual 析构函数。如果class 带有任何virtual 函数,它就应该拥有一个virtual 析构函数。这样,当用户delete基类指针时,会自动调用派生类的析构函数(而不是只调用基类的析构函数)。
(2)Classes 的设计目的如果不是作为base classes 使用,或不是为了具备多态性(polymorphically),就不该声明virtual 析构函数。这是因为,当用户将一个函数声明为virtual时,C++编译器会创建虚函数表(vtbl, virtualtable)以完成动态绑定功能,这将带来时间和空间上的花销。比如上面的 Uncopyable并不是为了多态。
(3)书上一句话:"如果你曾经企图继承一个标准容器或者任何其他带有non-virtual析构函数的class,拒绝诱惑"
(4)如果你想让一个类成为抽象类,但是手头有没有纯虚函数,那么用析构函数做纯虚函数是个好主意。不过要注意:你必须为这个purevirtual析构函数提供一份定义:AWOV::~AWOV(){ };
条款08:别让异常逃离析构函数(Preventexceptions from leaving destructors)
两种情况下析构函数会被调用:一个对象的声明周期自然地结束;或者是前面抛了个异常,由于异常处理机制,导致对象被删除。我们在析构函数中无法判断是那种情况激活了析构函数,如果析构再抛一个,那么控制权交到了函数外,C++将调用terminate函数。
(1)析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。析构不能抛出异常,主要是因为,我们捕捉不到,那样析构抛出的异常肆意乱跑,不受控制,导致不明确行为或者程序终止。针对上述的问题,我们有的解决方法:析构函数吞下任何异常,catch(...)
(2)如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class 应该提供一个普通函数(而非在析构函数中)执行该操作。如下面程序所示:
class DBConn{
public:
...
void close()
{
db.close();
closed = true;
}
~DBConn()
{
if (!closed)
{
try{ //如果客户没有关闭连接
db.close();
}
catch(...){
制作运转记录,记下对close的调用失败;//结束程序或吞下异常
}
}
}
private:
DBConnection db;
bool closed;
};
条款09: 绝不在构造和析构过程中调用virtual 函数(Nevercall virtual functions during construction or destruction)
不要在构造和析构过程调用virtual函数,这类调用从不下降至当前执行构造函数(析构函数)的derived class(比起当前执行构造函数和析构函数的那层)。因为在derived class对象的base class构造期间,对象的类型是base class而不是derived class;同理,一旦derived class析构函数开始执行,对象内的derived class成员变量便呈现未定义值,C++视他们仿佛不再存在,进入base class析构函数后对象就成了base class对象。
条款10: 令operator= 返回一个referenceto *this(Have assignment operators return a reference to *this)
本条款告诉程序员一个默认的法则:为了实现“连锁赋值”,应令operator=(operator +=、operator -=)返回一个reference to*this。
intx, y, z;
x = y = z = 15;
为了实现“连锁赋值”,赋值操作符必须返回一个“引用”指向操作符的左侧实参。
即:
Widget & operator = (constWidget &rhs)
{
...
return *this;
}
所有内置类型和标准程序库提供的类型如string,vector,complex或即将提供的类型共同遵守。
条款11: 在operator= 中处理”自我赋值”(Handleassignment to self in operator=)
本条款给出了写copy assignment函数时应该注意的几点和相应的解决技术:比较”来源对象”和”目标对象”的地址、精心周到的语句顺序、以及 copy-and-swap。
(1)为了防止自我赋值,在operator=最前面应该有一个“证同测试(identity test)”。
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs) return *this; //“证同测试”,如果是自我赋值,直接返回
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
(2)上面的代码中,如果“new Bitmap”导致异常(分配内存不足或是Bitmap的copy构造函数抛出异常),Widget 最终会持有一个指针指向被删除的Bitmap。解决方法是:先复制副本,再删除。
Widget& Widget::operator=(const Widget& rhs){
Bitmap* pOrig = pb; //保存副本
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
(3)一个确保“异常安全”和“自我赋值安全”的替代方案,使用copy-and-swap技术。
class Widget {
……
void swap(Widget& rhs); //交换*this 和rhs 的数据:详见条款29
……
};
Widget& Widget::operator=(Widget &rhs) //rhs是被传对象的一份复件(副本),注意这里是pass by value.
{
Widget temp(rhs);
swap(rhs); //将*this 的数据和副本的数据互换
return *this;
}
条款12: 复制对象时勿忘其每一个成分(Copycall parts of an object)
本条款阐释了复制对象时容易犯的一些错误,给出的警告是:
(1)Copying 函数应该确保复制“对象内的所有成员变量”及“所有base class 成分”。复制所有local成员变量;调用所有base classes内的适当的copying函数。
如果你在类中添加一个成员变量,你必须同时修改相应的copying函数(所有的构造函数,拷贝构造函数以及拷贝赋值操作符)。
(2)不要尝试以某个copying 函数实现另一个copying 函数。应该将共同机能放进第三个函数中,并由两个coping 函数共同调用。具体地,如果copy 构造函数和copy assignment 操作符有相近的代码,copy assignment操作符和copy构造函数之间不要互相调用。消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private 而且常被命名为init。