简单的异常处理
在C++中,如果除法运算符的除数为0,则会引发一个Integer division by zero错误。除了自动引发的这些错误之外,我们还可以手动抛出异常。使用关键字throw来抛出异常,例如:
int a, b;
std::cin >> a >> b;
if (a == b)
{
throw std::runtime_error("Same nums!");
a += b // 不会执行
}上面这段代码,如果输入的a和b相等,则会抛出一个runtime_error错误。此时程序会终止,throw语句后面的代码不会被执行。
有些时候,我们并不希望抛出错误就立刻终止程序,通常情况下我们希望对异常能够简单处理,除非引发了致命错误的异常,否则不要退出程序。
利用try和catch语句块就能实现对异常的捕获。如果在try语句块内引发的异常,会尝试寻找一个能够匹配的catch语句块,并执行内部的代码,之后便继续执行后面的代码。下面是一个简单的例子:
int a, b;
std::cin >> a >> b;
try
{
if (a == b)
{
throw std::runtime_error("Same nums!");
}
}
catch (std::runtime_error) // 捕获
{
std::cout << "program will not exit." << std::endl;
}
std::cout << "program continues." << std::endl; // 继续执行程序try、catch、throw组成了异常处理的最基本方法。看起来有些简单了?如果将其与面向对象编程结合起来呢?
抛出异常
前面提到,我们用throw关键字就能够抛出异常。执行throw时,跟在后面的语句将不再被执行。程序的控制权将转移给对应的catch模块。这样做有两个重要的含义:
沿用调用链的函数可能会提早退出;
一旦程序开始执行异常处理代码,则沿着调用链创建的对象将会被销毁。
#include <iostream>
#include <stdexcept>
class Resource {
public:
Resource(const std::string& name) : name(name) {
std::cout << "Resource " << name << " acquired.\n";
}
~Resource() {
std::cout << "Resource " << name << " released.\n";
}
private:
std::string name;
};
void functionC() {
Resource resC("C");
std::cout << "In functionC\n";
throw std::runtime_error("Exception in functionC");
}
void functionB() {
Resource resB("B");
std::cout << "In functionB\n";
functionC();
}
void functionA() {
Resource resA("A");
std::cout << "In functionA\n";
functionB();
}
int main() {
try {
functionA();
} catch (const std::exception& e) {
std::cout << "Caught exception: " << e.what() << '\n';
}
return 0;
}
上面的代码成功在执行到catch之前,成功释放掉了调用链上的对象,避免了内存泄漏。
栈展开
当在try语句块内时发生了异常,检查与该try块关联的catch字句,如果没有找到,则查找上一层try块中的catch字句。如果上一层没有try,程序则会调用标准库terminate,程序运行终止。
依次向上一层寻找catch字句的过程,被称为栈展开。和上面的示例代码相似,栈展开过程中对象也会发生自动销毁。
自动销毁过程中,会自动执行类对象的析构函数,一般情况下,析构函数不应该抛出错误,至少不应该抛出不能被自己处理的析构函数。即使析构函数执行某条函数语句可能导致异常抛出,也应该在析构函数块内使用catch进行捕获。
异常对象
throw抛出的是一个异常对象,在抛出时,对异常对象会进行拷贝初始化。因此异常对象必须是完全类型,必须含有一个可访问的析构函数和可访问的拷贝或移动构造函数。
抛出表达式的静态编译类型决定了异常对象的类型,因此用throw抛出基类指针,即使它指向了派生类,抛出的对象会被裁掉一部分。
栈展开不会释放动态内存
这是一段代码,分析会发生什么事?
void exercise(int *b, int *e)
{
vector<int> v(b, e);
int *p = new int[v.size()];
ifstream in("ints");
// 此处发生异常
}
vector<int>的构造函数会抛出异常,函数会立即退出,栈展开机制会释放已经构造的对象。这行代码之后发生了异常,
ifstream对象会被销毁,关闭文件。在异常抛出时,
vector<int> v会被销毁,但int *p的内存不会自动释放,因为它是通过new动态分配的。
解决方案是使用智能指针,例如下面这样:
#include <iostream>
#include <vector>
#include <memory>
#include <fstream>
void exercise(int *b, int *e)
{
std::vector<int> v(b, e);
std::unique_ptr<int[]> p(new int[v.size()]);
std::ifstream in("ints");
// 此处发生异常
}捕获异常
catch后面包含一个异常声明,就像函数的形参列表。如果catch无须访问抛出表达式,就可以忽略捕获形参的名字。
通常情况下,如果catch接受的异常和某个继承体系有关,则最好将该catch的参数定义成引用类型。
在寻找匹配时,我们寻找的catch未必是最佳匹配。越是专门的catch应该位于catch列表的最前端。除了一些极微小的差别之外,要求异常的类型和catch声明是精确匹配的:
允许非常量向常量的类型转换。
允许派生类向基类的类型转换。
允许数组转换成指向数组元素类型的指针,函数转换成指向函数类型的指针。
除此之外,标准算术类型转换和类类型转换在内,其他所有转换刚才则都不能在匹配catch的过程中使用。
举一个捕获基类的例子:
#include <iostream>
#include <stdexcept>
class MyException : public std::runtime_error {
public:
MyException(const std::string& message) : std::runtime_error(message) {}
};
void example2() {
try {
throw MyException("My custom exception");
} catch (const std::runtime_error& e) { // 捕获基类引用
std::cout << "Caught exception: " << e.what() << std::endl;
}
}
int main() {
example2();
return 0;
}
再举一个数组转换成类型指针的例子:
#include <iostream>
void example3() {
try {
throw "An exception"; // 抛出字符数组
} catch (const char* e) { // 捕获指向字符的指针
std::cout << "Caught exception: " << e << std::endl;
}
}
int main() {
example3();
return 0;
}
重新抛出
一个单独的catch有时不能单独的处理某个异常,在执行了某些矫正操作,需要再次抛出,这称作为重新抛出(rethrowing)。重新抛出将会传递到下一个catch语句。重新抛出无需制定新的表达式,而是将当前的异常对象沿着调用链向上传递。
#include <iostream>
#include <stdexcept>
void example() {
try {
try {
throw std::runtime_error("Initial error");
} catch (const std::runtime_error& e) {
std::cout << "Caught in inner try: " << e.what() << std::endl;
throw; // 重新抛出异常
}
} catch (const std::runtime_error& e) {
std::cout << "Caught in outer try: " << e.what() << std::endl;
}
}
int main() {
example();
return 0;
}
捕获所有异常
使用catch(...)可以捕获所有异常,可以与任意类型的异常匹配。
#include <exception>
#include <iostream>
#include <stdexcept>
void example() {
try {
throw std::exception("No thanks.");
} catch (const std::runtime_error& e) {
std::cout << "Caught in inner try: " << e.what() << std::endl;
} catch (...) {
std::cout << "Unknown Error!" << std::endl;
}
}
int main() {
example();
return 0;
}
构造函数与try语句块
构造函数执行前,需要执行初始化列表。如果在这里发生了错误,即使构造函数内部由try语句块也无济于事。解决方法就是在初始化列表中置于函数try语句块中。例如下面:
#include <initializer_list>
#include <iostream>
#include <memory>
#include <vector>
template<typename T>
class Blob
{
std::shared_ptr<std::vector<T>> data;
public:
Blob(std::initializer_list<T> il) try
: data(std::make_shared<std::vector<T>>(il))
{
// 构造函数体可以为空
}
catch (const std::bad_alloc &e)
{
// 处理异常
std::cout << "error! " << e.what() << std::endl;
}
};
int main()
{
try {
Blob<int> c{2, 3, 4, 5};
} catch (const std::exception &e) {
std::cout << "Caught an exception in main: " << e.what() << std::endl;
}
}
noexcept异常说明
为了优化考虑,C++11引入了新的关键字noexcept,标识某个函数不会抛出错误,因此编译器会对它进行特别的优化。其形式是noexcept紧跟在函数的参数列表后面。例如:
void function(int) noexcept;表明对function做出了不抛出说明。
违反异常说明
某函数声明了不抛出异常,但是还是在其内部抛出了异常,编译可以通过,但是这会导致抛出的异常不会被catch,而是直接调用terminate,程序会被直接终止。
异常说明的实参
异常说明后面可以跟一个实参,实参为true,则表明函数不会抛出异常,实参为false,表明函数会抛出异常。
void func2() noexcept(false)
{
throw std::runtime_error("Test"); // 没有warning
}
void func1() noexcept(true)
{
throw std::runtime_error("Test"); // warning C4297: “func1”: 假定函数不引发异常,但确实发生了
}利用noexcept运算符,可以判断某个函数有没有被表示不抛出异常,以上面的例子为例:
noexcept(func1) // 返回true
noexcept(func2) // 返回false异常说明与指针、虚函数和拷贝控制
如果函数指针被声明不抛出异常,则只能赋值为不抛出异常的函数指针。如果函数指针没做约束,则可以指向任意函数。
例子:
void func1(int) noexcept(true) {}
void func2(int) noexcept(false) {}
int main()
{
void (*pf1)(int) noexcept = func1;
void (*pf2)(int) = func2;
//pf1 = func2; // 错误:不抛出异常函数指针不能赋值为可以抛出异常的函数
pf2 = func1;
}
虚函数承诺不抛出异常,则派生类的虚函数必须做出一样的声明。若虚函数可以抛出异常,派生虚函数可以声明为不允许抛出异常;
当编译器生成默认的拷贝控制成员函数(如拷贝构造函数、拷贝赋值运算符等)时,它会同时生成一个异常说明。如果所有成员和基类的所有操作都承诺不会抛出异常,那么合成的成员函数将被标记为
noexcept,表示它不会抛出异常。如果合成的成员函数调用的任何一个函数可能抛出异常,那么合成的成员函数将被标记为noexcept(false),表示它可能会抛出异常。
class A {
public:
A() noexcept; // 构造函数不会抛出异常
A(const A&) noexcept; // 拷贝构造函数不会抛出异常
};
class B {
A a;
public:
B() = default; // 默认构造函数
B(const B&) = default; // 默认拷贝构造函数
};
A的构造函数和拷贝构造函数都被标记为noexcept,表示它们不会抛出异常。B的默认构造函数和拷贝构造函数由编译器合成。因为A的构造函数和拷贝构造函数都是noexcept的,所以B的合成默认构造函数和拷贝构造函数也将是noexcept的。
异常类层次
异常类的层次如下:

可以通过继承来重写我们自己的异常类。
评论区