异常的介绍和使用

今天我们来学习一下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&amp; 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&amp; 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&amp; errmsg, int id, const string&amp; 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&amp; 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&amp; s) {

// 最多重试3次

for (size_t i = 0; i < 4; i++) {

try {

_SendMsg(s);

break; // 发送成功,退出循环

} catch (const Exception&amp; 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&amp; 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;

}

七.标准库的异常体系

相关推荐
coding者在努力2 小时前
算法竞赛中根据数据规模猜测算法
c++·算法·stl·时间复杂度
jing-ya2 小时前
day 59 图论part10
java·开发语言·数据结构·算法·图论
love530love2 小时前
ComfyUI-3D-Pack:Windows 下手动编译 mesh_inpaint_processor C++ 加速模块
c++·人工智能·windows·python·3d·hunyuan3d·comfyui-3d-pack
楼田莉子2 小时前
C++高并发内存池:内存池调优与测试
c++·后端·哈希算法·visual studio
雾隐潇湘2 小时前
C++——第三篇 继承与多态
开发语言·c++
Marye_爱吃樱桃2 小时前
MATLAB R2024b的安装、简单设置——保姆级教程
开发语言·matlab
旺仔.2912 小时前
Linux系统基础详解(二)
linux·开发语言·网络
阿贵---2 小时前
分布式系统C++实现
开发语言·c++·算法
不染尘.2 小时前
最短路径之Bellman-Ford算法
开发语言·数据结构·c++·算法·图论