今天我们来学习一下C++中的异常,对以后大型项目的调试有非常重要的作用。
一.异常机制的诞生
在C++诞生之前,C语言主要通过错误码来处理报错。错误码的本质是通过对错误类型的编号,编译器来返回不同的数值。比如用-1表示文件打开,0表示执行成功,但是存在明显的弊端:
错误码需要自己手动检查每个函数的返回值,代码中存在着大量的if-else语句,导致业务代码被错误处理代码割裂,可读性就会大大降低。并且错误码只能传递简单的数值信息,无法携带详细的错误描述。而C++的异常机制从根本上解决这类问题,在遇到错误的时候直接抛出一个包含错误信息的对象,直接捕获代码来进行处理。
核心优选:
1.解耦错误检测与处理:检测错误的代码只需负责发现问题并抛出异常,处理错误的代码则集中在专门的捕获块中,业务逻辑与错误处理代码分离。
2.携带丰富的错误信息:异常对象可以是自定义的类对象,能够存储错误描述、错误码、调用栈等信息,便于调试和问题定位。
3.强制错误处理:如果异常未被捕获,程序会自动终止,避免了错误被忽略的情况,迫使开发者正视错误处理。
简单来说,异常机制让程序的错误处理从 "被动检查" 变成了 "主动通知",是 C++ 面向对象编程中错误处理的最佳实践。
二.异常的操作核心
操作核心是抛出和捕获(throw和catch),并且配合try来搭配使用。通过throw来抛出问题,然后用catch来进行处理,try则是划定了负责监控的区域。
#include <iostream>
#include <string>
using namespace std;
double Divide(int a, int b) {
if (b == 0) {
// 抛出字符串类型的异常
string errMsg = "Divide by zero condition!";
throw errMsg;
}
return static_cast<double>(a) / static_cast<double>(b);
}
需要注意的是throw语句执行后,这个函数后面的代码就不会再执行了。
抛出后进行捕获:
#include <iostream>
#include <string>
using namespace std;
double Divide(int a, int b) {
if (b == 0) {
// 抛出字符串类型的异常
string errMsg = "Divide by zero condition!";
throw errMsg;
}
return static_cast<double>(a) / static_cast<double>(b);
}
void Func() {
int len, time;
cin >> len >> time;
try {
//可能抛出异常的代码
cout << Divide(len, time) << endl;
}
catch (const char* errmsg) {
//捕获抛出的异常
cout << errmsg << endl;
}
//异常处理后,继续代码的执行
cout << Func << ":" << endl;
}
int main()
{
while (1) {
try {
Func();
}
catch (const string& errmsg) {
cout << errmsg << endl;
}
}
return 0;
}

在上面的代码中,Divide函数抛出的是string类型的异常,Func中的catch块试图捕获const char*类型的异常,类型不匹配,因此异常会继续向上传播到main函数中的catch块,最终被const string&类型的catch捕获。
捕获的匹配规则
异常的捕获遵循类型完全匹配的原则,但也存在一些特殊的类型转换允许:
非常量到常量的转换:允许捕获const类型的引用 / 指针来接收非const类型的异常对象。
数组 / 函数到指针的转换:数组会被转换成指向数组元素的指针,函数会被转换成指向函数的指针。
派生类到基类的转换:这是最实用的转换规则,允许用基类类型的catch块捕获派生类的异常对象,也是异常继承体系设计的核心基础。
如果catch块中没有找到匹配的类型,异常会继续向上传播。如果直到main函数都没有找到匹配的catch块,程序会调用标准库的terminate函数终止运行。为了避免程序意外终止,通常会在main函数的最后添加一个catch(...)块,它可以捕获任意类型的异常,作为异常处理的 "兜底" 方案:
catch (...) {
cout << "Unknown Exception" << endl;
}
三.栈展开:异常的传播途径
当异常被抛出后,如果try没有匹配到catch,系统会进行栈展开,沿着函数调用链先上查找匹配的catch块。

栈展开的实例:
#include <iostream>
#include <string>
using namespace std;
void func1() {
cout << "进入func1" << endl;
throw string("func1抛出的异常");
cout << "离开func1" << endl; // 不会执行
}
void func2() {
cout << "进入func2" << endl;
func1();
cout << "离开func2" << endl; // 不会执行
}
void func3() {
cout << "进入func3" << endl;
func2();
cout << "离开func3" << endl; // 不会执行
}
int main() {
try {
func3();
} catch (const string& errmsg) {
cout << "捕获异常:" << errmsg << endl;
} catch (...) {
cout << "未知异常" << endl;
}
return 0;
}
进入func3
进入func2
进入func1
捕获异常:func1抛出的异常
四.利用好继承体系
#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <cstdlib>
#include <ctime>
using namespace std;
// 基类异常
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 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 _SendMsg(const string& s) {
if (rand() % 2 == 0) {
// 102号错误:网络不稳定
throw HttpException("网络不稳定,发送失败", 102, "put");
} else if (rand() % 7 == 0) {
// 103号错误:非好友
throw HttpException("你已经不是对象的好友,发送失败", 103, "put");
} else {
cout << "发送成功:" << s << endl;
}
}
// 重试发送消息
void SendMsg(const string& s) {
// 最多重试3次
for (size_t i = 0; i < 4; i++) {
try {
_SendMsg(s);
break; // 发送成功,退出循环
} catch (const Exception& e) {
// 处理102号网络异常,其他异常重新抛出
if (e.getid() == 102) {
if (i == 3) {
// 重试3次失败,重新抛出异常
throw;
}
cout << "网络不稳定,开始第" << i + 1 << "次重试" << endl;
this_thread::sleep_for(chrono::seconds(1));
} else {
// 非网络异常,直接重新抛出
throw;
}
}
}
}
int main() {
srand(time(0));
string str;
while (cin >> str) {
try {
SendMsg(str);
} catch (const Exception& e) {
cout << "最终捕获异常:" << e.what() << endl;
} catch (...) {
cout << "未知异常" << endl;
}
}
return 0;
}
在这个例子中,_SendMsg函数可能抛出两种HttpException异常:102 号(网络不稳定)和 103 号(非好友)。SendMsg函数捕获异常后,对 102 号异常进行重试处理,若重试 3 次仍失败则重新抛出;对 103 号异常则直接重新抛出,最终由main函数捕获并输出异常信息。这种方式既实现了对特定异常的精细化处理,又保证了其他异常能够被上层代码捕获。
4.2 异常的继承体系设计
在大型项目中,不同模块(如数据库、缓存、网络)可能会抛出不同类型的异常,如果为每个异常都单独设计一个catch块,代码会变得臃肿且难以维护。此时,我们可以设计一套异常继承体系,让所有自定义异常都继承自一个基类(如Exception),在捕获时只需捕获基类类型,即可处理所有派生类异常。
这种设计的核心依据是异常捕获时派生类向基类的类型转换规则。下面以一个模拟服务端的场景为例,展示异常继承体系的设计与使用:
#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <cstdlib>
#include <ctime>
using namespace std;
// 基类异常
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 << "Unknown Exception" << endl;
}
}
return 0;
}
在这个例子中,SqlException、CacheException、HttpException都继承自基类Exception,并分别重写了what函数以返回具体的异常信息。main函数中只需通过const Exception&类型的catch块,就能捕获所有模块抛出的异常,大大简化了异常处理的代码。这种设计也符合开闭原则,后续新增模块的异常只需继承Exception基类,无需修改现有的捕获代码。
五、异常安全:避免资源泄漏的关键
异常机制虽然强大,但也带来了新的问题:异常安全。当异常抛出时,程序的执行流程会突然跳转到catch块,导致某些资源(如动态分配的内存、文件句柄、锁)无法被正常释放,从而引发资源泄漏。解决异常安全问题是使用异常机制的必修课,主要有以下两种思路:
5.1 手动捕获异常并释放资源
在可能抛出异常的代码块中,手动捕获异常,释放资源后再重新抛出异常。这种方式虽然繁琐,但在简单场景下非常有效。例如,在使用new动态分配数组后,如果发生异常,需要在catch块中释放数组内存:
#include <iostream>
#include <string>
using namespace std;
double Divide(int a, int b) {
if (b == 0) {
throw "Division by zero condition!";
}
return static_cast<double>(a) / static_cast<double>(b);
}
void Func() {
// 动态分配内存
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;
}
在Func函数中,new int[10]分配了动态内存,try块中的Divide函数可能抛出异常。如果异常发生,catch (...)块会捕获异常,释放array指向的内存,然后重新抛出异常;如果正常执行,最后也会释放内存。这种方式确保了无论是否发生异常,动态内存都能被正确释放。
5.2 使用 RAII 机制管理资源
手动释放资源的方式在复杂场景下容易出错,而RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制则是解决异常安全问题的终极方案。RAII 的核心思想是:将资源的生命周期与对象的生命周期绑定,利用类的构造函数获取资源,析构函数释放资源。由于栈展开过程中局部对象会被自动销毁,析构函数也会被自动调用,从而保证资源的释放。
C++ 中的智能指针(如unique_ptr、shared_ptr,后续会为大家详细介绍)就是 RAII 机制的典型应用。下面用智能指针改写上述代码,无需手动捕获异常即可保证内存安全:
#include <iostream>
#include <string>
#include <memory>
using namespace std;
double Divide(int a, int b) {
if (b == 0) {
throw "Division by zero condition!";
}
return static_cast<double>(a) / static_cast<double>(b);
}
void Func() {
// 使用unique_ptr管理动态内存
unique_ptr<int[]> array(new int[10]);
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
// 无需手动释放,unique_ptr析构时自动释放内存
}
int main() {
try {
Func();
} catch (const char* errmsg) {
cout << errmsg << endl;
} catch (...) {
cout << "Unkown Exception" << endl;
}
return 0;
}
在这个例子中,unique_ptr对象array在构造时获取了动态内存,当Func函数执行完毕(无论是正常结束还是因异常退出),array的析构函数都会被调用,自动释放所管理的内存,从根本上避免了资源泄漏。
此外,析构函数中应尽量避免抛出异常。如果析构函数在释放资源时抛出异常,可能导致后续的资源释放操作无法执行,进而引发更严重的资源泄漏。《Effective C++》的第 8 条明确指出:别让异常逃离析构函数。如果析构函数中必须处理可能抛出异常的操作,应在析构函数内部捕获异常并处理,避免异常传播。
六.noexcept关键字
noexcept关键字用来声明函数是否会抛出异常
程序也会对noexcept的调用进行检查,如果发生了异常,直接调用terminate来停止。
#include <iostream>
using namespace std;
double Divide(int a, int b) noexcept {
if (b == 0) {
throw "Division by zero condition!";
}
return static_cast<double>(a) / static_cast<double>(b);
}
int main() {
int i = 0;
// 检测表达式是否会抛出异常
cout << noexcept(Divide(1, 2)) << endl; // 输出1(true)
cout << noexcept(Divide(1, 0)) << endl; // 输出1(true),编译器无法检测运行时异常
cout << noexcept(++i) << endl; // 输出1(true)
return 0;
}
七.标准库的异常体系
