您身边的软件定制专家--9年开发经验为您护航

18678812288
0531-88887250

关于构造函数和异常的分析

文章作者:济南软件开发 时间:2016年12月20日

如果构造函数内发生异常,已经分配的资源是不会自动释放的,比如

 

 

class B{

public:

    B(){

        printf("into B constructor\n");

    }    

    ~B(){

        printf("into B destructor\n");

    }

};

 

class C{

public:

    C(){

        printf("into C constructor\n");

        throw std::runtime_error(" exception from C constructor");

    }    

    ~C(){

        printf("into C destructor\n");

        }

};

class A{

public:

    A(){

        printf("into A constructor \n");

    }

    ~A(){

        printf("into A destructor \n");

    }

};

 

class D:A{

public:

    D():A(), b(NULL),c(NULL) {

        printf("into D constructor\n");

        b = new B();

        c = new C();

    }

    ~D(){

        printf("into D destructor\n");

        delete b;

        delete c;

    }

private:

    B *b;

    C *c;

};

 

int main(int argc, char **argv){

    D d;

    return 0;

}

 

运行结果如下:

 

into A constructor

 

into D constructor

 

into B constructor

 

into C constructor

 

terminate called after throwing an instance of 'std::runtime_error'

 

what(): exception from C constructor

 

对象c在构造过程中抛出异常,指针b指向的内存空间不会被释放。

 

如何释放b的内存呢?首先我们可以看出c++是不会调用析构函数的,因为析构函数不知道对象已经构造了多少,哪些资源该释放,哪些不该释放,当然编译器可以记录这些内容,但是会严重影响效率。另外在语义上,c++认为,对象的生命周期是构造函数正常结束至析构函数结束之间,构造不完全的对象是一个没有生命的东西,是不存在的,你不能对一个不存在的对象调用析构函数。编译器默认会做的只是释放对象d的内存空间。对象b指向的堆内存可以通过使用异常显示释放

 

 

D():A(), b(NULL), c(NULL){

        printf("into D constructor\n");

        try{

            b = new B();

            c = new C();

        }catch(std::runtime_error &e){

            printf("into D constructor catch \n");

            delete b; b=NULL;

            delete c; c=NULL;

       }

 

运行结果如下:

 

into A constructor

 

into D constructor

 

into B constructor

 

into C constructor

 

into D constructor catch

 

into B destructor

 

into D destructor

 

into A destructor

 

b被正常释放了,我们再做一下改变,将b和c的构造放到初始化列表中做,将D的构造函数改成下面这样,

 

D::D() : A(),b(new B()),c(new C())

   {

   }

我们继续使用异常,会不会有效?

 

D() try:A(), b(new B()), c(new C()) {

        printf("into D constructor\n");

}catch(std::runtime_error &e){

        printf("in D constructor catch: %s \n", e.what());

        cleanup();

}

看上去very nice啊,跑一下试试,

 

into A constructor

 

into B constructor

 

into C constructor

 

into A destructor

 

in D constructor catch: exception from C constructor

 

into B destructor

 

into C destructor

 

*** glibc detected *** ./a.out: free(): invalid pointer: ***

 

指针错误!同时我们可以发现在进入catch语句前基类A执行了析构函数,这说明到达catch语句时,已经跳出了构造函数的范围,D和A的成员数据都已经是不可访问的了。

 

C++为什么要这样做呢,为什么构造函数体外的catch语句中无法访问数据成员?

 

首先,无法知道b和c是否已经初始化了,删除未初始化指针是非法的。

 

其次,我们假设可以知道b初始化了,c没有初始化,我们要删除b,如果catch中可以访问b和c的话(我们做个假设),改变B的类型,使B包含一个A*成员数据,考虑下这种情况,

 

D() try:A(), b(new B(static_cast<A*>(this))), c(new C()) {

         printf("into D constructor\n");

}catch(std::runtime_error &e){

         printf("in D constructor catch: %s \n", e.what());

         cleanup();

}

A已经析构了,b的数据成员A已经不存在,这是很危险的。

 

c++认为,不管是基类还是数据成员构造出现失败,那么整个对象构造都是失败的,不存在半成品。构造函数块外的catch语句(上面这种)即使没有显示地重新抛出异常,c++也会自动抛出,一直到最下层派生类对象构造处停住。比如以上例子,catch没有显示throw,在main函数里:

 

try{    

        D d;

}catch (std::runtime_error &e){

        printf("int main: %s\n", e.what());

}

仍然会捕捉到C构造函数抛出的runtime_error异常。

 

综上,可以总结以下规则

 

1. 构造函数体外的try-catch语句只有一个用途——转换捕捉到的异常对象

 

2. 必须在构造函数体内分配资源,不要在初始化列表中

 

3. 一定要用try-catch语句管理资源的话,在构造函数体内。

 

4. 使用RAII方式管理资源,这会少死亡很多脑细胞,像这样子

 

 

class D{

    auto_ptr<B> b;

    auto_ptr<C> c;

}

D::D() : A( ),b(new B()), c(new C())

{

}


想要了解更多详情欢迎来电咨询18678812288
登陆网址:www.jnydkj.cn。
联系人:王经理。