构造函数失败

什么函数都有可能失败,构造函数也不另外,比如new一个对象或空间不成功。当构造函数失败的时候,其实很多时候我们不想这个对象被继续生成,这个时候就可以在构造函数里面抛出异常。C++规定构造函数抛出异常之后,对象将不被创建,析构函数也不会被执行,但已经创建成功的部分(比如一个类成员变量)会被部分逆序析构,不会产生内存泄漏。但有些资源需要在抛出异常前自己清理掉,比如打开成功的一个文件,最好关闭掉再抛出异常(虽然系统也会把这个资源回收),因为抛出异常之后析构函数不会被执行了。

      网上比较经典的总结

       (1) C++中通知对象构造失败的唯一方法那就是在构造函数中抛出异常;(这句话并不是说我们只有这个方法才能让上层知道构造函数失败,虽然构造函数没有返回值,我们完全可以在构造函数中传入一个引用值,然后在里面设置状态,运行完构造函数之后任然可以知道是否失败,但这种情况下面对象其实还是被构造出来的,只是里面有资源分配失败而已,并且析构函数还是会执行。这和我们构造失败不生成对象的初衷不符。)
  (2) 构造函数中抛出异常将导致对象的析构函数不被执行;(但已经生产的部分成员变量还是会被逆向析构的)
  (3) 当对象发生部分构造时,已经构造完毕的子对象将会逆序地被析构;


网上的一个例子

一个实例对象的构造
第一步,分配足够的内存,如果失败就是栈溢出或抛出std::bad_alloc的异常,所以在这步你不用担心内存泄露,而且这一步你是不能插手的,如果这步成功,就进入第二步。

new运算符的实现保证了内存泄漏不会发生。例如

T *p = new T;

将被编译器转换给类似下面的样子:(其实和我们自己释放已经申请的资源的思想流程是一样的)

// 第一步,分配原始内存,若失败则抛出bad_alloc异常

try {

    // 第二步,调用构造函数构造对象

    new (p)T;       // placement new: 只调用T的构造函数

}

catch(...) {

    delete []p;     // 释放第一步分配的内存

    throw;          // 重抛异常,通知应用程序

}


第二步,调用构造函数,在通常情况下,如果构造函数为空或没有进行动态内存分配,你就不用关心内存泄露了
你需要关心的是构造函数中有动态内存分配
class A
{
  char* str[10];
public:
  A(){
    for(int i=0;i<10;i++)
       str[i]=NULL;     //对str[]初始化,这是必须的,不然再后面delete就会出现问题
    try{
      for(int i=0;i<10;i++)
      str[i]=new char[1024*1024*1024];   //要来就来狠的
    }
    catch(bad_alloc){
       for(int i=0;i<10;i++)
           delete []str[i];  //放心,即使delete NULL是不会出问题的
       throw;  //就抛出这个bad_alloc, 这才是构造函数抛出去的异常,外层会扑捉到,并且析构函数不会被调用
    }
  }
  ~A()
  {
    for(int i=0;i<10;i++)
     delete []str[i];         
  } 
};
int main()
{
    A *pA=NULL;
    try{
      pA=new A;
    }
    catch(bad_alloc){
       cout<<"Out of memory"<<endl;
    }
    delete pA;
    return 0;
}
pA是用NULL初始化的,即使在给A分配内存时(第一步)失败,这样就不会导致后面的delete pA出错。


对于构造函数可能失败的做法一般有两种

1. 在构造函数中抛出异常,本对象构造未完成,它的析构函数不会被调用。当然,我们有义务释放已经分配到的资源。简单,最常见。
2. 把资源的初始化工作放在另一个单独函数中,比如 bool init(...),由对象创建者(比如工厂方法)先调用构造函数,再调用init方法。ATL中常见。




c++构造函数中发生错误,如何处理? [复制链接]

   

Rank: 9Rank: 9Rank: 9

注册时间
2010-6-1
积分
228
跳转到指定楼层
1#
  发表于 2010-6-30 17:34:28  | 只看该作者  | 倒序浏览
本帖最后由 songge09 于 2010-6-30 17:37 编辑

如果构造函数中发生错误,如何处理较为妥当?不要说构造函数中不可能发生错误,那要么是你没有处理,要么是总是空的构造函数。

我目前得方法是这样的


  1. SomeClass obj;
  2. if (!obj.init())
  3. {
  4.     log("error.");
  5. }
复制代码



但总觉得这样是不是太c style了,并不是说c style不好,而是c++本身的style是什么?

c++作者搞了个raii的方案,我不喜欢,不够kiss。
望牛哥指点。
 
   

Rank: 13Rank: 13Rank: 13Rank: 13

注册时间
2010-5-11
积分
1988
2#
  发表于 2010-6-30 17:44:24  | 只看该作者
我会尽量减少在构造函数里的工作量,尽量往外移,构造函数尽可能仅赋值,以及进行无IO、无异步的内部构建。。。。
1

查看全部评分

 
 
   

Rank: 9Rank: 9Rank: 9

注册时间
2010-6-1
积分
228
3#
  发表于 2010-6-30 18:39:27  | 只看该作者
是的,析构函数同样有这个问题,所以我现在不得不把大部分类都加上

init
destroy

这两个函数,并显示调用判别并处理错误。

但是这样,构造函数和析构函数就完全变成无用的东西了
 
 
   

Rank: 24Rank: 24Rank: 24Rank: 24Rank: 24Rank: 24

注册时间
2010-3-12
积分
17395
4#
  发表于 2010-6-30 22:43:51  | 只看该作者
构造函数中的错误一般通过抛出异常来处理
1

查看全部评分

I Love 3D
 
   

Rank: 13Rank: 13Rank: 13Rank: 13

注册时间
2009-10-18
积分
1649
5#
  发表于 2010-7-1 00:00:03  | 只看该作者
嗯, 一般只用它初始化变量
 
 
   

Rank: 13Rank: 13Rank: 13Rank: 13

注册时间
2010-5-11
积分
1988
6#
  发表于 2010-7-1 10:31:58  | 只看该作者
构造函数中的错误一般通过抛出异常来处理
goncely 发表于 2010-6-30 22:43 



这是有问题的,构造函数,不应该抛出异常,考虑

  1. AClass * a = nullptr;

  2. try
  3. {
  4. a = new AClass();
  5. }
  6. catch(...)
  7. {
  8. // what is a ? a is nullptr! real a is missing!
  9. }
复制代码



这个情况就相当尴尬了,构造函数一旦抛出异常,a 根本就没有赋值,也就是说,new 的结果泄露了。

构造函数和析构函数是一定不能抛出异常的,否则将引来更多问题。
 
 
   

Rank: 9Rank: 9Rank: 9

注册时间
2010-6-1
积分
228
7#
  发表于 2010-7-1 12:10:07  | 只看该作者
本帖最后由 songge09 于 2010-7-1 12:14 编辑
这是有问题的,构造函数,不应该抛出异常,考虑




这个情况就相当尴尬了,构造函数一旦抛出异常,a 根 ...
mayax 发表于 2010-7-1 10:31 


new 失败会产生异常,如果不catch异常,那么怎么得知出现了错误呢?
new失败了,就证明内存没有分配,何来泄露之说呢?或者new失败是由于AClass的构造函数调用失败造成的,但是AClass的构造函数应该会catch这个异常并处理,而不是继续抛出吧。
我是比较反对仅仅在构造函数里写 pointer = 0; 这样的语句,完全没什么作用么。
我认为 Creation = Initializition,这也应该是c++本身所倡导的。
 
 
   

Rank: 24Rank: 24Rank: 24Rank: 24Rank: 24Rank: 24

注册时间
2009-10-18
积分
9306

会员荣誉勋章

8#
  发表于 2010-7-1 12:54:02  | 只看该作者
本帖最后由 lidudu 于 2010-7-2 10:31 编辑

构造函数执行时,对象的内存已经分配了。
按C++ Spec,构造函数抛异常时,会立刻执行已成功构造的成员对象的析构函数并释放内存。所以不会有内存泄露。

但是,这并不意味着没问题了,因为在析构函数里,你并不知道哪些成员已经初始化了而需要delete,这意味着需要在构造函数初始化列表里将指针都初始化成NULL,然后再构造函数体内创建对象赋值,这样析构才能判断。

即便不会出错误了,要满足C++ spec意味着编译器要对每个可能抛异常的构造的new操作自动生成一个try-catch来调用析构和释放内存。在意性能的程序员会很心痛~心痛~心痛~

所以,便有了构造析构不干活,而另做init()/destroy()的用法。

但C++创始人strovstrup认为应该用Resource Acqusition is Initialization的模式,也就是要用构造和析构。但出错时未必要抛异常,而可以将对象置于无效状态,如file("abc.txt")构造后,如果文件不存在,那么if (file) 为假。这个方法的麻烦就是构造之后要多做一步检查,但总比init()/destroy()简单。
1

查看全部评分

 
 
   

Rank: 8Rank: 8

注册时间
2009-9-8
积分
187
9#
  发表于 2010-7-1 14:30:00  | 只看该作者
标准方法: 抛异常
 
 
   

Rank: 9Rank: 9Rank: 9

注册时间
2010-6-1
积分
228
10#
  发表于 2010-7-1 16:15:18  | 只看该作者
回复 8# lidudu 


   回答的好!
 
 
   

Rank: 24Rank: 24Rank: 24Rank: 24Rank: 24Rank: 24

注册时间
2009-10-18
积分
9306

会员荣誉勋章

11#
  发表于 2010-7-2 10:28:21  | 只看该作者
本帖最后由 lidudu 于 2010-7-2 10:39 编辑

纠正一下,前面说的有错。当new obj()抛异常时,obj.~obj()不会执行,但其已经成功初始化的成员对象的析构会被执行。也就是说,成功分配内存的单纯的指针成员变量会导致内存泄露,所以必须用smart_ptr这种带析构函数的包装

另外,即使构造函数没有抛异常,但任何new运算符都可能因为out of memory抛异常(按C++ spec),那么理论上编译器总要在new外边套一层try-catch,才能保证正确释放资源。这个比较郁闷。

不论如何,C++的构造函数里最好还是避免异常,析构更是。STL里绝大多数类的构造都不抛异常。当然,这个不是太有说服力,因为STL最早出现时,C++还没有异常处理这个特性呢。

对于异常方面的问题,可以参考http://yosefk.com/c++fqa/exceptions.html#fqa-17.2
 
 
   

Rank: 9Rank: 9Rank: 9

注册时间
2010-6-1
积分
228
12#
  发表于 2010-7-2 12:18:23  | 只看该作者
对于异常方面的问题,可以参考http://yosefk.com/c++fqa/exceptions.html#fqa-17.2

lidudu,谢谢,这个上面的资料正是我想搞清楚的东西,你真有学问
 
 
   
头像被屏蔽

禁止访问

注册时间
2010-7-10
积分
87
13#
  发表于 2010-7-21 01:39:40  | 只看该作者
提示:  作者被禁止或删除 内容自动屏蔽
签名被屏蔽
 
   

Rank: 13Rank: 13Rank: 13Rank: 13

注册时间
2010-7-23
积分
1589
14#
  发表于 2010-7-23 03:33:12  | 只看该作者
没有深入研究过,学习.
貌似这里的氛围不错,是个好地方.
http://www.cnblogs.com/crazii
 
   

Rank: 4

注册时间
2010-8-13
积分
38
15#
  发表于 2010-8-16 16:27:20  | 只看该作者
构造函数只做成员变量的默认值的赋与,大量的操作还是放在Init()函数的好。
 
 
   

Rank: 12Rank: 12Rank: 12

注册时间
2009-10-9
积分
610
16#
  发表于 2010-8-17 18:48:33  | 只看该作者
本帖最后由 lisa 于 2010-8-17 18:52 编辑
另外,即使构造函数没有抛异常,但任何new运算符都可能因为out of memory抛异常(按C++ spec),那么理论上编译器总要在new外边套一层try-catch,才能保证正确释放资源。

并不是要靠try-catch才能保证正确释放资源的,调new的话最终会掉MSVCR90(D).dll这类CRT的函数(如果调试debug版的CRT会发现调new的话会调malloc,然后是一系列的debug版malloc实现,非常多,最终有一个函数调了HeapAlloc,release比较简单,最终也是调HeapAlloc),最终由HeapAlloc、VirtualAlloc这类函数负责分配内存,当程序结束时进程负责摧毁所有的堆(如果没掉HeapCreate的话就一个默认堆),堆摧毁时内存就释放了,所以编译器并不需要去给new加try-catch来保证资源被释放的,CRT会加try-catch,但它不是为了正确释放内存用的。
 
 
   

Rank: 17Rank: 17Rank: 17Rank: 17Rank: 17

注册时间
2010-4-29
积分
4945
17#
  发表于 2010-8-17 20:10:54  | 只看该作者
一开始是无序状态,构造函数使对象变成"空"的状态,Init()使它变成"有内容"的状态,Clear()使它变回"空"的状态,析构函数使它变回无序状态,这样比较清楚.
 
 
   

Rank: 9Rank: 9Rank: 9

注册时间
2009-9-6
积分
329
18#
  发表于 2010-8-19 14:11:08  | 只看该作者
本帖最后由 socket9999 于 2010-8-19 14:12 编辑
这是有问题的,构造函数,不应该抛出异常,考虑




这个情况就相当尴尬了,构造函数一旦抛出异常,a 根 ...
mayax 发表于 2010-7-1 10:31 

这个问题貌似C++ Primer和Effective C++里都提到过。
解决方法是把a改成智能指针,这样a被销毁时也会delete所指对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值