Effective C++第17条:要在单独的语句中使用智能指针来存储由new创建的对象
假设这里有一个函数用来显示处理优先级,另一个函数根据当前优先级为一个动态分配的 Widget 做一些处理:
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
一定要时刻记住“使用对象管理资源”这一真理(参见第 13 条)。 processWidget 中可以使用智能指针来动态分配其需要处理的 Widget 。
下面是对 progressWidget 的一次调用:
processWidget(new Widget, priority());
请稍等,不要试图这样调用。这将不会通过编译。 tr1::shared_ptr 的构造函数中包含了一个 explicit 的裸指针,于是便不存在从“ new Widget ”语句返回的裸指针到 processWidget 所需的 tr1::shared_ptr 的隐式转换。然而下边的代码将顺利通过编译:
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
看上去有些令人吃惊,尽管我们时时刻刻使用对象来管理资源,但是这里还是有出现资源泄漏的可能。了解这些发生的原由对我们深入理解是有一定帮助的。
在编译器能够生成对 processWidget 的调用之前,它必须评估传入的参数。第二个参数仅仅调用了一个函数 priority ,但是第一个参数(“ std::tr1::shared_ptr<Widget>(new Widget) ”)包含两部分:
- 运行 “new Widget” 语句
- 调用 tr1::shared_ptr 的构造函数
因此,在 processWidget 可以被调用之前,编译器必须自动生成代码来解决下面的三件事情:
- 调用 priority 。
- 执行 “new Widget” 。
- 调用 tr1::shared_ptr 的构造函数。
C++ 编译器对于这三项任务完成的顺序要求得很宽松。(这一点与 Java 和 C# 就很不一样了,这两门语言中的函数参数总是以一个特定的顺序得到评估。)由于“ new Widget ”语句为 tr1::shared_ptr 的构造函数传递了一个参数,因此它必须在 tr1::shared_ptr 的构造函数被调用之前得到执行。但是调用 priority 的工作可以放到第一,第二,也可以放在最后。如果编译器决定第二个处理它(这样可以使代码更高效),我们就会得到这样的执行序列:
1. 执行 “ new Widget ” .
2. 调用 priority 。
3. 调用 tr1::shared_ptr 的构造函数。
但是请想象一下如果调用 priority 时抛出了一个异常的话,将会发生些什么。在这种情况下,由于“ new Widget ”返回的指针不会如我们所愿保存在 tr1::shared_ptr 中,因此它很有可能会丢失,于是内存泄漏就发生了。在资源被创建以后和这个资源转交给一个资源管理对象之前的这段时间内,有可能发生异常,如果发生的话,那么调用 processWidget 就会造成资源泄漏。
防止这类问题发生的办法很简单:使用单独的语句,创建 Widget 并将其存入一个智能指针,然后将这个智能指针传递给 processWidget :
std::tr1::shared_ptr<Widget> pw(new Widget);
// 在一个单独的语句中创建 Widget 并存入一个智能指针
processWidget(pw, priority()); // 这样调用就不会泄漏了。
这样是可行的,因为编译器为多行的语句安排执行顺序要比单一的语句时严格得多。由于这段改进的代码中,“ new Widget ”语句以及对 tr1::shared_ptr 的构造函数的调用在单独的语句中,对 priority 的调用在另一个单独的语句中,所以编译器就没有机会调换处理顺序了。
牢记在心
- 在单独的语句中使用智能指针来保存由new创建的对象。如果不这样做,你的程序会在抛出异常时发生资源泄漏。