文章目录
- [1. 异常概念](#1. 异常概念)
- [2. 异常的抛出与捕获](#2. 异常的抛出与捕获)
-
- [2.1 异常抛出](#2.1 异常抛出)
- [2.2 异常捕获](#2.2 异常捕获)
- [2.3 异常抛出与捕获逻辑:栈展开](#2.3 异常抛出与捕获逻辑:栈展开)
- [2.4 异常抛出和捕获:特殊情况](#2.4 异常抛出和捕获:特殊情况)
-
- [2.4.1 throw](#2.4.1 throw)
- [2.4.2 catch](#2.4.2 catch)
- [3. 自定义异常类型](#3. 自定义异常类型)
-
- [3.1 自定义异常类型示例](#3.1 自定义异常类型示例)
- [4. 异常安全问题](#4. 异常安全问题)
-
- [4.1 noexcept关键字和操作符](#4.1 noexcept关键字和操作符)
1. 异常概念
C中处理错误的方式一般通过函数的异常返回值,设置错误码errno的形式实现。这种方式错误码提供的信息有限,并不能很好显示异常问题。
因此,C++中引入全新的一套错误处理机制:异常。
异常的处理中,涉及异常的抛出与捕获,实际抛出的异常本质是一个具体对象,类型是任意的,捕获时需要注意类型匹配,才可正确捕获。
2. 异常的抛出与捕获
2.1 异常抛出
异常抛出使用try
和throw
的结合。
cpp
try
{
throw exception;
}
try
后必须接{}
所含的语句块,throw
异常的行为,必须在try
中进行,只有在try中抛出的异常,才会被捕获。
2.2 异常捕获
异常捕获使用catch
。
cpp
catch(type exception)
{
}
catch
后,()
内是用于接收所抛出异常的对象,{}
是catch
处理异常的语句块,与try相同,必须采用语句块形式。
2.3 异常抛出与捕获逻辑:栈展开
实际程序中,会涉及到函数的多层调用,即函数中调用函数。
如果在一个函数栈帧较深的函数中,抛出一个异常,且每层函数调用中,均有相应的捕获逻辑,会有什么行为呢?
异常的捕获:优先匹配最近的且类型能够匹配的catch。
在整个catch
的匹配过程中,对于调用链上不匹配的catch
所对应的函数栈帧会直接退出,剩余的代码将不会再被执行。 直至跳转到匹配的catch
,就继续维护相应的函数栈帧,从其catch开始,继续执行之后的代码,直至函数返回。
明白整个异常抛出与捕获过程,自然会产生一个问题?抛出异常,往往是抛出一个函数栈内的对象,如果在当前函数栈内不匹配,回退到别的函数栈中匹配,那么此时这个对象不就被销毁了吗?
实际上,真正捕获的异常对象,并不是最初抛出的某个函数栈内的对象,编译器处理异常时,实际会构造一个新的异常对象(自定义类型,通常都是移动构造,转移资源),这个异常对象的生命周期持续整个catch匹配的过程,直至执行完所匹配catch的相关语句后,才被销毁。
这整个异常抛出和捕获的逻辑,其中函数调用链逐渐回退,直至匹配到相应catch
,就被称为栈展开。
2.4 异常抛出和捕获:特殊情况
2.4.1 throw
如果一个异常抛出,直到回到main函数中,都没有匹配的catch,会怎样?
这种情况是非法的,一个异常如果最终未被捕获,那么当前进程就会被异常终止。
同时,这也就说明一点,抛出异常的行为必须在try中进行,只有在try中抛异常方具备被捕获的必要条件。
异常捕获,除了指明类型外,还可以进行任意类型捕获,即任意类型的异常到来,都可匹配。
2.4.2 catch
cpp
catch(...)
{
}
上述写法便是任意类型捕获。
此外,已经被catch捕获的异常,可被再次抛出吗?可以在catch中抛出捕获的异常,且在catch中抛出的异常,与正常抛出异常行为无差别。
cpp
try
{
throw exception;
}
catch(const type& exception)
{
throw;//可以直接写throw,默认将捕获到的异常抛出
}
3. 自定义异常类型
实际工程中,一般都是专门的异常类型对象,这个异常类型可以是自定义的,也可是库中提供的。
比起抛出其它类型,专门的异常类型中包含更多的相关错误信息,能帮助程序员更好地进行错误的了解和处理。

上图中的exception
表示C++库中专门实现的异常类所在的头文件,包含了相关异常类型的声明和实现,本质是一个.hpp
文件。
异常类型一般也会设计父类与子类的继承体系 ,实际抛出异常时,可能抛出一个父类异常,也可能抛出子类异常,此时有两种捕获方案:写多个catch
,对父类和子类异常分别进行捕获;只对父类异常进行捕获,本质是利用多态机制 ,实际的子类对象可以给父类类型的引用,实际调用成员函数时,通过运行时多态,执行相应的函数。
实时使用时,一般不写多个catch捕获,而是统一使用父类类型的引用去统一捕获父类或子类的异常对象。
3.1 自定义异常类型示例
下面展示一个自行设计的异常类型,并完成抛出与捕获:
cpp
class Exception
{
public:
Exception(int id,const std::string& str):_id(id),_str(str)
{ }
virtual std::string what()
{
std::string str = _str + ' ' + std::to_string(_id);
return std::move(str);
}
virtual ~Exception()
{ }
protected:
int _id;//出错编号
std::string _str;//出错信息
};
class DevException :public Exception
{
public:
DevException(int id, const std::string& str, int client_num):Exception(id,str),_client_num(client_num)
{}
std::string what() override
{
std::string str = _str + ' ' + std::to_string(_id) + ' ' + std::to_string(_client_num);
return std::move(str);
}
~DevException() override
{ }
private:
int _client_num;//出错员工号
};
int main()
{
try
{
throw DevException(1, "测试继承异常中的抛出与捕获",100);
}
catch (Exception& e)
{
std::cout << e.what() << std::endl;
}
return 0;
}
异常自定义类型中,通常都会提供一个what接口,返回一个string对象,用于将相关异常错误信息打印输出。
4. 异常安全问题
C++11中,异常引入为错误处理带来极大遍历,但同时也引入异常安全问题。
最常见的异常安全问题就是内存泄漏。
例如,在存在多条new语句的时候,由于每条new语句都存在抛出异常的可能,而由于异常抛出与捕获的栈展开逻辑,如果不再每条new语句后都添加catch逻辑,最终很可能因为栈展开的跳转逻辑,引发内存泄漏(通常对于第一个new之后的new)。而如果每条new语句后,都添加catch,代码又会显得非常冗余。
为了解决异常安全问题,尤其是异常安全中的内存泄漏,C++中又引入智能指针,以RAII风格进行资源申请与释放。
4.1 noexcept关键字和操作符
noexcept是一个关键字和编译期操作符。
关键字时,用于声明一个函数不能抛出异常。如果一个函数可能抛出异常,声明为noexcept,通常编译不会报错,但是如果实际运行时,抛出异常,当前进程会被异常终止。
操作符时,用于检测一个表达式是否可能抛出异常,可能抛出异常,返回flase;否则,返回true。
比如通过noexcept() 检测一个函数调用表达式是否会抛出异常,不过这个检测实际会受函数noexcept声明的影响------一个函数可能抛出异常,但声明为noexcept,进行判断,仍会返回true。