读书笔记中涉及到的所有代码实例可通过https://github.com/LuanZheng/EffectiveCPlusPlus.git进行下载获得。
Item5 了解C++默默编写并调用哪些函数重点内容
默认构造函数
默认析构函数
拷贝构造函数
拷贝赋值操作符
若自己声明了一个构造函数,则编译器不会自动生成默认构造函数。
对于内置类型,会以拷贝每一个bits来完成初始化,而对于非内置类型,会通过调用拷贝构造函数初始化。
对于默认赋值操作符,一般而言,只有当生成出的代码合法且有适当机会证明它有意义,其表现才会自动生成默认拷贝赋值操作符。万一两个条件有一个不符合,编译器会拒绝为class生出operator=.
若class中内含引用或const成员,则不会自动生成“=”运算符重载。因为改变reference和const是非法的。如果打算在一个内含reference成员的class内支持赋值操作,必须自己定义copy assignment操作符。面对内含const成员的class也是一样。最后还有一种情况,如果某个base classes将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。
Item6 若不想使用编译器自动生成的函数,就该明确拒绝
可以将copy构造函数或copy assignment操作符声明为private,可防止外部调用。同时,将该种类的private函数设置为只有声明,而故意没有实现,则可保证成员函数,友元函数也不能调用它们。
另外一种做法是设计一个base class,将base class的copy构造函数或copy assignment操作符设置为private.则继承该base class的任何derived class均不会生成copy构造函数或copy assignment操作符。
例子见Item6
Item7 为多态基类声明virtual析构函数
当派生类对象经由一个基类指针被删除,而该基类带着一个non-virtual析构函数,其结果未有定义。实际执行时通常发生的是对象的派生成分没有被销毁。
消除这个问题的做法很简单: 给基类一个virtual析构函数。
但vitual函数会带来额外的系统开销,所以,只在必要时,才将函数设置为virtual。经验做法是:只有当class内含有至少一个vitural函数时,将析构函数设置为virtual。(因为这样的class往往被用作基类)
因此,如果企图继承一个标准容器或任何其他“带有non-virtual析构函数”的class时,要拒绝这种诱惑。
纯虚析构函数
若析构函数选择为纯虚析构函数,则纯虚析构函数必须有定义,这不同于一般的纯虚函数。因为析构函数在调用过程中,编译器会先调用派生类的析构函数,之后是基类的析构函数。如果基类(即使是抽象类)没有给出析构函数的定义,连接器会报错。而一般的纯虚函数,在基类中不需要有实现,因为派生类会分别实现一般的纯虚函数,从而基类中的纯虚函数永远得不到被调用的机会,因此,连接器也自然不会报错。
在visual studio 2015环境下,若纯虚析构函数没有在基类中给出定义,则出现以下的报错信息。
1>AtomicClock.obj : error LNK2019: 无法解析的外部符号 "public: virtual __thiscall TimeKeeper::~TimeKeeper(void)" (??1TimeKeeper@@UAE@XZ),该符号在函数 __unwindfunclet$??0AtomicClock@@QAE@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z$1 中被引用
1>WaterClock.obj : error LNK2001: 无法解析的外部符号 "public: virtual __thiscall TimeKeeper::~TimeKeeper(void)" (??1TimeKeeper@@UAE@XZ)
#include "AtomicClock.h"
#include "WaterClock.h"
int main()
{
TimeKeeper* timeKeeperClock = new AtomicClock("Atomic");
if (0 != timeKeeperClock)
{
delete timeKeeperClock;
timeKeeperClock = 0;
}
return 0;
}
当base class没有声明析构函数为virtual时,如下图,错误,derived class的析构函数没有机会得到执行。
当base class声明析构函数为virtual时,如下图,正确。Derived class的析构函数也得到执行。
例子见Item7
Item8 别让异常逃离析构函数
若析构函数吐出异常,常会导致程序结束执行或导致不明确的行为。C++不喜欢析构函数吐出异常。有两个办法可以避免这一问题:
1.如果析构函数可能会导致异常,就主动结束程序。记录发生问题的日志。
2.如果析构函数可能会导致异常,析构函数自己吞下异常,记录下发生问题的日志。
但该两种方法都有局限性,问题在于两者都无法对析构函数的异常做出反应。
一个较佳的解决方案是重新设计几口。增加一个成员函数来将析构函数中的可能抛出异常的部分代码移出到该成员函数。则用户可以通过该接口进行代码逻辑的处理。而在析构函数中,也调用该函数,形成“双保险 ”。而此时,析构函数可以放心的采取吞下异常或结束程序。因为已经提供了接口给客户进行第一时间的调用。