以下内容为本人的烂笔头,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/ogbL3vKmHefsVHz3JjgSCA
现代 C++ 的异常处理
相比之下,现代编程语言中的异常处理机制能够更好地分离错误处理和正常执行流程,使得代码结构更加清晰,而且易于维护,避免潜在的遗漏处理。
在现代编程语言的异常处理机制中,当发生错误时,不需要通过一层层嵌套的条件语句来检测错误,程序会立即跳转到相应的异常处理代码块。
C++ 语言层面提供的异常处理(下文统一使用「异常机制」代指)就可以使用在构造函数中,可有效避免这类错误的遗漏处理,并且从此和繁杂的 if-else 说拜拜。
如何应用异常机制?
异常机制本意是解决在本地无法处理异常信号的场景,比如上面提到过的在构造函数中申请资源失败,就只能先抛出异常信号,然后由上层调用者(或者嵌套调用者)专门处理捕捉到的异常信号。
#include <iostream>
#include <new>
class Memory {
int sz;
int* elem;
public:
Memory (int s)
: sz(s)
, elem(AllocateMem(s)) {
if (elem == nullptr)
throw std::bad_alloc();
}
// ...
};
int main () {
try {
Memory* gfg_array = new Memory(100000000);
}
catch (std::bad_alloc & ba) {
std::cerr << "bad_alloc caught: " << ba.what();
}
return 0;
}
类 Memory 构造一个指定大小的空间, 类 AllocateMem 负责调用 new 执行分配空间。实例化 Memory 时如果分配内存失败,就会抛出异常信号 std::bad_alloc()。
C++ 提供关键词 throw 抛出异常信号,当你在程序中使用 throw 抛出一个异常时,它就像发出一个错误信号,告诉程序当前发生了某种意外或错误情况,需要专门的处理。抛出异常信号的语法
throw expression;
expression 可以是语言内置类型的量,比如整数、浮点数、字符串等字面量,也可以是用户自定义的类型实例,只要这个类型是从 std::exception 或其派生类继承即可。
std::bad_alloc 是标准库提供的比较常用的异常信号,使用 new 分配内存失败即会抛出该信号。
try 语句块用于捕捉异常信号,一旦 try 语句块内的代码(或者嵌套调用的代码)抛出异常信号,则通知其后的 catch 语句块处理异常信号。try 语句块后面可以跟着多个 catch 语句块,分别处理不同的异常信号,类似 switch 语句块后边的 case 语句块。
try {
// 信号 expression1 属于类型 A
throw expression1;
// 信号 expression2 属于类型 B
throw expression2;
// ...
} catch (A sig) {
// deal with expression1
} catch (B sig) {
// deal with expression2
}
//...
当抛出这个异常信号时,程序会立即停止当前流程的执行,并试图在调用栈向上查找匹配的 catch 语句块来捕获并处理这个异常信号。如果没有找到匹配的 catch 块,程序将会终止。
最佳实践:
如果类构造函数会抛出异常,并且包含申请资源的操作,那么该类应该严格依据 RAII 的原则实现,避免资源泄漏。
如果某个函数内部会抛出异常,那么在 C++ 11 之前的标准中,需要在函数原型或声明中使用 throw 关键字声明可能抛出的异常类型,比如函数 func() 内部会抛出一些异常信号,那么定义函数时可以如下编写
void func() throw(optional_type_list) {
// ...
}
optional_type_list 是异常信号类型的列表。
如果函数 func() 内部不会抛出任何异常信号,那么定义函数时可以如下编写
void func() throw() {
// ...
}
在 C++ 11 及之后的标准中,推荐开发者使用 noexcept 修饰符来表明函数会不会抛出异常。
假设我们设计函数 func() 不在内部抛出异常,那么定义函数时可以如下编写
void func() noexcept {
// ...
}
函数被 noexcept 修饰后,一旦内部抛出异常就会触发执行 std::terminate(终止当前程序的运行)。
如果函数会抛出异常,那么修饰符改为 noexcept(false)
即可。
言过其实?有 bug ?
至于有人会说,曾经发现在构造函数中对 new 出来的资源执行异常处理会导致内存泄漏。
大可不必惊慌,这是一个老掉牙的 bug 了,只存在于非常老旧的编译器版本中。如果你有幸碰到,请先检查一下编译器的版本。
甚至有人会说,语言层面提供的异常处理机制运行效率太低,对于系统来说太昂贵了。
异常机制会给系统带来开销负担的问题,其实开销仅会发生在抛出异常时,以及引发的错误处理流程中。如果没有抛出异常,此时开销甚至比返回错误码和额外的条件判断还要小。
有一点我们应该注意的是,返回错误码和额外的条件判断同样是有开销的,也就是我们常常随手写的 if 语句会给系统带来负担。
另外,异常机制实现已经大幅改进,和未抛出异常时的正常流程相比,异常处理流程的开销大幅减少到了百分之几,比如 3%。
如果你不是从事飞控这类极度要求时效的领域(如果不够快就会危及性命安全),不需要过度担心异常机制带来的开销问题。
如何选择?
在 C++ 编程中,选择使用「异常机制」或者「错误码」来处理异常,有几种因素必须考虑好,下面是几种常见的指导原则:
推荐使用异常机制:
资源初始化失败:在构造函数中,如果无法初始化对象的状态或者分配必要的资源(从堆中申请内存),这时抛出异常是很自然的选择,因为构造函数无法返回错误码。
程序逻辑中的严重错误:当遇到无法恢复的错误,比如文件不存在、网络连接断开、无效的用户输入等,这类错误通常适合通过抛出异常来处理,因为它们打断了程序的正常流程,继续原有流程毫无意义,需要立即引起开发人员的注意。
异步操作的完成通知:在某些并发框架中,异常可以用作异步操作完成的通知方式,特别是当操作失败发生时。
层次分明的错误处理:当代码结构层次深、模块化要求强的时候,常见于各大框架工程中,异常可以简化错误传播过程,使得错误处理更加集中和结构化,避免在每个层级都需要检查错误码。
推荐使用错误码:
性能敏感场景:异常处理有一定的性能开销,包括创建异常对象、栈展开等。在性能极其关键的场合,如果错误是常态而偶尔情况,使用错误码会更为高效。因为异常机制的错误处理部份的开销的确比错误码的使用开销要大。
资源受限的嵌入式系统:在资源有限的嵌入式系统中,异常处理可能过于消耗栈空间或者 RAM,而采用错误码却极少消耗内存。
错误频率较高且无需中断程序执行:如果某种错误经常发生,继续执行流程仍然有必要,且不至于要终止程序,使用错误码可以让程序优雅地处理错误并继续执行下去。
跨语言或库边界:在与不支持异常的代码或库接口交互时,使用错误码的兼容性更好。
精确控制错误处理流程:使用错误码可以更精细地控制错误处理路径,程序员可以在每个函数调用后立即检查错误,而不用等待异常被捕获。
总的来说,异常处理主要用于处理非预期的、罕见的、严重影响程序业务流程的问题,而错误码则更适合处理可预见的、频发的、不影响程序主体流程的错误情况。
在实际编程中,可以根据具体情况灵活选择,甚至在同一个项目中同时使用异常处理和错误码机制。
全文已结束,欢迎关注公众号查看完整系列文章。如果各位同学朋友有什么疑问可以联系笔者,当然笔者也愿意和你进一步探讨这方面的问题。另外,八戒有自己的技术圈朋友群,如果读者朋友想进群交流技术问题,欢迎联系我。下拉到文章顶部有我的联系方式!
最后,非常感激各位朋友的点 「赞」 和点击 「在看」,谢谢!