【C++】 浅析异常

所谓异常,顾名思义就是不正常,有问题。

对于人来说有不正常的时候即生病身体不适,那么对于程序也一样,也有不正常即代码“有病”。

那么,既然有病就要治疗,就要对症下药!这样才能恢复正常。

废了这么多话,还是引出我们C++的“异常”概念。

异常,让一个函数可以在发现自己无法处理的错误时抛出一个异常,希望它的调用者可以直接或者间接处理这个问题。

而传统的异常处理方法:

1.终止程序

2.返回一个表示错误的值(很多系统函数都是这样,例如malloc,内存不足,分配失败,返回NULL指针)

3.返回一个合法值,让程序处于某种非法的状态(最坑爹的东西,有些第三方库真会这样)

4.调用一个预先准备好在出现"错误"的情况下用的函数。

第一种情况是不允许的,无条件终止程序的库无法运用到不能当机的程序里。

第二种情况,比较常用,但是有时不合适,例如返回错误码是int,每个调用
都要检查错误值,极不方便,也容易让程序规模加倍(但是要精确控制逻辑,我觉得这种方式不错)。

第三种情况,很容易误导调用者,万一调用者没有去检查全局
变量errno或者通过其他方式检查错误,那是一个灾难,而且这种方式在并发的情况下不能很好工作。

至于第四种情况,本人觉得比较少用,而且回调的代码不该多出现。



使用异常,就把错误和处理分开来,由库函数抛出异常,由调用者捕获这个异常,调用者就可以知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。

但是,错误的处理依然是一件很困难的事情,C++的异常机制为程序员提供了一种处理错误的方式,使程序员可以更自然的方式处理错误。

假设我们写一个程序,简单的除法程序:

int Div(int a, int b)
{
    if(b == 0)
       exit(1);// 若是return 0;呢?(不可取,返回0万一是10/100呢)
    return a/b;
}

int main()
{
    int a = 10;
    int b = 2;  // 若 b = 0 呢 ?
    cout<<Div(a,b)<<endl;
    return 0;
}

这样的程序,乍一看确实是没问题,但是程序在执行中当除数为0时终止了,终止意味着程序将不会继续往下执行,这就是所谓的异常。但是这样直接终止是不是有点简单粗暴呢?? 这一般不是我们想要的结果。

C++异常中的三把斧头:try,throw,catch

①. 测试某段程序会不会发生异常

②. 若有异常发生,则通过throw抛出该异常(注:抛出的是该变量的类型)

③. 捕获相应的异常,即匹配类型的异常,进行针对性的处理

对应代码:

float Div(int a, int b)
{
    if(b == 0)
    {
       throw b;//抛出异常      
    }
    return a/b;
}

int main()
{
    int a = 10;
    int b = 0;
    float result = 0.0f;
    try
    {
       result = Div(a,b);
    } 
    catch(int)
    {
        cout<<"Div error!,除数为0"<<endl;
    }
    cout<<"result = "<<Div(a,b)<<endl;
    
    return 0;
}

运行出结果:

我们发现,之前没有抛出异常时,程序会崩溃(除强制结束程序外),而现在没有崩溃,并且反映出了问题所在。

实际上,程序中所包含的异常现象在自身不做处理时,会交给操作系统来处理,而操作系统管理整个机器正常运转,遇到这种异常,它会直接一刀切,结束掉程序,所以会发生崩溃。而若是程序自己写了异常处理,则异常的处理由自己处理。也就是说,异常处理机制即是操作系统下发的二级机构,这个二级机构专门针对自己程序所设定的异常进行处理。



而,程序并非一个返回值,我们看下面:

左边正常返回,发生异常从右边返回,发生异常后,throw之后的代码不会再执行,直接找catch惊醒捕获。那么由异常规范:

class Test
{};
float Div(int a, int b)throw(int,double,short,Test)

这就是说该函数只能抛出基本类型int,double,short,以及自定义类型Test

float Div(int a, int b)throw()

这个代表该函数不能抛出异常

float Div(int a, int b)

这个代表可能抛出任何异常



此时又有一个捕获时的类型匹配问题:

float Div(int a, int b)
{
    if(b == 0)
    {
        short x = 0;
        throw x;//抛出异常      
    }
    return a/b;
}

int main()
{
    int a = 10;
    int b = 0;
    float result = 0.0f;
    try
    {
       result = Div(a,b);
    } 
    catch(int)
    {
        cout<<"Div error!(int),除数为0"<<endl;
    }
    catch(short)
    {
        cout<<"Div error!(short),除数为0"<<endl;
    }
    
    //如果抛出的是double或者char又或者其他类型呢?难道还要一直增加catch?
    //按照下面的方式可以对其他类型进行捕获
    
    catch(...) // 捕获除上面的int和short,且只能放在最后!
    {
        cout<<"Div error!(all),除数为0"<<endl;
    }
    cout<<"result = "<<Div(a,b)<<endl;
    
    return 0;
}

这有么有很像哦我们之前学习的switch() ;   case:  语句呢?

switch()
{
    case:
    case:
    .
    .
    default:
}

相当于说,不能匹配所有的case语句,再执行default。同样,异常中亦是如此。



总结:

异常的抛出和捕获

  1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个处理代码。
  2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
  3. 抛出异常后会释放局部存储对象,所以被抛出的对象也就还给系统了,throw表达式会初始化一个抛出特殊的异常对象副本(匿名对象),异常对象由编译管理,异常对象在传给对应的catch处理之后撤销。

    栈展开

1. 抛出异常的时候,将暂停当前函数的执行,开始查找对应的匹配catch子句。

2. 首先检查throw本身是否在catch块内部,如果是再查找匹配的catch语句。

3. 如果有匹配的,则处理。没有则退出当前函数栈,继续在调用函数的栈中进行查找。

4. 不断重复上述过程。若到达main函数的栈,依旧没有匹配的,则终止程序。

5. 上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。

找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。



以上,我们对于异常处理机制的原理有所了解。

对于大型的程序代码而言,就需要对于自定义类型的异常进行处理。

下面是自定义的类型匹配:

#include <iostream>
#include <string>
using namespace std;

class Exception
{
public :
     Exception(int errId, const char * errMsg)
         : _errId(errId )
         , _errMsg(errMsg )
    {}

     void What () const
    {
          cout<<"errId:" <<_errId<< endl;
          cout<<"errMsg:" <<_errMsg<< endl;
    }
private :
     int _errId ;       // 错误码
     string _errMsg ;  // 错误消息
};

void Func1 (bool isThrow)
{
     // ...
     if (isThrow )
    {
          throw Exception (1, "抛出 Excepton对象" );
    }
     // ...

     printf("Func1(%d)\n" , isThrow);
}

void Func2 (bool isThrowString, bool isThrowInt)
{
     // ...
     if (isThrowString )
    {
          throw string ("抛出 string对象" );
    }
     // ...
     if(isThrowInt )
    {
          throw 7;
    }

     printf("Func2(%d, %d)\n" , isThrowString, isThrowInt );
}

void Func ()
{
     try
    {
          Func1(false );
          Func2(true , true);
    }
     catch(const string& errMsg)
    {
          cout<<"Catch string Object:" <<errMsg<< endl;
    }
     catch(int errId)
    {
          cout<<"Catch int Object:" <<errId<< endl;
    }
     catch(const Exception& e)
    {
          e.What ();
    }
     catch(...)
    {
          cout<<" 未知异常"<< endl;
    }  
    printf ("Func()\n");
}

int main()
{
    Func();
    return 0;
}

异常的重新抛出

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

class Exception
{
public :
     Exception(int errId = 0, const char * errMsg = "" )
         : _errId(errId )
         , _errMsg(errMsg )
    {}

     void What () const
    {
          cout<<"errId:" <<_errId<< endl;
          cout<<"errMsg:" <<_errMsg<< endl;
    }
private :
     int _errId ;       // 错误码
     string _errMsg ;  // 错误消息
};

void Func1 ()
{
     throw string ("Throw Func1 string");
}

void Func2 ()
{
     try
    {
          Func1();
    }
     catch(string & errMsg)
    {
          cout<<errMsg <<endl;
          //Exception e (1, "Rethorw Exception");
          //throw e ;
          // throw;
          // throw errMsg;
    }
}

void Func3 ()
{
     try
    {
          Func2();
    }
     catch (Exception & e)
    {
          e.What ();
    }
}

异常与构造函数&析构函数

  1. 构造函数完成对象的构造和初始化,需要保证不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
  2. 析构函数主要完成资源的清理,需要保证不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)


exception类是C++定义的一个标准异常的类,通常我们通过继承exception类定义合适的异常类。

http://www.cplusplus.com/reference/exception/exception/

本文只是简单地从异常的使用场景,介绍了基本使用方法,一些高级的异常用法没有罗列,还有待补充,偏文可能有纰漏,希望大家指出

时间: 2024-09-29 16:16:54

【C++】 浅析异常的相关文章

SQL Server事务遭遇网络异常时的处理机制浅析

SQL Server数据库中,如果应用程序正在执行一个事务的时候突然遭遇了网络异常,例如网络掉包,网络中断等,那么这个事务会怎么样? SQL Server数据库是通过什么机制来判断处理呢? 估计很多人跟我一样都有不少疑问, 我们下面构造一个测试实验来测试验证一下.如下所示: 步骤1:在客户端连使用SSMS工具连接到测试数据库,执行下面脚本,显性事务既不提交也不回滚.模拟事务正在执行当中. USE AdventureWorks2012; GO SELECT @@SPID; BEGIN TRAN D

C++中的异常浅析

我们都知道,无论是在C语言中还是C++的编程调试中,我们都会遇到各种各样的错误,那么在遇到这些错误的时候我们要怎么处理它们呢? 这里有一些传统的处理错误的方法: ①终止错误 ②返回错误码 ③返回合法值,让程序处于某种错误状态, ④调用一个预先设置好的处理错误的函数--->(回调函数) 为了更好地处理这一类问题,在C++中提出了异常,当函数出现一个自己无法处理的错误时,就会抛出异常,让函数的调用者直接或间接的来处理这个问题. 来举一个例子: 例1: 对于函数div来说,程序没有一种机制来处理当nu

浅析Java异常

1.什么是异常 结构不佳的代码不能运行,这是Java的基本理念. 发现错误的理想时机是在编译期.然而,编译器并不能发现所有的错误,余下的问题就需要在程序运行时解决.这就需要错误能通过某种方式,把适当的信息传递给特定的接收者处理.Java中的异常处理的目的在于通过使用少量的代码来简化大型.可靠的程序的生成,通过此方式让你的应用中没有未处理的错误,而且它还带来了一个明显的好处:降低错误处理代码的复杂度. 异常,根据字面理解,有意外之意.把它置于代码层面来理解,即阻止了当前方法或作用域继续执行. 在J

浅析arm的异常、中断和arm工作模式的联系

说到异常向量,会让人联想到中断向量.其实,中断是属于异常的子集的,也就是说中断其实是异常其中的一种. 回到异常向量,他其实是一张表格,每个格子里存放的是一个地址,或者是一个跳转命令,不管是哪个,其目的都是让PC跳转到真正处理异常的代码的地方. 以下是arm的异常向量表: 图1 初步介绍完异常向量,就来对比下ARM的arm的7种工作模式: 图2 User : 非特权模式,大部分任务执行在这种模式 FIQ :   当一个高优先级(fast) 中断产生时将会进入这种模式 IRQ :   当一个低优先级

浅析c++异常

异常处理:异常,让一个函数发现自己无法处理的错误时抛出异常,让函数的调用者直接或间接的处理这个问题. 传统错误处理办法 1.终止程序.(如段错误等) 2.返回错误码. 3.返回合法值,让程序处于某种非法的状态.(坑货) 4.调用一个预先设置的出现错误时调用的函数. 虽然可以解决问题,但都存在缺陷,然而引入异常处理可以很好的解决问题. 异常的抛出和捕获 1.异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个处理代码. 2.被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一

浅析Java语言中两种异常的差别

Java提供了两类主要的异常:runtime exception和checked exception.所有的checked exception是从java.lang.Exception类衍生出来的,而runtime exception则是从java.lang.RuntimeException或java.lang.Error类衍生出来的. 它们的不同之处表现在两方面:机制上和逻辑上. 一.机制上  它们在机制上的不同表现在两点:1.如何定义方法;2. 如何处理抛出的异常.请看下面CheckedEx

阿里P7浅析Java虚拟机如何处理异常

Exceptions Exceptions允许您顺利处理程序运行时发生的意外情况.要演示Java虚拟机处理异常的方式,请考虑一个名为NitPickyMath的类.它提供了对整数执行加法,减法,乘法,除法和余数的方法.NitPickyMath在溢出,下溢和被零除的条件下抛出已检查的异常.Java虚拟机将在整数除零上抛出一个ArithmeticException,但不会在溢出和下溢上抛出任何异常.方法抛出的异常定义如下: class OverflowException extends Excepti

Python之encode与decode浅析

 Python之encode与decode浅析 在 python 源代码文件中,如果你有用到非ASCII字符,则需要在文件头部进行字符编码的声明,声明如下: # code: UTF-8 因为python 只检查 #.coding 和编码字符串,为了美观等原因可以如下写法: #-*-coding:utf-8-*- 常见编码介绍: GB2312编码:适用于汉字处理.汉字通信等系统之间的信息交换. GBK编码:是汉字编码标准之一,是在 GB2312-80 标准基础上的内码扩展规范,使用了双字节编码.

PM2源码浅析

PM2工作原理 最近在玩一个游戏,<地平线:黎明时分>,最终Boss是一名叫黑底斯的人,所谓为人,也许不对,黑底斯是一段强大的毁灭进程,破坏了盖娅主进程,从而引发的整个大陆机械兽劣化故事. 为什么要讲这么一段呢,是希望大家可以更好地理解pm2的原理,要理解pm2就要理解god和santan的关系,god和santan的关系就相当于盖娅和黑底斯在pm2中的01世界中,每一行代码每一个字节都安静的工作god就是Daemon进程 守护进程,重启进程,守护node程序世界的安宁,santan就是进程的