C++异常概念
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。
C++的异常主要有三个关键字:
- throw:当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
- catch:在您想要处理问题的地方,通过异常处理程序捕获异常;catch 关键字用于捕获异常,可以有多个catch进行捕获。
- try:try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。
如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。
异常的使用
异常的抛出和捕获
首先演示一下简单的异常机制:
#include <exception>
try {
// 受保护的代码段
}
catch (std::exception& e) { // 捕获具体的异常类型
// 处理代码
}
catch(...){
// 处理代码
}
其中,catch中的参数表示的是具体的异常类型,对于捕获到的异常会根据类型进行匹配,以执行对应处理代码;
代理代码的选择遵循就近原则,会自动匹配距离抛出位置最近的catch语句,并对异常类型进行匹配;
抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以生成一个拷贝对象较为安全,该拷贝对象会在catch后自动销毁;
catch(...)可以捕获任意类型的异常对象,使用了C++11新特性——可变参数;
在捕获过程中,基类对象可以捕获派生类对象抛出的异常对象,而这通常与多态相联系,以实现自定义异常类;
函数调用链中异常栈展开匹配原则
1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch 的地方进行处理。
- 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
- 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
- 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
下面进行代码演示:
#include <iostream>
#include <string>
class Exception {
public:
virtual const std::string what() = 0;
Exception(std::string errmsg = "", int id = 0)
:_errmsg(errmsg),
_id(id)
{}
virtual const int Get_ErrID() = 0;
virtual const std::string Get_Errmsg() = 0;
protected:
std::string _errmsg;
int _id;
};
class Divide_By_Zero :public Exception {
public:
Divide_By_Zero(std::string errmsg = "Divide By Zero", int id = 1)
:Exception(errmsg, id)
{}
const std::string what() {
return this->_errmsg;
}
const std::string Get_Errmsg() {
return this->_errmsg;
}
const int Get_ErrID() {
return this->_id;
}
};
void Func1() {
int a, b;
std::cin >> a >> b;
if (b == 0) {
throw Divide_By_Zero();
}
std::cout << a / b << std::endl;
}
void Func2() {
Func1();
}
void Func3(){
Func2();
}
int main(void) {
try {
Func3();
}
catch (Exception& e) {
std::cout << "Error Message->" << e.Get_Errmsg() << std::endl;
std::cout << "Error ErrID->" << e.Get_ErrID() << std::endl;
}
return 0;
}
这里,我首先创建了一个简单的异常类,这个接下来我会详细讲解,随后我创建了三个函数Func1、Func2、Func3,层层嵌套调用,并在Func1中throw了一个异常对象,在main函数中进行了捕获;
所以异常的捕获过程是这样的:Func1->Func2->Func3->main,最后在main函数中完成对异常对象的捕获,并输出对应的信息;
异常的重新抛出
重新抛出可以理解成异常的嵌套,它是将之前捕获的异常重新抛出,从而实现异常的重新抛出,还是以我的自定义异常类进行演示:
#include <iostream>
#include <string>
class Exception {
public:
virtual const std::string what() = 0;
Exception(std::string errmsg = "", int id = 0)
:_errmsg(errmsg),
_id(id)
{}
virtual const int Get_ErrID() = 0;
virtual const std::string Get_Errmsg() = 0;
protected:
std::string _errmsg;
int _id;
};
class Divide_By_Zero :public Exception {
public:
Divide_By_Zero(std::string errmsg = "Divide By Zero", int id = 1)
:Exception(errmsg, id)
{}
const std::string what() {
return this->_errmsg;
}
const std::string Get_Errmsg() {
return this->_errmsg;
}
const int Get_ErrID() {
return this->_id;
}
};
void Func1() {
int a, b;
std::cin >> a >> b;
if (b == 0) {
throw Divide_By_Zero();
}
std::cout << a / b << std::endl;
}
void Func() {
try {
Func1();
}
catch (...) {
throw;
}
}
int main(void) {
try {
Func1();
}
catch (Exception& e) {
std::cout << "Error ID->" << e.Get_ErrID() << std::endl;
std::cout << "Error Message->" << e.Get_Errmsg() << std::endl;
}
return 0;
}
这里在Func函数中,我对Func1中捕获的异常对象进行了重新抛出(这里只是演示,实际上不会这样使用),随后在main函数中对抛出的异常对象进行了最终处理;
异常安全
- 构造函数和析构函数中不要抛出异常,可能会导致构造/析构不完成从而导致错误;
- new/delete和lock/unlock之间不要抛出异常,可能导致内存资源泄露/死锁等问题的出现;
针对以上问题,我们可以采用RAII机制进行避免,之后我将专门写一篇文章来介绍一下这个机制,同时这个机制也是智能指针的核心思想;
自定义异常体系
这个我之前的演示样例中有所涉及,这里我将详细介绍一下它的具体实现方式:
#include <iostream>
#include <string>
class Exception {
public:
virtual const std::string what() = 0;
Exception(std::string errmsg = "", int id = 0)
:_errmsg(errmsg),
_id(id)
{}
virtual const int Get_ErrID() = 0;
virtual const std::string Get_Errmsg() = 0;
protected:
std::string _errmsg;
int _id;
};
class Divide_By_Zero :public Exception {
public:
Divide_By_Zero(std::string errmsg = "Divide By Zero", int id = 1)
:Exception(errmsg, id)
{}
const std::string what() {
return this->_errmsg;
}
const std::string Get_Errmsg() {
return this->_errmsg;
}
const int Get_ErrID() {
return this->_id;
}
};
class Null_Pointer_Exception :public Exception {
public:
Null_Pointer_Exception(std::string errmsg = "Null Pointer Exception.", int id = 2)
:Exception(errmsg, id)
{}
const std::string what() {
return this->_errmsg;
}
const std::string Get_Errmsg() {
return this->_errmsg;
}
const int Get_ErrID() {
return this->_id;
}
};
class Array_Index_Out_Of_Bounds_Exception :public Exception {
public:
Array_Index_Out_Of_Bounds_Exception(std::string errmsg = "Array Index Out Of Bounds Exception.", int id = 3)
:Exception(errmsg, id)
{}
const std::string what() {
return this->_errmsg;
}
const std::string Get_Errmsg() {
return this->_errmsg;
}
const int Get_ErrID() {
return this->_id;
}
};
class Illegal_Argument_Exception :public Exception {
public:
Illegal_Argument_Exception(std::string errmsg = "Illegal Argument Exception.", int id = 4)
:Exception(errmsg, id)
{}
const std::string what() {
return this->_errmsg;
}
const std::string Get_Errmsg() {
return this->_errmsg;
}
const int Get_ErrID() {
return this->_id;
}
};
可以看到,我先是定义了一个父类Exception,随后实现了具体的异常派生类,这样我只需要通过捕获父类的异常类型就可以接受子类对象抛出的异常;
可以将此理解成多态的一种体现:可以不去指定捕获的具体的异常类型,通过直接捕获父类对象实现具体的异常处理;
C++标准库的异常体系
这里不再加以介绍,其实现结构类似于自定义类型,都是通过捕获父类来抛出具体的异常类型,可以自行检索。
异常的优缺点
此处引用ChatGPT的说法,来说明C++异常的优缺点: