我们所设计的函数在运行过程中出现异常是一件十分正常的事,有些异常是我们在设计时可以预见并且是可以进行恢复的,由于异常的发生往往比较底层,异常通常不能在当前层次的函数调用中就地恢复,而是需要上报到之前的函数调用中处理,为了实现这个目的,我们在C语言的实践中,往往可以设计一个错误码,将错误码层层上报,直到某层可以处理错误码。
在C++中,语言特性为我们设计了一个新的实现路径,那就是C++的异常处理机制。
C++ 的异常处理通过throw
关键字和try…catch
语句结构实现。
抛出异常
throw
关键字负责抛出异常:
throw 表达式;
其中的表达式可以是常量、变量或对象。如果函数调用出现异常,就可以通过throw
将表达式中的异常抛出。该对象一般用于传达有关错误的信息。大多数情况下,建议使用 std::exception
类或标准库中定义的派生类之一。如果其中的类不合适,建议从 std::exception
派生自己的异常类。
派生自己的异常类:
class MyException : public std::exception { // 继承自 std::exception
public:
MyException(const std::string& message, int errorCode)
: m_message(message), m_errorCode(errorCode) {}
// 重写what()
const char* what() const noexcept override {
return m_message.c_str();
}
int getErrorCode() const {
return m_errorCode;
}
private:
std::string m_message;
int m_errorCode;
};
捕获异常
使用 try-catch
程序块来捕获异常。
try {
// 可能会引发异常的代码
} catch (ExceptionType1& e1) {
// 处理 ExceptionType1 类型的异常
} catch (ExceptionType2& e2) {
// 处理 ExceptionType2 类型的异常
} catch (...) {
// 处理其他类型的异常
}
以下是对 try-catch 块的各个部分的解释:
try
:包含可能引发异常的代码块。catch
:定义了异常处理的块。可以有多个 catch 块用于处理不同类型的异常。ExceptionType1
、ExceptionType2
:是异常类型的具体类名。你可以根据需要添加多个 catch 块,以适应不同类型的异常。& e1
、& e2
:是异常对象的引用。它们用于访问异常对象并进行相应的处理。
最后一个 catch
块中的省略号 ...
表示捕获所有未被前面的 catch
块捕获的异常。这个 catch
块通常用于处理未知类型的异常,或者作为异常处理的最后一个备选项。此动作并不安全,可能将不具备处理能力的异常类型捕获,但是也有后悔药,我们可以重新抛出这个异常:
在catch
块内重新抛出异常,直接使用throw
:
try {
throw CSomeOtherException();
}
catch(...) {
// Catch all exceptions - dangerous!!!
// Respond (perhaps only partially) to the exception, then
// re-throw to pass the exception to some other handler
// ...
throw;
}
关于try-catch
程序块需要注意的是:
在一个
try-catch
块中只会执行一个catch
块。一旦匹配到了一个catch
块,程序将跳转到该块执行,并且后续的catch
块将被忽略;try-catch
后面都必须跟大括号,即使只有一条语句;异常处理完成后,将继续执行
try-catch
后面的代码,而不会跳转回异常发生处;try
和catch
语句不能单独使用,必须搭配使用。
catch块的计算方式
虽然通常建议引发派生自 std::exception
的类型,但 C++ 使您能够引发任何类型的异常。可以通过指定与引发的异常相同的类型的 catch
处理程序或通过可捕获任何类型的异常的处理程序来捕获 C++ 异常。
如果引发的异常的类型是类,它还具有基类(或类),则它可由接受异常类型的基类和对异常类型的基的引用的处理程序捕获。请注意,当异常由引用捕获时,会将其绑定到实际引发的异常对象;否则,它将为一个副本(与函数的参数大致相同)。
引发异常时,将由以下类型的 catch
处理程序捕获该异常:
可以接受任何类型的处理程序(使用省略号语法)。
接受与异常对象相同的类型的处理程序;由于它是副本,因此
const
和volatile
修饰符将被忽略。接受对与异常对象相同的类型的引用的处理程序。
接受对与异常对象相同类型的
const
或volatile
形式的引用的处理程序。接受与异常对象相同的类型的基类的处理程序;由于它是副本,因此
const
和volatile
修饰符将被忽略。基类的catch
处理程序不得在派生类的catch
处理程序之前。接受对与异常对象相同的类型的基类的引用的处理程序。
接受与异常对象相同的类型的基类的
const
或volatile
形式的引用的处理程序。接受可通过标准指针转换规则将引发的指针对象转换为的指针的处理程序。
catch
处理程序按照出现顺序计算,因此,对于catch(...)
需要放置到最后,否则,位于catch(...)
之后的catch
块都会被直接跳过。
// ...
try
{
// ...
}
catch( ... )
{
// Handle exception here.
}
// Error: the next two handlers are never examined.
catch( const char * str )
{
cout << "Caught exception: " << str << endl;
}
catch( CExcptClass E )
{
// Handle CExcptClass exception here.
}
异常安全讨论
一般来说,异常安全的讨论与一个函数可提供的三个异常保证有关:无故障保证、增强保证和基本保证。
无异常保证
无异常保证是一个函数可提供的最有力的保证。此保证声明,该函数不会引发异常或允许异常传播。
能做到无异常保证,除非满足:
我们明确该函数调用的所有函数也是无异常的;
我们知道所有异常将在到达该函数前被捕捉;
我们知道如何捕获和正确处理到达该函数的所有异常。
标准库中的所有容器和类型保证析构函数不会抛出错误,同时,标准库需要用户提供的模板自变量类型必须具有不会抛出错误的析构函数。
#include <vector>
void doSomething(const std::vector<int>& vec) noexcept {
// 不会引发异常的函数
for (int num : vec) {
// 对容器中的每个元素进行操作
}
}
在上面的函数中,使用noexcept
表明该函数是一个不会引发异常的函数。该函数绝不会引发异常,也绝不允许在其范围外传播异常。一旦有异常试图退出标记为noexcept
的函数,程序直接挂掉。当函数被声明为 noexcept
时,它使编译器可以在多种不同的上下文中生成更高效的代码。
增强保证
增强保证声明,如果函数因异常退出,不会造成内存泄漏且不会修改程序状态,即增强保证提供了“回滚”语义,它要么完全成功,要么无任何效果。
#include <vector>
void removeElement(std::vector<int>& vec, int index) {
// 在函数执行之前备份容器状态
std::vector<int> backup = vec;
try {
vec.erase(vec.begin() + index); // 可能引发异常
} catch (...) {
// 处理异常
// 回滚到调用函数之前的状态
// 恢复容器的备份
vec = backup;
throw; // 重新抛出异常,以便上层处理
}
}
上面的函数中,如果因异常退出,vec
会恢复到最初的状态,也就是“回滚”。
基本保证
基本保证是最弱的一个, 基本保证声明,如果出现异常,内存不会泄漏并且对象将仍处于可用状态,但是数据可能修改。
#include <vector>
void addElement(std::vector<int>& vec, int num) {
try {
vec.push_back(num); // 可能引发 std::bad_alloc 异常
} catch (const std::bad_alloc& ex) {
// 处理内存分配失败的异常
// 函数保证容器状态不会被破坏
throw; // 重新抛出异常,以便上层处理
}
}
关于异常处理的最佳实践
要实现可靠的错误处理颇具挑战性,设计者应当在设计代码时考虑到异常。设计代码时,应当遵循以下的基本准则:
使用断言来检查永远不应发生的错误。使用异常来检查可能发生的错误,例如公共函数参数的输入验证错误。
当处理错误的代码与通过一个或多个中间函数调用检测错误的代码分离时,请使用异常。当处理错误的代码与检测错误的代码紧密耦合时,请考虑是否在性能关键型循环中使用错误代码。
对于每个可能引发或传播异常的函数,请提供三项异常保证之一:强保证、基本保证或 nothrow (noexcept) 保证。
通过值引发异常,通过引用捕获异常。不要捕获无法处理的异常。
不要使用 C++11 中已弃用的异常规范。
使用适用的标准库异常类型。从
exception
类层次结构派生自定义的异常类型。不要允许异常从析构函数或内存解除分配函数中逃逸。
性能问题
如果未引发异常,则异常机制的性能开销极低。如果引发异常,则堆栈遍历和展开的开销与函数调用的开销大致相当。进入 try
块后,需要使用额外的数据结构来跟踪调用堆栈,如果引发异常,则还需要使用额外的指令来展开堆栈。但是,在大多数情况下,性能和内存占用的开销并不高。异常对性能的不利影响可能仅在内存受限的系统上才比较明显。或者,这种影响在性能关键型循环中(其中的错误可能经常发生,并且处理错误的代码与报告错误的代码之间存在紧密耦合)可能比较明显。但是,相比较于设计良好的异常策略所提供的更高正确性、更方便的维护性和其他优势,一定的性能损失往往是可以接受的。
主要参考:https://learn.microsoft.com/zh-cn/cpp/cpp