Item 13: Use objects to manage resources.
Effective C++ 12:复制对象时勿忘其每一个成分
Item 12: Copy all parts of an object
正确拷贝函数实现:
1 | class Customer{ |
情形一: 新添加了一个数据成员,忘记了更新拷贝函数
1 | class Customer{ |
这时 lastTransaction
便被你忽略了,编译器也不会给出任何警告(即使在最高警告级别)
情形二: 继承父类忘记了拷贝父类的部分
1 | class PriorityCustomer: public Customer { |
正确写法:
1 | class PriorityCustomer: public Customer { |
Effective C++ 11:赋值运算符需要考虑自我赋值问题
Item 11: Handle assignment to self in operator=
我们在重载一个类的赋值运算符时要考虑自我赋值的问题。有了指针和引用自我赋值不总是第一时间能够识别出来。
1 |
|
自我赋值主要考虑到 自我赋值安全性 和 异常安全性
1 | class Bitmap { ... }; |
既不自我赋值安全性也不异常安全性, 当 rhs == *this时,delete pb使得rhs.pb成为空值,接下来 new 的数据便是空的。
1 | Widget& Widget::operator=(const Widget& rhs) { |
判断两个地址是否相同,如果是自我赋值,就不做任何事。但开始就delete pb, 但 new 出现异常, pb就会置空出现风险。
1 | Widget& Widget::operator=(const Widget& rhs) { |
在C++中仔细地排列语句顺序通常可以达到异常安全, 比如我们可以先申请空间,最后再delete:
1 | Widget& Widget::operator=(const Widget& rhs) { |
一个更加通用的技术便是复制和交换(copy and swap):
1 | class Widget { |
Effective C++ 10:赋值运算符要返回自己的引用
Item 10:Have assignment operators return a reference to *this.
赋值运算符要返回自己的引用只是个协议,并无强制性。这份协议被所有内置类型和标准程序库提供的类型如string
, vector
, complex
std::shared_ptr
等共同遵守。可以用来支持链式的赋值语句。
1 | int x, y, z; |
相当于:
1 | x = ( y = ( z = 15 ) ); |
我们自定义的对象最好也能支持链式的赋值,这需要重载=运算符时返回当前对象的引用:
1 | class Widget { |
Effective C++ 9:绝不在构造和析构过程中调用 virtual 函数
Item 9: Never call virtual functions during construction or destruction.
在构造和析构期间不要调用 virtual
函数,因为这类调用不会下降至 derived class
(比起当前执行构造函数和析构函数的那层)。
1 | class Transaction { // base class for all |
b
在构造时,调用到父类Transaction的构造函数,其中对 logTransaction
的调用会被解析到 Transaction
类。 那是一个纯虚函数,因此程序会非正常退出。
在derived class
对象的 base class
构造期间,对象的类型是 base class
而不是 derived classo
不只 virtual
函数会被编译器解析至(resolve to) base class
,若使用运行期类型信息 RTTI
(runtime type information, 例如 dynamic_cast
typeid
) ,也会把对象视为 base class
类型。
1 | class Transaction{ |
输出
1 | P11Transaction |
相同道理也适用于析构函数.
Effective C++ 8:析构函数不要抛出异常
Item 8: Prevent exceptions from leaving destructors.
C++
本身不阻止在析构函数抛出异常,但在析构函数中抛出的异常往往会难以捕获,引发程序非正常退出或未定义行为。例如:
1 | class Widget { |
当v被调用析构函数,它包含的所有Widget对象也都会被调用析构函数。又因为v是一个容器,如果在释放第一个元素时触发了异常,它也只能继续释放别的元素,否则会导致其它元素的资源泄露。如果在释放第二个元素的时候又触发了异常,那么程序同样会导致崩溃。
不仅仅是std::vector,所有STL容器的类甚至包括数组也都会像这样因为析构函数抛出异常而崩溃程序,所以在 C++
中,不要让析构函数抛出异常!
但是如果析构函数所使用的代码可能无法避免抛出异常呢?
1 | class DBConnection{ //某用来建立数据库连接的类 |
析构函数所调用的 close()
方法可能会抛出异常,那么有什么方法来解决呢?
吞掉异常
1 | DBConn::~DBConn(){ |
主动关闭程序
1 | DBConn::~DBConn(){ |
把可能抛出异常的代码移出析构函数
客户在需要关闭的时候主动调用 close()
函数
1 | class DBConn{ |
- 析构函数绝对不要抛出异常。如果一个被析构函数调用的函数可能抛出异常,析
构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。 - 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么
class
应该提
供一个普通函数(而非在析构函数中)执行该操作。
Effective C++ 7:为多态基类声明 virtual 析构函数
Item 7: Declare destructors virtual in polymorphic base classes.
析构函数声明为虚函数目的在于以基类指针调用析构函数时能够正确地析构子类部分的内存。 否则子类部分的内存将会泄漏,正确的用法如下:
1 | // 基类 |
- polymorphic (带多态性质的) base classes 应该声明一个 virtual 析构函数。如果
class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数。 - Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性
(polymorphically) ,就不该声明 virtual 析构函数。
Effective C++ 6:若不想使用编译器自动生成的函数,就该明确拒绝
Item 6: Explicitly disallow the use of compiler-generated functions you do not want.
在C++中,编译器会自动生成一些你没有显式定义的函数。可以参考:了解c++默默编写并调用哪些函数
然而有时候我们希望禁用掉这些函数,可以通过把自动生成的函数设为 private
来禁用它或者在 c++11
中使用 delete
关键字。
比如我们禁用拷贝的功能:
1 | class HomeForSale |
我们可以专门设计一个阻止copying
的类
1 | namespace noncopyable_ { |
1 | class HomeForSale : private noncopyable_::noncopyable |
Effective C++ 5:了解c++默默编写并调用哪些函数
Item 5: Know what functions C++ silently writes and calls
默认函数
在 C++
中,一个类有八个默认函数:
1 | class Empty { |
调用时机
只有你需要用到这些函数并且你又没有显示的声明这些函数的时候,编译器才会贴心的自动声明相应的函数。
引用成员
如果你打算在一个“内含引用成员”或者“内含const
成员”的类内支持赋值操作,就必须定义自己的默认拷贝赋值操作符。因为 C++
本身不允许引用改指不同的对象,也不允许更改 const
成员。
1 | class Person { |
1 | error: object of type 'Person' cannot be assigned because its copy assignment operator is implicitly deleted |
Effective C++ 4:确定对象被使用前已先被初始化
Item 4: Make sure that objects are initialized before they’re used.
手工初始化内置对象
为内置对象进行手工初始化,因为C++
不保证初始化他们。
1 | int x = 0; //对 int 进行手工初始化 |
构造函数最好使用成员初值列
1 | class PhoneNumber { ... } |
构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和他们在class
中的声明次序相同。
1 | ABEntry::ABEntry(const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones) : theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) {} |
local static
对象替换 non-local static
对象。
为免除”跨单元之初始化次序“问题,请以 local static
对象替换 non-local static
对象。
1 | class FileSystem { |
1 | class Directory { |
客户使用使用:
1 | Directory tempDir( params ); |
现在初始化次序的重要性体现出来了,除非 tfs
在 tempDir
之前先被初始化,否则tempDir
的构造函数会用到尚未初始化的tfs
。但tfs
和tempDir
是不同的人在不同的时间于不同的源文件建立起来的,它们是定义于不同编译单元内的 non-local static
对象。它们初始化相对次序并无明确定义。但我们可以将 local static
对象替换non-local static
对象来解决。这也是Singleton模式的常见实现手法。
这个手法的基础在于:C++保证,函数内的 local static
对象会在调用该函数时首次遇上该对象的定义式时被初始化。
1 | class FileSystem { ... } |
1 | class Directory { ... } |