与几乎所有带来便利的机制一样,异常使用不当亦会暴露若干潜在问题。本文不图面面俱到,仅列出在采用(或决定采用)异常时应当重点权衡的几大风险。
资源清理
初学者最常碰到的难题是:异常发生时如何正确释放资源。考虑如下示例:
#include <iostream>
try
{
openFile(filename);
writeFile(filename, data);
closeFile(filename);
}
catch (const FileException& exception)
{
std::cerr << "Failed to write to file: " << exception.what() << '\n';
}
若 writeFile()
失败并抛出 FileException
,控制流将跳转至异常处理器,打印错误后退出。注意:文件尚未关闭!上述代码应改写为:
#include <iostream>
try
{
openFile(filename);
writeFile(filename, data);
}
catch (const FileException& exception)
{
std::cerr << "Failed to write to file: " << exception.what() << '\n';
}
closeFile(filename); // 确保文件关闭
类似问题在动态内存中也常见:
#include <iostream>
try
{
auto* john{ new Person{ "John", 18, PERSON_MALE } };
processPerson(john);
delete john;
}
catch (const PersonException& exception)
{
std::cerr << "Failed to process person: " << exception.what() << '\n';
}
如 processPerson()
抛出异常,控制流跳转至 catch 块,结果 john
未被释放。更棘手的是,john
属于 try 块的局部作用域,try 块退出时其生命周期结束,异常处理器已无法访问该指针,因而无法手动释放。
有两种相对简单的修复方式:
- 将
john
声明在 try 块外,使其在异常处理器中依然可见:
#include <iostream>
Person* john{ nullptr };
try
{
john = new Person("John", 18, PERSON_MALE);
processPerson(john);
}
catch (const PersonException& exception)
{
std::cerr << "Failed to process person: " << exception.what() << '\n';
}
delete john;
- 使用“离开作用域即自动清理”的类(常称“智能指针”)。标准库提供
std::unique_ptr
模板,离开作用域时自动释放资源:
#include <iostream>
#include <memory> // for std::unique_ptr
try
{
auto* john{ new Person("John", 18, PERSON_MALE) };
std::unique_ptr<Person> upJohn{ john }; // upJohn 拥有 john
processPerson(john);
// upJohn 离开作用域时自动释放 john
}
catch (const PersonException& exception)
{
std::cerr << "Failed to process person: " << exception.what() << '\n';
}
相关内容:详见《std::unique_ptr》。
最佳做法(只要可行)是优先在栈上分配实现了 RAII 的对象(构造时获取资源,析构时自动释放)。这样,无论因何种原因离开作用域,资源都会被自动回收,无需手动干预。
异常与析构函数
与构造函数不同,析构函数绝不应抛出异常。若在栈回卷期间析构函数抛出异常,编译器将陷入两难:继续回卷还是处理新异常?最终程序会被立即终止。
因此,最稳妥的做法是完全避免在析构函数中使用异常,改而将错误写入日志。
规则
若析构函数在栈回卷期间抛出异常,程序将立即终止。
性能考量
异常并非零成本。它们会增加可执行文件体积,并因额外检查而可能降低运行速度。主要开销出现在异常真正抛出时:需回卷栈并匹配处理器,代价相对高昂。
现代部分架构支持“零成本异常”模型;若被支持,则在无异常路径上无额外开销(这正是我们最常关心的场景)。然而,异常触发时代价更大。
何时应当使用异常
异常处理最适用于以下四个条件同时成立之情形:
- 该错误极少发生;
- 错误严重,无法继续正常执行;
- 错误无法在发生点就地解决;
- 缺乏向调用者返回错误码的合理替代方案。
举例:编写函数要求用户传入磁盘文件名,函数打开文件、读取数据、关闭文件并返回结果。若用户传入不存在的文件名或空字符串,是否应抛异常?
- 前两条显然成立:不常发生,且无法计算结果。
- 第三条亦成立:函数不应负责重新提示用户。
- 第四条是核心:是否存在返回错误码的合理方式?若可行(如返回空指针或状态码),则优先返回码;否则异常是合理选择。