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 };

如果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 };

不需要担心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 };

以上代码可以比较好的解决构造函数内抛出异常时对象的析构问题,但是如果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 };

但是很明显又会产生成员的析构问题,遗憾的是我们无法在初始化列表中使用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 };

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

一个更好的办法是,接受条款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类对象)被销毁时,它们说所指内存也会被自动释放

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

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

时间: 2024-10-16 16:29:08

More Effective C++ 条款10 在构造函数内阻止内存泄露的相关文章

effective c++ 条款10 handle assignment to self operator =

非强制性,但是个好习惯 当使用连锁赋值时很有用 x=y=z=10; class Window { public: Window& operator=(int size) { ... return *this; } } 这个规则适用于 -,+, +=,-= etc effective c++ 条款10 handle assignment to self operator =

Effective C++——条款10条,条款11和条款12(第2章)

条款10:    令operator=返回一个reference to *this Have assignment operators return a reference to *this 关于赋值,可以把它们写成连锁形式: int x, y, z; x = y = z = 15; // 赋值连锁形式 赋值采用右结合律,所以上述连锁赋值被解析为: x = (y = (z = 15)); 这里15先被赋值给z,然后其结果(更新后的z)再被赋值给y,然后其结果(更新后的y)再被赋值给x. 为了实现

Effective C++ 条款10

令operator= 返回一个reference to *this.  Have assignment operators return a reference to *this. 赋值时,我们可以写成如下形式: int x,y,z; x=y=z=10; 这种连锁方式采用右结合方式,也就是 x=(y=(z=10)); 为了实现这种连锁赋值,赋值操作符需要返回一个 *this. 这是一个协议,并不是强制性的.

Effective C++ (笔记) : 条款05 -- 条款10

条款05:了解C++默默编写并调用哪些函数 编译器可以暗自为class创建default构造函数.copy构造函数.copy assignment操作符,以及析构函数. 只有这些函数需要(被调用)时,它们才会被编译器创建出来.在编译器产生的复制构造函数和赋值运算符执行的都是浅拷贝.当数据成员是引用或者常量的时候,编译器不知道该怎么处理,两手一摊,无能为力. 当某个基类将copy assignment操作符声明为私有的,编译器将拒绝为派生类生成copy assignment操作符,因为它无权(也不

More Effective C++----(10)在构造函数中防止资源泄漏

Item M10:在构造函数中防止资源泄漏 如果你正在开发一个具有多媒体功能的通讯录程序.这个通讯录除了能存储通常的文字信息如姓名.地址.电话号码外,还能存储照片和声音(可以给出他们名字的正确发音). 为了实现这个通信录,你可以这样设计: class Image { // 用于图像数据 public: Image(const string& imageDataFileName); ... }; class AudioClip { // 用于声音数据 public: AudioClip(const

effective C++ 读书笔记 条款10

条款10:  令operator= 返回一个reference to *this; 关于赋值,我们可以这样写: int  x,y,x; x =y = z; 这就是所谓的连续赋值 为了实现"连锁赋值"赋值操作符必须返回一个reference指向操作符的左侧实参.这是我们为class实现赋值操作符时应该遵循的协议: #include <iostream> using namespace std; class Widget { public: Widget() { cout<

More Effective C++ 条款34 如何在一个程序中结合C++和C

1. C++和C混合使用的前提之一就是编译器产生兼容的目标文件(.lib和.dll等).所谓"兼容",指的是编译器在"预编译器相依的特性上"一致,如int和double大小,参数压栈机制等,只有在这个基础上才能讨论结合使用C++和C模块的问题. 2. 在1的基础上,要结合使用C++和C的模块,主要有以下几点需要注意: 1). name mangling(名称重整) Name mangling是C++用于支持函数重载的机制,它对重载的函数名称进行一定改变,使得每个函数

Effective C++——条款13(第3章)

第3章    资源管理 Resource Management 所谓资源就是,一旦用了它,将来必须还给系统.C++程序中最常使用的资源就是动态内存分配(如果分配内存从来都增归还,会导致内存泄露).其他常见的资源还有文件描述符(file descriptors),互斥锁(mutex locks),图形界面中的字型和笔刷,数据库连接,以及网络sockets.不论哪一种资源,重要的是,不再使用它时,必须将它还给系统. 条款13:    以对象管理资源 Use objects to manage res

Effective C++ 条款11,12 在operator= 中处理&ldquo;自我赋值&rdquo; || 复制对象时不要忘记每一个成分

1.潜在的自我赋值     a[i] = a[j];     *px = *py; 当两个对象来自同一个继承体系时,他们甚至不需要声明为相同类型就可能造成别名. 现在担心的问题是:假如指向同一个对象,当其中一个对象被删,另一个也被删,这会造成不想要的结果. 该怎么办? 比如:   widget& widget:: operator+ (const widget& rhs) {    delete pd;    pd = new bitmap(*rhs.pb);    return *thi