C++异常处理全解析
立即解锁
发布时间: 2025-08-16 01:20:53 阅读量: 2 订阅数: 29 


C++编程语言第四版:核心概念与技术
### C++ 异常处理全解析
#### 1. 断言与测试控制
在 C++ 编程里,断言是验证程序假设的关键工具。比如下面这个函数:
```cpp
void f(int n)
// n should be in [1:max)
{
Assert::dynamic((n<=0 || max<n),Assert::compose(__FILE__,__LINE__,"range problem");
// ...
}
```
这里使用 `Assert::dynamic` 来检查 `n` 是否在指定范围 `[1:max)` 内。若不在此范围,会触发断言错误。为简化断言表达,可借助命名空间指令和默认消息:
```cpp
void f(int n)
// n should be in [1:max)
{
dynamic(n<=0||max<n);
// ...
}
```
通过构建选项(如控制条件编译)和程序代码里的选项,能控制测试及对测试的响应。这样就能有一个广泛测试并进入调试器的调试版本系统,以及一个几乎不做测试的生产版本系统。
个人建议在程序的最终(发布)版本中保留至少一些测试。例如,使用 `Assert` 时,标记为零级的断言会始终被检查。因为在持续开发和维护的大型程序里,很难找出最后一个 bug。而且,即便其他方面都正常工作,保留一些“健全性检查”来应对硬件故障也是明智之举。
只有最终完整系统的构建者能决定失败是否可接受。库或可重用组件的编写者通常不能无条件终止程序。所以,对于通用库代码,报告错误(最好通过抛出异常)是必不可少的。另外,析构函数不应抛出异常,因此不要在析构函数中使用会抛出异常的 `Assert()`。
#### 2. 抛出异常
我们能抛出任何可复制或移动类型的异常。例如:
```cpp
class No_copy {
No_copy(const No_copy&) = delete;
// prohibit copying (§17.6.4)
};
class My_error {
// ...
};
void f(int n)
{
switch (n) {
case 0:
throw My_error{};
// OK
case 1:
throw No_copy{};
// error : can’t copy a No_copy
case 2:
throw My_error;
// error : My_error is a type, rather than an object
}
}
```
被捕获的异常对象原则上是抛出对象的副本(尽管优化器可尽量减少复制)。也就是说,`throw x;` 会用 `x` 初始化一个 `x` 类型的临时变量。这个临时变量在被捕获前可能会被进一步复制多次。异常会从被调用函数传递回调用函数,直到找到合适的处理程序。异常的类型用于在某个 `try` 块的 `catch` 子句中选择处理程序。异常对象中的数据(如果有的话)通常用于生成错误消息或帮助恢复。
从抛出点到处理程序传递异常“向上堆栈”的过程称为堆栈展开。在退出的每个作用域中,会调用析构函数,以便正确销毁每个完全构造的对象。例如:
```cpp
void f()
{
string name {"Byron"};
try {
string s = "in";
g();
}
catch (My_error) {
// ...
}
}
void g()
{
string s = "excess";
{
string s = "or";
h();
}
}
void h()
{
string s = "not";
throw My_error{};
string s2 = "at all";
}
```
在 `h()` 中抛出异常后,所有已构造的字符串会按构造的逆序销毁:"not", "or", "excess", "in",但不包括 "at all"(控制流从未到达)和 "Byron"(不受影响)。
由于异常在被捕获前可能会被复制多次,所以通常不会在其中放入大量数据。包含几个单词的异常很常见。异常传播的语义是初始化语义,所以具有移动语义的类型(如字符串)抛出起来并不昂贵。一些最常见的异常不携带任何信息;类型的名称足以报告错误。例如:
```cpp
struct Some_error { };
void fct()
{
// ...
if (something_wrong)
throw Some_error{};
}
```
标准库中有一个小的异常类型层次结构,可直接使用或作为基类。例如:
```cpp
struct My_error2 : std::runtime_error {
const char* what() const noexcept { return "My_error2"; }
};
```
标准库异常类(如 `runtime_error` 和 `out_of_range`)将字符串参数作为构造函数参数,并具有一个虚函数 `what()` 来返回该字符串。例如:
```cpp
void g(int n)
// throw some exception
{
if (n)
throw std::runtime_error{"I give up!"};
else
throw My_error2{};
}
void f(int n)
// see what exception g() throws
{
try {
void g(n);
}
catch (std::exception& e) {
cerr << e.what() << '\n';
}
}
```
#### 3. noexcept 函数
有些函数不会抛出异常,有些则真的不应该抛出异常。为表明这一点,可将此类函数声明为 `noexcept`。例如:
```cpp
double compute(double) noexcept; // may not throw an exception
```
现在,`compute()` 不会抛出异常。声明函数为 `noexcept` 对程序员推理程序和编译器优化程序非常有价值。程序员无需担心为 `noexcept` 函数提供 `try` 子句(用于处理失败),编译器也无需担心异常处理的控制路径。
然而,`noexcept` 并非完全由编译器和链接器检查。如果程序员“撒谎”,使得 `noexcept` 函数故意或意外地抛出一个在离开该函数前未被捕获的异常,会怎样呢?例如:
```cpp
double compute(double x) noexcept;
{
string s = "Courtney and Anya";
vector<double> tmp(10);
// ...
}
```
`vector` 构造函数可能无法为其十个双精度数获取内存,并抛出 `std::bad_alloc`。在这种情况下,程序将终止。它会无条件调用 `std::terminate()` 来终止。不会调用调用函数的析构函数。是否调用抛出点和 `noexcept` 之间作用域的析构函数(例如 `compute()` 中的 `s`)是实现定义的。由于程序即将终止,无论如何都不应依赖任何对象。通过添加 `noexcept` 说明符,表明代码未编写为处理抛出异常的情况。
#### 4. noexcept 运算符
可以将函数声明为有条件的 `noexcept`。例如:
```cpp
template<typename T>
void my_fct(T& x) noexcept(Is_pod<T>());
```
`noexcept(Is_pod<T>())` 意味着如果谓词 `Is_pod<T>()` 为真,`My_fct` 可能不会抛出异常;如果为假,则可能抛出异常。若 `my_fct()` 复制其参数,可能会这样写。因为知道复制 POD 不会抛出异常,而其他类型(如字符串或向量)可能会。
`noexcept()` 说明符中的谓词必须是常量表达式。单纯的 `noexcept` 意味着 `noexcept(true)`。标准库提供了许多类型谓词,可用于表达函数可能抛出异常的条件。
如果想使用的谓词不能仅使用类型谓词轻松表达怎么办?例如,如果可能抛出或不抛出的关键操作是函数调用 `f(x)` 呢?`noexcept()` 运算符将表达式作为其参数,如果编译器“知道”它不能抛出异常,则返回 `true`,否则返回 `false`。例如:
```cpp
template<typename T>
void call_f(vector<T>& v) noexcept(noexcept(f(v[0]))
{
for (auto x : v)
f(x);
}
```
这里两次提到 `noexcept` 看起来有点奇怪,但 `noexcept` 不是常见的运算符。`noexcept()` 的操作数不会被求值,所以在示例中,如果传递给 `call_f()` 的是一个空向量,不会出现运行时错误。
`noexcept(expr)` 运算符不会竭尽全力确定 `expr` 是否能抛出异常;它只是查看 `expr` 中的每个操作,如果它们都有求值为 `true` 的 `noexcept` 说明符,则返回 `true`。`noexcept(expr)` 不会查看 `expr` 中使用的操作的定义。
条件 `noexcept` 说明符和 `noexcept()` 运算符在应用于容器的标准库操作中很常见且重要。例如:
```cpp
template<class T, size_t N>
void swap(T (&a)[N], T (&b)[N]) noexcept(noexcept(swap(*a, *b)));
```
#### 5. 异常规范
在较旧的 C++ 代码中,可能会找到异常规范。例如:
```cpp
void f(int) throw(Bad,Worse); // may only throw Bad or Worse exceptions
void g(int) throw();
// may not throw
```
空异常规范 `throw()` 被定义为等同于 `noexcept`。即,如果抛出异常,程序将终止。非空异常规范(如 `throw(Bad,Worse)`)的含义是,如果函数(这里是 `f()`)抛出列表中未提及或未从列表中提及的异常公开派生的任何异常,将调用意外处理程序。意外异常的默认效果是终止程序。非空 `throw` 规范很难用好,并且意味着可能需要进行昂贵的运行时检查来确定是否抛出了正确的异常。此功能并不成功,已被弃用,不建议使用。如果想动态检查抛出了哪些异常,可使用 `try` 块。
#### 6. 捕获异常
##### 6.1 捕获条件
考虑以下代码:
```cpp
void f()
{
```
0
0
复制全文
相关推荐










