1.问题的引入
假设现在有个函数priority()来处理程序的优先级,另一个函数用来在某动态分配的Widget对象上进行某些带有优先权的处理。
1class Widget{ 2 // ... 3}; 4 5int priority(); 6 7void processWidget(std::shared_ptr<Widget> pw, int priority);
现在,有下面的语句调用函数processWidget(),如下所示:
1processWidget(new Widget, priority());
上面的语句调用必然会导致编译器报错,这是由于std::shared_ptr构造函数需要一个原始指针,但该构造函数是一个explicit显式构造函数,无法进行隐式转换。下面调用方式将new Widget原始指针转换为processWidget()所要求的std::shared_ptr。,如下所示:
1processWidget(std::shared_ptr<Widget>(new Widget), priority());
重点来了:即使经过上面一系列的骚操作(即使用对象管理式资源),上面的调用也可能发生内存泄漏。
2.内存泄漏发生的原因及解决方法
先看看编译器的工作原理。编译器在生成对processWidget()函数的调用之前,必须先解析其中的参数。processWidget()函数接收两个参数,分别是智能指针的构造函数和整型的函数priority()。在调用智能指针构造函数之前,编译器必须先解析其中的new Widget语句。因此,解析该函数的参数分为三步:
(1).调用priority();
(2).执行new Widget;
(3).调用std::shared_ptr构造函数;
C+编译器以什么样的固定顺序去完成上面的这些事情呢?答案是未知。这和其他语言如Java和C#不同,这两种语言总是以固定的顺序完成函数参数的解析。但是,在C++中可以确定步骤(2)一定先于步骤(3)执行,因为new Widget还要被传递作为std::shared_ptr构造函数的一个实参。然而,对于priority()的调用可以在步骤(1)、步骤(2)、步骤(3)执行。假设编译器选择以步骤(2)执行它,最终的操作次序如下:
(1).执行new Widget; (2).调用priority(); (3).调用std::shared_ptr构造函数
但是,如果priority()函数抛出了异常呢?那么从new语句动态分配的资源在到达智能指针构造函数之前就已经泄露了。解决方法:使用一个单独的语句来创建智能指针对象。
1std::shared_ptr<Widget> pw(new Widget); // 放在单独的语句中 2processWidget(pw, priority()); // 不会泄露资源
编译器是逐语句编译的,通过使用一个单独的语句来构造智能指针对象,编译器就不会随意改动解析顺序,保证了生成的机器代码顺序是异常安全的,以及这样的代码写出来也更加美观。
3.总结
(1) 以独立语句将new对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的内存泄漏。