《C++ Primer》 异常
1、throw
抛出异常
当执行一个throw
语句,必然会引发并抛出一个异常,throw
后面的语句不再被执行。程序的控制权从throw
转到与抛出对象类型匹配的最近的catch
模块。
栈展开(栈解旋)
指的是当发生异常后,不断向外层匹配catch
模块的过程。
如果异常在try
里,检查与该try
关联的catch
,没有;若该try
嵌套在另一个try
里,就查找与另一个try
关联的catch
,没有;直接退出当前函数,在调用当前函数的外层函数中继续匹配。
栈展开就是一心寻找匹配的catch
,别的啥都不管。
栈展开过程中对象被自动销毁
(这里说的对象是那些抛出异常之前创建的对象)
栈展开过程中(实际上是离开作用域之后),编译器负责正确的销毁对象,对于内置类型,编译器什么也不用做;对于类类型,编译器调用他们的析构函数。
若异常发生在构造函数中,程序员要确保已构造的成员能被正确销毁。
类似的,异常也可能发生在数组或标准库容器的元素初始化过程中,程序员也要确保一构造的元素被正确销毁。
但若一个块分配了资源,并在负责释放这些资源的代码前发生了异常,则释放资源的代码将不会被执行。这时应在捕获代码中释放这些资源。
析构函数与异常
所有标准库类型都能确保他们的析构函数不会引发异常。
异常对象
异常对象就是throw
抛出的那个东西,这里的对象是指广义的对象,即,指向对象的指针也算是异常对象。异常对象可以是内置类型,像int
和double
这种的;也可以是C++标准库中定义的异常对象(except
族系);当然喽,也可以是我们自己定义的异常类对象。
注意①:
异常对象比较特殊,他们既不在堆区,也不在栈区,而是位于由编译器管理的内存空间中,编译器确保无论最终调用的是哪个catch
模块,都能访问该空间,且该空间的数据都有效。当异常处理完毕后,编译器调用析构函数将其销毁。虽说编译器做了上述保证,但还是不能确保catch
真的能访问到异常对象。如果throw
的异常对象是个裸指针的话,编译器在特殊空间中维护的是指针的值,而不是其指向的对象,当throw
抛出异常,离开{}
后,裸指针指向的局部对象被销毁,此时执行catch
一定会出现错误。
throw
一个指针的正确姿势是,使用new
返回的指针,这样产生的对象存储在堆区,由程序员管理,不会被编译器销毁。
注意②:
当throw
一个表达式时,编译器真正维护的异常对象的类型是 该表达式的静态编译时类型。即,此处无法使用多态。如果throw
表达式解引用一个基类(except
)指针,而指针实际指向的是子类对象,则抛出的对象将被切掉一部分,即,编译器真正在特殊空间中维护的对象只有基类的部分。
整个C++异常处理,只有这里无法使用多态,其他的地方都可以使用多态。
2、捕获对象
catch
捕获异常有三种方式。
- ①:使用异常对象的普通类型捕获,此时
catch
捕获到的实际上是特殊空间中异常对象的一个副本。 - ②:使用指针捕获,此时可以使用多态,
catch
操作的就是特殊空间中的异常对象,catch
的末尾必须delete
掉该指针。 - ③:使用引用捕获,也能使用多态,操作的也是特殊空间中的异常对象,不需要
delete
。一般我们只使用引用来捕获。
三种捕获在代码上的区别:
#include<iostream>
#include<exception>
using namespace std;
class MyException : public exception
{
public:
MyException()
{
cout << "默认构造" << endl;
}
MyException(const MyException& v)
{
m_str = v.m_str;
cout << "拷贝构造" << endl;
}
MyException(const string& s)
{
m_str = s;
cout << "string 的构造函数" << endl;
}
const char* what() const noexcept // 此处的 noexcept 必须加
{
return m_str.c_str();
}
~MyException()
{
cout << "析构函数" << endl;
}
private:
std::string m_str;
};
int main()
{
try
{
throw MyException("我自己的异常");
}
catch (MyException e)
{
cout << e.what() << endl;
}
cout << endl;
try
{
throw (new MyException("我自己的异常"));
}
catch (exception* e)
{
cout << e->what() << endl;
delete e;
}
cout << endl;
try
{
throw MyException("我自己的异常");
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
程序输出:
重新抛出
有时,一个单独的 catch
语句不能完整地处理某个异常。在执行了某些校正操作之后,当前的catch
可能会决定由调用链更上一层的函数接着处理异常。此时要做的操作就是重新抛出,体现在代码上就是一条throw
语句,但不包括任何表达式:
throw;
捕获所有异常
catch(...)
能够匹配所有异常类型。
处理构造函数初始值时发生了异常该怎么办?
通常情况下,程序执行的任何时刻都可能发生异常,处理构造函数初始值的过程中也不例外。构造函数在进入其函数体之前首先执行初始值列表。因为在初始值列表抛出异常时构造函数体内的try
语句块还未生效,所以构造函数体内的catch
语句无法处理构造函数初始值列表抛出的异常。
class X
{
public:
X(std::inirializer_list<T> il) try : m_vec(std::make_shared<std::vector<T>>(il)
{
// 构造函数的的函数体
}
catch(catch std::bad_alloc& e)
{
handle_out_of_memory(e);
}
private:
vector<int>* m_vec;
};
函数try
语句块中:关键字try
出现在构造函数初始值列表的冒号之前,catch
出现在函数体之后。这个try
关联的catch
既能处理构造函数体抛出的异常,也能处理初始化列表抛出的异常。
还有一种情况需要注意,在初始化构造函数的参数时也可能发生异常,这样的异常不属于函数try
语句块的一部分。函数try
语句块只能处理构造函数开始执行后发生的异常。和其他函数调用一样,如果在参数初始化的过程中发生了异常,则该异常属于调用表达式的一部分,需要在调用者所在的上下文中处理。
3、noexcept
异常说明
noexcept
关键字有两种含义,noexcept
说明符,以及noexcept
运算符。其具体含义由出现位置决定。
noexcept
说明符
noexcept
关键字指定某个函数不会抛出异常。通过这个指定,我们就能在函数体内部执行某些特殊的优化操作。(这些优化操作不适用于可能出错的代码)
noexcept
的出现位置很有讲究,具体有如下要求:
- 要想用
noexcept
修饰一个函数,该函数的所有声明语句和定义语句中都必须有noexcept
关键字 noexcept
要在函数的位置返回类型之前- 在
typedef
和类型别名中不能出现noexcept
- 成员函数中,
noexcept
需要跟在const
以及引用限定符之后,在final
、override
以及虚函数的=0
之前。
noexcept
说明符可以接收一个实参,该实参必须能转换为bool
类型,最终,若实参是true
,指定函数不会抛出异常;若实参是false
,则指定函数可能抛出异常。
void func1() noexcept(true); func1() 不会抛出异常
void func2() noexcept(false); func2() 可能抛出异常
noexcept
运算符
noexcept
做运算符时是个一元运算符,返回值是一个bool
类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。它的返回值一般会作为另一个noexcept
说明符的实参,如下代码所示:
void function1() noexcept(noexcept(func1())); func1()承诺不会抛出异常,则function1()也不会抛出异常
void function2() noexcept(noexcept(func2())); func2()可能抛出异常,则function2()也可能抛出异常
对于noexcept
运算符需要注意:只有当它接受的运算对象中调用的所有函数都做了不抛出说明且它自己本身不含有throw
语句时,noexcept
运算符才会返回true
,否则都会返回false
。
异常说明 与 函数指针、虚函数、拷贝控制
异常说明 与 函数指针
函数指针以及该指针所指向的函数必须具有意志的异常说明。即,如果为某个函数指针做了不抛出异常的声明,则该指针只能指向不抛出异常的函数。但是,说明了可能抛出异常的函数指针 可以指向任何函数。
void (*pf1)() noexcept = func1(); 正确
void (*pf2)() = func1(); 也正确
pf1 = func2(); 错误
pf2 = func2(); 正确
异常说明 与 虚函数
如果一个虚函数承诺了它不会抛出异常,则后续派生出来的虚函数也必须做出同样的承诺;与之相反,如果基类的虚函数允许抛出异常,则子类的对应函数既可以允许抛出异常,也可以不允许抛出异常。
异常说明 与 拷贝控制
4、异常类层次
标准库的异常类层次
基类exception
仅仅定义了默认构造函数、拷贝构造函数、拷贝赋值运算符、虚析构函数、名为what
的纯虚成员函数。what
返回const char*
,该指针指向以NULL
结尾的字符数组,并确保不抛出任何异常。
自定义异常对象
注意事项:
- 要继承自标准异常基类
exception
- 必须写默认构造、有参构造、析构
- 必须重写
what
方法,并使用noexcept
修饰,因为except
的what
就使用了noexcept