C++异常处理
该文记录 C++
标准异常和使用,以及自定义异常。
C++异常处理
1 异常定义
用官方的话来说就是程序在执行过程中产生的问题,换句通俗的话来讲就是程序执行的出现的异常,比如程序崩了、内存泄漏了、数组越界以及其他异常信息的出现。
2 异常处理
2.1 处理流程
异常提供了一种转移程序控制权的方式。C++
异常处理涉及到三个关键字:try
、catch
、throw
。
try
:try
块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个catch
块。throw
: 当问题出现时,程序会抛出一个异常。这是通过使用throw
关键字来完成的。catch
: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch
关键字用于捕获异常。
如果有一个块抛出一个异常,捕获异常的方法会使用 try
和 catch
关键字。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的用法