More Effective C++ 条款10 在构造函数内阻止内存泄露

1. “C++ 只会析构已完成的对象”,“面对未完成的对象,C++ 拒绝调用其析构函数”,因为对于一个尚未构造完成的对象,构造函数不知道对象已经被构造到何种程度,也就无法析构。当然,并非不能采取某种机制使对象的数据成员附带某种指示,“指示constructor进行到何种程度,那么destructor就可以检查这些数据并(或许能够)理解应该如何应对。但这种机制无疑会降低constructor的效率,,处于效率与程序行为的取舍,C++ 并没有使用这种机制。所以说,”C++ 不自动清理那些’构造期间跑出exception‘的对象“。

2. 当在构造函数中抛出异常的时候,由于对象尚未构造完全,因此并不会调用其析构函数,问题是此时如果对象已经被部分构造,那么我们应当保证被部分构造的内容适当地析构。

考虑这样一个类:

 1 class A{
 2 public:
 3     A(const int& number,const string&a,const string& b):num(number),ptr_a(0),ptr_b(0){
 4         if (a != "")
 5             ptr_a = new string(a);
 6         if (b != "")
 7         ptr_b = new string(b);
 8     }
 9     ~A(){ delete ptr_a; delete ptr_b; }
10 private:
11     int num;
12     string* ptr_a;
13     string* ptr_b;
14 };
View Code

如果A的构造函数中在为ptr_b所指向的对象分配内存的时候抛出一个异常,正如1所说,编译器不会调用A的析构函数,那么释放ptr_a所指向的内存的任务就落到了程序员身上,可以将构造函数改写为以下形式来适当的释放ptr_a所指向的内存:

 1 class A{
 2 public:
 3     A(const int&  number,const string&a,const string& b):num(number),ptr_a(0),ptr_b(0){
 4         try{
 5             if (a != "")
 6                 ptr_a = new string(a);
 7             if (b != "")
 8                 ptr_b = new string(b);
 9         }
10         catch(...){
11             delete ptr_a; //可以直接delete,因为delete一个空指针不会有影响
12             delete ptr_b; //同上
13             throw;
14         }
15     }
16     ~A(){ delete ptr_a; delete ptr_b; }
17 private:
18     int num;
19     string* ptr_a;
20     string* ptr_b;
21 };
View Code

不需要担心A类的non-pointer data members(指的是num),data member会在A的构造函数内代码执行之前就被初始化好(因为使用了初始化列表),一旦A类型对象被销毁,这些data member会想已经被构造好的对象一样被自动销毁。

你可以发现catch语句块内的动作与A类的析构函数动作相同, 因此可以把他们放入一个辅助函数内,如下:

 1 class A{
 2 public:
 3     A(const int&  number,const string&a,const string& b):num(number),ptr_a(0),ptr_b(0){
 4         try{
 5             if (a != "")
 6                 ptr_a = new string(a);
 7             if (b != "")
 8                 ptr_b = new string(b);
 9         }
10         catch(...){
11             cleanup();
12             throw;
13         }
14     }
15     ~A(){ cleanup(); }
16     void cleanup(){
17         delete ptr_a;
18         delete ptr_b;
19     }
20 private:
21     int num;
22     string* ptr_a;
23     string* ptr_b;
24 };
View Code

以上代码可以比较好的解决构造函数内抛出异常时对象的析构问题,但是如果A类的ptr_a和ptr_b被声明为const,则需要另外一番考虑(此时必须在初始化列表内初始化ptr_a和ptr_b),A的构造函数像以下这样定义:

 1 class A{
 2 public:
 3     A(const int&  number,const string&a,const string& b):num(number),ptr_a(0),ptr_b(0){
 4         try{
 5             if (a != "")
 6                 ptr_a = new string(a);
 7             if (b != "")
 8                 ptr_b = new string(b);
 9         }
10         catch(...){
11             cleanup();
12             throw;
13         }
14     }
15     ~A(){ cleanup(); }
16     void cleanup(){
17         delete ptr_a;
18         delete ptr_b;
19     }
20 private:
21     int num;
22     const string* ptr_a;
23     const string* ptr_b;
24 };
View Code

但是很明显又会产生成员的析构问题,遗憾的是我们无法在初始化列表中使用try/catch语句来处理异常,因为try/catch是语句,而初始化列表只接受表达式(或函数,其实可以把表达式看成的调用),这也是为什么代码里使用?:代替if语句。一个解决办法正如括号内所说,使用函数代替?:,在函数内完成异常处理的工作,代码如下:

 1 class A{
 2 public:
 3     A(const int&  number, const string&a, const string& b):num(number), ptr_a(initStringa(a)), ptr_b(initStringb(b)){
 4     }
 5     ~A(){ cleanup(); }
 6 private:
 7     void cleanup(){ delete ptr_a; delete ptr_b; }
 8     string* initStringa(const string& a){
 9         if (a != "") return new string(a);
10         else return 0;
11     } //ptr_a首先被初始化,所以即使失败也不需要担心内存泄露问题,所以不需要处理exceptioon
12     string* initStringb(const string& b){
13         try{
14             if (b != "") return new string(b);
15             else return 0;
16         }
17         catch (...){
18             delete ptr_a;
19             throw;
20         }
21     } //ptr_b第二个被初始化,如果它在被初始化的过程中有异常抛出,它必须保证将ptr_a所指资源释放掉
22     int num;
23     const string* ptr_a;
24     const string* ptr_b;
25 };
View Code

到这里解决方法已经比较完美了,但还是优缺点,那就是构造函数的任务被分散在各个函数当中,造成我们维护上的困扰。

一个更好的办法是,接受条款9的忠告,使用标准库类模板auto_ptr,将ptr_a封装在auto_ptr之中,如下:

 1 class A{
 2 public:
 3     A(const int&  number, const string&a, const string& b) :num(number), ptr_a(a != "" ? new string(a) : 0), ptr_b(b != "" ? new string(b) : 0){
 4     }
 5 private:
 6     int num;
 7     const auto_ptr<string> ptr_a;
 8     const auto_ptr<string> ptr_b;
 9 };
10 //在此设计中,如果ptr_b在构造过程中有任何异常,由于ptr_a已经是构造好的对象,在运行其析构函数时,所指内存会被自动释放,此外,由于ptr_a和ptr_b如今都是对象,当当其”宿主“(A类对象)被销毁时,它们说所指内存也会被自动释放
View Code

最终版代码由于使用了标准库auto_ptr类模板,代码大大简化了!

3. 由2过程可知,auto_ptr类模板的使用可以在不增加代码量的情况下完美处理构造函数抛出异常的问题,auto_ptr类模板的设计目的正在于当指向动态分配的内存的指针本身停止活动(被销毁)时,所指内存被释放掉。

原文地址:https://www.cnblogs.com/reasno/p/4603060.html