• C++异常及异常优缺点


    🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸
    在这里插入图片描述

    一、C语言传统的处理错误的方式

    1.终止程序
    比如assert去断言(assert只在Debug模式生效)
    缺陷:用户难以接受,比如发生内存错误,除零错误时就会终止程序
    2.返回错误码
    缺陷:需要程序员自己去查对应的错误信息,比如系统的很多库接口函数都是把错误码放在error中,表示错误。

    二、C++异常

    异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或者间接的调用者去处理这个错误(调用链)

    1.throw catch

    **throw:**当问题出现时,程序会抛出一个异常,就是通过throw关键字来完成的
    **catch:**在想处理问题的地方,通过异常处理程序捕获异常。catch关键字用于捕获异常,可以有多个catch进行捕获。

    如果有一个块抛出一个异常,捕获异常的方法会使用try个catch关键字,try块中放置可能抛出异常的代码,try块中的代码被称为保护代码,使用try/catch语句的语法如下

    try
    {
        //保护代码
    }catch(ExceptionName e1)
    {
        //catch块
    }catch(ExceptionName e2)
    {
        //catch块
    }catch(ExceptionName en)
    {
        //catch块
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    面向对象的语言就讲究抛出一个对象,这个对象就可以抛出很多信息,当然也可以抛出一个整型,把错误码抛出来,但是一般不会这么做

    2.异常的抛出和捕获

    double Divison (int a,int b)
    {
    	if(b==0)
    		throw "Division by zero condition!"
    	else
    		return ((double)a/(double)b);
    }
    void Func()
    {
    	int len,time;
    	cin>>len>>time;
    	cout<<Divison(len,time)<<endl;
    }
    int main()
    {	
    	try
    	{
        //保护代码
    	    Func();
    	}catch(const char* errmsg)
    	{
        //catch块
        	cout<<errmsg<<endl;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    结果:
    如果没发生除零错误,则正常运行,会跳过catch块
    如果发生除零错误,出现异常,则会直接沿着调用链找到最近的而且匹配的catch块,其他的会被跳过,比如上面的cout<

    3.异常的抛出和捕获原则

    1.异常是通过抛出对象而引发的,该对象的类型决定应该了应该激活哪个catch的处理代码。
    2.被选中的被选中的处理代码是调用链中与该对象类型匹配且离抛出对象最近的那一个,当走完了都还没有匹配的就会直接报错,终止程序是最严重最严重的情况了,所以一般情况下必须捕获异常。
    为了提高系统的健壮性,会在某个合适位置放置一个…,这个catch块能捕获任何类型的异常,也可以放置出现未知的异常,程序终止

    	try
    	{
    	    Func();
    	}catch(const char* errmsg)
    	{
     
    	}catch(...)//捕获任意类型,所以一般放在函数调用链的最后
    	{
    	}
    	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3.这个抛出的对象e和接收的对象e不是同一个,因为抛出的通常是在那个栈帧创建出的临时变量,当这个栈帧释放后就没了,所以是传值返回,会把拷贝对象给给catch的e,所以这里也会用到之前我们说的移动构造,将这个右值(将亡值)转移给我们

    4.抛出和捕获异常的捕获原则有个例外,并不是要类型完全匹配,可以抛出派生类对象,使用基类捕获(赋值兼容转换)

    4.函数调用链 异常 栈展开匹配原则

    1.首先检查throw本身是否在try块内部,如果是就查找它匹配的catch语句,如果有匹配的,则调用catch的地方进行处理。
    2.没有匹配的catch则退出当前的栈帧,依旧没有匹配的,继续在函数的栈帧中进行查找catch
    3.如果到达main函数的栈,依旧没有匹配的,则终止进程(一般都会增加…,所以不会终止),上述这个沿着调用链查找匹配的catch子句的过程就被称为栈展开
    4.找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

    5.异常的重新抛出

    出现了某些异常,可能还想重新试几次,比如出现网络错误,必须要重试几次才行,不然直接报错了用户很难受

    void SendMsg(string str)
    {
    	srand(time(0));
    	//要求出现网络错误要重试三次
    	if(rand()%3==0)
    	{
    		throw HttpServerException("请求资源不存在",100,"get");//自定义异常类,细节不展示
    	}
    	else if(rand()%4==0)
    	{
    		throw HttpServerException("权限不足",101,"post");
    	}
    	cout<<"发送成功:"<<str<<endl;
    }
    void HttpServer()
    {
    	string str ="zhupi";
    	int n=3;
    	while(n--)
    	{
    		try
    		{
    			SendMsg(str);
    			break;//不抛异常就结束
    		}
    		catch(const Exception&e)//自定义异常基类,不展示细节
    		{
    			if(e.getid()=100&&n>0)
    			{
    				continue;
    			}
    			else
    			{
    				throw e;//异常的重新抛出
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    1、当遇到网络层错误就多次加载,遇到其他错误就重新抛出(异常被捕获后进行部分处理,不能处理的重新抛出交给上一层 调用链异常栈展开)
    2、这里的SendMsg必须放在try块中,如果实在while中进行SendMsg,那抛异常的话就会直接跳出该函数栈帧了,因为在try中才能捕获到

    三、异常安全问题

    1.构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全不初始化。
    2.析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏,句柄未关闭等(文件流))
    3.C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题(智能指针的核心原理)

    比如说

    void Func
    {
    	int *array1 = new int[10];
    	int *array2 = new int[10];
    	......
    	try
    	{
    	}catch(...)
    	{
    		delete[] array1;
    		delete[] array2;
    		throw;//捕获什么重新抛出什么
    	}
    	delete[] array1;
    	delete[] array2;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这样写代码有两个问题
    1、代码非常丑陋,这样看都还好,那多几个new呢?
    2、有可能会发生new抛异常的问题,也会造成内存泄漏的问题,防不胜防,这只有通过RAII的思想来完成,之后会用智能指针来说这个问题

    四、异常规范

    1.异常规范说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。
    可以在函数后面接上 throw(类型1,类型2,…),列出这个函数可能抛掷的所有异常
    2.函数后面接上throw()就代表不抛出异常
    3.若无异常接口声明,则次函数可以抛掷任何类型的异常

    //C++98
    void fun() throw(A,B,C,D);
    //这里表示这个函数会抛掷A/B/C/D中的某种类型的异常
    
    void*operator new(size_t size) throw (bad_alloc);
    //表示只会抛出bad_alloc的异常
    
    void*operator delete(size_t size,void*ptr) throw();
    //表示该函数不会抛出异常
    
    //C++11中新增的noexcept,表示不会抛异常
    thread() noexcept;
    thread(thread&& x) noexcept;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    C++98太理想了,比如这里声明throw(),但是有人在里面抛异常,再比如throw(A,B,C,D),有人在里面抛E,所以C++11就只用一个新的关键字noexcept来表示有还是没有异常,编译器不能强制规定,因为需要兼容C

    五、异常优缺点

    C++异常的优点:
    1.异常对象的抛出,相比于错误码的方式可以清晰准确的展示出错的各种信息,甚至还可以包含堆栈的信息,这样可以很好的定位程序的Bug
    2.返回错误码的传统方式有个问题就是:在函数调用链中,深层的函数返回了错误,我们我们需要层层返回,最外层才拿得到。而异常在调用链很深的时候,必须希望出错之后直接返回到main,那以前只有C的goto语句能够做到,现在的异常也能够做到。(中间跳过的栈帧是会被正常销毁的)
    3.很多的第三方库都包含异常,我们使用的时候也需要使用异常
    4.部分函数使用异常会更好处理,比如构造函数没有返回值,不方便使用错误码方式处理,再比如T&operator[](size_t)这样的函数,如果pos越界了只能通过终止程序或者使用异常来处理,没办法通过返回值表示错误。

    C++异常的缺点:
    1.异常会导致程序执行流乱跳的问题,而且非常混乱,并且是运行时出错抛异常就会乱跳,这导致我们跟踪调试时以及分析程序时,造成很大困难,比如在throw这条语句的后面位置打一个断点,抛异常的话这个程序是停不下来的。
    2.异常会有一些性能的开销,现代硬件速度很快的情况下,这个基本忽略不计
    3.C++没有垃圾回收机制,资源需要自己管理,有了异常就非常容易导致内存泄漏,死锁等异常安全问题,需要RAII思想来处理,学习成本高
    4.C++标准库的异常体育定义得很不好,导致都定义自己的异常体系,非常混乱
    5.异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户很难受

    异常的致命缺点就是执行流乱跳
    在这里插入图片描述

  • 相关阅读:
    Java练习题-输出斐波那契(Fibonacci)数列
    护眼灯亮度多少合适?2023最专业的护眼灯品牌推荐
    JavaScript基础: 异步
    CanOpen协议的伺服驱动控制
    关于 OPENSSL_Uplink(XX……XX,08): no OPENSSL_Applink 处理
    go使用dlib人脸检测和人脸识别
    vue 使用v-cloak
    智慧医疗应用和研究资料合集
    LeetCode(30)长度最小的子数组【滑动窗口】【中等】
    Docker(六)、Docker-compose简单了解
  • 原文地址:https://blog.csdn.net/zhu_pi_xx/article/details/128137764