编译器视角下的 C++ 异常:探究 throw 与 catch 的编译原理
目录
0.写在前面
1.C++异常概念
异常的定义:
异常处理的基本组成部分:
1. throw表达式
2. try块
3. catch块
2. 异常的使用
异常的抛出和匹配原则:
在函数调用链中异常栈展开匹配原则:
3.异常的重新抛出
4.异常安全
异常安全的定义:
5.异常规范
6.自定义异常体系
7.C++标准库的异常体系
8.异常的优缺点
优点:
缺点:
0.写在前面
在C++中引入了一种处理错误的新方式,那就是异常。来想想C语言是如何处理错误的:
1. 返回错误码
函数在执行过程中遇到错误时,会返回一个特定的值来表示错误状态。通常,返回值为 0 表示正常执行,而负数或特定的非零值表示不同类型的错误。调用者需要检查函数的返回值,根据返回值来判断函数是否执行成功,并进行相应的处理。但是,如果有多个函数嵌套进去,错误码就需要层层返回,颇为麻烦。
2. 使用全局错误变量errno
errno
是一个全局整数变量,定义在<errno.h>
头文件中。当某些库函数执行失败时,会设置errno
为一个特定的错误码,不同的错误码代表不同的错误类型。可以使用strerror
函数将errno
转换为对应的错误信息字符串。
3. 使用assert函数
使用assert函数需要包含头文件<assert.h>,assert又称断言,可以来对输入的变量或者其他特殊关系进行断言,如果表达式为真那么不进行报错,如果表达式为假,那么就会强制终止程序,做的很决断。
下面将要介绍异常的使用和优缺点:
1.C++异常概念
异常的定义:
异常是程序在执行过程中出现的不正常情况,这些情况可能会导致程序无法按照正常流程继续执行。例如,除零错误、内存分配失败、文件打开失败等都属于异常情况。C++ 异常机制允许程序在出现异常时,能够将控制权从错误发生的位置转移到专门处理该异常的代码块。
异常处理的基本组成部分:
1. throw表达式
throw用于抛出异常。当程序中检测到异常情况时,可以使用throw语句抛出一个异常对象。这个异常对象可以是基本数据类型(如int,double等),也可以是自定义的类对象。
2. try块
try块包含可能抛出异常的代码。在try块中执行的代码如果抛出了异常,程序会立即停止当前try块的执行,转而寻找匹配的catch块。
3. catch块
catch块用于捕获和处理异常。当try块中抛出异常时,程序会在try块后面的catch块中寻找类型匹配的catch块。如果找到匹配的catch块,就会执行该catch块中的代码;如果没有找到匹配的catch块,最后会终止程序。
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
return "Division by zero condition!";
else
return ((double)a / (double)b);
}
void Func()
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
int main()
{
try {
Func();
}
catch (const string errmsg)
{
cout << errmsg << endl;
}
return 0;
}
2. 异常的使用
异常的抛出和匹配原则:

上面可以看到,throw的类型不同,catch中的处理情况也不同!
补充:如果抛出异常的的代码不在try块内,那么异常不会被捕捉到;如果没有catch的类型与throw的类型相互匹配,那么异常就会捕捉失败,程序终止。

可以看到,在Func函数中直接被catch捕捉到了,那么main函数中的catch就没有机会再去捕捉了。

在Division中throw一个string对象,那么会做传值返回,string会发生拷贝生成临时对象,出函数后string被销毁。
在函数调用链中异常栈展开匹配原则:



3.异常的重新抛出
有时候我们不想立马处理异常,想要将异常留到最后处理,或写成日志,或统一处理该怎么办呢?这里可以这样使用:catch(...)搭配上throw;这样就可以接收任意了类型的throw并重新抛出任意类型的异常。
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
// 重新抛出去。
int* array = new int[10];
try {
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
catch (...)
{
cout << "delete []" << array << endl;
delete[] array;
throw;
}
// ...
cout << "delete []" << array << endl;
delete[] array;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
return 0;
}
请注意:图中的代码在Division函数中如果不在throw;前加上对array的释放那么就会发生内存泄露,这里throw;后直接返回了main函数!
4.异常安全
异常安全的定义:
异常安全指的是当程序中抛出异常时,程序仍能保持一种合理、安全的状态。这包括保证资源的正确释放、数据的一致性以及程序的可继续执行性等。如果一个程序不具备异常安全性,那么在发生异常时可能会导致资源泄漏(如内存、文件句柄等未释放)、数据处于不一致状态(如对象的部分成员更新,部分未更新)等问题。
对于异常安全,要注意以下问题:
- 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
- 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
- C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题,RAll后面讲解。
5.异常规范
异常规范是 C++ 中用于声明函数是否会抛出异常以及抛出哪些异常类型的机制,但在不同标准版本中有不同的实现和语义。以下是关键点:
1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的 后面接throw(类型),列出这个函数可能抛掷的所有异常类型。2. 函数的后面接throw(),表示函数不抛异常。3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;
补充:C++11中新加了noexcept关键字,可以直接在函数名后面加上noexcept表示不会抛异常。
异常规范有什么用?在函数名后面加上可能抛出的异常类型可以大大提高调试的效率,为了养成好的编程习惯,建议加上!
6.自定义异常体系


上面是一个Exception定义的一个异常类,注意到有一个virtual的what函数,我们再来看看继承了这个基类的派生类:
看,这个派生类中对what函数进行了重写,那么调用的时候就已经满足了虚函数的重写,只需要使用基类的指针或者引用调用就可以实现多态,即不同的派生类对象传值产生不同的异常信息。

7.C++标准库的异常体系
C++库中也实现了多个派生类继承自基类exception,了解一下即可:
我们来尝试捕捉一下库中exception的异常信息:小试牛刀:~
void test2()
{
vector<int> v;
try
{
v.reserve(10000000000000);
//v[9999999999910000000];
}
catch (const exception& e)
{
cout << e.what() << endl;
//常用的是bad_alloc out_of_range 两种异常
}
}
结果是什么?
可以看到这是库中自带的异常信息,我们开辟一个巨大的空间,vector开辟失败并抛出了一个异常,可见必须掌握异常的捕捉,才可以更加熟练的使用STL~
8.异常的优缺点
优点:
分离错误处理与业务逻辑
异常机制将正常代码与错误处理解耦。开发者只需在可能出错的地方抛出异常,调用方通过try/catch
集中处理错误,避免了函数返回值中混杂大量错误检查代码,使代码更简洁、可读性更高。支持跨层级错误传递
当深层嵌套的函数发生错误时,异常能自动沿调用栈向上传递,直到被最近的catch
捕获。这避免了逐层手动传递错误码的繁琐(C语言的做法),尤其适合复杂调用链的场景。携带丰富错误信息
异常对象可自定义(如继承std::exception
),允许封装错误描述、错误码、上下文数据等。相较于简单的错误码,这为调试和日志记录提供了更详细的信息。在公司中,通常在exception中定义错误信息和错误id,这方便开发者去查看bug的具体错误。
缺点:
性能开销
异常处理机制(如栈展开、类型匹配)会引入额外开销。在频繁抛出异常或对性能敏感的场景(如实时系统),可能影响效率。比如在上述代码中,我们返回string,这就需要开辟空间。破坏代码执行流
异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。