前言
在 C++ 编程中,异常处理是一种强大的机制,能够帮助程序在运行时优雅地处理错误,避免崩溃,提高程序的健壮性和可维护性。相比 C 语言通过错误码进行错误处理的方式,C++ 的异常处理提供了更清晰、更结构化的错误处理方法。本文将详细介绍 C++ 异常的概念、异常的抛出与捕获、栈展开、异常匹配、异常的重新抛出、异常安全以及异常规范等内容。
1. 异常的概念
在 C++ 中,异常(Exception)是一种特殊的事件,当程序运行过程中发生错误时,程序可以抛出异常,并由相应的异常处理代码进行处理。
异常处理的核心思想
异常处理的核心思想是将问题的检测与问题的解决分开:
- 检测错误的代码 负责发现问题,并抛出异常。
- 异常处理代码 负责捕获异常并执行相应的错误处理逻辑。
C 语言与 C++ 异常的区别
在 C 语言中,通常使用 错误码 进行错误处理:
- 需要手动检查函数的返回值,以判断是否出现错误。
- 错误码需要进行分类编号,程序需要额外的错误查询机制,处理逻辑复杂。
C++ 通过异常对象进行错误处理:
- 直接抛出一个异常对象,其中可以包含完整的错误信息。
- 由异常处理机制自动捕获异常,避免手动检查返回值,使代码更清晰。
2. 异常的抛出与捕获
当程序检测到错误时,可以使用 throw
关键字抛出异常对象,并使用 catch
语句捕获异常进行处理。
异常的处理流程
- 发生错误时,程序会
throw
一个异常对象。 - 异常会沿着调用链向上传播,直到找到匹配的
catch
语句。 - 如果找到匹配的
catch
,则执行其中的异常处理代码。 - 如果没有找到匹配的
catch
,则程序会终止。
抛出异常的规则
throw
语句后面的代码不会被执行,程序会立即跳转到匹配的catch
语句。- 异常对象会被复制一份,并在
catch
语句处理后销毁。 catch
语句必须与异常的类型匹配,否则异常会继续向上传播。
匹配规则
- 严格匹配:
catch
语句的类型必须与throw
语句的类型一致,或者是它的基类。 - 基类匹配:如果
catch
语句的参数是基类对象,可以捕获派生类异常(多态)。 - 通配符捕获:使用
catch (...)
语句,可以捕获所有类型的异常。
throw
语句后面的代码不会被执行,程序会立即跳转到匹配的 catch
语句。根据错误类型,会跳到main函数的catch语句。
#include <iostream>
#include <vector>
using namespace std;
double divide(int a, int b) {
try {
if (b == 0) {
string error = "Divide by zero";
throw error;
}
else
return (double)a / b;
}
catch (int errid) {
cout << errid << endl;
}
return 0;
}
void Func(){
int len, time;
cin >> len >> time;
try
{
cout << divide(len, time) << endl;
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}
int main() {
while (1) {
try {
Func();
}
catch (const string& errmsg) {
cout << errmsg << endl;
}
}
return 0;
}
下面j就是调用最近的Func函数的catch语句
double divide(int a, int b) {
if (b == 0) {
throw string("Divide by zero"); // 直接抛出 std::string
}
return (double)a / b;
}
void Func() {
int len, time;
cin >> len >> time;
try {
cout << divide(len, time) << endl;
}
catch (const string& errmsg) {
cout << "Error in Func: " << errmsg << endl;
}
cout << __FUNCTION__ << ":" << __LINE__ << " 行执行" << endl;
}
int main() {
while (true) {
try {
Func();
}
catch (const string& errmsg) {
cout << "Error in main: " << errmsg << endl;
}
}
return 0;
}
3. 栈展开
栈展开(Stack Unwinding)指的是当异常发生时,程序会沿着函数调用栈回溯,查找匹配的 catch
语句的过程。
栈展开的过程
- 检查当前函数 是否有匹配的
catch
语句:- 如果有,跳转到
catch
语句处理异常。 - 如果没有,退出当前函数,并在上层调用链中继续查找。
- 如果有,跳转到
- 清理局部变量:
- 退出函数时,会调用当前作用域内的局部对象的析构函数,释放资源。
- 继续向上查找
catch
:- 如果找到了匹配的
catch
,程序继续执行catch
语句中的代码。 - 如果一直找不到
catch
,最终会到达main
函数。
- 如果找到了匹配的
- 程序终止:
- 如果
main
函数也没有捕获异常,程序会调用std::terminate()
终止执行。
- 如果
4. 异常的匹配规则
当异常被抛出时,程序会寻找匹配的 catch
语句进行处理,匹配规则如下:
- 完全匹配:抛出的异常类型和
catch
语句的参数类型完全一致。 - 常量转换:允许从非常量向常量转换(如
int
→const int
)。 - 数组和指针转换:允许数组转换成指向数组元素类型的指针。
- 派生类向基类转换:允许派生类异常被基类的
catch
语句捕获(多态)。 - 通配符匹配:
catch (...)
可捕获所有异常,但无法识别异常类型。
#include<thread>
// ⼀般⼤型项⽬程序才会使⽤异常,下⾯我们模拟设计⼀个服务的⼏个模块
// 每个模块的继承都是Exception的派⽣类,每个模块可以添加⾃⼰的数据
// 最后捕获时,我们捕获基类就可以
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
{
string str = "SqlException:";
str += _errmsg;
str += "->";
str += _sql;
return str;
}
private:
const string _sql;
};
class CacheException : public Exception
{
public:
CacheException(const string& errmsg, int id)
:Exception(errmsg, id)
{}
virtual string what() const
{
string str = "CacheException:";
str += _errmsg;
return str;
}
};
class HttpException : public Exception
{
public:
HttpException(const string& errmsg, int id, const string& type)
:Exception(errmsg, id)
, _type(type)
{}
virtual string what() const
{
string str = "HttpException:";
str += _type;
str += ":";
str += _errmsg;
return str;
}
private:
const string _type;
};
void SQLMgr()
{
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
else
{
cout << "SQLMgr 调用成功" << endl;
}
}
void CacheMgr()
{
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw CacheException("权限不存在", 101);
}
else
{
cout << "CacheMgr 调用成功" << endl;
}
SQLMgr();
}
void HttpServer()
{
if (rand() % 3 == 0)
{
throw HttpException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw HttpException("权限不足", 101, "post");
}
else
{
cout << "HttpServer调用成功" << endl;
}
CacheMgr();
}
int main()
{
srand(time(0));
while (1)
{
this_thread::sleep_for(chrono::seconds(1));
try
{
HttpServer();
}
catch (const Exception& e) // 这⾥捕获基类,基类对象和派⽣类对象都可以被捕获
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
return 0;
}
5. 异常的重新抛出
在某些情况下,一个 catch
语句可能需要部分处理异常,然后重新抛出它,让更外层的 catch
处理剩余部分。可以使用 throw;
重新抛出当前异常。
典型应用
- 先记录错误日志,然后重新抛出异常,让外层代码进行更详细的处理。
#include <iostream>
using namespace std;
void func() {
int* arr = new int[10]; // 申请内存
try {
throw runtime_error("Memory error"); // 模拟异常
}
catch (const exception& e) {
cout << "Error in func: " << e.what() << endl;
delete[] arr; // 释放资源
throw; // 重新抛出异常
}
}
int main() {
try {
func();
}
catch (const exception& e) {
cout << "Main caught: " << e.what() << endl;
}
return 0;
}
6. 异常安全问题
异常处理的一个关键问题是如何确保资源不会因为异常而泄漏,例如:
- 动态内存分配:如果在
new
之后异常被抛出,delete
可能不会被执行,导致内存泄漏。 - 锁管理:如果在获取锁后抛出异常,而没有释放锁,则可能导致死锁。
- 文件操作:打开文件后,异常可能导致文件未能正确关闭。
解决方案
RAII(资源获取即初始化)
- 资源在构造函数中分配,在析构函数中释放,确保异常安全。
- 例如使用 智能指针(
std::unique_ptr
和std::shared_ptr
) 来管理内存。
try-catch 处理资源释放
- 在
catch
语句中手动释放资源,然后重新抛出异常。
- 在
避免异常逃离析构函数
- 析构函数不应抛出异常,否则会导致程序
std::terminate()
终止。
- 析构函数不应抛出异常,否则会导致程序
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array没有得到释放。
// 所以这⾥捕获异常后并不处理异常,异常还是交给外层处理,这⾥捕获了再重新抛出去。
int* array = new int[10];
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
// 捕获异常释放内存
cout << "delete []" << array << endl;
delete[] array;
throw; // 异常重新抛出,捕获到什么抛出什么
}
cout << "delete []" << array << endl;
delete[] array;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
return 0;
}
7. 异常规范
C++ 提供了一些异常规范,用于声明函数是否会抛出异常。
C++98 的异常规范
throw()
:表示函数不会抛出任何异常。throw(int, char, std::exception)
:表示函数可能抛出指定类型的异常。
⚠ 问题:C++98 的异常规范很复杂,难以维护,并且编译器不会强制检查。
C++11 的 noexcept
noexcept
关键字替代了throw()
,表示函数不会抛出异常:void func() noexcept;
noexcept(expression)
:可以检测表达式是否可能抛出异常:bool canThrow = noexcept(func());
- 如果
noexcept
修饰的函数抛出异常,程序会终止!
实践建议
- 仅在确保函数不会抛出异常时使用
noexcept
,否则可能导致程序异常终止。
总结
C++ 的异常处理机制提供了一种清晰、结构化的错误处理方式,避免了 C 语言繁琐的错误码处理。关键点如下:
throw
抛出异常,catch
捕获异常,异常对象可以携带详细错误信息。- 异常沿着调用链传播(栈展开),直到找到匹配的
catch
,否则程序终止。 - 异常安全 是编写健壮代码的重要原则,应使用 RAII、智能指针 等机制避免资源泄漏。
- C++11
noexcept
规范化了异常处理,提高了代码的可维护性和优化能力。
掌握 C++ 异常处理,将有助于编写更加稳定和健壮的程序! 🚀