c++异常处理

  异常:在一个函数发现有自己无法处理的问题时便抛出一个异常,让他的调用者可以直接或间接的处理这个问题。
  软件开发过程中的一个主要问题就是管理错误情况。无论我们的软件有多么的优秀,由于一些原因(编程错误,无法预料的操作系统错误,资源枯竭等),总会出现错误。一个设计优秀的软件必须可以很好的预料和处理这种异常错误。

注意!
本文中讨论的异常指软件异常,这不能和硬件异常混淆。软件异常指由于程序员编写代码引起的。他和硬件异常没有关系。软件异常是由于某段遇到异常情况的代码引起的。


一旦引发了异常,如果程序员只是忽略它,则他不会消失--*异常不可以忽略或者禁止*。异常情况仅仅在程序员认识到并且处理后才会消失。没有处理的异常会在动态调用链中传播,直至它达到顶层函数,如果c++的main函数也没能处理异常,则会毫不留情的结束掉应用程序。

一般的异常处理方法一共可以分为4种:
1. 终止程序
2. 返回一个表示错误的值
3. 返回一个合法的值,让程序处于非法的状态
4. 调用一个预选准备好的在出现错误时可以调用的函数(回调函数)
使用异常就是把错误和处理分开,有库函数抛出异常,有调用者来捕获这个异常,捕获之后调用者就可以知道程序函数调用出现错误并且处理,而且最终是否结束程序也就把握在调用者手里。

void FunTest()
{
    //some code that will cause an exception to be throw
    //...
    throw 1;//从这里控制返回到控制者
    //...

}
int main()
{
    try
    {
        FunTest();/*在FunTest()发生异常时,在调用之后try块中代码的其余部分不执行*/
        //more code here
    }
    catch (...)/*在FunTest()发生一个异常时,便会进入catch块,只是因为它符合所有类型的异常*/
    {
        //catch any exception thrown by FunTest()
        //do whatever is necessary
    }
    //normal code of main() continue 
}

上述例子中, FunTest()只是一个简单的过程,当他遇到错误条件的时候发出一个异常,这个动作是通过如下的语句实现的:

throw 1;

throw表达式的操作数是一个对象,通常发出带有错误信息的对象.
在main()函数中,对FunTest()的调用是被try块所包围的。代码如下:

try
    {
        FunTest();
    }

上述为一个try块,换句话说,他是在try语句中封闭的一块代码。try块编译器指出了可能的异常。
catch()块捕获指定类型的异常。在上述例子中:

catch (...)

指出他将捕获任意类型的异常,因为(…)匹配任何参数,捕获表达式类似带有一个参数的过程,当传递(发出)类型匹配的实际参数的时候,将调用它。


C++异常机制的工作方式

当发出某种类型的异常之后控制块将转交给合适类型的最近的处理程序。运行时异常系统立即开始查找一个与传递给throw语句的参数匹配的catch块。在过程内部,catch块必须在try块之后,当FunTest()发出一个异常后,无法在函数内部处理异常,因为在函数内部没有任何处理异常的catch块。在这种情况下,将从FunTest()返回异常,并且不执行throw语句之后的的任何语句。控制返回到FunTest()的调用者即main()函数,这种返回和从FunTest()的正常返回没有任何区别。在由于异常而离开FunTest()之前,将调用所有局部对象的析构函数。换句话说,从FunTest()的正常返回时所进行的清理工作同样也会在由于异常而返回的情况下进行。
现在,控制被转交给main函数,但异常情况依旧存在,运行系统为处于等待状态的异常查找一个合适的处理程序即一个匹配的catch块。在main()中的try块之后紧跟着一个catch块,这个catch块因为(…)参数与任何类型的异常匹配。所以现在找到了一个异常处理程序,因此进入catch块,并执行catch块的内部代码。因此整个程序执行起来是连续的,就好像没有发生异常一样。
异常会进行传播,直到遇到一个匹配的处理程序。异常只能由catch块(也称为catch处理程序)处理,考虑到匹配问题,catch块总是跟在try块之后。try块是对编译器的一个提示,用于找到catch块。也就是说,try块建立了一个动态调用链。一旦进入catch块,则立即处理异常;当退出相应的catch块时,认为异常结束(异常条件不再存在)。
如果main()中,没有任何匹配的catch块。那么,异常仍然没有处理,而且一旦控制从FunTest()转向main(),将调用预定义的terminate()。注意,在这种情况下,对FunTest()的调用之后的try块之内的其他代码将永远不会执行。一旦由于FunTest()发出的异常而导致控制转向main(),则处于异常处理模式,函数terminate()将导致应用程序的异常终止。
如果main()函数也无法处理异常,则调用预定义的terminate(),这导致程序终止

发出异常和在正常执行期间从函数中返回非常相似。从函数返回时进行的清理工作在由于异常而离开函数的时候同样也要进行。从这个角度看,由于异常而退出函数是非常安全的。通常如果函数中没有动态分配内存资源,则不用担心资源清理的工作。当由于异常而退出时:

  1. 将为每一个在函数中建立的局部对象调用析构函数
  2. 所有按值传递的参数被删除
  3. 在函数中创建的临时对象也被删除
  4. 将从堆栈中删除函数的堆栈构架,并将值返回给调用者
    上面的步骤对于在查找匹配处理程序期间退出的所有函数(动态调用链中)重复进行,为自动对象调用析构函数并将控制返回给调用者的过程称为堆栈解退

try块的主要性

再出现异常的状况下,try块提示编译器在哪里找catch块。没有紧跟try块的catch块是没有作用的(在一个try块之后紧跟的一系列catch块都被认为是紧跟相同的try块)。当发出异常时,从从控制线程最近进入的,而且至今没有退出的try块开始查找匹配的处理程序,很可能一个try块作为另一个try块的一部分而进入。try块的动态切套反映了嵌套函数调用的本质。在上面的例子中,对FunTest()的调用是在main()的try块内部进行的。查找匹配处理程序从main()中开始,因为他是最近进入的try块,而且没有从中退出。
当没有发生异常的时候,几乎没有和try块想关的运行时成本,查找匹配捕获处理程序的过程只是在发生异常的情况下才会进行,依次加入try块的成本并不非常昂贵(一些编译器甚至宣称0成本异常)。但是,很明显,代码增加了。还要记住,异常的成本取决于编译器。


throw表达式的重要性

当引发异常的时候,所发出的是一个对象。编译器初始化一个throw操作数的静态类型的临时对象,这个临时对象将用于在匹配catch处理程序中初始化类型合适的变量。临时对象的内存将以实现相关方式的分配,由编译器的编写者决定分配多少内存。只要存在异常就存在临时对象。换句换说,直至处理程序执行完毕,才从临时对象中退出。
throw表达式可以发出任何类型的对象。下面是一些例子:

throw ("hello world");

将发出一个char*。他可以被如下类型表达式所捕获:

catch (const char* message);

与此类似:

throw (3.14);

发出一个浮点数,这可以被如下类型表达式所捕获:

catch (double X);

当然:

catch (...);

可以捕获任意类型的异常,事实上,查找匹配的捕获块和分析重载函数的调用非常类似。他们使用相同的参数匹配转换规则。差别在于,编译器可以以静态方式分析重载函数。而当发生异常时,查找匹配的捕获处理程序是动态的。
没有任何操作数的throw表达式将再次引起处理的异常。

try
{
    //...
}
catch (...)
{
    throw;
}

当异常一这种方式再次发出时,不会对原来发出的对象进行复制。他将再次发出相同的异常,当catch处理程序无法获得处理异常所需的信息,到仍然要进行清理工作时,这种再次引发工具是非常有用的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值