首页 > 编程语言 >[C++] 异常详解

[C++] 异常详解

时间:2024-08-25 17:24:05浏览次数:13  
标签:匹配 抛出 C++ 详解 double catch 异常

标题:[C++] 异常详解

@水墨不写bug



目录

一、错误处理方式

C语言

Java语言

二、异常的概念

三、异常的使用

1.异常的抛出和捕获(基本用法)

 2.异常的重新抛出(特殊情况)

3.异常的规范和常见坑点

四、标准库的异常体系

五、 C++异常小结


正文开始:

一、错误处理方式


        在程序运行中,不乏会出现一些错误,这些错误或许在我们的意料之中,也可能在意料之外。有错误就要处理,关于错误处理,不同语言有不同的错误处理方法;简单举几个例子:


C语言

错误处理方式

  • 返回值:C语言通常通过函数的返回值来指示错误。例如,标准I/O库中的fopen函数在成功时返回一个指向FILE对象的指针,在失败时返回NULL
  • 全局变量:C语言还使用全局变量(如errno)来记录最近一次系统调用的错误码。通过检查errno的值,程序可以获取到更多关于错误的信息。
  • assert断言:可直接终止程序,一般对程序的影响较大。

Java语言

错误处理方式

  • 异常处理:Java采用面向对象的异常处理机制,通过try-catch-finally-throw块来捕获和处理异常。Java中的异常分为检查型异常(checked exceptions)和非检查型异常(unchecked exceptions,如RuntimeException及其子类)。
  • 资源自动管理:Java 7引入了try-with-resources语句,自动管理实现了AutoCloseable接口的资源,如文件、数据库连接等,在try块执行完毕后自动关闭资源。

         而C++的错误处理方式虽然与Java类似,都是异常,但是C++与Java仍是有一些区别的。


二、异常的概念

        异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数直接或间接的调用者处理这个错误(准确来说就是通过返回栈帧的方式来返回异常,从而让上一级处理这个异常)

C++的异常处理机制具体是通过下面三个关键字实现的: 

        throw: 当问题出现时,程序会抛出一个异常。
        catch: 设置在想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获。
        try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。

 
        如果有一个块抛出一个异常,捕获异常的方法会使用 trycatch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码catch 块中写某种异常发生后需要做的后续处理,catch 块进入的原则是 抛出的异常的类型与catch 后()内变量的类型完全一致。

try
{
    // 保护的标识代码
}
catch( ExceptionName e1 )
{
    // catch 块
}
catch( ExceptionName e2 )
{
    // catch 块
}
catch( ExceptionName eN )
{
    // catch 块
}

 

三、异常的使用


1.异常的抛出和捕获(基本用法)

        抛出异常是通过关键字 “throw” 实现的,假设throw 关键字会抛出一个A类型的对象,则立刻终止当前逻辑,一层一层向上返回栈桢,最终会有两个结果:1.找到匹配的catch ,进入catch;2.没有匹配的catch,终止进程。


异常的抛出和匹配原则


        1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。(注意:catch时不会发生隐式类型转换,比如:抛出int,用size_t就无法匹配;抛出const char* ,用string就无法匹配


实例一:

double Div(double a, double b)
{
	if (b == 0)
		throw "div by 0";//抛出异常的类型为常量字符串类型
	else
		return a / b;
}
void func()
{
	double a,b;
	cin >> a >> b;
	cout << Div(a, b) << endl;
}
int main()
{
	try {
		func();
	}
	catch (const char* my_exception)//通过const char* 类型变量catch,可以匹配
	{
		cout << my_exception << endl;
	}
	return 0;
}
double Div(double a, double b)
{
	if (b == 0)
		throw "div by 0";//抛出异常的类型为常量字符串类型
	else
		return a / b;
}
void func()
{
	double a,b;
	cin >> a >> b;
	cout << Div(a, b) << endl;
}
int main()
{
	try {
		func();
	}
	catch (string my_exception)//通过string来catch
		//尽管可以通过string构造函数进行隐式类型转换,但是仍然不能匹配
	{
		cout << my_exception << endl;
	}
	return 0;
}


        2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。(如果有多个catch可以与抛出的异常匹配时,只有最先匹配到的catch 会起作用


        3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象(比如实例一的Div抛出的const char*类型),所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch 匹配以后销毁。(这里的处理类似于函数的传值返回)


        4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。(这也可以用来兜底:在我们自己设计的所有catch块之后,手动添加一个catch(...),这样不至于因为异常没有被捕捉导致进程直接结束)


        5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对、象,使用基类捕获。(在实际项目中十分好用

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

在抛出异常后,会终止当前代码逻辑,然后:


        1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。


        2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。


        3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开

        所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。


        4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。(特别注意)
 

 2.异常的重新抛出(特殊情况)

        有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。

实例二:


double Div(double a, double b)
{
	if (b == 0)
		throw "div by 0";//抛出异常的类型为常量字符串类型
	else
		return a / b;
}
void func()
{
	//在func中开辟堆区空间,需要手动释放
	int* arr = new int[12];
	double a,b;
	cin >> a >> b;

    //调用Div,首先其内部可能会抛出异常
    //其次,如果抛出异常,则会直接终止当前逻辑,转而去寻找匹配的catch
    //最终,会直接跳转到main函数的catch (const char* my_exception)中
    //在一系列操作中国,忽视了delete[] arr, 导致内存泄漏
	cout << Div(a, b) << endl;

	delete[] arr;
}
int main()
{
	try {
		func();
	}
	catch (const char* my_exception)
	{
		cout << my_exception << endl;
	}
	return 0;
}

        如何改呢?如果没有发生异常,走正常逻辑;如果发生异常,则我们需要在func栈桢层中,捕获异常,然后delete[] arr 后,重新抛出异常,再返回main 处理:

double Div(double a, double b)
{
	if (b == 0)
		throw "div by 0";//抛出异常的类型为常量字符串类型
	else
		return a / b;
}
void func()
{
	//在func中开辟堆区空间,需要手动释放
	int* arr = new int[12];
	double a,b;
	cin >> a >> b;
	try {
		cout << Div(a, b) << endl;
	}
	catch (...)
	{
		//捕获任意类型异常,在delete arr 之后,再重新抛出任意类型的异常,交给main的逻辑捕获
		delete[] arr;
		throw;
	}
	delete[] arr;
}
int main()
{
	try {
		func();
	}
	catch (const char* my_exception)
	{
		cout << my_exception << endl;
	}
	return 0;
}

3.异常的规范和常见坑点

在《Effective C++》这本书中,强调了一下几点: 

             1.构造函数完成对象的构造和初始化,不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。

              2.析构函数主要完成资源的清理,不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)。

        C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题。

         


关键字noexcept 

        在C++11之前,如C++98,规定在函数头后面加上声明 throw(),throw括号内部写 可能会抛出异常的类型,但是由于这个语法的不规范使用,加上throw可能会导致一些意想不到的错误,所以在C++11,新增了关键字:noexcept;表示保证这个函数不会抛出异常。

        其次throw()括号内什么都不写,也表示一样的效果。

 

thread() noexcept;
thread() throw();

四、标准库的异常体系

         C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的。

        根据规则:

        5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对、象,使用基类捕获。(在实际项目中十分好用

        我们可以得出启示:实际项目组中,我们实际会设计基类异常,在基类的基础上封装自己项目组的异常,这样一来不仅容易区分,也因为 可以抛出的派生类对、象,使用基类捕获,异常就不容易被忽略。


五、 C++异常小结

优点:
        1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。

缺点:

        1. 异常会导致程序的执行流程乱跳,导致非常的混乱,这会导致我们跟踪调试时以及分析程序时,比较困难。


完~

未经作者同意禁止转载 

标签:匹配,抛出,C++,详解,double,catch,异常
From: https://blog.csdn.net/2301_79465388/article/details/141505327

相关文章

  • [C++] 初识 智能指针
    标题:[C++]初识智能指针@水墨不写bug目录一、前言二、智能指针1.什么是RAII?2.智能指针分类 三、智能指针简介1.std::auto_ptr2.std::unique_ptr3.std::shared_ptr正文开始:一、前言    C++智能指针的出现是有一定的背景的:    Java有专属......
  • 莫队算法C/C++实现
    目录简介 算法原理算法步骤C++实现应用场景莫队算法(Mo'sAlgorithm)是一种用于解决区间查询和更新问题的算法,由俄罗斯选手莫洛佐夫(MoMorozov)提出。它在算法竞赛和某些计算密集型任务中非常有用,尤其是在需要处理大量区间查询和更新操作时。莫队算法以其高效性和简洁性......
  • A*算法C/C++实现
    A*算法是一种在图形平面上,有多个节点的路径中,寻找一条从起始点(source)到目标点(goal)的最短遍历路径的算法。它属于启发式搜索算法,因为它使用启发式方法来计算图中的节点,从而减少实际计算的节点数量。A*(A星)算法是一种启发式搜索算法,用于在图中找到从起始点(source)到目标点(goal)的......
  • [RT-Thread记录]DFS虚拟文件系统文件夹操作异常
    项目场景:系统:RT-Thread5.0.2硬件:STM32H743问题描述1.文件系统打开文件夹再关闭后,申请的内存没有释放2.elm-fatFs文件系统重复操作同一个文件夹,如复制,会引起系统崩溃原因分析:        DFS虚拟文件系统文件打开关闭逻辑错误,文件系统版本升级更新后,dfs_file结......
  • 【C++PCL】点云处理贪婪三角化曲面重建
    作者:迅卓科技简介:本人从事过多项点云项目,并且负责的项目均已得到好评!公众号:迅卓科技,一个可以让您可以学习点云的好地方重点:每个模块都有参数如何调试的讲解,即调试某个参数对结果的影响是什么,大家有问题可以评论哈,如果文章有错误的地方,欢迎来指出错误的地方。目录   ......
  • [C++ Error] f0202.cpp(13): E2268 Call to undefined function 'system'
    system('pause');解决方法,修改代码:system("pause");[C++Error]f0202.cpp(13):E2268Calltoundefinedfunction'system'错误解释:这个错误表明您在C++代码中尝试调用了一个未定义的函数system。system函数是C标准库中的函数,用于执行一个字符串中给出的命令。在C++中,......
  • C++暂停黑窗口 system( “pause “);
    在编写的c++程序中,如果是窗口,有时会一闪就消失了,如果不想让其消失,在程序结尾处添加:system("pause");注意:不要再return的语句之后加,那样就执行不到了。分析:system()是调用系统命令;pause暂停命令;这样在运行到此处时,会显示“Pressanykeytocontinue...”也就是“按任意键......
  • C++拾趣——转换编译器生成的类型名为代码中的类型名
    大纲代码测试代码地址在软件开发中,特别是在使用C++这类静态类型语言时,编译器在编译过程中会生成许多内部表示,包括类型信息。这些内部类型名通常用于编译器的内部处理,比如类型检查、优化和代码生成等。然而,在编写源代码或进行调试时,我们更习惯于使用人类可读和易于理......
  • C++函数调用栈从何而来
    竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~个人主页:rainInSunny | 个人专栏:C++那些事儿、Qt那些事儿目录写在前面原理综述x86架构函数调用栈分析如何获取rbp寄存器的值总结写在前面  程序员对函数调用栈是再熟悉不过了,无论是使用IDE调试还是GDB等工具进行调试,都离......
  • C++函数调用栈从何而来
    竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~个人主页:rainInSunny | 个人专栏:C++那些事儿、Qt那些事儿文章目录写在前面原理综述x86架构函数调用栈分析如何获取rbp寄存器的值总结写在前面  程序员对函数调用栈是再熟悉不过了,无论是使用IDE调试还是GDB......