为什么需要 C++ 异常
C 语言的错误处理方式存在明显缺陷,这也是 C++ 引入异常的原因。
C 语言传统错误处理方式
-
**终止程序(如 assert)**直接终止程序,用户体验极差,适用于致命错误(如内存错误),但无法恢复。
#include <assert.h> int Division(int a, int b) { assert(b != 0); // b为0时直接终止程序,打印断言信息 return a / b; } -
返回错误码函数返回特定值表示错误(如 - 1、errno),但需程序员手动检查,且深层调用需层层传递错误码,代码冗余。
#include <errno.h> #include <stdio.h> int Division(int a, int b) { if (b == 0) { errno = EINVAL; // 设置错误码 return -1; } return a / b; } int Func() { int ret = Division(10, 0); if (ret == -1) return -1; // 需手动传递错误码 return ret; } int main() { if (Func() == -1) { printf("错误:%d\n", errno); // 需手动解析错误码 } return 0; }
C++ 异常的优势
- 错误信息更丰富:可抛出任意类型对象(包含详细错误描述);
- 无需层层传递错误码:异常直接跳转到最近的匹配 catch,简化代码;
- 适配特殊场景:构造函数无返回值,无法用错误码,异常是唯一选择。
C++ 异常核心概念与基础用法
异常的核心是「抛出 - 捕获」机制,依赖三个关键字:try、throw、catch。
1. 核心概念
try:包裹可能抛出异常的代码(保护代码块),后续必须跟一个或多个catch;throw:当检测到错误时,抛出异常对象(触发异常机制);catch:捕获特定类型的异常并处理,catch(...)可捕获任意类型异常(兜底)。
2. 基础语法框架
#include <iostream>
using namespace std;
int main() {
try {
// 保护代码:可能抛出异常的逻辑
int a = 10, b = 0;
if (b == 0) {
throw "除0错误!"; // 抛出字符串类型异常
}
cout << a / b << endl;
}
catch (const char* errmsg) { // 捕获字符串类型异常
cout << "捕获异常:" << errmsg << endl;
}
catch (...) { // 兜底捕获:处理所有未匹配的异常
cout << "捕获未知异常" << endl;
}
return 0;
}
3. 运行结果
捕获异常:除0错误!
异常的抛出与捕获规则
1. 核心规则
规则 1:异常类型决定匹配的 catch
抛出的异常对象类型,需与 catch 的参数类型完全匹配(或派生类匹配基类,后续讲)。
#include <iostream>
using namespace std;
void Func() {
throw 10; // 抛出int类型异常
}
int main() {
try {
Func();
}
catch (int err) { // 匹配int类型,触发
cout << "捕获int异常:" << err << endl;
}
catch (double err) { // 不匹配,跳过
cout << "捕获double异常:" << err << endl;
}
catch (...) { // 兜底,若前面无匹配则触发
cout << "未知异常" << endl;
}
return 0;
}
运行结果 :捕获int异常:10
规则 2:栈展开机制(调用链查找)
若抛出异常的位置不在try块,或当前try无匹配的catch,则退出当前函数栈,向上层调用链查找,直到找到匹配的catch或到达main函数(未找到则终止程序)。
#include <iostream>
using namespace std;
void Func1() {
cout << "Func1:抛出异常" << endl;
throw "Func1的错误"; // 抛出异常
}
void Func2() {
cout << "Func2:调用Func1" << endl;
Func1(); // 调用Func1,未处理异常
}
void Func3() {
cout << "Func3:调用Func2" << endl;
Func2(); // 调用Func2,未处理异常
}
int main() {
try {
cout << "main:调用Func3" << endl;
Func3();
}
catch (const char* errmsg) { // 最终在此捕获
cout << "main捕获异常:" << errmsg << endl;
}
return 0;
}
运行结果:
main:调用Func3
Func3:调用Func2
Func2:调用Func1
Func1:抛出异常
main捕获异常:Func1的错误
异常对象的拷贝与销毁
抛出的异常对象会生成一个临时拷贝(类似函数传值返回),捕获后原临时对象会销毁。
#include <iostream>
using namespace std;
class Error {
public:
Error() { cout << "Error构造" << endl; }
Error(const Error&) { cout << "Error拷贝构造" << endl; }
~Error() { cout << "Error析构" << endl; }
};
int main() {
try {
throw Error(); // 抛出临时对象,触发拷贝
}
catch (Error e) { // 捕获拷贝后的对象
cout << "捕获Error异常" << endl;
}
return 0;
}
运行结果:
Error构造
Error拷贝构造
捕获Error异常
Error析构
Error析构
异常高级用法
1. 异常的重新抛出
单个catch无法完全处理异常时,可通过throw;重新抛出,交给上层函数处理(常用于资源释放)。
场景 :函数内new了资源,抛出异常前需先释放资源,再重新抛异常。
#include <iostream>
#include <functional>
using namespace std;
double Division(int a, int b) {
if (b == 0) {
throw "除0错误"; // 抛出异常
}
return (double)a / b;
}
void Func() {
int* arr = new int[10]; // 申请堆内存
try {
int len = 10, time = 0;
cout << Division(len, time) << endl; // 触发除0异常
}
catch (...) { // 捕获所有异常
cout << "Func:释放arr内存" << endl;
delete[] arr; // 释放资源
throw; // 重新抛出异常,交给上层处理
}
// 正常执行时也需释放资源
delete[] arr;
}
int main() {
try {
Func();
}
catch (const char* errmsg) {
cout << "main捕获异常:" << errmsg << endl;
}
return 0;
}
运行结果:
Func:释放arr内存
main捕获异常:除0错误
异常安全(避免资源泄漏)
异常可能导致资源泄漏(如new未delete、锁未释放),需遵守以下原则:
- 构造函数:避免抛异常(可能导致对象初始化不完整);
- 析构函数:避免抛异常(可能导致资源释放失败);
- 用RAII(资源获取即初始化)管理资源(如智能指针、锁封装类)。
反例:构造函数抛异常导致内存泄漏
class A {
public:
A() {
_p = new int[10];
throw "构造失败"; // 抛出异常,_p未释放
}
~A() {
delete[] _p; // 构造异常时,析构函数不会执行!
}
private:
int* _p;
};
int main() {
try {
A a;
}
catch (const char* err) {
cout << err << endl; // 输出"构造失败",但_p内存泄漏
}
return 0;
}
异常规范(明确函数异常行为)
异常规范用于告知函数使用者:该函数可能抛出哪些异常(C++98)或是否抛出异常(C++11)。
(1)C++98 语法(逐渐被淘汰)
-
throw(类型列表):函数仅抛出列表中的异常; -
throw():函数不抛出任何异常。// 仅抛出int或const char类型异常
void Func1() throw(int, const char);
// 不抛出任何异常
void Func2() throw();
(2)C++11 noexcept(推荐)
noexcept 表示函数不会抛出异常 ,编译器会优化代码(比throw()更高效)。
// 明确表示不会抛异常
void Func() noexcept {
cout << "不会抛异常的函数" << endl;
}
// 移动构造函数常用noexcept
class MyString {
public:
MyString(MyString&& other) noexcept {
// 移动资源,不抛异常
}
};
自定义异常体系(工程实践)
实际项目中,需统一异常类型(避免混乱),通常设计一套继承体系 :基类Exception,派生类对应不同业务异常(如 SQL 异常、缓存异常),利用多态捕获。
完整示例(服务器开发常用)
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
using namespace std;
// 异常基类
class Exception {
public:
Exception(string errmsg, int id)
: _errmsg(errmsg), _id(id) {}
// 虚函数:支持多态
virtual string what() const {
return "错误ID:" + to_string(_id) + ",错误信息:" + _errmsg;
}
protected:
string _errmsg;
int _id;
};
// SQL异常(派生类)
class SqlException : public Exception {
public:
SqlException(string errmsg, int id, string sql)
: Exception(errmsg, id), _sql(sql) {}
virtual string what() const override {
return "SqlException:" + Exception::what() + ",SQL语句:" + _sql;
}
private:
string _sql;
};
// 缓存异常(派生类)
class CacheException : public Exception {
public:
CacheException(string errmsg, int id)
: Exception(errmsg, id) {}
virtual string what() const override {
return "CacheException:" + Exception::what();
}
};
// HTTP服务异常(派生类)
class HttpException : public Exception {
public:
HttpException(string errmsg, int id, string method)
: Exception(errmsg, id), _method(method) {}
virtual string what() const override {
return "HttpException:" + Exception::what() + ",请求方法:" + _method;
}
private:
string _method;
};
// 模拟SQL操作
void SqlOperate() {
srand(time(0));
if (rand() % 7 == 0) {
throw SqlException("权限不足", 100, "select * from user where name='张三'");
}
}
// 模拟缓存操作
void CacheOperate() {
srand(time(0));
if (rand() % 5 == 0) {
throw CacheException("数据不存在", 200);
}
SqlOperate();
}
// 模拟HTTP服务
void HttpServer() {
srand(time(0));
if (rand() % 3 == 0) {
throw HttpException("资源不存在", 300, "GET");
}
CacheOperate();
}
int main() {
while (1) {
this_thread::sleep_for(chrono::seconds(1)); // 每秒执行一次
try {
HttpServer();
cout << "服务正常运行" << endl;
}
catch (const Exception& e) { // 多态捕获:仅需捕获基类
cout << e.what() << endl;
}
catch (...) { // 兜底捕获未知异常
cout << "未知异常" << endl;
}
cout << "------------------------" << endl;
}
return 0;
}
运行结果(示例):
HttpException:错误ID:300,错误信息:资源不存在,请求方法:GET
------------------------
服务正常运行
------------------------
SqlException:错误ID:100,错误信息:权限不足,SQL语句:select * from user where name='张三'
------------------------
C++ 标准库异常体系
C++ 标准库提供了一套预定义异常(定义在<exception>头文件),所有异常均继承自std::exception基类,核心子类如下:
| 异常类 | 用途 |
|---|---|
std::bad_alloc |
内存分配失败(如new失败) |
std::out_of_range |
越界访问(如vector::at()) |
std::invalid_argument |
无效参数(如atoi("abc")) |
std::bad_cast |
动态类型转换失败(如dynamic_cast) |
代码示例:捕获标准库异常
#include <iostream>
#include <vector>
#include <exception>
using namespace std;
int main() {
try {
// 1. 内存分配失败(模拟)
vector<int> v;
v.reserve(1000000000000); // 申请超大内存,触发bad_alloc
// 2. 越界访问(若上面未触发,执行此句)
// vector<int> v(10);
// v.at(10) = 100; // at()会抛out_of_range
}
catch (const std::out_of_range& e) {
cout << "越界异常:" << e.what() << endl;
}
catch (const std::bad_alloc& e) {
cout << "内存分配异常:" << e.what() << endl;
}
catch (const std::exception& e) { // 捕获所有标准库异常
cout << "标准库异常:" << e.what() << endl;
}
return 0;
}
运行结果:
内存分配异常:std::bad_alloc
执行流的变动
异常处理的核心逻辑是「跳转执行」而非「恢复执行」------ 当异常被成功捕获并处理后,程序会从最后一个匹配的catch块之后的代码继续执行,而不会回到抛出异常的原始位置(原位置的执行环境已被栈展开破坏)。
步骤 1:异常抛出,触发栈展开(离开原执行位置)
当throw语句执行时,程序会立即终止当前函数的执行,且不会回到throw之后的代码。随后启动「栈展开」:退出当前函数栈,向上层调用链查找匹配的catch块(期间会销毁沿途函数的局部变量)。
步骤 2:找到匹配的catch,执行异常处理
找到第一个类型匹配的catch块后,执行其中的处理逻辑(如打印错误、释放资源)。
步骤 3:处理完成,从catch之后继续执行
异常处理完毕后,程序会跳过所有剩余的catch块,直接执行该catch块后面的代码 ,永远不会回到throw语句所在的原始位置。
直观验证执行流
我们基于Division除 0 异常例子,添加执行痕迹,清晰展示执行流走向:
#include <iostream>
using namespace std;
double Division(int a, int b) {
cout << "Division:开始执行(a=" << a << ", b=" << b << ")" << endl;
if (b == 0) {
throw "除0错误!"; // 异常抛出点(执行流从此处跳出Division函数)
// 👇 以下代码永远不会执行(throw后函数已终止)
cout << "Division:throw之后的代码(永远不会运行)" << endl;
}
return (double)a / b;
}
void Func() {
cout << "Func:开始执行" << endl;
int len = 10, time = 0;
cout << "Func:调用Division" << endl;
// 👇 调用Division时抛出异常,Func函数立即终止,不会执行后续代码
cout << Division(len, time) << endl;
// 👇 以下代码永远不会执行(异常导致Func提前退出)
cout << "Func:Division调用后的代码(永远不会运行)" << endl;
}
int main() {
cout << "main:程序开始" << endl;
try {
cout << "main:进入try块,调用Func" << endl;
Func(); // 调用Func,触发异常
// 👇 以下代码永远不会执行(异常导致try块内后续代码跳过)
cout << "main:Func调用后的代码(永远不会运行)" << endl;
}
catch (const char* errmsg) {
// 👇 异常处理逻辑
cout << "main:捕获异常:" << errmsg << endl;
}
catch (...) {
cout << "main:捕获未知异常" << endl;
}
// 👇 异常处理完后,从这里继续执行(核心!)
cout << "main:异常处理完成,程序继续执行(不会回原抛出点)" << endl;
return 0;
}
运行结果(关键执行流标记):
main:程序开始
main:进入try块,调用Func
Func:开始执行
Func:调用Division
Division:开始执行(a=10, b=0)
main:捕获异常:除0错误!
main:异常处理完成,程序继续执行(不会回原抛出点)
关键观察:
Division中throw之后的代码(cout << "Division:throw之后的代码")完全没执行;Func中Division调用之后的代码(cout << "Func:Division调用后的代码")完全没执行;main的try块中Func调用之后的代码(cout << "main:Func调用后的代码")完全没执行;- 最终执行流停在
catch块之后的代码(main:异常处理完成...)。
C++ 异常的优缺点
优点
- 错误信息更清晰:异常对象可包含详细信息(如错误 ID、SQL 语句、堆栈),便于调试;
- 无需层层传递错误码 :异常直接跳转到匹配的
catch,简化深层调用链的错误处理; - 适配特殊场景 :构造函数、
operator[]等无返回值的函数,只能用异常处理错误; - 兼容第三方库:Boost、GTest 等主流库均使用异常,使用这些库需支持异常。
缺点
- 执行流混乱:异常会打断正常执行流,调试时难以跟踪代码逻辑;
- 性能开销:异常的栈展开和拷贝会有少量性能损耗(现代硬件可忽略);
- 资源泄漏风险 :若未正确释放资源(如
new、锁),容易导致内存泄漏(需用 RAII 解决); - 标准库异常体系不完善:不同项目可能自定义异常体系,缺乏统一标准;
- 学习成本高:需掌握异常规则、异常安全、RAII 等知识点。
最佳实践
- 统一异常体系:所有异常继承自同一个基类;
- 明确异常规范:用
noexcept标记不抛异常的函数; - 优先使用 RAII:用智能指针、锁封装类管理资源,避免泄漏;
- 兜底捕获:
main函数中必须加catch(...),防止程序意外终止。
总结
C++ 异常是一把 "双刃剑",虽有学习成本和潜在风险,但在工程实践中利大于弊,是处理错误的主流方式。核心是:规范异常类型、保证异常安全、合理使用 try/catch/throw,结合 RAII 机制,可大幅提升代码的健壮性和可维护性。