`
jarfield
  • 浏览: 200523 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

异常与构造函数、析构函数

    博客分类:
  • C++
阅读更多

写Java代码的时候,遇到错误总是喜欢抛出异常,简单实用。最近开始写C++代码,发现异常没那么简单,使用须谨慎。

 

翻阅了《Effective C++》 《More Effective C++》《Inside The C++ Object Model》的相关章节,大概弄明白了一些东东,总结在本文。

 

本文不是总结普适的C++异常机制,还没有这个内力哈! 主要是结合构造函数和析构函数,来总结异常对他俩的影响。构造函数和析构函数本来就很折磨脑筋,再叠加上异常机制,确实比较复杂。

 

异常与析构函数

本节内容较少,因此先说。构造函数放到下一节讨论。

 

绝对不要将异常抛出析构函数

这一条在《Effective C++》 《More Effective C++》中均被作为独立章节讲解,可见其重要性。

 

有一点不要误解:析构函数的代码当然可以throw异常,只是这个异常不要被抛出析构函数之外。如果在析构函数中catch住异常,并且不再抛出,这就不会带来问题。

 

至于原因,有两点。我们先看第一点。

 

异常被抛出析构函数之外,往往意味着析构函数的工作没有做完。如果析构函数需要释放一些资源,异常可能导致资源泄露,使得程序处于一个不安全的状态。

 

如下面的伪代码所示,异常导致p不能free,从而造成内存泄露。

class A
{
public:
    ~A()
   {
        throw exception;
        free(p);
   }
};
 

OK,这个问题好办,我好好写代码,确保析构函数释放所有的资源之后,才抛出异常。这还不行吗?

class A
{
public:
    ~A()
   {
        free(p);
        throw exception;
   }
};

 

嗯,确实不行。我们来看第二个原因。

 

如果两个异常同时存在:第一个异常还没有被catch,第二个异常又被抛出,这会导致C++会调用terminate函数,把程序结束掉!

 

这简直是灾难,远比资源泄漏要严重。

 

那么,什么时候会同时出现两个异常呢?看下面的代码。

void f()
{
    A a;   // 没错,就是前面的class A
    throw exception;
}

 

f()抛出异常后,会进行stack-unwinding。在这个过程中,会析构所有的active local object。所谓active local object,就是已经构造完成的局部对象,例如上面的对象a。

 

调用a的析构函数时,(第一个)异常还没有被catch。可是a的析构函数也抛出了(第二个)异常。这时,两个异常同时存在了。程序会毫不留情地结束!

 

这个理由足够充分了:再也不要让异常逃离你的析构函数!

 

异常与构造函数

构造函数本来就是一件难以琢磨的东东,背后做了很多事情:成员对象的构造、基类成分的构造、虚表指针的设置等。这些事情本来就很纠结了,再让构造函数抛出异常,会出现怎样的悲剧呢?

 

有一点比较安慰:异常即使被抛出构造函数之外,也不会造成程序结束。那么,是否存在资源泄漏的问题呢?不可一概而论,我们分情况分析。

 

对象自身的内存如何释放

对象有可能在栈上,也可能在堆上,我们分两种情况讨论。

// 对象在栈上
f()
{
    A a;
}

// 对象在堆上
f()
{
    A * a = new A();
}

如果对象是在栈上,那么函数退栈自然会释放a占用的空间,无需多虑。

 

如果对象是在堆上,我们还得两种情况讨论:

  1. 如果是new运算符抛出的异常,那么堆空间还没有分配成功,也就无需释放
  2. 如果是构造函数抛出的异常,堆空间已经分配成功,那么编译器会负责释放堆空间(Inside The C++ Object Model, p301)

可见,对象本身的内存,是不会泄露的。

 

成员对象和基类成分怎么办

成员对象和基类成分的内存,会随着对象自身内存的释放而被一起释放,没什么问题。

 

但是,有一点需要谨记:如果一个对象的构造函数抛出异常,那么该对象的析构函数不会被调用。

 

原因很简单:如果对象没有被构造完整,析构函数中的某些代码可能会有风险。为了避免这类意外问题,编译器拒绝生成调用析构函数的代码。

 

那么,成员对象的基类成员对象的析构函数,会被调用吗?如果不会调用,则可能出现资源泄漏。答案是,会被调用。见下面的代码。

class B : class C
{
    A a;
    A * pa;
public:
    B()
    {
        pa = new A();
    }

    ~B()
    {
        delete pa;
    }
};

 

如果B的构造函数抛出异常,编译器保证:成员对象a的析构函数、基类C的析构函数会被调用(Inside The C++ Object Model, p301)。

 

成员指针怎么办

注意上述代码中的pa,它指向一块堆空间,由于B的析构函数不会被调用了,内存就会出现泄漏。

 

这还真是一个问题,编译器也不能帮我们做更多事情,只能由程序员自己负责释放内存。

 

我们可能要这样写代码

class B : class C
{
    A a;
    A * pa;
public:
    B()
    {
        pa = new A();
        try {
            throw exception;
        } catch(...)
        {
            delete pa; //确保释放pa
            throw;
        }
    }

    ~B()
    {
        delete pa;
    }
};
 

这样的代码难看很多,有一种建议的做法就是:用智能指针包装pa。智能指针作为B的成员对象,其析构函数是可以被自动调用的,进而释放pa。

 

析构函数如何被自动调用

上面提到:

  1. 普通函数抛出异常时,所有active local object的析构函数都会被调用
  2. 构造函数抛出异常时,所有成员对象以及基类成分的析构函数都会被调用

那么,这是怎么实现的呢?

 

我们以第一种情况为例,分析实现细节。看下面的代码:

f()
{
    A a1;
    if (...) {  // 某些条件下,抛出异常
        throw exception;
    }
    A a2;
    throw exception; // 总会抛出异常
}

 

如果L5抛出异常,那么对象a1会被析构。如果L8抛出异常,那么对象a1 a2都要被析构。编译器是怎么知道,什么时候该析构哪些对象的呢?

 

支持异常机制的编译器,会做一些”簿记“工作,将需要被析构的对象登记在特定的数据结构中。编译器将上述代码分成不同的区段,每个区段中需要被析构的对象,都不相同。

 

例如,上述代码中,L3 L4~L7 L8就是三个不同的区段:

  1. 如果L3抛出异常,那么没有对象需要析构
  2. 如果L4~L7抛出异常,那么a1需要被析构
  3. 如果L8抛出异常,那么a1和a2都要析构

编译器通过分析代码,簿记这些区段以及需要析构的object list。运行时,根据异常抛出时所在的区段,查找上述的数据结构,就可以知道哪些对象需要被析构。

 

构造函数抛出异常时,成员对象及基类成分被析构的原理,是类似的。在C++运行时看来,构造函数只是普通的函数而已。

 

总结

C++的异常机制,给编译器和运行时均带来了一定的复杂度和代价。上述的”簿记“工作,只是冰上一角。

 

关于异常的使用,也有很多坑。怎么throw 怎么catch,都是有讲究的。有空下次再做总结。

分享到:
评论

相关推荐

    构造函数中抛出的异常

    标准C++中定义构造函数是一个对象构建自己,分配所需资源的地方,一旦构造函数执行完毕,则表明这个对象已经诞生了,有自己的行为和内部的运行状态,之后还有对象的消亡过程(析构函数的执行)。可谁能保证对象的...

    C++课程设计 学生宿舍管理系统

    基类dormitory,其有DormiNum和静态变量DormiMaxNum,构造函数,析构函数,一般函数GetDormiNum(), GetCountMan(), SetDormiNum()及一个纯虚函数display();2. dormitory的派生类room类,内有公有成员变量RN,构造函数、...

    C++构造函数抛出异常需要注意的地方

    因为在构造函数中抛出异常,在概念上将被视为该对象没有被成功构造,因此当前对象的析构函数就不会被调用。同时,由于构造函数本身也是一个函数,在函数体内抛出异常将导致当前函数运行结束,并释放已经构造的成员...

    C++构造函数中抛出的异常

     1、标准C++中定义构造函数是一个对象构建自己,分配所需资源的地方,一旦构造函数执行完毕,则表明这个对象已经诞生了,有自己的行为和内部的运行状态,之后还有对象的消亡过程(析构函数的执行)。可谁能保证...

    CSDN技术文档大全(CHM)

    如果Object Pascal的类在构造函数中抛出异常,编译器会自动调用类的析构函数(由于析构函数不允许被重载,可以保证只有唯一一个析构函数,因此编译器不会迷惑于多个析构函数之中)。析构函数中一般会析构成员对象,...

    EthsonLiu#personal-notes#栈展开1

    1. 为局部对象调用析构函数 2. 析构函数应该从不抛出异常 3. 异常与构造函数 4. 未捕获的异常将会终止程序

    上海交大C++面向对象

    第十二章 构造函数与析构函数 第十三章 面向对象程序设计 第十四章 堆与拷贝构造函数 第十五章 静态成员与友员   第十六章 继承 第十七章 多重继承 第十八章 运算符重载   第十九章 I/O流 第二十章 ...

    C++面向对象程序设计

    本课程为计算机及应用(独立本科段)的一门专业课。课程内容有:面向对象的基本概念和要素,包括类、对象、...类,包括封装、数据成员、函数成员、可见性控制 、构造函数与析构函数、继承、多态性等;模板;异常处理等。

    C++ 语法详解

    C++整型、字符型、浮点型、声明、定义、typedef、运算符、表达式、左值、选择语句、循环语句、指针、数组、函数和标识符的作用域、类基础、类作用域及相关运算符、构造函数、复制构造函数、析构函数、名称空间、类中...

    C++编程思想习题

    14.9析构函数和虚拟析构函数 14.10小结 14.11练习 第15章 模板和包容器类 15.1包容器和循环子 15.2模板综述 15.2.1C方法 15.2.2Smalltalk方法 15.2.3模板方法 15.3模板的语法 15.3.1非内联函数定义 15.3.2栈模板...

    C++中String类的实现

    只是由于时间关系,可能只要求实现构造函数、析构函数、拷贝构造函数等关键部分。  String的实现涉及很多C++的基础知识、内存控制及异常处理等问题,仔细研究起来非常复杂,本文主要做一个简单的总结和归纳。  ...

    More Effiective C++.zip

    Item M9 析构函数与资源泄露 Item M10 构造函数与资源泄露 Item M12 异常 Item M17 懒惰计算法 Item M18 热心计算法 Item M24 虚表 没有完全吃透 Item M26 限制类个数 Item M27 对象在/不在堆上 Item M29 引用计数 ...

    C++分数类计算器,完美运行

    其中,包括构造函数、析构函数、显示函数等。 (2)输入/输出:对流提取和流插入运算符进行重载。 (3)计算功能:可进行分数的加、减、乘和除法运算。 (4)化简功能:将分数化简为最简分数。 (5)异常处理功能:...

    PHP学习手册(PHP知识大全)

     在 PHP4 中,当函数与对象同名时,这个函数将成为该对象的构造函数,并且在 PHP4 中没有析构函数的概念。  在 PHP5 中,构造函数被统一命名为 __construct,并且引入了析构函数的概念,被统一命名为 __destruct...

    lab7 实验报告1

    Exp2:(习题16.30 构造函数、析构函数和异常处理)1. Description of the Problem(英文)Write a program il

    EffectiveC++ and more Effective C++

     ·条款十一:禁止异常信息(exceptions)传递到析构函数外  ·条款十二:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异  ·条款十三:通过引用(reference)捕获异常  ·条款十四...

    C++ 编程入门讲义大全

    目录 介绍 基本语法 变量与数据类型 输入和输出 运算符 控制流程 条件语句 循环语句 ...构造函数与析构函数 成员函数与访问控制 继承与多态 异常处理 标准库简介 输入输出流 字符串处理 容器与算法

    第一章C++视频介绍视频

    构造函数与析构函数 访问修饰符:public, private, protected 进阶特性 指针与引用 动态内存分配 STL库介绍:vector, map, set等 异常处理 C++的实际应用 C++在游戏开发中的应用 C++在系统编程中的应用 C++与性能...

    零起点学通C++多媒体范例教学代码

    10.4.析构函数和delete运算符 10.4..1 默认析构函数 10.4.2 调用构造函数进行类型转换 10.5 浅层复制构造函数 10.6 深层复制构造函数 第11章 运算符重载 11.1 运算符重载 11.2 在成员函数中实现自加 11.3 重载前置...

    C++Primer视频(高级)下载地址

    11.13章 析构函数 12.13章 深复制、浅复制 13.13章 管理指针成员 14.14章 重载操作符的定义 15.14章 重载输入输出操作符 16.14章 重载算术操作符 17.14章 重载关系操作符(一) 18.14章 重载关系操作符...

Global site tag (gtag.js) - Google Analytics