异常和析构函数(二)

 
两种情况下会调用析构函数。第一种是正常情况下删除一个对象,例如对象超出了作用域或被显式地delete。第二种是异常传递的堆栈辗转开解(stack-unwinding)过程中,由异常处理系统删除一个对象

 在上述两种情况下,调用析构函数时异常可能处于激活状态也可能没有处于激活状态。遗憾的是没有办法在析构函数内部区分出这两种情况。因此在写析构函数时必须保守地假设有异常被激活,因为如果在一个异常被激活的同时,析构函数也抛出异常,并导致程序控制权转移到析构函数外,C++将调用terminate函数。这个函数的作用正如其名字所表示的:它终止程序的运行,而且是立即终止,甚至连局部对象都没有被释放。

 下面举一个例子,一个Session类用来跟踪在线计算机的sessions,session就是运行在从登录计算机开始一直到注销出系统为止的这段期间的某种东西。每个Session对象关注的是它建立与释放的日期与时间:
class Session {
  public:
   Session();
   ~Session();
   ...
  private:
   static void logCreation(Session *objAddr);
   static void logDestruction(Session *objAddr);
  };

 函数logCreation 和 logDestruction被分别用于记录对象的建立与释放。我们因此可以这样编写Session的析构函数:
  Session::~Session()
  {
   logDestruction(this);
  }

 一切看上去很好,但是如果logDestruction抛出一个异常,会发生什么事呢?异常没有被Session的析构函数捕获住,所以它被传递到析构函数的调用者那里。但是如果析构函数本身的调用就是源自于某些其它异常的抛出,那么terminate函数将被自动调用,彻底终止我们的程序。程序没有记录下释放对象的信息,这是不幸的,甚至是一个大麻烦。那么事态果真严重到了必须终止程序运行的地步了么?如果没有,必须防止在logDestruction内抛出的异常传递到Session析构函数的外面。唯一的方法是用try和catch blocks。一种很自然的做法会这样编写函数:
  Session::~Session()
  {
   try {
    logDestruction(this);
   }
   catch (...) {
    cerr << "Unable to log destruction of Session object "
       << "at address "
             << this
             << "./n";
   }
  }

 但是这样做并不比原来的代码安全。如果在catch中调用operator<<时导致一个异常被抛出,我们就又遇到了老问题,一个异常被转递到Session析构函数的外面。

 我们可以在catch中放入try,但是这总得有一个限度,否则会陷入循环。因此我们在释放Session时必须忽略掉所有它抛出的异常:
  Session::~Session()
  {
   try {
    logDestruction(this);
   }
   catch (...) { }
  }

 catch表面上好像没有做任何事情,这是一个假象,实际上它阻止了任何从logDestruction抛出的异常被传递到session析构函数的外面。我们现在能高枕无忧了,无论session对象是不是在堆栈辗转开解(stack unwinding)中被释放,terminate函数都不会被调用。

 不允许异常传递到析构函数外面还有第二个原因。如果一个异常被析构函数抛出而没有在函数内部捕获住,那么析构函数就不会完全运行(它会停在抛出异常的那个地方上)。如果析构函数不完全运行,它就无法完成希望它做的所有事情。例如,我们对session类做一个修改,在建立session时启动一个数据库事务(database transaction),终止session时结束这个事务:
  Session::Session() // 为了简单起见
  { // 这个构造函数没有
   // 处理异常

   logCreation(this);
   startTransaction(); // 启动 database transaction
  }

  Session::~Session()
  {
   logDestruction(this);
   endTransaction(); // 结束database transaction
  }

 如果在这里logDestruction抛出一个异常,在session构造函数内启动的transaction就没有被终止。我们也许能够通过重新调整session析构函数内的函数调用顺序来消除问题,但是如果endTransaction也抛出一个异常,我们除了回到使用try和catch外,别无选择。

 综上所述,我们知道禁止异常传递到析构函数外有两个原因,第一能够在异常转递的堆栈辗转开解(stack-unwinding)的过程中,防止terminate被调用。第二它能帮助确保析构函数总能完成我们希望它做的所有事情。
 
C++中,构造函数析构函数在语法上都可以抛出异常,但从逻辑风险控制角度,应尽量避免。 ### 构造函数抛出异常 - **情况**:构造函数在执行过程中,若遇到无法处理的错误,如资源分配失败、初始化条件不满足等,可能会抛出异常。例如,在构造函数中动态分配内存时,若内存不足就可能抛出异常。 - **影响**:当构造函数抛出异常时,对象的析构函数将不会被执行,任何在构造函数中分配但未完全初始化的资源(如动态内存)将无法自动释放,可能导致资源泄漏 [^4][^5]。 ```cpp class MyClass { public: MyClass() { resource = new int[100]; if (/* some condition */) { throw std::runtime_error("Error in constructor"); } // 如果在这里抛出异常,resource 将不会被释放 } private: int* resource; }; ``` - **处理方法**:可采用RAII(Resource Acquisition Is Initialization)原则,尽量使用RAII技术来管理资源,确保即使发生异常也能自动释放资源。例如,使用智能指针代替原始指针 [^5]。 ```cpp #include <memory> class MyClass { public: MyClass() : resource(std::make_unique<int[]>(100)) { if (/* some condition */) { throw std::runtime_error("Error in constructor"); } // 使用智能指针,即使抛出异常也会自动释放资源 } private: std::unique_ptr<int[]> resource; }; ``` ### 析构函数抛出异常 - **情况**:析构函数在释放资源过程中,若遇到无法处理的错误,如关闭文件失败、释放内存时出现错误等,可能会尝试抛出异常。 - **影响**:若析构函数抛出异常,则异常点之后的程序不会被执行,如果析构函数异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会被执行,会造成诸如资源泄露的问题。通常异常发生时,C++机制会调用已经构造对象的析构函数来释放资源,此时如析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃 [^2][^4]。 - **处理方法**:当析构函数中有一些可能发生的异常时,要把可能发生的异常完全封装在析构函数内部,决不能让它抛出到函数之外。可使用try - catch代码块进行捕获处理异常 [^3][^4]。 ```cpp class MyClass { public: ~MyClass() { try { // 可能抛出异常的操作 } catch (...) { // 处理异常 } } }; ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值