目录
一、什么是异常
二、C语言中处理错误的方式
三、异常处理机制的基本语法
1. try和catch
2. thow
3. noexcept
4. 标准库中的异常类
5. 自定义异常
四、如何编写可维护的代码
1. 使用RAII确保资源安全
2. 避免在构造函数和析构函数中抛出异常
3. 利用异常规范说明来提高代码可读性
4. 使用异常安全的设计模式
五、来自C++之父Bjarne Stroustrup的建议
参考文献
异常是程序运行过程中出现的意外情况,它可能会导致程序无法正常执行。
无论是因为程序员的失误、不可预测的输入,还是其他外部因素,错误总是难以避免。必须在程序中添加特殊代码,来捕获异常并采取适当的措施确保程序的稳定运行。
异常处理机制将问题检测和问题处理相分离,让一个函数在发现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或者间接)调用者能够处理这个问题。
在C语言中,处理错误有两种方法:
当然,C++中仍然可以用这两种方法。然而,这两种方法有一定缺陷:
try {
// 可能发生异常的代码
}
catch (异常类型1 参数1) {
// 处理异常类型1的代码
}
catch (异常类型2 参数2) {
// 处理异常类型2的代码
}
// 更多的catch块...
try块用于定义可能发生异常的代码段。
catch块用于捕获并处理异常。
throw语句用于抛出异常。当程序执行到throw语句时,程序将立即终止当前函数的执行,并跳转到最近的try块。然后,程序开始查找与抛出的异常类型匹配的catch块。如果没有找到匹配的catch块,程序将继续在调用栈中向上查找,直到找到匹配的catch块或程序终止。
throw exceptionData; //throw 异常类型
exceptionData 是“异常数据”的意思,它可以包含任意的信息,完全有程序员决定。exceptionData 可以是 int、float、bool 等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型。
从C++11开始,用关键字noexcept表示函数中不会发生异常,便于编译器对程序做更多的优化。
如果在运行时,noexecpt函数向外抛出了异常(如果函数内部捕捉了异常并完成处理,这种情况不算抛出异常),程序会直接终止,调用std::terminate()函数,该函数内部会调用std::abort()终止程序。通常情况,编译器会帮助程序员排除一些显示的异常抛出问题。
在实践中,一般两种异常抛出方式是常用的:
void swap(Type& x, Type& y) throw() //C++11之前, c++17弃用
{
x.swap(y);
}
void swap(Type& x, Type& y) noexcept //C++11
{
x.swap(y);
}
void swap(Type& x, Type& y) noexcept(noexcept(x.swap(y))) // 表明在一定条件下不发生异常。
{
x.swap(y);
}
以下情形鼓励使用noexcept:
pair& operator=(pair&& __p)
noexcept(__and_,
is_nothrow_move_assignable<_T2>>::value)
{
first = std::forward(__p.first);
second = std::forward(__p.second);
return *this;
}
没把握的情况下,不要轻易使用noexception。
使用条件异常noexception的规范如下:
void f1();
void f2() noexcept; // different type since c++17
void (*fp)() noexcept; // pointer to function that doesn’t throw
fp = f2; // OK
fp = f1; // ERROR since C++17
void (*fp2)(); // pointer to function that might throw
fp2 = f2; // OK
fp2 = f1; // OK
void f();
void f() noexcept; // ERROR
class Base {
public:
virtual void foo() noexcept;
...
};
class Derived : public Base {
public:
void foo() override; // ERROR: does not override
// 它不会覆盖基类的foo()
// 也不能编译
...
};
void f1();
void f2() noexcept;
void f3() noexcept(sizeof(int)<4); // same type as either f1() or f2()
void f4() noexcept(sizeof(int)>=4); // different type than f3()
标准异常类:
异常名称 |
描述 |
exception | 所有标准异常类的父类。 |
bad_alloc | 当operator new and operator new[],请求分配内存失败时。 |
bad_exception | 如果函数的异常抛出列表里声明了bad_exception异常,当函数内部抛出了异常抛出列表中没有的异常,这是调用的unexpected函数中若抛出异常,不论什么类型,都会被替换为bad_exception类型。 |
bad_typeid | 使用typeid操作符,操作一个NULL指针,而该指针是带有虚函数的类,这时抛出bad_typeid异常。 |
bad_cast | 使用dynamic_cast转换引用失败的时候。 |
ios_base::failure | io操作过程出现错误。 |
logic_error | 逻辑错误,可以在运行前检测的错误。 |
runtime_error | 运行时错误,仅在运行时才可以检测的错误。 |
logic_error的子类:
异常名称 |
描述 |
length_error | 试图生成一个超出该类型最大长度的对象时,例如vector的resize操作。 |
overflow_error | 参数的值域错误,主要用在数学函数中。例如使用一个负值调用只能操作非负数的函数。 |
underflow_error | 超出有效范围。 |
invalid_argument | 参数不合适。在标准库中,当利用string对象构造bitset时,而string中的字符不是’0’或’1’的时候,抛出该异常。 |
runtime_error的子类:
异常名称 |
描述 |
range_error | 计算结果超出了有意义的值域范围 |
overflow_error | 算术计算上溢 |
underflow_error | 算术计算下溢 |
方法一:构造函数中透传
#include
#include
namespace exception {
class ExampleException final : public std::runtime_error {
public:
explicit ExampleException(std::string const& what)
: std::runtime_error{what} {}
explicit ExampleException(const char* what)
: std::runtime_error{what} {}
explicit ExampleException(const char* tag, const char* what)
: std::runtime_error{std::string{tag} + what} {}
}; // class ExampleException
} // namespace exception
// use
throw exception::ExampleException{"Invalid parameter!"};
throw exception::ExampleException{"UserName", "Invalid parameter!"};
方法二:重写what函数
可以为自定义异常类提供一个默认构造函数,以便在不提供详细信息的情况下创建异常对象。
自定义异常类的析构函数通常应该声明为虚函数,并使用noexcept关键字标记,以确保在异常处理过程中不会抛出新的异常。这是因为在C++中,析构函数中抛出异常可能导致未定义行为。
#include
#include
class MyException : public std::exception {
public:
MyException(const std::string& message)
: mMessage(message) {}
virtual ~MyException() noexcept {}
const char* what() const noexcept override {
return mMessage.c_str();
}
private:
std::string mMessage; // string内部用一个智能指针来管理内存,因此会被适时自动清理。
};
// =========== use =============================
void foo() {
throw MyException("Something went wrong in foo()");
}
int main() {
try {
foo();
} catch (const MyException& e) {
std::cerr << "Caught MyException: " << e.what() << std::endl;
} catch (...) {
std::cerr << "Caught an unknown exception" << std::endl;
}
return 0;
}
资源分配即初始化(RAII,Resource Acquisition Is Initialization)的基本思想是将资源的生命周期与对象的生命周期绑定,通过对象的构造和析构函数来分配和释放资源。当异常发生时,已经构造的对象会自动调用其析构函数,确保资源被正确释放。
在构造函数中抛出异常可能导致对象处于无效状态。如果在构造函数中抛出异常,应确保已分配的资源被正确释放,以避免内存泄漏等问题。
避免在析构函数中抛出异常,因为在异常处理过程中,析构函数抛出的异常可能导致未定义行为。
异常规格泛化(Exception Specification)是一种旧的异常处理机制,用于指定函数可能抛出的异常类型。这种机制通过在函数声明后添加throw关键字来实现。然而,异常规格泛化在实践中往往导致问题,如代码膨胀和运行时开销,因此不推荐使用。
在C++11及更高版本中,可以使用noexcept关键字来指定函数不会抛出异常,帮助编译器生成更高效的代码,并提高代码的可读性。当确定函数不会抛出异常时,可以考虑使用noexcept关键字。
异常安全的设计模式是指在异常发生时能保持程序的正确性和稳定性,异常安全性包含三个级别:
节选自《The C++ Programming Language》
1. Don’t use exceptions where more local control structures will suffice; 当局部的控制能够处理时,不要使用异常;
2. Use the "resource allocation is initialization" technique to manage resources; 使用“资源分配即初始化”技术去管理资源;
3. Minimize the use of try-blocks. Use "resource acquisition is initialization" instead of explicit handler code; 尽量少用try-catch语句块,而是使用“资源分配即初始化”技术。
4. Throw an exception to indicate failure in a constructor; 如果构造函数内发生错误,通过抛出异常来指明。
5. Avoid throwing exceptions from destructors; 避免在析构函数中抛出异常。
6. Keep ordinary code and error-handling code separate; 保持普通程序代码和异常处理代码分开。
7. Beware of memory leaks caused by memory allocated by new not being released in case of an exception; 小心通过new分配的内存在发生异常时,可能造成内存泄露。
8. Assume that every exception that can be thrown by a function will be thrown; 如果一个函数可能抛出某种异常,那么我们调用它时,就要假定它一定会抛出该异常,即要进行处理。
9. Don't assume that every exception is derived from class exception; 要记住,不是所有的异常都继承自exception类。
10. A library shouldn't unilaterally terminate a program. Instead, throw an exception and let a caller decide; 编写的供别人调用的程序库,不应该结束程序,而应该通过抛出异常,让调用者决定如何处理(因为调用者必须要处理抛出的异常)。
11. Develop an error-handling strategy early in a design; 若开发一个项目,那么在设计阶段就要确定“错误处理的策略”。
[1] c++ 11 noexcept_c++ noexcept-CSDN博客
[2] 【C++ 异常】C++异常处理:掌握高效、健壮代码的秘密武器_黑马c++异常处理-CSDN博客
[3] C++的异常处理_c++ 在什么时候需要异常处理-CSDN博客