当前位置: 首页 > article >正文

编译器视角下的 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块,最后会终止程序。

      如果有一个块抛出一个异常,捕获异常的方法会使用 try catch 关键字。try 块中放置可能抛
出异常的代码,try 块中的代码被称为保护代码。使用 try/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. 异常的使用

异常的抛出和匹配原则:

1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
上面可以看到,throw的类型不同,catch中的处理情况也不同!
补充:如果抛出异常的的代码不在try块内,那么异常不会被捕捉到;如果没有catch的类型与throw的类型相互匹配,那么异常就会捕捉失败,程序终止。
2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
可以看到,在Func函数中直接被catch捕捉到了,那么main函数中的catch就没有机会再去捕捉了。
3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,
所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似
于函数的传值返回)
在Division中throw一个string对象,那么会做传值返回,string会发生拷贝生成临时对象,出函数后string被销毁。
4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么,catch (...) //用来捕捉不知道什么类型或者不规范的异常,防止异常被抛出不被捕获导致程序异常结束。通常我们把catch(...)放到最后。
在第一点已经演示过,就不再重复演示了~

在函数调用链中异常栈展开匹配原则:

1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则
调到catch的地方进行处理。
2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的
catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异
常,否则当有异常没捕获,程序就会直接终止。
4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

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++中异常经常会导致资源泄漏的问题,比如在newdelete中抛出了异常,导致内存泄漏,在lockunlock之间抛出了异常导致死锁,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.异常的优缺点

优点:

  1. 分离错误处理与业务逻辑
          异常机制将正常代码与错误处理解耦。开发者只需在可能出错的地方抛出异常,调用方通过try/catch集中处理错误,避免了函数返回值中混杂大量错误检查代码,使代码更简洁、可读性更高。

  2. 支持跨层级错误传递
          当深层嵌套的函数发生错误时,异常能自动沿调用栈向上传递,直到被最近的catch捕获。这避免了逐层手动传递错误码的繁琐(C语言的做法),尤其适合复杂调用链的场景。

  3. 携带丰富错误信息
          异常对象可自定义(如继承std::exception),允许封装错误描述、错误码、上下文数据等。相较于简单的错误码,这为调试和日志记录提供了更详细的信息。在公司中,通常在exception中定义错误信息和错误id,这方便开发者去查看bug的具体错误。

缺点: 

  1. 性能开销
          异常处理机制(如栈展开、类型匹配)会引入额外开销。在频繁抛出异常或对性能敏感的场景(如实时系统),可能影响效率。比如在上述代码中,我们返回string,这就需要开辟空间。

  2. 破坏代码执行流

           异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。

http://www.kler.cn/a/583844.html

相关文章:

  • AI日报 - 2025年3月14日
  • iOS开发,SQLite.swift, Missing argument label ‘value:‘ in call问题
  • 计算机视觉算法实战——驾驶员玩手机检测(主页有源码)
  • 手机遥控开关,是一种能让用户通过手机远程控制电器开关
  • 基于全局分析SpringCloud各个组件所解决的问题?
  • 【AIGC】OpenAI 集成 Langchain 操作实战使用详解
  • python元组(被捆绑的列表)
  • 【毕业论文格式】word分页符后的标题段前间距消失
  • Redis超高并发分key实现
  • vue3+ts+vite环境中使用json-editor-vue3,一个比较强大的编辑json文件的插件!!!
  • UE HDRI插件
  • 增强深度学习的残差Kolmogorov-Arnold网络(RKAN)详解与PyTorch实现
  • 【QT】事件系统入门——QEvent 基础与示例
  • 【HeadFirst系列之HeadFirstJava】第18天之深入理解原型模式:从问题到解决方案(含 Java 代码示例)
  • Spring 事务失效的 8 种场景!
  • HOT100——链表篇Leetcode160. 相交链表
  • Tool和Agent
  • 如何重置 MySQL root 用户的登录密码?
  • 项目型公司如何规避项目风险
  • Mysql表的查询