目录
[1. 异常处理的基本概念](#1. 异常处理的基本概念)
[1.1 什么是异常处理?](#1.1 什么是异常处理?)
[1.2 异常的基本语法](#1.2 异常的基本语法)
[2. 异常的抛出和捕获机制](#2. 异常的抛出和捕获机制)
[2.1 抛出异常](#2.1 抛出异常)
[2.2 栈展开](#2.2 栈展开)
[3. 异常匹配机制](#3. 异常匹配机制)
[3.1 匹配规则](#3.1 匹配规则)
[3.2 继承体系中的异常处理](#3.2 继承体系中的异常处理)
[4. 异常重新抛出](#4. 异常重新抛出)
[5. 异常安全](#5. 异常安全)
[5.1 资源泄漏问题](#5.1 资源泄漏问题)
[5.1.1 问题定义](#5.1.1 问题定义)
[5.1.2 常见类型](#5.1.2 常见类型)
[5.1.3 影响与后果](#5.1.3 影响与后果)
[5.2 解决方案](#5.2 解决方案)
[5.3 析构函数中的异常处理](#5.3 析构函数中的异常处理)
[6. 异常规范](#6. 异常规范)
[6.1 C++98 vs C++11异常规范](#6.1 C++98 vs C++11异常规范)
[6.2 现代C++异常规范](#6.2 现代C++异常规范)
[7. 标准库异常体系](#7. 标准库异常体系)
[7.1 标准异常类层次结构](#7.1 标准异常类层次结构)
[7.2 常用标准异常](#7.2 常用标准异常)
[8. 实际项目中的异常处理策略](#8. 实际项目中的异常处理策略)
[8.1 异常处理最佳实践](#8.1 异常处理最佳实践)
[9. 性能考虑](#9. 性能考虑)
[9.1 异常处理的成本](#9.1 异常处理的成本)
[9.2 优化建议](#9.2 优化建议)
[10. 总结](#10. 总结)
本文将深入探讨C++异常处理机制,涵盖核心概念、使用方法和最佳实践,帮助开发者构建更健壮的应用程序。
1. 异常处理的基本概念
1.1 什么是异常处理?
异常处理是C++中处理程序运行时错误的重要机制。与C语言通过错误码处理错误的方式不同,C++异常机制通过抛出对象来传递错误信息,提供了更加丰富和灵活的错误处理能力。
传统错误码处理 vs 异常处理对比:
特性 | 错误码处理 | 异常处理 |
---|---|---|
错误信息 | 有限的错误码 | 丰富的对象信息 |
传播方式 | 手动检查返回值 | 自动沿调用栈传播 |
代码结构 | 错误处理与业务逻辑混合 | 清晰的分离关注点 |
性能 | 无额外开销 | 有栈展开开销 |
1.2 异常的基本语法
C++异常处理使用三个关键字:try、catch 和 throw,构成完整的异常处理机制:
cpp
// 抛出异常
throw exception_object;
// 捕获异常
try {
// 可能抛出异常的代码
} catch (exception_type1& e) {
// 处理特定类型异常
} catch (exception_type2& e) {
// 处理其他类型异常
} catch (...) {
// 处理所有其他异常
}
2. 异常的抛出和捕获机制
2.1 抛出异常
当程序检测到错误时,通过throw表达式抛出一个异常对象:
cpp
double Divide(int a, int b) {
if (b == 0) {
// 抛出字符串异常
throw "Division by zero condition!";
}
return static_cast<double>(a) / b;
}
抛出异常的关键特性:
-
throw后面的语句不会被执行
-
控制权立即转移到匹配的catch块
-
会创建异常对象的拷贝
2.2 栈展开
-
抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理。
-
如果当前函数中没有try/catch子句,或者有try/catch子句但是类型不匹配,则退出当前函数,继续 在外层调用函数链中查找,上述查找的catch过程被称为栈展开。
-
如果到达main函数,依旧没有找到匹配的catch子句,程序会调用标准库的terminate函数终止程序。
-
如果找到匹配的catch子句处理后,catch子句代码会继续执行。

代码演示栈展开:
cpp
void func1() {
throw string("异常来自func1");
}
void func2() {
func1(); // 异常从这里抛出
}
void func3() {
func2();
}
int main() {
try {
func3(); // 调用链: main -> func3 -> func2 -> func1
} catch (const string& e) {
cout << "捕获异常: " << e << endl;
}
return 0;
}
3. 异常匹配机制
3.1 匹配规则
异常匹配遵循特定规则,支持以下转换类型:
转换类型 | 示例 | 说明 |
---|---|---|
权限缩小 | throw int → catch(const int) |
非常量到常量 |
指针转换 | throw int[] → catch(int*) |
数组到指针 |
继承转换 | throw Derived → catch(Base&) |
派生类到基类 |
3.2 继承体系中的异常处理
在项目实践中,我们通常采用继承体系来组织异常类结构:
cpp
// 异常基类
class Exception {
public:
Exception(const string& errmsg, int id)
: _errmsg(errmsg), _id(id) {}
virtual string what() const {
return _errmsg;
}
int getid() const {
return _id;
}
protected:
string _errmsg;
int _id;
};
// 具体异常类型
class SqlException : public Exception {
public:
SqlException(const string& errmsg, int id, const string& sql)
: Exception(errmsg, id), _sql(sql) {}
virtual string what() const override {
return "SqlException:" + _errmsg + "->" + _sql;
}
private:
const string _sql;
};
class CacheException : public Exception {
public:
CacheException(const string& errmsg, int id)
: Exception(errmsg, id) {}
virtual string what() const override {
return "CacheException:" + _errmsg;
}
};
继承异常体系的优势:
-
提供统一的异常处理接口
-
实现异常的多态处理能力
-
提升代码的可扩展性和可维护性
4. 异常重新抛出
有时catch到一个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对象直接抛出。
cpp
void SendMsg(const string& s) {
// 最多重试3次
for (size_t i = 0; i < 4; i++) {
try {
_SeedMsg(s); // 尝试发送消息
break; // 成功则退出循环
} catch (const Exception& e) {
// 网络不稳定错误,尝试重试
if (e.getid() == 102) {
if (i == 3) {
throw; // 重试3次后仍失败,重新抛出
}
cout << "开始第" << i + 1 << "次重试" << endl;
} else {
throw; // 其他错误直接重新抛出
}
}
}
}
5. 异常安全
5.1 资源泄漏问题
5.1.1 问题定义
资源泄漏是指程序在运行过程中未能正确释放已分配的系统资源(如内存、文件句柄、数据库连接等),导致这些资源无法被其他程序或后续操作重新利用的现象。
5.1.2 常见类型
(1) 内存泄漏
-
动态分配的内存未被释放
-
示例:
cppvoid memory_leak() { char *buffer = malloc(1024); // 忘记调用 free(buffer) }
(2)文件句柄泄漏
-
打开的文件未关闭
-
示例:
pythondef file_leak(): f = open("data.txt", "r") # 忘记调用 f.close()
(3)数据库连接泄漏
-
获取的数据库连接未释放
-
示例(Java JDBC):
javapublic void dbLeak() throws SQLException { Connection conn = DriverManager.getConnection(url); // 忘记调用 conn.close() }
(4)图形资源泄漏
-
图形界面中的GDI对象未释放
-
示例(Windows API):
cppvoid gdiLeak() { HDC hdc = GetDC(hWnd); // 忘记调用 ReleaseDC(hWnd, hdc) }
5.1.3 影响与后果
-
系统性能下降:累积的泄漏会导致可用资源减少
-
程序崩溃:当资源耗尽时程序可能异常终止
-
系统不稳定:可能影响其他程序的正常运行
-
安全风险:可能被利用进行拒绝服务攻击
5.2 解决方案
方案1:使用try-catch确保资源释放
cpp
void SafeFunc1() {
int* array = new int[10];
try {
SomeOperationThatMightThrow();
} catch (...) {
delete[] array; // 异常时释放资源
throw; // 重新抛出异常
}
delete[] array; // 正常流程释放资源
}
方案2:使用RAII技术(推荐)
RAII(Resource Acquisition Is Initialization)是C++中管理资源的重要技术,其核心思想是将资源生命周期与对象生命周期绑定。
cpp
// 使用智能指针自动管理资源
#include <memory>
void SafeFunc2() {
std::unique_ptr<int[]> array(new int[10]);
// 即使抛出异常,array也会自动释放
SomeOperationThatMightThrow();
// 不需要手动delete,unique_ptr会自动处理
}
5.3 析构函数中的异常处理
在析构函数中抛出异常是极其危险的编程实践,可能导致程序异常终止或资源泄漏。主要原因包括:
**1.**栈展开机制冲突 当异常发生时,C++会进行栈展开(stack unwinding)过程,在此期间会调用对象的析构函数。如果析构函数本身又抛出异常,就会导致同时存在两个未处理的异常,此时程序会调用 std::terminate() 强制终止。
示例场景:
cpp
class ResourceHolder {
public:
~ResourceHolder() {
if (cleanup_failed) {
throw std::runtime_error("Cleanup failed"); // 危险操作!
}
}
};
**2.**资源泄漏风险 析构函数通常负责释放资源,如果抛出异常,可能导致资源释放不完全。例如:
-
未正确关闭文件描述符
-
未及时释放内存资源
-
未断开数据库连接
**3.**推荐处理方式应在析构函数内部捕获并处理所有异常:
cpp
~ResourceHolder() {
try {
// 资源清理代码
} catch (...) {
// 记录日志或采取其他恢复措施
std::cerr << "析构中发生异常" << std::endl;
}
}
**4.**特殊注意事项
-
对于noexcept声明的析构函数,抛出异常会直接导致std::terminate调用
-
某些标准库容器(如std::vector)对元素类型的析构函数有异常安全要求
-
在多重继承场景下,异常处理会更加复杂
安全实践建议:
-
避免在析构函数中执行可能抛出异常的操作
-
如果必须执行,确保在析构函数内部处理所有异常
-
使用RAII模式管理资源,将复杂操作移到普通成员函数中
6. 异常规范
6.1 C++98 vs C++11异常规范
版本 | 语法 | 说明 |
---|---|---|
C++98 | throw() |
不抛出任何异常 |
C++98 | throw(type1, type2) |
可能抛出指定类型异常 |
C++11 | noexcept |
不抛出任何异常 |
C++11 | noexcept(expr) |
条件性异常说明 |
6.2 现代C++异常规范
现代C++提供了更完善的异常处理机制,主要包括以下特性:
noexcept规范
1. 基本用法 :noexcept
关键字用于指定函数是否会抛出异常
cpp
void func() noexcept; // 保证不抛出异常
void func2() noexcept(true); // 等价于noexcept
void func3() noexcept(false); // 可能抛出异常
2. 条件性noexcept:可以根据表达式结果决定是否noexcept
cpp
template <typename T>
void swap(T& a, T& b) noexcept(noexcept(a.swap(b)));
3. 移动构造函数/赋值运算符:标准库容器会对noexcept移动操作进行优化
cpp
class MyClass {
public:
MyClass(MyClass&&) noexcept; // 推荐标记为noexcept
};
7. 标准库异常体系
7.1 标准异常类层次结构
-
C++标准库也定义了一套自己的异常继承体系,库基类是exception,所以我们日常写程序,需要在主函数捕获exception即可.要获取异常信息,调用what函数,what是一个虚函数,派生类可以重写。

7.2 常用标准异常
异常类型 | 说明 | 典型应用场景 |
---|---|---|
std::logic_error |
程序逻辑错误 | 前置条件检查 |
std::runtime_error |
运行时错误 | 外部因素导致的错误 |
std::bad_alloc |
内存分配失败 | new操作失败 |
std::out_of_range |
访问越界 | 容器访问操作 |
8. 实际项目中的异常处理策略
8.1 异常处理最佳实践
1. 合理设计异常层次结构
cpp
class MyProjectException : public std::exception {
// 项目统一的异常基类
};
class NetworkException : public MyProjectException {
// 网络相关异常
};
class DatabaseException : public MyProjectException {
// 数据库相关异常
};
2.在适当的层次捕获异常
cpp
void processRequest() {
try {
parseRequest();
validateData();
saveToDatabase();
sendResponse();
} catch (const DatabaseException& e) {
// 数据库错误,可能重试或回滚
handleDatabaseError(e);
} catch (const NetworkException& e) {
// 网络错误,可能重试
handleNetworkError(e);
} catch (const std::exception& e) {
// 其他标准异常
logError(e);
throw; // 重新抛出
}
}
使用异常安全的编程模式
-
优先使用RAII管理资源
-
避免在析构函数中抛出异常
-
使用swap技法实现强异常安全保证
9. 性能考虑
9.1 异常处理的成本
异常处理的性能开销主要出现在异常抛出时,而非正常流程中。具体开销来源包括:
-
栈展开过程中的资源清理
-
异常对象的构造与拷贝
-
异常类型匹配的查找过程
9.2 优化建议
-
仅在真正异常情况下使用异常处理
-
避免在程序关键性能路径上使用异常机制
-
采用移动语义降低异常对象拷贝带来的性能损耗
10. 总结
C++异常处理作为一种强大的错误处理机制,相比传统错误码方式,能提供更清晰安全的错误处理方案。通过合理设计异常体系、正确应用RAII技术并遵循最佳实践,可以构建出兼具健壮性和可维护性的C++应用程序。
需要注意的是,异常处理并非适用于所有场景。在性能关键型应用中,可能需要考虑其他错误处理方案。但对于大多数应用程序而言,合理使用异常处理能有效提升代码质量和可维护性。
参考资料:
-
《Effective C++》条款8:别让异常逃离析构函数
-
《C++ Primer》第5版,异常处理章节