侧边栏壁纸
博主头像
LittleAO的学习小站 博主等级

在知识的沙漠寻找绿洲

  • 累计撰写 125 篇文章
  • 累计创建 27 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

C++之 异常处理

LittleAO
2024-09-16 / 0 评论 / 0 点赞 / 9 阅读 / 0 字
温馨提示:
本文最后更新于2024-09-22,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

简单的异常处理

在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 // 不会执行
}

上面这段代码,如果输入的ab相等,则会抛出一个runtime_error错误。此时程序会终止,throw语句后面的代码不会被执行。

有些时候,我们并不希望抛出错误就立刻终止程序,通常情况下我们希望对异常能够简单处理,除非引发了致命错误的异常,否则不要退出程序。

利用trycatch语句块就能实现对异常的捕获。如果在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

异常说明与指针、虚函数和拷贝控制

  1. 如果函数指针被声明不抛出异常,则只能赋值为不抛出异常的函数指针。如果函数指针没做约束,则可以指向任意函数。

例子:

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;
}
  1. 虚函数承诺不抛出异常,则派生类的虚函数必须做出一样的声明。若虚函数可以抛出异常,派生虚函数可以声明为不允许抛出异常;

  2. 当编译器生成默认的拷贝控制成员函数(如拷贝构造函数、拷贝赋值运算符等)时,它会同时生成一个异常说明。如果所有成员和基类的所有操作都承诺不会抛出异常,那么合成的成员函数将被标记为 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 的。

异常类层次

异常类的层次如下:

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

0

评论区