Exception
Badly formed code will not be run.
Exception 翻译为“异常”。
C++ 的目的之一:比 C 更严格的类型检查
Python 在 21 年开始使用自带的类型检查
TypeScript:有类型的 JavaScript
引入
如何处理文件:
- 用指令打开文件
- 计算文件大小
- 分配内存
- 将文件读入内存
- 关闭文件
各个环节都可能发生异常。
异常的三个特征:
- 可预见
- 不可避免
- 不一定发生
C 语言中的异常处理方式:返回一个固定的错误值
如果全部直接处理,造成的后果:
- 业务逻辑淹没在异常处理中,难以阅读
- 修改内容、增减内容非常困难
- 有时未必能通过程序本身解决异常,需要停止运行
异常处理
语法
异常使用 try
、catch
和 throw
三个关键字处理。
try
用于声明某个代码段内可能抛出异常。
其后应该跟随若干个 catch
来处理异常。
try{
...
}
catch
处理异常的条件语句。
在 catch
后的括号内,表示接收的异常的类型。捕获不同类型的异常可以由不同的代码处理。
花括号内的代码称为 handler。
在捕获时,类型的匹配按照代码的顺序,而 ...
表示捕捉任何异常。
推荐捕获一个引用 &
。
catch(ErrorType& e){
...
}
catch(...){
...
}
throw
抛出异常:
template<typename T>
T& Vector<T>::operator[](int indx){
if(indx < 0 || indx >= m_size){
throw VectorIndexError(indx);
}
return m_elements[indx];
}
可以在捕捉异常后再抛出:
catch(VectorIndexError& e){
throw;
}
异常规范
在函数原型中写出异常规范,声明函数可能返回何种异常:
void print(Document& p) throw(PrintOffLine, BadDocument);
void goodguy() throw();// throw no exceptions, until C++11
void alloc() throw(...);// can throw any exception
void abc() noexcept;// throw no exceptions, since C++11
如果在函数中返回了规范之外的异常,系统会调用 std::unexpected()
来处理。
std::unexpected()
默认调用 std::terminate()
来终止程序。
可以用 std::set_unexpected(func)
将 std::unexpected()
重载为 func()
;
也可以用 std::set_terminate(func)
将 std::terminate()
重载为 func()
。
如果 std::unexpected()
被调用后,抛出的异常仍然不符合异常规范,则会抛出 std::bad_exception
异常。
在 C++17 之后,异常规范说明已被弃用。使用 noexcept
说明函数不会抛出任何异常时,若抛出了异常,则会直接调用 std::terminate()
。
机制
任何一句话抛出了异常,其下面的语句都不能执行。
异常由 catch
转接到对应的代码。
执行完 catch
中的代码后,将跳过 try
中剩余语句。
总的来说,当
-
检查是否被
try
包围。- 是:跳到第 2 步
- 否:跳到第 3 步
-
检查是否有条件匹配的
catch
。- 是:执行
catch
下的语句 - 否:跳回第 1 步
- 是:执行
-
检查是否在函数内
- 是:回溯到调用该函数的代码,跳回第 1 步
- 否:退出
优点
分离了业务代码和异常处理。使得修改异常的处理方式更加简洁易读。
在异常接收中还可以利用造型(up-casting),来简化处理。
示例
读取文件
try{
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
}catch(fileOpenFailed){
dosomething
}catch(sizeDeterminationFailed){
dosomething
}catch(...){
}
Vector 访问
template<typename T>
T& Vector<T>::operator[](int indx){
return m_elements[indx];
}
indx 可能是非法下标。如何通知调用的函数此处的异常?
- C 语言的做法:返回特殊值,这个特殊值不在正常返回值的值域中
- C 语言的做法:保存有一个全局变量,发生异常时修改全局变量来通知。
- C++ 的做法:使用异常。
class VectorIndexError{
public:
VectorIndexError(int v): bad_value(v)
private:
int bad_value;
};
template<typename T>
T& Vector<T>::operator[](int indx){
if(indx < 0 || indx >= m_size){
throw VectorIndexError(indx);
}
return m_elements[indx];
}
如果发生了异常,则返回到上一个处理异常的函数处。
void func(){
int i = vec[4];
return i * 5;
}
void outer(){
try{
func();
func2();
}catch(VectorIndexError& e){
e.diagnostic();
}
cout << "Control is here!" << endl;
}
void outer2(){
string err("exception caught");
try{
func();
}catch(VectorIndexError& e){
throw;
}
}
void outer3(){
try{
outer2();
}catch(...){// catch any exception
}
}
new
的异常
在 C 中,malloc
在未成功分配空间时会返回 NULL
,作为判断依据。
new
在未成功分配空间时不会返回 nullptr
,而是会抛出 std::bad_alloc
异常。
void func(){
try{
while(true){
int *p = new int[100000000ul];
}
}catch(const std::bad_alloc& e){
std::cerr << "fail to new the array! " << e.what() << '\n';
}
}
STL 异常定义在 <exception>
中。
exception
:所有异常的公共基类
- bad 系列
bad_alloc
:new
无法分配空间抛出的异常bad_cast
:dynamic_cast
对引用的类型检查出错,抛出的异常bad_typeid
:对多态类型的空指针使用typeid
抛出的异常bad_exception
:当前抛出异常的拷贝构造出错时,抛出的异常
runtime_error
:事件超出程序范围抛出的异常overflow_error
:算数上溢抛出的异常(STL 中仅std::bitset::to_ulong
)range_error
:算数超界抛出的异常
logic_error
:程序逻辑错误引发的异常,并且可能是可以预防的domain_error
:当输入超出了其类型的定义域时抛出的异常length_error
:当对容器的操作使其超出了预定义的长度上限时抛出的异常out_of_range
:当对容器的操作超出了其当前范围时抛出的异常invalid_argument
:当传入参数不合法时抛出的异常
空间安全
构造函数
在 try-catch
中,所有的本地变量都能正确地调用自己的析构函数,不需要自己释放空间;
但如果结合了 new
,可能在分配好空间,执行构造函数的过程中抛出异常,这时候申请的空间需要另外的 delete
。
这时候会发生很麻烦的问题,指针并没有正确地指向申请的空间。
解决方法:使用两步构造
- 在构造函数内对基本变量赋值
- 任何需要申请资源和空间的操作,在显式的
init()
函数内执行
这样保证构造函数不会抛出异常,在 init()
抛出异常时可以正常地析构。
析构函数
在析构函数中抛出异常,我们应该全部解决,这样才能正确地释放空间。
在抛出异常导致的析构中,如果再抛出异常,就会调用 std::terminate()
。
开发策略
异常处理的各种手段:
- 返回特殊值(首选)
- 抛出异常
- 函数入口的参数检查