条款05:了解C++默认编写并调用的函数
a) 当C++处理一个类之后,如果编写者没有声明,编译器就会默认生成一个copy构造函数,一个copy assignment操作符(即等号操作符)和一个析构函数,并且这些函数都是public和inline的属性。
b) 对象在初始化的同时进行赋值,如student s1 = s2;调用的不是等号操作符,而是拷贝构造函数,形式类似于student s1(s2);
c) 对于copy构造函数和copy assignment操作符,编译器创建的版本只是简单的将源对象的每一个non-static成员变量拷贝到目标对象。
d) 只要编写者声明了一个对应类型的函数,如构造函数,编译器就不会为其创建default的版本。
e) 如果类内含有reference成员或者const成员,必须自己定义copy assignment操作符,因为C++不能让reference所指向的对象被改动替换,const成员也不能再次赋值。
条款06:若不想使用编译器生成的default函数,就应当明确拒绝
a) 如果你希望你设计的类不支持拷贝构造功能和等号赋值对象的功能,可以将他们声明为private类型,从而阻止编译器创建默认版本,并阻止使用者的调用(private)。
b) 只声明为private函数并非足够安全,因为member函数和friend函数还是可以对其进行调用,所以一个比较完善的办法是将其声明为private并且不定义它。
c) 可以设计一个父类令其符合以上规范,之类继承它后自然也就有了对应的限制,因为拷贝复制子类首先需要拷贝复制父类。
条款07:为多态基类声明virtual析构函数
a) 一个派生类对象被一个基类指针所指向,则其在析构时调用析构函数分为两种情况:
1.1 第一种是析构函数为虚函数,则是动态绑定,调用时通过对象类的虚表指针来调用,其会先调用子类的析构函数再调用父类的析构函数;
1.2 第二种情况是析构函数为non-virtual函数,则通过对象名加上函数名的方式调用,即base::funcname()的方式,则直接调用父类的析构函数,造成子类的资源泄漏。
b) 为了实现virtual函数,对象必须携带一个vptr(virtual table pointer)指针来指向虚函数表vtbl(virtual table),即一个函数指针数组。
c) 每个带有virtual函数的类都有一个队形的虚表vtbl,当对象通过指针调用的虚函数时,就通过虚表指针来查找对应的虚函数进行调用,实现多态。
d) 虚表指针包含在对象体所在的内存中,所以如果有虚函数对象的体积将会增加4个字节或8个字节(64位系统),此时一个对象就不能再装入64-bit寄存器(体积过大),所以将所有函数全都声明为virtual而不考虑其使用环境是不正确的。
e) 如果类有任何virtual函数,那它也应该拥有一个虚析构函数;但如果类不是为了作为基类被继承而设计,也不是为了拥有多态性,就不应该声明虚析构函数。
条款08:别让异常逃离析构函数
a) 在析构函数中如果有异常抛出就会导致内存泄露,特别是当要析构一个对象数组时。
b) 如果一个析构函数调用的函数可能抛出异常,那么析构函数应该捕获该异常并吞下它(不传播)或结束程序。
c) 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数而非析构函数执行该操作。
d) 析构函数调用该函数进行异常处理,就提供给了客户一个处理异常的机会(普通函数中处理)。
条款09:绝不在构造和析构过程中调用Virtual函数
a) 在一个派生类对象生成的构造中,总是先调用基类的构造函数生成基类所拥有的部分再调用派生类的构造函数生成派生类独有的部分。
b) 同理,在一个派生类的析构过程中,总是属于派生类独有的部分先进行析构,然后基类析构函数析构基类独有的部分。
c) 在基类的构造函数和析构函数调用期间,派生类对象独有的部分都是不存在的,所以其只能操作基类独有的部分,在base class构造期间virtual函数绝对不会下降到derived classes阶层。或者说,在base class构造期间,virtual函数不是virtual函数。
d) 如果在base class构造函数中调用了virtual函数,因为调用base class构造时派生类对象尚未生成,而virtual函数下降至派生类版本,必然调用其local成员,但这些成员根本没有初始化,这将是一张通往彻夜调试大会的车票。
条款10:令operator=返回一个reference to *this
a) 返回一个refere to *this可以实现对于该对象的再次操作和嵌套赋值等,这只是个协议,0并无强制性。
例如,若声明为如下形式
Obj& operator=(const Obj& rhs)
{
···············
return *this;
}
则可以实现
Obj x,y,z;
x=y=z=············;
(x=y)=z;
连锁赋值和多次赋值,赋值操作符是右结合的,而等号操作符函数形参传递常引用的原因可以见条款20。
条款11:在operator=中处理“自我赋值”
a) 对象的自我赋值可能导致两个指针指向同一内存,则析构时将导致后一个对象析构的内存已经不存在;即使赋值时是为每个对象的指针单独分配一块内存,那也需要先对左操作数对象的指针内容进行释放再赋值,但释放的时候会把右操作数指向的内存一起释放掉(指向同一对象),导致赋值失败。
b) 为阻止这种错误,传统的做法是在函数最前面做“证同测试”,达到自我赋值检验的目的;亦可利用copy and swap的方法来进行,即通过copy生成一个临时对象,将等号右操作数拷贝给他,然后左操作数与其进行值交换,最终释放掉临时对象。
条款12:复制对象是勿忘其每一个成分
a) 当自行声明拷贝构造函数时,编译器提供的缺省拷贝构造函数将不会提供,而自定义拷贝构造函数如果有拷贝成员不完全等错误也得不到提醒。如在复制派生类对象时忘记对其对应部分的基类对象进行复制,导致基类对象只有调用默认构造函数生成。
b) Copying函数应该确保复制了”对象内部的所有成员变量“及”所有base class“成分。
c) 不要尝试用一个copying函数实现另一个copying函数,应该将共同机能放进到第三个函数中,并由两个copying函数共同调用。