文章

C++异常处理

C++异常处理

该文记录 C++ 标准异常和使用,以及自定义异常。

C++异常处理

1 异常定义

用官方的话来说就是程序在执行过程中产生的问题,换句通俗的话来讲就是程序执行的出现的异常,比如程序崩了、内存泄漏了、数组越界以及其他异常信息的出现。

2 异常处理

2.1 处理流程

异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:trycatchthrow

  • try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。

如果有一个块抛出一个异常,捕获异常的方法会使用 trycatch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 示例
#include <iostream>
#include <stdexcept>

int main()
{
    int a = 2;
    int b = 0;
    try
    {
        if (b == 0)
        {
            throw std::logic_error("除数不能为 0");
        }
    }
    catch(const std::exception& e)
    {
        std::cerr << e.what() << '\n';
    }
    return 0;
}

2.2 标准异常

C++ 提供了一系列标准的异常,我们可以在程序中使用这些标准的异常。

1
2
3
4
5
// 包含头文件
#include <exception>
#include <new>
#include <typeinfo>
#include <stdexcept>

它们是以父子类层次结构组织起来的,如下所示:

异常描述
std::exception该异常是所有标准 C++ 异常的父类
std::bad_alloc该异常可以通过 new 抛出
std::bad_cast该异常可以通过 dynamic_cast 抛出
std::bad_typeid该异常可以通过 typeid 抛出
std::bad_exception这在处理 C++ 程序中无法预期的异常时非常有用
std::logic_error逻辑错误:可在运行前检测到的问题
std::domain_error逻辑错误:参数的结果值不存在
std::invalid_argument逻辑错误:不合适的参数
std::length_error逻辑错误:试图生成一个超出该类型最大长度的对象
std::out_of_range逻辑错误:使用一个超出有效范围的值
std::runtime_error运行时错误:仅在运行时才能检测到的问题
std::range_error运行时错误:生成的结果超出了有意义的值域范围
std::overflow_error运行时错误:计算上溢
std::underflow_error运行时错误:计算上溢

2.3 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <stdexcept>
#include <vector>

void InvalidArgument()
{
    try
    {
        float f = std::stof("(3.14)"); // 抛出异常:没有转换
    }
    catch (const std::invalid_argument& e)
    {
        std::cout << "std::invalid_argument: " << e.what() << '\n';
    }
}

void LengthError()
{
    std::vector<int> intVec = {1, 2, 3};
    try
    {
        intVec.reserve(intVec.max_size() + 1); // 抛出异常:长度错误
    }
    catch (const std::length_error& e)
    {
        std::cout << "std::length_error: " << e.what() << '\n';
    }
}

void OutOfRange()
{
    std::vector<int> intVec = {1, 2, 3};
    try
    {
        intVec.at(intVec.max_size() + 1); // 抛出异常:超出范围
    }
    catch (const std::out_of_range& e)
    {
        std::cout << "std::out_of_range: " << e.what() << '\n';
    }
}

int main()
{
    // 逻辑错误
    InvalidArgument();
    LengthError();
    OutOfRange();
    // 运行错误
    return 0;
}

3 自定义异常

3.1 标准定义

Linux 作为参考。

基类 exception 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class exception
{
public:
    exception() _GLIBCXX_NOTHROW { }
    virtual ~exception() _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_NOTHROW;
#if __cplusplus >= 201103L
    exception(const exception&) = default;
    exception& operator=(const exception&) = default;
    exception(exception&&) = default;
    exception& operator=(exception&&) = default;
#endif
    virtual const char* what() const _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_NOTHROW;
};

子类 logic_error 定义如下:

1
2
3
4
5
6
7
8
9
class logic_error : public exception
{
    __cow_string _M_msg;

public:
    explicit logic_error(const string& __arg) _GLIBCXX_TXN_SAFE;
    virtual ~logic_error() _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_NOTHROW;
    virtual const char* what() const _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_NOTHROW;
};

子类 domain_error 定义如下:

1
2
3
4
5
6
class domain_error : public logic_error
{
public:
    explicit domain_error(const string& __arg) _GLIBCXX_TXN_SAFE;
    virtual ~domain_error() _GLIBCXX_NOTHROW;
};

可以自己查看源码,显示源码删除了条件编译。

  • 基类 exception 定义了构造、虚析构、错误显示接口
  • 子类 logic_error 同理定义,但是多了一个保存错误信息的属性
  • 子类 domain_error 则沿用了父类 logic_error 接口和属性

3.2 自定义

自定义一个文件异常类,子类分为打开异常、读写异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <exception>
#include <fstream>
#include <iostream>
#include <string>

// 文件异常类
class ExceptionFile : public std::exception
{
    std::string m_msg;

public:
    explicit ExceptionFile(const std::string& msg) { m_msg = msg; };
    virtual ~ExceptionFile(){};
    virtual const char* what() const noexcept { return m_msg.c_str(); };
};

// 文件打开异常类
class ExceptionFileOpen : public ExceptionFile
{
public:
    explicit ExceptionFileOpen(const std::string& msg)
        : ExceptionFile(msg){};
    virtual ~ExceptionFileOpen(){};
};

// 文件读写异常类
class ExceptionFileError : public ExceptionFile
{
public:
    explicit ExceptionFileError(const std::string& msg)
        : ExceptionFile(msg){};
    virtual ~ExceptionFileError(){};
};

int main()
{
    std::fstream file;
    try
    {
        int num;
        file.open("test.txt");
        if (file.is_open())
        {
            file >> num;
            if (file.fail())
            {
                throw ExceptionFileError("读取格式错误");
            }
            std::cout << num << "\n";
        }
        else
        {
            throw ExceptionFileOpen("打开文件失败");
        }
    }
    catch (const ExceptionFileOpen& e)
    {
        std::cout << e.what() << "\n";
    }
    catch (const ExceptionFileError& e)
    {
        std::cout << e.what() << "\n";
    }
    file.close();
    return 0;
}

4 总结

4.1 栈展开

栈展开指的是:当异常抛出后,匹配 catch 的过程。抛出异常时,将暂停当前函数的执行,开始查找匹配的 catch 子句。沿着函数的嵌套调用链向上查找,直到找到一个匹配的 catch 子句,或者找不到匹配的 catch 子句。栈展开的时候,会通过析构函数或者是 delete 销毁局部对象(从开始匹配位置到确认匹配这一段中间位置的资源会被释放)

4.2 析构函数应该从不抛出异常。

如果析构函数中出现异常,那么就应该在析构函数内部将这个异常进行处理,而不是将异常抛出去。为什么不应该?抛出异常的就是栈展开的过程,而栈展开会调用析构函数销毁局部对象,这样多次调用析构函数会导致程序崩溃(内存泄漏)

4.3 构造函数可以抛出异常

当构造函数内出现异常,可以选择将异常抛出,在栈展开的过程调用析构函数释放已申请的内存,也可以在内部将异常处理,手动调用 delete 释放

4.4 catch捕获所有异常

语法:在 catch 语句中,使用三个点(…)。即写成:catch(...) 这里三个点是“通配符”,类似可变长形式参数。

参考

[1] C++ 异常处理

[2] C++异常处理机制

[3] c++中try catch的用法

本文由作者按照 CC BY 4.0 进行授权