C++异常机制:抛出、捕获与栈展开
作者:无限进步_
C++异常机制通过 throw(抛出)、try/catch(捕获)和栈展开(Stack Unwinding)实现结构化错误处理,核心是“检测与分离”+“自动资源清理”。
- 抛出(
throw):在检测到错误时用throw 表达式;抛出一个异常对象(可为内置类型、字符串或类对象,推荐继承std::exception);抛出后立即跳转,后续代码不执行,并生成异常对象的拷贝用于传播。 - 捕获(
try/catch):try块包裹可能异常的代码,后续接一个或多个catch(类型 参数)处理匹配异常;类型需精确匹配或允许隐式转换(如const权限缩小、派生类→基类引用/值);catch(...)为万能兜底,但无法获取异常详情;匹配的是离抛出点最近的、类型兼容的catch。 - 栈展开(Stack Unwinding):若当前函数无匹配
catch,自动逆序析构当前函数所有已构造的局部对象,逐层返回调用栈,直至找到匹配catch或抵达main未捕获则调用std::terminate()终止程序;此过程确保 RAII 资源(如锁、内存)正确释放,但析构函数自身不得抛异常(否则立即terminate)。
异常传播中,throw;(无表达式)可在 catch 内重新抛出原异常(保持原始类型);C++11 起弃用 throw() 规格说明,改用 noexcept 声明“不抛异常”,违反则直接 terminate。合理使用可分离错误逻辑与主流程,但滥用影响性能与可读性。
错误处理是程序里绕不开的话题。C语言用错误码,函数返回一个int,调用方去查表,麻烦不说,还容易把业务逻辑和错误处理搅在一起。C++的异常机制提供了一种不同的思路:把发现错误和处理错误的代码分开。出错的模块只负责抛出异常,至于怎么处理,由调用链上合适的捕获点决定,两边互不侵入。
这篇文章把异常的抛出、捕获、栈展开过程以及类型匹配规则梳理一遍,最后用一组自定义异常体系展示实际项目中怎么用。
1. 抛出与捕获的基本姿势
1.1 throw干了什么
当程序运行到throw语句时,会构造一个异常对象,然后函数的剩余代码不再执行,控制权开始沿着调用链向上转移,寻找匹配的catch。这个过程有三个关键点:
- throw后面的语句被跳过。
- 当前函数可能提前退出。
- 局部对象会按顺序析构(栈展开时完成)。
throw的异常对象如果是局部的,会生成一份拷贝交给catch,原对象在栈展开过程中销毁。这份拷贝会在catch处理完毕后销毁。
1.2 try/catch怎么接
void Func() {
int len, time;
cin >> len >> time;
try {
cout << Divide(len, time) << endl;
} catch (const char* errmsg) {
cout << errmsg << endl;
}
cout << "Func continuing..." << endl;
}
double Divide(int a, int b) {
if (b == 0) {
string s("Divide by zero condition!");
throw s;
}
return (double)a / (double)b;
}Divide里抛出的是string对象,而Func里捕获的类型是const char*,不匹配。所以Func的catch会被跳过,继续向外层调用者(这里是main)寻找匹配的catch(string)。如果main也找不到,程序就调用terminate终止。
catch的匹配规则:
- 一般情况下要求类型完全匹配。
- 允许从非常量向常量的转换(权限缩小方向),但不允许其他隐式类型转换(比如int转double)。
- 数组会被转换为指向元素类型的指针,函数转换为函数指针。
- 支持派生类向基类的转换。这个在实际项目中非常实用,后面会展开。
如果外层没有匹配的catch,main函数最后通常会写一个catch(...)兜底,防止程序崩溃,但它只能捕获,没法知道具体错误信息。
2. 栈展开:异常的传播路径
栈展开(stack unwinding)是理解异常行为的关键。从throw开始,编译器会依次查找:
- 检查throw本身是否在try块内。如果在,看当前try块的catch有没有匹配的。
- 有匹配就跳到catch执行,之后从这个catch结束后的第一条语句继续运行。
- 没有匹配,则退出当前函数,析构所有局部对象,回到调用方函数的调用点,重复第一步。
- 到达main还没匹配,调用
terminate终止程序。
假设调用链是:main() → func3() → func2() → func1(),func1抛出异常,catch在main里。它会依次退出func1、func2、func3的栈帧,直到在main里找到匹配的catch。这里“退出”不是简单的跳转,而是沿着调用链,层层析构局部对象,这保证了RAII资源能被正确释放。
3. 自定义异常体系:利用派生类到基类的转换
在实际项目中,通常不会到处抛string或基本类型,而是构建一个异常类体系,基类可以是std::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) {}
string what() const override {
return "SqlException:" + _errmsg + "->" + _sql;
}
private:
string _sql;
};
class CacheException : public Exception {
public:
CacheException(const string& errmsg, int id)
: Exception(errmsg, id) {}
string what() const override {
return "CacheException:" + _errmsg;
}
};
class HttpException : public Exception {
public:
HttpException(const string& errmsg, int id, const string& type)
: Exception(errmsg, id), _type(type) {}
string what() const override {
return "HttpException:" + _type + ":" + _errmsg;
}
private:
string _type;
};业务函数模拟抛出各类异常:
void SQLMgr() {
if (rand() % 7 == 0)
throw SqlException("权限不足", 100, "select * from name = '张三'");
cout << "SQLMgr 调用成功" << endl;
}
void CacheMgr() {
if (rand() % 5 == 0)
throw CacheException("权限不足", 100);
else if (rand() % 6 == 0)
throw CacheException("数据不存在", 101);
cout << "CacheMgr 调用成功" << endl;
SQLMgr();
}
void HttpServer() {
if (rand() % 3 == 0)
throw HttpException("请求资源不存在", 100, "get");
else if (rand() % 4 == 0)
throw HttpException("权限不足", 101, "post");
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 << "Unknown Exception" << endl;
}
}
}捕获基类引用配合虚函数what(),既能拿到具体类型的信息,又不需要写一堆分支类型的catch。新增一个派生类异常,只要继承Exception并重写what(),上层代码一行不改。这就是开放-封闭原则在错误处理中的体现。
这里有一个细节:catch的参数应该用引用,否则会发生拷贝切片,派生类的额外信息就丢了。
小结
异常机制的本质是把错误检测和处理解耦,代价是引入了栈展开带来的控制流复杂性和资源管理风险。下一篇文章会顺着这个思路往下走:异常重新抛出、异常安全问题、以及C++11引入的noexcept规范——它们试图解决“异常本身带来的问题”。
到此这篇关于C++异常机制:抛出、捕获与栈展开的文章就介绍到这了,更多相关C++异常抛出、捕获与栈展开内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
