一、异常处理
异常处理用于处理程序在调用过程中的非正常行为。
-
传统的处理方法:传返回值表示函数调用是否正常结束。
例如,返回
0
表示成功,非0
表示失败。这种方法的是函数的返回值被错误处理逻辑占用,不能用于其他目的。 -
C++ 中的处理方法:通过关键字 try/catch/throw 引入
通过
try/catch/throw
引入了结构化的错误处理机制,使得错误处理逻辑与正常逻辑分离,提高了代码的可读性和可维护性。
异常触发时的系统行为—栈展开:
-
抛出异常后续的代码不会被执行
一旦抛出异常,
throw
语句之后的代码将不会被执行。控制流会立即转移到异常处理机制。 -
局部对象会按照构造相反的顺序自动销毁
在栈展开过程中,局部对象(包括由
new
分配的对象)会按照它们构造的相反顺序自动销毁。这是为了保证资源的正确释放,防止内存泄漏。 -
系统尝试匹配相应的 catch 代码段
-
如果找到匹配的
catch
块:- 执行
catch
块中的异常处理逻辑。 - 异常被“捕获”后,
catch
块之后的代码会继续执行。
- 执行
-
如果没有找到匹配的
catch
块:-
栈展开会继续进行,直到找到匹配的
catch
块或者退出当前函数。 -
如果当前函数中没有找到匹配的
块,栈展开会继续,直到:
-
找到一个匹配的
catch
块。 -
达到
main
函数。如果在
main
函数之前的所有函数中都没有找到匹配的catch
块,程序将退出main
函数。如果程序在退出main
函数之前没有捕获到异常,std::terminate
函数将被调用。这通常会导致程序立即终止。
-
-
-
异常对象:
- 系统会使用抛出的异常拷贝初始化一个临时对象,称为异常对象
- 异常对象会在栈展开过程中被保留,并最终传递给匹配的 catch 语句
try / catch语句块:
-
一个 try 语句块后面可以跟一到多个 catch 语句块(至少跟一个)
-
每个 catch 语句块用于匹配一种类型的异常对象
可以有多个
catch
块,每个用于处理不同类型的异常。 -
catch 语句块的匹配按照从上到下进行
catch
块按照它们在代码中出现的顺序(从上到下)进行匹配。一旦找到匹配的异常类型,就执行相应的catch
块,忽略后面的catch
块。 -
使用 catch(…) 匹配任意异常
catch(...)
是一个通用的异常捕获器,它可以捕获任何类型的异常,包括未被前面的catch
块捕获的异常。 -
在 catch 中调用 throw 继续抛出相同的异常
在
catch
块中,可以使用throw;
(不带参数)来重新抛出当前捕获的异常,这将导致继续搜索外层catch
块。
示例:
#include <iostream>
struct Str{};
struct Base{};
struct Derive : Base{};
void f1()
{
int x;
Str obj;
//throw Derive{}; //打印Derive exception is called in f2
throw Str{}; //打印exception is called in f2
}
void f2()
{
int x2;
Str obj2;
try
{
f1();
}
catch(Derive& e)
{
std::cout << "Derive exception is called in f2 " << "\n";
}
catch(Base& e)
{
std::cout << "Base exception is called in f2 " << "\n";
}
catch(...)
{
std::cout << "exception is called in f2" << "\n";
throw; //重新抛出当前捕获的异常
}
std::cout << "other logic in f2.\n";
}
void f3()
{
try
{
f2();
}
catch(Str& e)
{
std::cout << "exception is called in f2" << "\n";
}
}
int main()
{
f3();
}
在一个异常未处理完成时抛出新的异常会导致程序崩溃:
- 不要在析构函数或 operator delete 函数重载版本中抛出异常
- 通常来说,
异常与构造、析构函数:
-
使用 function-try-block保护初始化逻辑
在C++中,function-try-block允许你在函数的初始化列表和函数体中使用
try
和catch
。这在构造函数中特别有用,因为它可以保护对象的初始化代码。示例:
#include <iostream> struct Str { Str() { throw 100; } } class Resource { public: Resource() try : m_str() { } catch(int) { std::cout << "Exception is catched at Resource::Resource" << std::endl; throw; } private: Str m_str; }; int main() { try { Resource obj; } catch(int) { std::cout << "Exception is catched at main" << std::endl; } }
运行结果:
Exception is catched at Resource::Resource Exception is catched at main
-
在构造函数中抛出异常:
-
已经构造的成员对象会被销毁
如果在构造函数中抛出异常,已经构造的成员对象将按照它们构造的相反顺序自动销毁。
-
类本身的析构函数不会被调用
如果异常是在对象的构造过程中抛出的,并且没有被捕获,那么类的析构函数不会被调用。这是因为对象被视为未完全构造,因此析构函数不适用。
-
局部对象的销毁
如果对象是局部的(即在栈上),异常抛出时,局部对象会自动销毁,但不会调用其析构函数。
-
动态分配对象的销毁
如果对象是动态分配的(即使用
new
),在构造函数中抛出异常且未被捕获时,需要手动释放分配的内存,因为析构函数不会被调用。
-
描述函数是否会抛出异常:
-
如果函数不会抛出异常,则应表明,从而为系统提供更多的优化空间
- C++ 98 的方式:
- throw() :表明不会抛出异常
- throw(int, char):表明可能抛出异常,显式给定了要抛出异常的类型
- C++11 后的改进:
- noexcept :表明不会抛出异常
- noexcept(false):表明可能抛出异常,不需要显式给定会抛出哪种类型的异常
- C++ 98 的方式:
-
noexcept
- 限定符:接收 false / true 表示是否会抛出异常
- 操作符:接收一个表达式,根据表达式是否可能抛出异常返回 false/true
- 在声明了 noexcept 的函数中抛出异常会导致 terminate 被调用,程序终止
- 不作为函数重载依据,但函数指针、虚拟函数重写时要保持形式兼容
示例:
#include <iostream>
void fun2()
{
}
void fun() noexcept(noexcept(fun2()))
{
fun2();
}
int main()
{
std::cout << noexcept(fun()) << std::endl;
}