以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」mp.weixin.qq.com/s/UEXQEXED3…
大家好,欢迎来到《掌握 C++ 异常艺术:构建健壮程序的秘诀与实战策略》系列文章的第七篇!
如果你已经熬过了前几篇文章,恭喜你,你的 C++ 技能树已经点亮了相当一部份,想想就开心了。
但别高兴得太早,因为今天我们要探讨的是一个足以让你怀疑人生的主题——析构函数中的异常。
你知道吗?C++ 中的异常处理就像是一场危险的高空马戏表演,稍有不慎就会摔得粉身碎骨。
而析构函数,就像是在表演结束时,老板突然要求你加演的高难度动作。这时如果你处理不当,别说收场了,连命都保不住!
所以今天,让我们一起探讨这个险象环生的场景,并将深入挖掘那些你可能未曾思索过的陷阱,和处理秘诀。
系好安全带,接下来这趟旅程绝对不会让你失望,当然,轻松也是不可能的。
析构发生异常
一般编码实践中,都不鼓励在析构函数中抛出异常,因为会有可能形成异常的嵌套抛出。
比如:
class A {
public:
~A() {
throw std::runtime_error("A throw exception");
}
};
class B {
public:
B() {
A a;
throw std::runtime_error("B throw exception");
}
};
int main() {
try {
B b;
} catch (const std::exception& e) {
std::cerr << "Caught an exception: "
<< e.what() << std::endl;
}
}
这段代码里,在构造 B 类型对象时抛出异常信号,然后在调用栈内已完成初始化的局部对象 a 的析构函数释放对象 a 时再次抛出异常。
在前一个异常信号未被处理完成之前,再次叠加抛出其它异常信号,这会导致栈展开(stack unwinding)过程中异常链的断裂,进而引起程序行为不可预测。
不可预测指的是,此时系统会变得左右为难,如果系统有两个选择可以取,那么,其一是按照最早的异常处理路径执行,其二是按照最新的异常处理路径执行,这是一种无法确定的状态,最终导致系统执行 terminate()。
我们可以试着运行一下上面的代码:
terminate called after throwing an instance of 'std::runtime_error'
what(): A throw exception
Aborted (core dumped)
果然,程序在抛出异常信息 “A throw exception” 后被终止了。
虽然在析构函数中不是绝对不能抛出异常,但有的编译器实现会为了简化处理逻辑,一旦检测到析构函数抛出异常就直接终止程序执行。
所以建议尽可能不在析构函数中抛出异常,依据经验,这是一种「魔咒」,除非你有魔法,否则不要轻易模仿。
那么为什么会出现在析构函数抛出异常的情况?如果真发生了,又该怎么正确处理?
答案是,天有不测之风云,各种库或者你的所忽都会是原因。另外,秉持内部矛盾不扩散的原则,截住它,不要让异常信号从析构函数跑了。
中国有句古话叫,家丑不外扬,这是一种智慧。
下面以操作文件读句柄为例,创建文件句柄类 FileHandle,实例化该类可以获取文件流指针,通过该类可以读取文件。
class FileHandle {
public:
FileHandle(const std::string& filename) :
file_(nullptr),
filename_(filename) {
file_ = new std::fstream(filename_,
std::ios::in);
if (!file_->is_open()) {
std::cerr << "Error opening file: "
<< filename_ << std::endl;
}
}
~FileHandle() {
if (file_) {
try {
if (!file_->is_open()) {
throw std::runtime_error("file has not been open");
}
file_->close();
} catch (const std::runtime_error& e) {
std::cerr << "Error closing file: "
<< filename_ << ". Reason: "
<< e.what() << std::endl;
}
delete file_;
file_ = nullptr;
}
}
void DoSomeThing() {
if (!file_->is_open()) {
throw std::runtime_error("when do some thing with file...");
}
}
private:
std::fstream* file_;
std::string filename_;
};
int main() {
try {
FileHandle file("test.txt");
file.DoSomeThing();
} catch (const std::exception& e) {
std::cerr << "Caught an exception: "
<< e.what() << std::endl;
}
return 0;
}
实例化类 FileHandle 时,传入一个不存在的文件名作为初始化参数。构造对象时,打开文件失败,返回的文件流状态无效。在这里没有抛出异常,而是记录错误信息,方便后边操作时引发异常。
实例化类 FileHandle 后,由于文件流无效,如果调用对象的方法操作文件,则会失败并抛出异常。在匹配对应异常处理代码之前,会把当前执行栈的内存空间清理,而 FileHandle 对象就分配在当前栈内,所以调用 FileHandle 对象析构函数释放资源。
调用类 FileHandle 的析构函数时,需要关闭文件,如果此时判断文件流无效,在某些业务场景下可以选择抛出异常。为了演示析构触发异常的情形,这里直接抛出异常。
为避免触发 terminate() 的执行,在析构函数中抛出的异常应该在析构函数退出之前就被处理,而且不能再次抛出。
程序的输出:
Error opening file: test.txt
Error closing file: test.txt. Reason:
file has not been open
Caught an exception: when do some thing with file...
在析构函数中选择抛出异常的做法需要小心谨慎,最好避免,毕竟考虑的流程越复杂就越是容易出问题,测试的同学也少挨罪。
要是真的在析构函数中又触发了异常抛出,避免异常被抛出到上一层调用栈也是亡羊补牢。