【原创】Performanced C++ 经验规则 第三条:你不知道的构造函数(下)

第三条:你不知道的构造函数(下)

前面两篇,我们已经讨论了C++构造函数中诸多细枝末节,但百密一疏,还有一些地方我们没有考虑到。这一篇将对这些问题进行完结。

7、构造函数中的异常

当你在构造函数中写代码的时候,你有没有想过,如果构造函数中出现异常(别告诉我,你不抛异常。“必要”时系统会替你抛的),那会出现怎样的情况?

对象还能构建完成吗?构造函数中已经执行的代码产生的负面效应(如动态分配内存)如何解决?对象退出其作用域时,其析构函数能被调用吗?

上述这些问题,正是构造函数中产生异常要面临的问题。让我们先看结论,再分析过程:尽可能不要在构造函数中产生(抛出)异常,否则,一定会产生问题

我们先看一段代码:

 1 #include <iostream>
 2 #include <exception>
 3 #include <stdexcept>
 4 using namespace  std;
 5 
 6 
 7 class ConWithException
 8 {
 9 public:
10     ConWithException() : _pBuf(NULL)
11     {
12         _pBuf = new int[100];
13         throw std::runtime_error("Exception in Constructor!");
14     }
15 
16     ~ConWithException()
17     {
18         cout << "Destructor!" << endl;
19         if( _pBuf != NULL )
20         {
21             cout <<  "Delete buffer..." << endl;;
22             delete[] _pBuf;
23             _pBuf = NULL;
24         }
25     }
26 
27 private:
28     int* _pBuf;
29 };
30 
31 int main(int argc, char** argv)
32 {
33     ConWithException* cwe = NULL;
34     try
35     {
36         cwe = new ConWithException;
37     }
38     catch( std::runtime_error& e )
39     {
40         cout<< e.what() << endl;
41     }
42 
43     delete cwe;
44 
45     return 0;
46 }
47                                                         

这段代码运行结果是什么呢?

输出

1 Exception in Constructor!

输出“Exception in Constructor!"说明,我们抛出的异常已经成功被捕获,但有没有发现什么问题呢?有一个很致命的问题,那就是,对象的析构函数没有被调用!也就是说,delete cwe这一句代码没有起任何作用,相当于对delete NULL指针。再往上推,我们知道cwe值还是初始化的NULL,说明对象没有成功的构建出来,因为在构造函数中抛出了异常,终止了构造函数的正确执行,没有返回对象。即使我们把cwe = new ConWithException换成在栈中分配(ConWithException cwe;),仍是相同的结果,但cwe退出其作用域时,其析构函数也不会被调用,因为cwe根本不是一个正确的对象!继续看,在这个构造函数中,为成员指针_pBuf动态申请了内存,并计划在析构函数中释放这一块内存。然而,由于构造函数抛出异常,没有返回对象,析构函数也没有被调用,_pBuf指向的内存就发生了泄露!每调用一次这个构造函数,就泄露一块内存,产生严重的问题。现在,你知道了,为什么不能在构造函数中抛出异常,即使没有_pBuf这样需要动态申请内存的指针成员存在。

然而很多时候,异常并不是由你主动抛出的,也就是说,将上述构造函数改造成这样:

   ConWithException() : _pBuf(NULL)
   {
        _pBuf = new int[100];
   }

这是我们十分熟悉的格式吧?没错,但是,这样的写法仍然可能产生异常,因为这取决于编译器的实现。当动态内存分配失败时,编译器可能返回一个NULL指针(这也是惯用方式),OK,那没有问题。但是,有些编译器也有可能引发bad_alloc异常,如果对异常进行捕获(通常也不会这样做),结果将同上述例子所示。而如果未对异常进行捕获,结果更加糟糕,这将产生Uncaught exception,通常将导致程序终止。并且,此类问题是运行阶段可能出现的问题,这将更难发现和处理。

说了半天,就是认为上述写法,还不够好,不OK,接下来讲述解决方案。

解决方案一:使用智能指针shared_ptr(c++0x后STL提供,c++0x以前可采用boost),注意,在此处不能使用auto_ptr(因为要申请100个int,而即使申请的是单个对象,也不建议使用auto_ptr,关于智能指针,本系列后面的规则会有讲述);

解决方案二:就是前面多次提到的,采用"工厂模式"替换公有构造函数,从而尽可能使构造函数“轻量级“

class ConWithException //为和前面比对,类名没改,糟糕的类名
{
public:
    ConWithException* factory(some parameter...)
    {
        ConWithException* cwe = new ConWithException;
        if(cwe)
        {
            cwe->_pBuf = new int[100];
            //other initialization...
        }
        return cwe;
    }
    
    ~ConWithException()
    {
        if(cwe->_pBuf)
        {
            delete[] cwe->_pBuf;
            _pBuf = NULL;
        }
        //other destory process...
    }
private:
    ConWithException() : _pBuf(NULL) {} //如果有非静态const成员还需要在初始化列表中进行初始化,否则什么也不做
    int* _pBuf;
};

使用“工厂模式”的好处是显而易见的,上述构造函数中异常的问题可以得到完美解决?why?因为构造函数十分轻量级,可轻松的完成对象的构建,“重量级”的工作都交由“工厂”(factory)方法完成,这是一个公有的普通成员函数,如果在这个函数中产生任何异常,因为对象已经正确构建,可以完美的进行异常处理,也能保证对象的析构函数被正确地调用,杜绝memory leak。构造函数被声明为私有,以保证从工厂“安全”地产生对象,使用“工厂模式”,还可以禁止从栈上分配对象(其实Java、Objective-C都是这么做的),在必要的时候,这会很有帮助。

8、构造函数不能被继承:虽然子类对象中包含了基类对象,但并不能代表构造函数被继承,即,除了在子类构造函数的初始化列表里,你可以显式地调用基类的构造函数,在子类的其它地方调用父类的构造函数都是非法的。

9、当类中有需要动态分配内存的成员指针时,需要使用“深拷贝“重写拷贝构造函数和赋值操作符,杜绝编译器“用心良苦”的产生自动生成版本,以防资源申请、释放不正确。

10、除非必要,否则最好在构造函数前添加explicit关键字,杜绝隐式使构造函数用作自动类型转换。

终于写完了,这三篇有关构造函数的“经验”之谈,其实,这些问题,也是老生常谈了。经过这三篇的学习,为敲开C++的壁垒,我们又添加了一把强有力的斧头。

posted @ 2012-12-24 14:38  Jone Zhang  阅读(2364)  评论(5编辑  收藏  举报