C++异常安全:构造函数失败处理深度解析
立即解锁
发布时间: 2025-08-21 01:43:10 阅读量: 2 订阅数: 7 


C++编程深度解析与实践
### C++ 异常安全:构造函数失败处理深度解析
在现代 C++ 编程中,若不了解异常安全问题,几乎不可能编写出健壮的代码。一旦使用 C++ 标准库,哪怕只是使用 `new` 操作符,都必须为可能出现的异常做好准备。本文将深入探讨异常安全问题,尤其是构造函数失败相关的技术和概念。
#### 1. 构造函数失败与对象生命周期
首先,我们来看以下类的定义:
```cpp
// Example 17-1
class C : private A
{
B b_;
};
```
当构造函数抛出异常时,会发生什么呢?如果异常来自子对象或成员对象的构造尝试,又会怎样?在 `C` 类的构造函数中,如何捕获来自基子对象(如 `A`)或成员对象(如 `b_`)构造函数抛出的异常呢?这就需要用到函数尝试块(Function Try Blocks)。
```cpp
// Example 17-1(a): Constructor function try block
C::C()
try
: A ( /*...*/ ) // optional initialization list
, b_( /*...*/ )
{
}
catch( ... )
{
// We get here if either A::A() or B::B() throws.
// If A::A() succeeds and then B::B() throws, the
// language guarantees that A::~A() will be called
// to destroy the already-created A base subobject
// before control reaches this catch block.
}
```
函数尝试块的作用是捕获基子对象或成员对象构造函数抛出的异常。如果 `A::A()` 成功,而 `B::B()` 抛出异常,语言会保证在进入 `catch` 块之前调用 `A::~A()` 来销毁已经创建的 `A` 基子对象。
接下来,我们考虑以下代码:
```cpp
// Example 17-2
{
Parrot p;
}
```
这里涉及到几个关键问题:
- **对象生命周期的开始**:当对象的构造函数成功完成并正常返回时,即控制到达构造函数体的末尾或执行了提前的 `return` 语句,对象的生命周期开始。
- **对象生命周期的结束**:当对象的析构函数开始执行时,即控制到达析构函数体的开头,对象的生命周期结束。
- **对象生命周期结束后的状态**:对象生命周期结束后,就如同一位软件大师对类似代码片段中局部对象的形象描述:“这只鹦鹉不再渴望!它已经逝去!这只鹦鹉不复存在!它已经停止存在!它已经过期,去见它的造物主了!它僵硬了!没有了生命,它安息了!” 简单来说,在对象生命周期开始之前和结束之后,对象是不存在的。
- **构造函数抛出异常的含义**:这意味着构造失败,对象从未存在过,其生命周期也从未开始。实际上,报告构造失败(即无法正确构建给定类型的可用对象)的唯一方法就是抛出异常。
#### 2. 构造函数异常的吸收问题
现在我们来分析,在 `C` 类的构造函数中,如果 `A` 或 `B` 构造函数抛出异常,`C` 构造函数是否可以吸收该异常而不抛出任何异常呢?
```cpp
// Example 18-1(a): Absorbing exceptions?
C::C()
try
: A ( /*...*/ ) // optional initialization-list
, b_( /*...*/ )
{
}
catch( ... )
{
// ?
}
```
`try` 块的处理程序如何退出呢?有以下几种情况:
- 处理程序不能简单地使用 `return;`,因为这是非法的。
- 如果处理程序使用 `throw;`,它将重新抛出 `A::A()` 或 `B::B()` 最初抛出的异常。
- 如果处理程序抛出其他异常,该异常将替代基子对象或成员子对象构造函数最初抛出的异常被抛出。
- 根据 C++ 标准,如果处理程序不通过抛出异常(无论是重新抛出原始异常还是抛出新异常)退出,并且控制到达构造函数或析构函数的 `catch` 块末尾,那么原始异常将自动重新抛出,就好像处理程序的最后一条语句是 `throw;` 一样。
这意味着,构造函数或析构函数的函数尝试块处理程序代码必须以抛出异常结束。在 C++ 中,如果任何基子对象或成员子对象的构造失败,整个对象的构造也必须失败。构造函数在其基子对象或成员子对象的构造函数抛出异常后,无法恢复并做一些合理的事情,甚至不能将自己的对象置于编译器可识别的 “构造失败” 状态。
为了更直观地理解,我们来看一个流程图:
```mermaid
graph TD;
A[开始构造C对象] --> B{A构造是否成功};
B -- 是 --> C{B构造是否成功};
B -- 否 --> D[抛出异常,构造失败];
C -- 是 --> E[C对象构造成功];
C -- 否 --> F[调用A的析构函数];
F --> G[抛出异常,构造失败];
```
#### 3. 函数尝试块的道德准则
- **构造函数函数尝试块**:其处理程序仅适用于转换基子对象或成员子对象构造函数抛出的异常(可能还包括进行相关日志记录或对此类失败做出其他副作用响应),不适用于其他目的。
- **析构函数函数尝试块**:几乎没有实际用途,因为析构函数不应抛出异常。即使存在异常(例如成员子对象的析构函数可能抛出异常),处理程序也无法抑
0
0
复制全文
相关推荐









