本文为《C++ Primer》的读书笔记
try
语句块throw
表达式throw
表达式引发一个异常throw
表达式包含关键字 throw
和紧随其后的一个表达式, 其中表达式的类型就是抛出的异常类型;抛出的异常类型可以是任意类型,不一定非得是标准异常类// 初始化`runtime_error`的对象
throw runtime_error("Data must refer to same ISBN");
try
语句块try {
program-statements
} catch (exception-declaration) {
handler-statements
} catch (exception-declaration) {
handler-statements
} // ...
catch
子句包括三部分:
catch
catch
子句处理异常之后,执行与之对应的块。catch
一旦完成, 程序跳转到try
语句块最后一个catch
子句之后的那条语句继续执行try
语句块内声明的变量在块外部无法访问,特别是在 catch
子句内也无法访问while (cin >> iteml >> item2) {
try {
// 如果失败, 代码抛出一个 runtime_error 异常
} catch (runtime_error err) {
cout << err.what()
<< "\nTry Again? Enter y or n" << endl;
char c;
cin >> c;
if(!cin || c == 'n')
break; // 跳出 while 循环
}
}
catch
子句throw
出现在一个 try
语句块 (try block) 内时, 检查与该 try
块关联的catch
子句。如果找到了匹配的 catch
, 就使用该 catch
处理异常catch
且该 try
语句嵌套在其他try
块中, 则继续检查与外层 try
匹配的catch
子句。如果还是找不到匹配的 catch
, 则退出当前的函数, 在调用当前函数的外层函数中继续寻找try
语句块内, 则检查与该 try
块关联的catch
子句。如果找到了匹配的catch
, 就使用该catch
处理异常。否则, 如果该try
语句嵌套在其他try
块中,则继续检查与外层try
匹配的catch
子句。如果仍然没有找到匹配的catch
, 则退出当前这个主调函数,继续在调用了刚刚退出的这个函数的其他函数中寻找, 以此类推catch
子句为止: 或者也可能一直没找到匹配的catch
, 则退出主函数后查找过程终止
catch
子句, 则程序进入该子句并执行其中的代码。当执行完这个catch
子句后, 找到与try
块关联的最后一个catch
子句之后的点, 并从这里继续执行catch
子句, 程序将调用 标准库函数 terminate
terminate
的行为与系统有关, 一般情况下, 执行该函数将导致程序非正常退出,从而终止程序的执行过程使用类控制资源的分配
析构函数不应该抛出不能被它自身处理的异常
try
语句块当中, 并在析构函数内部得到处理throw
语句中的表达式必须拥有完全类型
catch
子句都能访问该空间。当异常处理完毕后, 异常对象被销毁catch
语句之前就已经退出了, 则意味着在执行catch
语句之前局部对象已经被销毁了
throw
表达式解引用一个基类指针, 而该指针实际指向的是派生类对象, 则抛出的对象将被切掉一部分, 只有基类部分被抛出catch
子句 (catch clause) 中的异常声明看起来像是只包含一个形参的函数形参列表。如果catch
无须访问抛出的表达式的话, 则我们可以忽略捕获形参的名字
catch
语句后, 通过异常对象初始化异常声明中的参数。如果catch
的参数类型是非引用类型,则该参数是异常对象的一个副本; 如果参数是引用类型, 则和其他引用参数一样, 该参数是异常对象的一个别名catch
的参数是基类类型, 则我们可以使用其派生类类型的异常对象对其进行初始化。此时, 如果catch
的参数是非引用类型, 则异常对象将被切掉一部分, 这与将派生类对象以值传递的方式传给一个普通函数差不多catch
的参数是基类的引用, 则该参数将以常规方式绑定到异常对象上。但异常声明的静态类型将决定catch
语句所能执行的操作。如果catch
的参数是基类类型, 则catch
无法使用派生类特有的成员通常情况下,如果
catch
接受的异常与某个继承体系有关,则最好将该catch
的参数定义成引用类型
catch
语句的过程中,我们最终找到的catch
未必是异常的最佳匹配。和反,挑选出来的应该是第一个与异常匹配的catch
语句。因此, 越是专门的catch
越应该置于整个catch
列表的前端当程序使用具有继承关系的多个异常时必须对
catch
语句的顺序进行组织和管理, 使得派生类异常的处理代码出现在基类异常的处理代码之前
catch
异常声明的匹配规则受到更多限制。此时, 绝大多数类型转换都不被允许, 除了一些极细小的差别之外, 要求异常的类型和catch
声明的类型是精确匹配的:
throw
语句可以匹配一个接受常量引用的catch
语句catch
的过程中使用catch
语句不能完整地处理某个异常。在执行了某些校正操作之后, 当前的catch
可能会决定由调用链更上一层的函数接着处理异常。一条catch
语句通过重新抛出(rethrowing)的操作将异常传递给另外一个catch
语句:throw;
throw
语句只能出现在catch
语句或catch
语句直接或间接调用的函数之内。如果在处理代码之外的区域遇到了空throw
语句, 编译器将调用terminate
catch
语句会改变其参数的内容。如果在改变了参数的内容后catch
语句重新抛出异常, 则只有当catch
异常声明是引用类型时我们对参数所做的改变才会被保留并继续传播:catch (my_error &eObj) { // 引用类型
eObj.status = errCodes::severeErr; // 修改了异常对象
throw; //异常对象的status成员是severeErr
} catch (other_error eObj) { // 非引用类型
eObj.status = errCodes::badErr; // 只修改了异常对象的局部副本
throw; // 异常对象的status成员没有改变
}
catch(...)
,使其可以与任意类型的异常匹配
catch(...)
通常与重新抛出语句一起使用, 其中catch
执行当前局部能完成的工作, 随后重新抛出异常:catch(...)
与其他几个catch
语句一起出现,则**catch(...)
必须在最后的位置**。出现在捕获所有异常语句后面的catch
语句将永远不会被匹配void manip() {
try {
// 抛出一个异常
catch (...) {
// 处理异常的某些特殊操作
throw;
}
}
try
语句块 与 构造函数try
语句块还未生效, 所以构造函数体内的catch
语句无法处理构造函数初始值列表抛出的异常try
语句块(也称为函数测试块, function try block)的形式try
语句块使得一组catch
语句既能处理构造函数体(或析构函数体), 也能处理构造函数的初始化过程(或析构函数的析构过程)例如:
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il) try :
data(std::make_shared<std::vector<T>>(il)) {
/* 空函数体 */
} catch(const std::bad_alloc &e) { handle_out_of_memory(e); }
try
出现在表示构造函数初始值列表的冒号以及表示构造函数体的花括号之前。与这个try
关联的catch
既能处理构造函数体抛出的异常,也能处理成员初始化列表抛出的异常try
语句块的一部分。函数try
语句块只能处理构造函数开始执行后发生的异常。和其他函数调用一样,如果在参数初始化的过程中发生了异常,则该异常属于调用表达式的一部分,并将在调用者所在的上下文中处理noexcept
异常说明对用户及编译器来说,预先知道某个函数不会抛出异常显然大有裨益
在C++11新标准中,我们可以通过提供 noexcept
说明 指定某个函数不会抛出异常
noexcept
紧跟在函数的参数列表之后,尾置返回类型之前。在成员函数中,noexcept
说明符需要跟在const
及引用限定符之后,而在final
、override
或虚函数的=0
之前noexcept
说明要么出现在该函数的所有声明语句和定义语句中,要么一次也不出现noexcept
,在typedef
或类型别名中则不能出现noexcept
void recoup(int) noexcept; // 不会抛出异常
noexcept
说明。实际上,如果一个函数在说明了noexcept
的同时又含有throw
语句或者调用了可能抛出异常的其他函数,编译器将顺利编译通过noexcept
函数抛出了异常,程序就会调用terminate
以确保遵守不在运行时抛出异常的承诺。上述过程对是否执行栈展开未作约定, 因此noexcept
可以用在两种情况下:
noexcept
说明符接受一个可选的实参, 该实参必须能转换为bool
类型:
true
, 则函数不会抛出异常false
, 则函数可能抛出异常:void recoup(int) noexcept(true); // recoup不会抛出异常
void alloc(int) noexcept(false); // alloc可能抛出异常
noexcept
运算符noexcept
说明符的实参常常与 noexcept
运算符混合使用
noexcept
运算符是一个一元运算符, 它的返回值是一个bool
类型的右值常量表达式,用于表示给定的表达式是否会抛出异常noexcept
也不会求其运算对象的值noexcept(recoup(i)) //如果recoup不抛出异常则结果为true; 否则结果为false
更普通的形式是:
noexcept(e)
e
调用的所有函数都做了不抛出说明且e
本身不含有 throw
语句时, 上述表达式为true
我们可以使用noexcept
运算符得到如下的异常说明:
void f() noexcept(noexcept(g())); // f和g 的异常说明一致
尽管 noexcept
说明符不属于函数类型的一部分, 但是函数的异常说明仍然会影响函数的使用
// recoup 和 pf1 都承诺不会抛出异常
void (*pf1)(int) noexcept = recoup;
// 正确: recoup 不会抛出异常, pf2 可能抛出异常, 二者之间互不干扰
void (*pf2)(int) = recoup;
pf1 = alloc; // 错误: alloc 可能抛出异常, 但是pf1 已经说明了它不会抛出异常
pf2 = alloc; // 正确: pf2 和 alloc 都可能抛出异常
class Base {
public:
virtual double f1(double) noexcept; //不会抛出异常
virtual int f2() noexcept(false); // 可能抛出异常
virtual void f3(); // 可能抛出异常
};
class Derived : public Base {
public:
double f1(double); // 错误: Base::f1 承诺不会抛出异帘
int f2() noexcept(false); // 正确: 与Base::f2 的异常说明一致
void f3() noexcept; // 正确: Derived 的f3 做了更严格的限定
}
noexcept
的标准库异常类的继承体系如下图所示:
exception
只报告异常的发生,不提供任何额外信息
what
的虚成员 对于只有默认初始化函数的异常类型来说, what
返回的内容由编译器决定what
函数返回一个 const char*
, 该指针指向一个字符串数组, 并且确保不会抛出任何异常
what
是在catch
异常后用于提取异常基本信息的虚函数。如果what
抛出异常,则会在新产生的异常中由于what
继续产生异常,将会产生抛出异常的死循环。因此,what
必须确保不抛出异常
exception
、bad_cast
和bad_alloc
定义了默认构造函数。因此,只能以默认初始化的方式初始化exception
、bad_alloc
和bad_cast
对象, 不允许为这些对象提供初始值runtime_error
和logic_error
没有默认构造函数, 但是有一个可以接受 C 风格字符串或者标准库string
类型实参的构造函数, 这些实参负责提供关于错误的更多信息
runtime_error
表示的是只有在程序运行时才能检测到的错误logic_error
一般指的是我们可以在程序代码中发现的错误what
负责返回用于初始化异常对象的信息exception
(或者exception
的标准库派生类)的派生类以扩展其继承体系。这些面向应用的异常类表示了与应用相关的异常条件// 为某个书店应用程序设定的异常类
// out_of_stock 类表示在运行时可能发生的错误, 比如某些顺序无法满足
class out_of_stock : public std::runtime_error
{
public:
explicit out_of_stock(const std::string &s):
std::runtime_error(s) {}
};
// isbn_mismatch 类是logic_error的一个特例,
// 程序可以通过比较对象的 isbn() 结果来阻止或处理这一错误
class isbn_mismatch : public std::logic_error
{
public:
explicit isbn_mismatch(const std::string &s):
std::logic_error(s) {}
isbn_mismatch(const std::string &s,
const std::string &lhs, const std::string &rhs):
std::logic_error(s), left(lhs), right(rhs) { }
const std::string left, right;
};
// 如果参与加法的两个对象并非同一书籍, 则抛出一个异帘
Sales_data&
Sales_data::operator+=(const Sales_data& rhs)
{
if(isbn() != rhs.isbn())
throw isbn_mismatch("wrong isbns", isbn(), rhs.isbn());
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
Sales_data item1, item2, sum;
while (cin >> item1 >> item2) {
try {
sum = item1 + item2;
} catch (const isbn_mismatch &e) {
cerr << e.what() << ": left isbn(" << e.left << ") right isbn(" << e.right << ")" << endl;
}
}