目录
[1. C 语言传统的处理错误的方式](#1. C 语言传统的处理错误的方式)
[2. C++ 异常概念](#2. C++ 异常概念)
[3. 异常的用法](#3. 异常的用法)
[3.1 异常的抛出和捕获](#3.1 异常的抛出和捕获)
[3.2 异常的重新抛出](#3.2 异常的重新抛出)
[3.3 异常安全 (Exception Safety)](#3.3 异常安全 (Exception Safety))
[3.4 异常规范 (Exception Specifications)](#3.4 异常规范 (Exception Specifications))
[4. 自定义异常体系](#4. 自定义异常体系)
[5. 标准库异常体系](#5. 标准库异常体系)
[6. 异常的优缺点](#6. 异常的优缺点)
在 C++ 的开发过程中,错误处理是保证软件健壮性的核心环节。不同于 C 语言的"返回码"模式,C++ 提供了一套完善的**异常(Exception)**机制。本文将系统梳理 C++ 异常的知识体系,帮助你在阅读时快速抓住重点。
1. C 语言传统的处理错误的方式
在 C 语言时代,我们通常通过返回值来判断函数执行是否成功。这种方式简单直接,但也存在明显的缺陷。
终止程序 :遇到严重错误直接调用
abort()或exit()。这对于库函数来说是不可接受的,因为直接杀死了整个进程。返回错误码 :函数返回
int或enum,通过特定的数字(如 -1)或全局变量errno来标识错误。
cpp
int openFile(const char* filename)
{
FILE* f = fopen(filename,"r");
if (f == NULL) {
return -1; // 返回错误码
}
// ...后续正常逻辑
return 0; // 正常退出码
}
// 缺陷:必须显示检查返回值
if (openFile("config.txt") < 0) {
perror("Open faild");
}
传统方式的痛点:
逻辑混淆 :错误处理代码与业务逻辑代码高度耦合,代码可读性差(到处都是
if-else)。容易忽略:调用者可能忘记检查返回值,导致程序在错误状态下继续运行。
信息有限:仅凭一个错误码很难携带详细的错误描述信息。
2. C++ 异常概念
C++ 的异常机制是为了解决上述问题而生的。它是一种控制流转移机制。
核心思想 :当一个函数发现自己无法处理的错误时,它会抛出(throw)一个异常,让函数的调用者(或调用者的调用者)去捕获(catch)并处理这个错误。
栈展开(Stack Unwinding) :当异常被抛出后,程序控制流会沿着调用栈向上回溯,直到找到匹配的
catch块。在这个过程中,栈上已经构造的局部对象会自动调用析构函数,这是 C++ 异常安全的基础。
3. 异常的用法
3.1 异常的抛出和捕获
C++ 使用 try、catch 和 throw 关键字。
throw:抛出异常对象(可以是基本类型,也可以是类对象)。
try:包裹可能抛出异常的代码块。
catch:捕获特定类型的异常。
cpp
void divide(int a, int b)
{
if (0 == b) {
throw string("Division by zero condition");
}
cout << "Result : " << a / b << endl;
}
int main()
{
try {
divide(10, 0);
}
catch (const string & e){ // 捕获异常类型为string
cout << "Error caught :" << e << endl;
}
catch (...) { // 捕获前面未被捕获的异常
cout << "Unknown erro occured." << endl;
}
}

异常的抛出和匹配原则
异常是通过抛出对象而引发 的,该对象的类型决定了应该激活哪个catch的处理代码。
被选中的处理代码 是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。
实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用,我们后面会详细讲解这个。
在函数调用链中异常栈展开匹配原则
-
首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
-
没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
-
如果到达main函数的栈,依旧没有匹配的,则终止程序(会报错) 。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
-
找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

重点:
按引用捕获 :始终使用
catch (const Exception& e)。如果按值捕获(catch (Exception e)),会发生对象切片(Slicing),导致子类特有的信息丢失,且有拷贝开销。异常匹配顺序 :
catch块是按顺序匹配的,必须将子类异常放在父类异常之前,否则子类异常永远无法被捕获。一般我们会在main函数处理各种异常,但是有的异常需要在栈中就处理了,也有的只处理了一部分然后继续向外面抛出。
trow的后续代码都不执行
3.2 异常的重新抛出
有时候,我们在当前层捕获异常只是为了记录日志或做部分清理,但无法完全处理它,需要通知更上层。这时可以使用不带参数的 throw。
cpp
void networkOperation() {
try {
// 假设这里抛出了连接超时的异常
connectToServer();
} catch (const std::exception& e) {
logError(e.what()); // 1. 记录日志
throw; // 2. 重新抛出原异常,交给上层处理
}
}
有些时候还会面临内存泄漏的风险需要先对资源清理再重新抛出交给外层处理。
cpp
void divide(int a, int b)
{
if (0 == b) {
throw string("Division by zero condition");
}
cout << "Result : " << a / b << endl;
}
void Func()
{
int* arr = new int[10];
try {
divide(10, 0);
}
catch(const string & e){
cout << "delete []" << arr << endl;
delete[] arr;
throw; // 再抛出除0异常给外层处理
}
// throw后的代码都不执行了
cout << "delete []" << endl;
delete[] arr;
}

3.3 异常安全 (Exception Safety)
这是 C++ 异常编程中最核心、也是最难的概念。异常安全是指:当异常发生时,程序不会出现内存泄漏,且数据处于有效状态。
实现异常安全的基石是 RAII (资源获取即初始化) 。不要手动 new/delete,而是使用智能指针(std::shared_ptr, std::unique_ptr)和栈对象。
异常安全的三个保证等级:
基本保证 (Basic Guarantee):如果异常发生,程序中没有资源泄露,对象处于某个有效状态(但不确定是哪个状态)。
强保证 (Strong Guarantee) :事务性语义。操作要么完全成功,要么像没发生过一样(回滚到操作前的状态)。
- 例子 :
std::vector::push_back具有强保证。如果扩容失败,vector 保持原样。无异常保证 (No-throw Guarantee):承诺函数绝不抛出异常。
- 例子:析构函数、移动构造函数通常必须是无异常的。
为了保证上面的三个等级的实现,建议遵守以下的规定:
- 构造函数完成对象的构造和初始化 , 最好不要 在构造函数中抛出异常,否则 可能导致对象不 完整或没有完全初始化
- 析构函数主要完成资源的清理 , 最好不要 在析构函数内抛出异常,否则 可能导致资源泄漏 ( 内存泄漏、句柄未关闭等)
- C++ 中异常经常会导致资源泄漏的问题,比如在 new 和 delete 中抛出了异常,导致内存泄漏,在lock 和 unlock 之间抛出了异常导致死锁, C++ 经常使用 RAII 来解决以上问题,关于 RAII我们智能指针这节进行讲解
3.4 异常规范 (Exception Specifications)
C++98 风格(已废弃) :
void func() throw(int, char);表示只抛出 int 或 char。这种写法在现代 C++ 中应避免。C++11 及以后 :
noexcept。
cpp
// 告诉编译器和调用者,这个函数不会抛出异常
// 有助于编译器进行优化(如 std::vector 扩容时使用移动而非拷贝)
void swap(int& a, int& b) noexcept {
int temp = a;
a = b;
b = temp;
}
4. 自定义异常体系
在实际项目中,我们通常需要定义自己的异常类。
最佳实践 :不要抛出 int 或 std::string,而是继承自 std::exception 及其子类。
这里写了一个demo:用自定义异常体系来throw网络通信中可能会发生的异常
cpp
class Exception
{
public:
Exception(const string& msg,int id)
:_errmsg(msg),
_id(id)
{}
virtual string what() const = 0;
protected:
string _errmsg;
int _id;
};
class SqlException : public Exception
{
public:
SqlException(const string & msg, int id,string sql)
:Exception(msg,id),
_sql(sql)
{}
virtual string what() const
{
string str = "SqlException";
str += _errmsg;
str += "->";
str += _sql;
return str;
}
private:
const string _sql;
};
class CacheException : public Exception
{
public:
CacheException(const string & msg, int id)
:Exception(msg,id)
{}
virtual string what() const
{
string str = "CacheException";
str += _errmsg;
return str;
}
};
void CacheMgr()
{
srand(time(0));
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw CacheException("数据不存在", 101);
}
}
void SQLMgr()
{
if (rand() % 3 == 0) {
throw SqlException("权限不足 ", 100, "select * from name = 'mike'");
}
else if (rand() % 4 == 0){
throw SqlException("表不存在 ", 101, "desc table1");
}
CacheMgr();
}
int main()
{
srand(time(0));
while (1)
{
this_thread::sleep_for(chrono::seconds(1));
try {
SQLMgr();
cout << "数据发送成功 " << endl << endl;
}
catch (const Exception & e) // 直接捕获父对象
{
cout << e.what() << endl << endl; // 多态
}
catch (...) {
cout << "Unkown Exception " << endl << endl;
}
}
}
5. 标准库异常体系
C++ 标准库定义了一套异常继承体系,根类是 std::exception
说明 :实际中我们可以可以去继承 exception 类实现自己的异常类。但是实际中很多公司像上面一
样自己定义一套异常继承体系。因为 C++ 标准库设计的不够好用。
|------------------------|----------------------------------------------|----------------------------------------------------------------------------------|
| 异常类型 | 描述 | 常见子类 |
| std::logic_error | 逻辑错误。通常是程序的 Bug, theoretically 可以在运行前修正。 | std::invalid_argument (参数无效) std::out_of_range (数组/容器越界) std::domain_error |
| std::runtime_error | 运行时错误。程序运行期间发生的不可预见事件,非代码逻辑本身问题。 | std::overflow_error (算术上溢) std::underflow_error std::system_error |
| 其他 | 直接继承自 exception | std::bad_alloc (new 失败) std::bad_cast (dynamic_cast 失败) |
6. 异常的优缺点
优点
清晰的控制流:将正常业务逻辑和错误处理代码分离,代码结构更清晰。
强制处理:不同于返回码可以被忽略,异常如果没有被捕获,程序会直接崩溃(terminate),这迫使开发者必须关注错误。
部分函数错误报告:构造函数没有返回值,如果初始化失败,抛出异常是报告错误的唯一合理方式。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。
携带丰富信息:异常对象可以包含错误码、错误描述、堆栈信息等多种数据。
缺点
执行流混乱 :异常会导致程序的执行流"跳跃",使得代码的实际执行路径难以追踪(类似
goto),增加了调试难度。性能开销 :虽然现代编译器对
try-catch的"零开销"优化做得很好(进入 try 块几乎无开销),但一旦抛出 异常,栈展开的开销是非常大的。因此,异常不应用于控制正常的程序逻辑(如循环结束)。异常安全难保证:编写异常安全的代码(即在任何异常抛出时都不泄露资源)需要极高的编码素养和严格使用 RAII。
代码膨胀:异常处理机制会增加编译后的二进制文件大小。
结语:驾驭异常,构建健壮系统
C++ 的异常机制不仅仅是一套语法规则,更是一种将"错误处理"与"业务逻辑"解耦 的设计哲学。从 C 语言时代的 errno 和错误码,到 C++ 的 try-catch,我们获得了更优雅的代码结构和更强的错误传递能力。
但在实际应用中,请务必牢记:异常是一把双刃剑。
异常安全的基石是 RAII :没有 RAII(资源获取即初始化)的配合,异常机制极易导致内存泄漏和资源死锁。**"在栈展开时自动调用析构函数"**是 C++ 异常最核心的机制,也是我们编写异常安全代码的唯一依赖。请养成使用智能指针(
std::shared_ptr,std::unique_ptr)管理资源的肌肉记忆。区分"错误"与"异常":不要将异常用于正常的控制流(如循环结束、查找未找到)。异常应该留给那些**"极其罕见、当前上下文无法处理、必须打断程序执行"**的情况。
规范与体系 :在大型项目中,建立一套清晰的继承自
std::exception的自定义异常体系,并严格遵守noexcept规范,是保证系统可维护性的关键。
掌握 C++ 异常,意味着你不再仅仅关注代码"怎么跑通",而是开始思考代码在"跑不通"时如何优雅地着陆。这也是从 C++ 初学者迈向资深工程师的重要一步。