c++构造和析构异常

C++ 构造函数的异常是一个比较难缠的问题,很多时候,我们可能不去考虑这些问题,如果被问到,有人可能会说使用RAII管理资源。

但你真的考虑过如果构造函数失败了,到底会发生什么吗,前面构造成功的成员、基类对象应该怎样回收?

最近在知乎上看到有人提到这个问题:

http://www.zhihu.com/question/22889420

看了陈硕的回答,抱着怀疑的心态写代码加以验证。

在此之前,先不急着上代码,啰嗦几句话。

首先问4个问题,这是从 Herb sutter 的《More Exceptrional C++》看到的,我觉得问的很好,类似保安的哲理问题:你是谁、你从哪里来、你要到哪里去?

1:对象生命周期何时开始

一个构造函数成功执行完毕,并成功返回之时,也就是构造函数成功执行到函数体尾,没有发生异常。

2:对象生命周期何时结束

当一个对像的析构函数开始执行,也就是达到析构函数开始指出,这里暂且不讨论析构函数是否发生异常,只要进入析构函数体,该对象生命周期就已经结束

3:在生命周期开始之前,与生命结束之后,对象处于什么状态

这时候”对象”已不是对象。理论上”它”根本就不存在

4:接着第三个答案,如果构造函数异常,对象处于什么状态?

构造函数异常,即构造函数甚至没有到达函数体的尾部,即对象的生命周期还没有开始,所以他根本不是一个的对象,或者说它什么都不是,

所以更不会执行析构函数了。

那么问题来了,如果构造失败,之前成功分配的资源怎么办呢?

Herb sutter的答案是:这个是语言本身来负责回收了,编译器来实现,没程序员的事,即使之前成功构造的对像也不会执行析构函数。

下面是陈硕列举构造函数失败可能发生的场景,他举了5个例子,我这里写了4个,我的结论如下

1:构造函数的初始化列表里抛异常,前面已经构造好的成员由编译器负责回收,不会调用析构函数

2:数组元素构造时抛异常,前面已经构造好的元素由编译器回收,不会调用对象的析构函数。

3:多继承中某个基类的构造函数抛异常,已经构造成功的基类对象由编译器回收,不会调用析构函数

4:智能指针,STL 容器 存放auto_ptr

第一种:构造函数初始化列表抛出异常,前面成功构造的对像由编译器负责回收,不会调用析构函数

1 #include
 2
 3 using namespace std;
 4
 5 class B{
 6
 7 public:
 8     B(){
 9         cout << "construct B default" << endl;
10         throw 3; //故意在默认构造函数中抛出异常
11     }
12     B(int num){
13         age = num;
14         cout << "constructor B ,age =" << num << endl;
15     }
16     ~B(){
17         cout << "destructor B ,age=" << age << endl;
18     }
19 private:
20     int age;
21 };
22
23 class A{
24 public:
25     A():_data(new char[1]), b(B(10)), bp(new B()){
26         cout << "construct A " << endl;
27
28         *_data = '\0';
29
30     }
31     ~A(){
32         cout << "destructor A" << endl;
33         delete [] _data;
34         delete bp;
35
36     }
37 private:
38     char *_data;
39     B b;
40     B *bp;
41 };
42
43 int main(void){
44
45     A a;
46 }

第十行出,我故意throw 3

所以在25行class A构造函数的初始化列表,调用B(10)发生异常,而B()则会异常退出。结果如下

c++构造和析构异常

B的默认构造函数和B(int)都执行完毕,但析构函数没有执行,当然A的构造也失败了,更不会执行析构函数,这些资源都是编译器负责回收了

如果你不信的会,我注释第9行,是另一种结果

c++构造和析构异常

第2种:数组元素构造发生异常,前面构造成功的对象由编译器负责回收,不会调用析构函数

1  1 class C{
 2  2 public:
 3  3     C(){
 4  4         //得到指定范围[m,n]的随机数:r = rand()%(n - m + 1) + m;
 5  5         int r = rand()%(8-0)+0;
 6  6         num = r;
 7  7         cout << "constuctor C ,num = " << num << endl;
 8  8         if(!(r % 4)){
 9  9             throw r;
10 10         }
11 11     }
12 12     ~C(){
13 13         cout << "destructor C, num = " << num << endl;
14 14     }
15 15 private:
16 16     int num;
17 17 };
18 18
19 19 int main(void){
20 20
21 21
22 22     C arr[10];
23 23
24 24 }

当随机数r 整除 4是,即throw异常,则前面成功构造的对象不会析构

c++构造和析构异常

若注释第9行的throw r 结果是:

c++构造和析构异常

上面说的这点和下面这个观点相悖,我试了一下发现不管异常是在构造函数中抛还是正常的调用中抛,要想让系统调用已经构造好的对象,需要在外面try catch,否则让std catch住是不会调用析构直接raise信号了

栈展开级析构不应抛异常原因

在步入正题前,我们先来讲讲什么叫栈展开(stack unwinding),才能更好理解C++异常(exception)的机制是怎样运作的:

void f1() throw(int){           //函数f1会抛出一个整型的异常代码
  cout<

在C++里,当有异常被抛出,调用栈(call stack),即栈中用来储存函数调用信息的部分,会被按次序搜索,直到找到对应类型的处理程序(exception handler)。而这里的搜索顺序就是f1->f2->f3。f1没有对应类型的catch块,因此跳到了f2,但f2也没有对应类型的catch块,因此跳到f3才能处理掉这个异常。

以上这个寻找异常相应类型处理器的过程就叫做栈展开。同时在这一过程中,当从f1返回到f2时,f1里局部变量的资源会被清空,即调用了对象的析构函数。同样,在从f2返回到f3时,f2里局部变量也会被调用析构函数并清空资源。

现在可以回到正题了,C++并不阻止在类的析构函数中抛出异常,但这是一个非常不好的做法!因为栈展开的前提是已经有一个未处理的异常,并且栈展开会自动调用函数本地对象的析构函数,如果这时对象的析构函数时又抛出一个异常,现在就同时有两个异常出现,但C++最多只能同时处理一个异常,因此程序这时会自动调用std::terminate()函数,导致我们所谓的闪退或者崩溃。

此外,如下的栗子也会导致程序同时出现多个异常:

class Widget{
  public:
    ...

    ~Widget(){...}        //假设此析构函数可能会抛出异常
};

void doSomething(){
  std::vector v;
}                         //在这一行调用了v的析构函数,资源被释放

当v被调用析构函数,它包含的所有Widget对象也都会被调用析构函数。又因为v是一个容器,如果在释放第一个元素时触发了异常,它也只能继续释放别的元素,否则会导致其它元素的资源泄露。如果在释放第二个元素的时候又触发了异常,那么程序同样会导致崩溃。

不仅仅是std::vector,所有STL容器的类甚至包括数组也都会像这样因为析构函数抛出异常而崩溃程序,所以在C++中,不要让析构函数抛出异常!

more effective c++提出两点理由(析构函数不能抛出异常的理由):

1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。

2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

附:std::thread 收到的异常其实在std内部处理过一次,已经不是原始的异常信息(没有抛异常的栈位置这些);而boost::thread会原封不动的转发原始异常栈!

Original: https://www.cnblogs.com/wangshaowei/p/14295148.html
Author: 大老虎打老虎
Title: c++构造和析构异常

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/535466/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • C++强大背后

    在31年前(1979年),一名刚获得博士学位的研究员,为了开发一个软件项目发明了一门新编程语言,该研究员名为Bjarne Stroustrup,该门语言则命名为——C with c…

    C++ 2023年5月29日
    056
  • c++ 书籍

    c++ 书籍 入门 c++0x G:\doc\bianchengsuixiang\BUPSDXFA3TP7KCMLHALRHLIX2FEJEUJFEIT(信息技术)\IT\软件开发…

    C++ 2023年5月29日
    064
  • 哈希表查找(散列表查找) c++实现HashMap

    算法思想: 哈希表 什么是哈希表 在前面讨论的各种结构(线性表、树等)中,记录在结构中的相对位置是随机的,和记录的关键字之间不存在确定的关系,因此,在结构中查找记录时需进行一系列和…

    C++ 2023年5月29日
    071
  • 从C++到GO

    刚开始接触Go语言,看了两本Go语言的书,从c++开发者的角度来看看go语言的新特性,说下自己感触较深的几点: 并发编程 Go语言层面支持协程,将并发业务逻辑从异步转为同步,大幅提…

    C++ 2023年5月29日
    052
  • C/C++ 中带空格字符串输入的一些小trick

    今天在重温 C++ 的时候发现自己存在的一些问题,特此记录下来。 我们可以看一下下面这段代码: #include #include #include #include using …

    C++ 2023年5月29日
    052
  • 用C/C++实现对STORM的执行信息查看和控制

    近期公司有个需求。须要在后端应用server上实时获取STORM集群的执行信息和topology相关的提交和控制,经过几天对STORM UI和CMD源代码的分析,得出能够通过其th…

    C++ 2023年5月29日
    083
  • JNI支持C++与C的区别

    C++的代码后缀是”.cpp” 在JNI.h 文件中有两套代码。一套是支持c的, 一套是支持JNI的。 JNI中针对C的代码是: C中调用方式: JNI中针…

    C++ 2023年5月29日
    062
  • [招聘]高级软件工程师(C/C++), 惠州大亚湾,BYD

    岗位职责: 参与工业基础软件技术方案的调研,制定软件模块方案 开发基于Linux的工控自动化通用平台软件模块 相关技术文档的编写 其他相关的工作任务 任职要求:1. 3 年以上 C…

    C++ 2023年5月29日
    073
  • 【C++服务端技术】队列

    链表和锁实现的队列,锁的代码请看其他文章 #pragma once #include #include "AutoLock.h" namespace Extra…

    C++ 2023年5月29日
    050
  • [C++]一份Linq to object的C++实现

    几个月的构想+0.5小时的设计+4小时的linq.h编码+3小时的测试编码。 大量使用C++11的特性,在GCC 4.7.2下编译通过。 关于实现相关的描述就不说了,我表达能力差,…

    C++ 2023年5月29日
    047
  • [C++] 对象指针使用方法

    对象指针:指向类对象的指针 类指针指向类变量(对象)的地址 对象指针定义格式: 举例: #include using namespace std; class Student { …

    C++ 2023年5月29日
    054
  • 设计模式C++实现——工厂模式

    软件领域中的设计模式为开发人员提供了一种使用专家设计经验的有效途径。设计模式中运用了面向对象编程语言的重要特性:封装、继承、多态,真正领悟设计模式的精髓是可能一个漫长的过程,需要大…

    C++ 2023年5月29日
    044
  • c++内存管理

    我们写一个函数,里面必然会用到变量,每个变量都会占用内存,这些内存分成三个种类。 第一个是栈内存,函数内部局部变量是栈内存。栈内存不用我们手动管理,在调用完函数之后 函数会自动释放…

    C++ 2023年5月29日
    057
  • 拓扑排序(二)之 C++详解

    拓扑排序(Topological Order)是指,将一个有向无环图(Directed Acyclic Graph简称DAG)进行排序进而得到一个有序的线性序列。 这样说,可能理解…

    C++ 2023年5月29日
    044
  • c++ 条件变量

    http://blog.csdn.net/hemmanhui/article/details/4417433 互斥锁:用来上锁。 条件变量:用来等待,当条件变量用来自动阻塞一个线程…

    C++ 2023年5月29日
    043
  • 聊聊 C++ 中的几种智能指针 (上)

    一:背景 我们知道 C++ 是手工管理内存的分配和释放,对应的操作符就是 new/delete 和 new[] / delete[], 这给了程序员极大的自由度也给了我们极高的门槛…

    C++ 2023年5月29日
    083
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球