C++ 异常和捕获
1. 传统方式的错误处理和C++的异常处理
传统方式:
逐层返回:对象能够被正确析构,流程控制复杂
远程跳转:流程控制简单,对象无法被正确析构
C++方式:
流程控制简单,对象能够被正确析构
2. 异常语法
-
抛出异常
throw 异常对象;
-
捕获异常
try { // try块
可能抛出异常的语句块;
try {
}
catch (...) {
}
}
catch (异常类型1 异常对象) { // catch块
异常处理语句;
try {
}
catch (...) {
}
}
catch (异常类型2 异常对象) { // catch块
异常处理语句;
}
...
catch (...) {
异常处理语句;
}
3. 异常处理流程
-
抛出异常(throw)
-
当代码检测到无法处理的错误时,使用
throw
抛出异常对象:if (error_condition) { throw std::runtime_error("Error message"); // 抛出标准异常类对象 }
- 异常对象可以是基本类型、字符串或自定义类对象。
- 抛出异常后,当前函数立即停止执行,进入异常处理流程。
-
-
栈展开(Stack Unwinding)
- 程序从抛出异常的位置开始,沿函数调用链反向逐层退出栈帧,寻找匹配的
catch
块:- 若当前函数有
try
块包裹异常代码,优先检查其关联的catch
块是否匹配异常类型。 - 若未找到匹配的
catch
,则释放当前函数栈帧,继续向上层调用函数回溯查找。 - 若回溯到
main
函数仍未找到匹配的catch
,调用std::terminate()
终止程序。
- 若当前函数有
- 程序从抛出异常的位置开始,沿函数调用链反向逐层退出栈帧,寻找匹配的
-
捕获异常(catch)
-
catch
块按声明顺序检查类型匹配,仅第一个匹配的catch
会被执行:try { // 可能抛出异常的代码 } catch (const std::exception& e) { // 捕获标准异常 std::cerr << e.what(); } catch (...) { // 捕获所有未被处理的异常 std::cerr << "Unknown exception"; }
-
匹配规则:
- 允许派生类异常被基类
catch
捕获(如std::runtime_error
可被std::exception
捕获)。 - 严格类型匹配,不支持隐式类型转换(如
int
异常无法被double
类型catch
捕获)。
- 允许派生类异常被基类
-
catch(...)
用于捕获所有未明确处理的异常,通常作为最后一个catch
块。
-
-
处理异常
- 执行匹配的
catch
块内代码,处理错误(如日志记录、资源释放等)。 - 若需重新抛出异常(如仅记录日志后继续传递),可用
throw;
语句:
catch (const MyException& e) { log_error(e.what()); throw; // 重新抛出当前异常 }
- 执行匹配的
-
恢复执行
- 异常处理后,程序从最后一个
catch
块之后继续执行,而非回到抛出异常的位置。 - 若异常未被处理(无匹配
catch
),程序终止。
- 异常处理后,程序从最后一个
[示例流程]
void func3() { throw 42; } // 步骤1:抛出int异常
void func2() { func3(); } // 步骤2:栈展开,退出func2
void func1() { func2(); } // 步骤2:栈展开,退出func1
int main() {
try {
func1(); // 步骤2:栈展开到main的try块
} catch (int e) { // 步骤3:匹配int类型异常
std::cout << "Caught: " << e;
} // 步骤5:继续执行后续代码
return 0;
}
4. 标准异常类
C++标准库提供了一系列异常类(定义在<stdexcept>
中),例如:
- 逻辑错误:
std::logic_error
、std::invalid_argument
- 运行时错误:
std::runtime_error
、std::out_of_range
- 内存错误:
std::bad_alloc
(由new
抛出)
自定义异常类通常继承自std::exception
以保持一致性。
5. 异常说明符noexcept
函数标记为noexcept
表示该函数保证不抛出任何异常(违反时程序直接终止)
1. 核心作用
-
优化代码生成
编译器会为noexcept
函数生成更简洁的异常处理代码,甚至完全省略异常栈展开逻辑:int add(int a, int b) noexcept { return a + b; // 基础运算不会抛异常 }
-
影响容器行为(关键场景)
当容器(如vector
)需要扩容时,若元素的移动构造函数标记为noexcept
,则会优先使用移动而非拷贝。
2. 语法形式
-
无条件声明
void func() noexcept;
表示该函数保证不抛出任何异常 -
条件声明
void func() noexcept(expression);
当expression
为true
时不抛异常(常用于模板) -
运算符用法
noexcept(expression)
返回布尔值,判断表达式是否可能抛异常
3. 基础用法示例
示例1:基本函数声明
int add(int a, int b) noexcept {
return a + b; // 明确不抛出异常
}
示例2:移动构造函数
class MyData {
public:
// 声明移动构造函数为noexcept
MyData(MyData&& other) noexcept
: data_(std::move(other.data_)) {}
private:
std::vector<int> data_;
};
// 当vector扩容时,会优先调用noexcept的移动构造函数
std::vector<MyData> vec;
vec.push_back(MyData()); // 触发移动而非复制
4. 高级用法
示例3:条件性noexcept
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(std::move(a))) &&
noexcept(a.operator=(std::move(b)))) {
T tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}
- 内层
noexcept
运算符检查表达式是否不会抛出异常。 - 外层
noexcept(...)
根据内层结果动态决定函数是否为noexcept
。
示例4:虚函数中的限制
class Base {
public:
virtual void foo() noexcept {} // 基类声明noexcept
};
class Derived : public Base {
public:
void foo() override {} // 错误!派生类不能放宽异常规范
// void foo() noexcept override {} // 正确
};
5. 关键注意事项
- 谨慎使用:若
noexcept
函数内部抛出异常,程序直接终止(如调用throw
或调用可能抛出的函数)。 - 与STL协同:标准库算法(如
std::sort
)和容器对noexcept
有特殊优化逻辑。 - 类型系统影响:函数指针的
noexcept
属性是类型的一部分(C++17起)。 - 如果一个函数的异常说明如下:
void foo (void) throw () {...}
表示该函数不抛出任何异常,属于已弃用标准。-
void foo() throw();
表示函数承诺不抛出任何异常。若违反(函数实际抛出异常),会调用std::unexpected()
,默认终止程序,但允许通过set_unexpected()
自定义处理。 -
C++11引入的
noexcept
替代方案,更高效且明确。 -
void foo() noexcept;
或void foo() noexcept(true)
(等价)。若违反(函数实际抛出异常),直接调用std::terminate()
终止程序,无自定义处理机会。 -
throw()
旧规范无法触发优化,且因运行时检查(调用unexpected
)可能引入额外开销。 -
throw()
仅支持无条件声明,灵活性差。noexcept
支持条件表达式,可根据编译时条件声明是否抛出异常。 -
C++17后
throw()
完全移除,noexcept
是唯一标准方式。throw()
的遗留问题,新项目应避免。
-
5. 使用的注意事项
-
性能开销
异常处理比普通返回码慢,应避免在频繁执行的代码路径中使用。 -
设计原则
- 仅用于处理不可恢复的错误(如文件不存在、内存不足)。
- 避免滥用异常控制正常流程。
-
如果是类类型的异常,catch时最好用引用。
class MyException {...}; throw MyException (...); catch (MyException& ex) { ... }
-
不要抛出局部变量(对象)的指针。
MyException ex (...); ... if (...) throw &ex; ... catch (MyException* ex) { ... }
-
资源管理(RAII)
栈展开时,局部对象的析构函数会被自动调用,确保资源(如内存、文件句柄)释放。 -
异常对象生命周期
throw
会生成匿名临时对象,catch
通过值、引用或指针接收该对象。- 若抛出局部对象,其拷贝可能被优化(如通过引用捕获)。
-
异常说明
如果一个函数不带任何异常说明,表示它可以抛出任何异常。
void foo (void) {...}
如果一个函数被说明为只抛出有限的异常,那么它一旦抛出该范围之外的异常,此异常无法被调用者捕获。
void foo (void) throw (int, MyException, string) {...}
-
子类中的覆盖版本不能比基类中的虚函数抛出更多的异常。
class A { virtual void foo (void) throw (int, double) {...} }; class B : public A { void foo (void) throw (int) {...} // void foo (void) throw (string) {...} // errror // void foo (void) {...} // error };
-
在类的构造函数中可以抛出异常,但是在抛出之前应该手动释放已分配的资源,因为这种对象是非完整对象,其析构函数不会执行。
class A { public: A (void) { m_p = new char[1024]; ... if (! fp) { delete[] m_p; throw 1; } } ~A (void) { delete[] m_p; } private: char* m_p; char m_data[1024]; }; try { A a; a.foo (); } catch (int ex) { .... }
-
永远不要在析构函数中抛出异常,也不要让异常从析构函数中被抛出。
A::~A { try { ... } catch (...) { } }
6.异常的使用示例
// 通过异常机制表示出错
#include <iostream>
#include <cstdio>
using namespace std;
class A {
public:
A (void) {
cout << "A构造" << endl;
}
~A (void) {
cout << "A析构" << endl;
}
};
void func3 (void) {
A a;
FILE* fp = fopen ("none.dat", "r");
if (! fp)
throw 1;
// ...
fclose (fp);
}
void func2 (void) {
A a;
func3 ();
// ...
}
void func1 (void) {
A a;
func2 ();
// ...
}
int main (void) {
try {
func1 ();
// ...
}
catch (int ex) {
if (ex == 1) {
cout << "打开文件失败!" << endl;
return -1;
}
}
// ...
return 0;
}
结果:
A构造
A构造
A构造
A析构
A析构
A析构
打开文件失败!