目录
条款10:令operator=返回一个reference to *this
条款05:了解c++默默编写并调用了哪些函数
当定义一个类时,如果没有手动申明,编译器会默认声明一个拷贝构造函数、拷贝赋值操作符、析构函数,如果类没有声明任何构造函数时,编译器还会自动声明一个默认构造函数。
当使用编译器自动生成的拷贝构造和拷贝赋值时,需要完全合法且有意义,当成员中有引用和const变量时尤其有可能出现错误。
当派生类中没有声明拷贝构造函数和拷贝赋值函数时,派生类会尝试调用基类的拷贝构造雨赋值函数,如果出错则会报错。
条款06:若不想使用编译器自动生成的函数,就应该明确拒绝
并不是所有的类都需要拷贝构造函数和拷贝赋值操作符,为了不让编译器为我们自动生成拷贝构造函数和拷贝赋值操作符,一般有两种做法:
1).将拷贝构造与拷贝赋值符声明为private且不进行定义(c++11的新标准中采用=delete的方法)
2).定义一个无法拷贝的基类uncopyable定义方式参考方法1),令不需要编译器生成拷贝构造函数和拷贝赋值符的类继承(不一定是public继承)uncopyable类即可,原因可以参考条款05中的最后一段。
条款07:为多态基类声明virtual析构函数
如果基类的析构函数是non-virtual的,基类指针或引用指向派生类时,释放基类指针或引用的话会默认调用基类的析构函数,这样就很有可能造成内存泄漏。
为了避免这样造成内存泄漏,一个可行的解决办法就是将基类的析构函数声明为virtual函数,这样就可以用基类的指针或引用调用派生类的析构函数。
当要注意当类并不作为基类或基类并没有发生多态时析构函数不要声明为virtual,因为virtual函数的实现是依靠虚函数指针和虚函数表的,这样会造成多余的容量和多余的操作。
总而言之就是当基类是作为多态基类或类中有virtual函数时,析构函数应该声明为virtual,相反,当类不是作为基类或不具备多态时析构函数不要声明为yirtual
条款08:别让异常逃离析构函数
c++中并不禁止析构函数抛出异常,但还是不建议在析构函数中抛出异常,因为如果析构函数抛出异常的话就意味着在还未完成析构就跳出类析构函数,会造成未定义的异常可能是内存泄漏等,避免这一问题有两种方法:
1).当析构函数抛出异常时,立即调用abort函数中止程序(提醒程序员出现错误了)
2).抛出异常时将异常记录下来继续执行程序,也就是吞下异常
但两者都无法对导致抛出异常的情况做出反应,缓解这一问题的方法是:
将可能抛出异常的部分分离出来,由程序员手动析构(如果发现问题及时补救),并在析构函数中判断是否已手都析构,如果没有则由析构函数再次析构(双重保险)
总之尽量不要在析构函数中抛出异常。
条款09:绝不在构造和析构的过程中调用virtual函数
众所周知对于发生了继承的类的构造顺序是先构造基类再依次构造派生类,析构的顺序则是相反,先析构最末端的派生类再依次向上析构,基于这一原因,如果在构造函数中调用virtual函数,由于派生类还未被构造实际上调用的还是基类的函数,无法达到实际的目的甚至发生异常,析构函数也是同样的道理。
如果仍要实现在基类构造函数中调用同一函数的不同版本,可以考虑向构造函数传递不同的信息,且在派生类中产生信息的函数必须时static的(详见P51)
条款10:令operator=返回一个reference to *this
为了能满足链式赋值即a=b=c这样的式子,最好令operator=返回一个reference to *this
条款11:在operator=中处理"自我赋值"
一旦你为类重载类拷贝赋值符就必须要考虑自赋值这种特殊的情况(很容易在赋值前就把要赋的值给释放掉),一般而言有3种解决方法:
1).在进行赋值操作前进行"证同测试",如if(this == &rhs)判断两变量地址是否相同,这样做可以避免自我赋值的错误,但仍可能在heap中开辟空间时发生错误导致赋值失败
2).为了避免上述这类"异常性安全",可以考虑改变总体赋值顺序,先将被赋值的内容做一个备份,然后在heap中开辟新的变量同时进行赋值,最后释放备份,这样做的好处是如果在heap中开辟新空间时发生异常,备份的释放会被跳过(不确定),同时因为是在赋值之后再释放备份的也避免了自赋值问题。
3).最后一种做法就是利用copy and swap,将要赋值的变量先拷贝一份,再将拷贝下来的变量与要被赋值的变量进行swap,这样效果类似于第二种方法。
最后要注意的是不光是拷贝赋值符,当函数操作多个对象时,也要确保即使这多个对象实际指向同一个对象时函数的正确性。
条款12:复制对象时勿忘其每一个成分
当我们手动写入拷贝构造函数和拷贝赋值符时,需要注意的是,即使我们的拷贝函数没有对所有的成员都进行拷贝编译器也不会报错
当我们手动定义拷贝构造函数的类发生继承时要更加注意,因为如果我们在子类中仅对子类本身的成员进行拷贝赋值,子类中继承而来的成员是没有没有赋值的,我们需要在子类中的拷贝构造函数中手动的调用父类的拷贝构造函数,子类的拷贝赋值操作符中手动的调用父类的拷贝赋值操作符,这样才能确保每个成员都进行了赋值。
最后要注意不能在拷贝构造函数中调用拷贝赋值符,也不能在拷贝赋值符中调用拷贝构造函数,不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中(一般是private),由两个copying函数调用。
派生类可以的构造函数显式和隐式的调用基类的构造函数,隐式是当基类有默认构造函数且派生类构造函数没有对基类成员进行定义时会自动调用,显示则是在派生类的初始化列表中直接调用基类的构造函数(可以是默认也可以是其他的构造函数)