Effective C++ 读书笔记02
前言
本文是阅读《Effective C++ 改善程序与设计的55个具体做法(第三版)》的心得笔记第二部分,文章也会按照原书的顺序依次记录各个条款。
第一部分的阅读笔记参见effective C++ 读书笔记01。
资源管理
C++中最常使用的资源就是动态内存分配,除此之外,其他常见的资源还有文件描述符(file descriptors)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接和网络sockets等。无论是何种资源,当不再使用它时,必须将其归还给系统。
条款13:以对象管理资源
关于资源泄漏的风险,考虑如下这样一种场景:
void f { |
上面代码的问题在于可能由于...
中的某些操作导致代码无法执行到delete语句,比如:
...
中存在某种特定的判断逻辑会提前触发return语句;- delete语句存在于某个循环当中,而该循环又存在
continue
或break
语句导致提前退出; ...
中的语句抛出异常;
当然你在函数设计之初可以拍着胸脯保证不会出现这样的低级错误,你可以在return前加上delete语句,可以try-catch逻辑中增加delete操作等等来规避风险。但代码不只你一个人在写,你后面的维护者依然有可能会在...
中重新添加如上所述的跳过delete操作的逻辑,你不能寄希望于每个人都能完全理解他所增加的逻辑对资源管理的影响。所以不能单纯的依赖f函数总能执行到其delete语句。
还有就是上面这个例子在createInvestment
内分配资源,然后却把锅甩给外部,寄希望于外部去释放资源,本身就不是一个好的接口设计,这点会在条款18中进行讨论。
为了确保createInvestment
分配的资源总是能被释放,可以用对象管理资源:
-
获得资源后立即放进管理对象(managing object)中。即资源取得时机便是初始化时机(Resource Acquisition Is Initialization,RAII),我们在获得资源时立即初始化(或赋值给)某个管理对象,这个管理对象的类可以是自己特制的资源管理类,但更常见的是智能指针。其中原书中提到的
auto_ptr
在C++11标准中以弃用,取而代之的是unique_ptr
,而当时还在TR1中的shared_ptr
现也正式引入到C++11标准中。关于智能指针的详细讨论,请查看这里。 -
管理对象(managing object)运用析构函数确保资源被释放。将资源释放操作放在管理对象的析构函数中,一旦管理对象将要离开作用域,对象将被销毁,其析构函数自动被调用,从而自动的释放资源。关于资源释放动作可能会抛出异常的处理方式,请参照条款08。
还有一点需要注意,原书中提到tr1::shared_ptr
不支持数组资源的释放,即析构函数默认调用delete
,而不是delete[]
这一论断也成为了过去时。新的C++标准下(不确定是14/17/20)的shared_ptr
已经重载了[]
,即如下写法时合法的:
std::shared_ptr<Investment[]> spInv(new Investment[5]); |
但通常情况,更好的选择是使用std::vector<Investment>(5)
。
条款14:在资源管理类中小心copying行为
并不是所有的资源都是在堆上管理的(heap-based),这时候智能指针就不适用,需要自己实现一个资源管理类。考虑如下互斥锁(一种资源)的设计:
class Mutex {/*省略实现*/}; |
在上述用法中,Lock
能工作的很好。但当Lock
对象被拷贝时(虽然有点奇怪,但你需要对此负责),事情就有点不太好了:
Lock ml1(&m); // 锁定m |
首先,在未自定义的情况下,default版本的copy构造函数和copy赋值操作符仅单纯的复制成员变量,ml1
和ml2
持有相同的pm_
:
- 若
ml1
和ml2
的生命周期不同,比如ml1
提前析构,这时锁已经解开,ml2
已经失效; - 即便
ml1
和ml2
的生命周期相同,当它们被析构时,会unlock
两次,这种未定义行为的危害程度取决于unlock
的实现方式;
以此为例,那么该如何处理RAII对象的拷贝行为呢?有如下几个选择:
- 禁止复制:参考条款06,私以为是最适合上述例子的处理,确实没有什么理由去拷贝
Lock
对象。
class Lock : private Uncopyable { |
- 共享底层资源的所有权:成员用
shared_ptr
管理,并自定义deleter
(析构时unlock
而不是释放Mutex
对象)。对于上述例子,虽不那么适合,但也不至于招致危害。
class Lock{ |
- 深度复制底层资源:自定义copy构造函数和copy赋值操作符,在拷贝RAII对象时,连带其所管理的资源(不仅是指针,还有指针所指的内容)一并做一份深拷贝。并不适合用于处理上述例子。
- 转移底层资源的所有权:当你希望资源只有一份时,将底层资源用独占所有权的
unique_ptr
进行管理,自定义copy构造函数和copy赋值操作符以完成所有权转移(std::move
)的动作。
class Lock { |
既然自己定义了包含std::move
的copy构造函数,你应当清楚地知道,所有权转移后原对象ml1
便不可用了,之后你应当对错误的使用ml1
负责。
条款15:在资源管理类中提供对原始资源的访问
很多时候,APIs直接指涉资源管理类中的原始资源(raw resources),这就需要资源管理类要提供访问原始资源的方法。
对智能指针而言,提供了get
方法来获取其中的裸指针;此外,智能指针还重载了operator->
和operator*
操作符,使其能够隐式地转换成裸指针,从而能像裸指针一样去访问类的成员函数。
对于自定义的资源管理类,同样也有显式和隐式两种方式转换成原始资源。一般来讲,显示转换更安全,隐式转换更方便,具体怎么选,我个人是倾向于显示转换,坚持条款18的忠告,尤其是在多人合作开发的时候,因为你无法保证所有人都与你的想法一致。一个例子如下:
FontHandle getFont(); // C API 获取字体资源 |
最后要澄清一点,RAII 类提供对原始资源的访问也许看起来违反了封装性,但这并不是什么设计缺陷。因为RAII类的设计初衷是保证资源得以合理释放,而不是保证封装。封装并不是什么都不暴露,而是暴露有必要暴露的部分。
条款16:成对使用new和delete时要采取相同形式
一句话总结:new
与delete
搭配使用,new[]
与delete[]
搭配使用。不可遗漏,也不可错配。
尽量不要对数组形式使用typedef
,容易造成new
和delete
的错配。
typedef std::string AddressLines[4]; |
条款17:以独立语句将 newed 对象置入智能指针
考虑如下代码:
int getPriority(); // 获取优先级,可能抛出异常 |
解释一下为什么用法2中会有资源泄漏的风险:
line7代码总共做了3件事:
-
- 调用
getPriority
;
- 调用
-
- 执行
new Widget
;
- 执行
-
- 调用
std::shared_ptr
的构造函数;
- 调用
C++编译器总能保证先步骤2后步骤3的执行次序(因为2是3的入参),但却无法保证步骤1的执行次序不在2和3之间(可能出于优化性能的考虑)。若在这种情况下,步骤1的执行过程中抛出异常,步骤2返回的指针将会被遗弃,无法置入智能指针中,从而导致了资源泄漏。以独立语句将 newed 对象置入智能指针(用法3)可以规避该风险,因为编译器对于跨越语句的各项操作,没有重新排列的自由。