C-中的异常处理机制

异常处理

传统的C语言处理方法

传返回值表示函数调用是否结束

int f1()
{
    return 0;
    //...
    return 1;
}

这种方法比较简洁明了,但对异常处理的位置(例如我想在main()里处理异常)进行调整局就实现起来十分麻烦了。

更明显的缺点是,这种方法会对函数原本的返回格式产生影响。

C++的异常处理方法:try/catch/throw机制

示例:

#include<iostream>

void f1()
{
   throw 1;
}


int main(int argc, char* argv[])
{
    try {
    f1();
    }
    catch (int)
    {
    std::cout << "exception is occurred.\n";
    }
}

输出:

exception is occurred.

异常触发时的系统行为:栈展开

系统首先会为main()建立一个栈帧,然后为f1()建立一个栈帧。接下来在f1()抛出异常后,在main()里处理异常,同时抛弃f1()的栈帧。这种抛弃的过程称为栈展开。

自然,抛出异常后,会立即处理栈展开,f1()后续的代码不会被处理。局部对象的销毁则按照构造相反的顺序。

若尝试匹配相应的catch代码段,如果匹配则执行其中的逻辑,之后执行catch后续的代码。例如:

#include<iostream>

void f1()
{
  throw 1;
}

void f2()
{   
 try {
  f1();
 }
 catch (int)
 {
  std::cout << "f2 exception is occurred.\n";
 }
 std::cout << "other f2 logic is called.\n";


}

void f3()
{
 f2();
}
int main(int argc, char* argv[])
{
 try {
  f3();
 }
 catch (int)
 {
 std::cout << "exception is occurred.\n";
 }
}

输出:

f2 exception is occurred.
other f2 logic is called.

显然,由于f2()已经捕获到了异常,所以main()内部的catch不再执行,且f2()后续的逻辑可继续执行。

如果不匹配,则继续进行栈展开,直到跳出main()函数,触发terminate结束运行。

异常对象

系统会使用抛出的异常,拷贝初始化一个临时对象,称为异常对象。

try {
  f3();
 }
 catch (int e)
 {
 std::cout << "exception is occurred: " << e <<"\n";
 }

输出:

exception is occurred: 1

在上面的代码中,系统会构造一个值为1的对象,并且不会在栈展开过程中被销毁,并最终传递给对应的catch子句。

try/catch的具体使用方法

1个try子句可以跟一到多个catch子句块。每个catch子句匹配一种类型的异常对象,从上到下依次进行匹配。

假如我们传入的异常对象是一个派生类,例如:

struct Base {};
struct Derive:Base {};

void f1()
{
 throw Derive{};
}

try {
  f1();
 }
 catch (int e)
 {
  std::cout << "exception is occurred: " << e <<"\n";
 }
 catch (Base& e)
 {
  std::cout << "Base exception is occurred.\n";
 }
 catch (Derive& e)
 {
  std::cout << "Deriver exception is occurred.\n";
 }

此时会输出:

Base exception is occurred.

此时先构造一个Derive对象,从上至下先匹配到Base&的catch子句,导致后面的匹配永远不会执行。如果我们想匹配Derive的子句,需要人为调换二者的顺序。

还有一种特殊的catch子句catch(...):

catch(...)
{
    std::cout << "exception is occurred.\n"; 
}

这种catch子句可以匹配任意的异常对象,通常放在各个catch子句的最后(如果放在前面,部分编译器甚至会直接报错)。

某些情况下,我们可以在catch子句内部继续throw相同的异常。

void f3()
{
 try {
  f2();
 }
 catch (int e)
 {
  std::cout << "f3 exception is occurred.\n";
  throw;
 }
 std::cout << "other f3 logic is called.\n";
}
int main(int argc, char* argv[])
{
 try {
  f3();
 }
 catch (int e)
 {
 std::cout << "main exception is occurred: " << e <<"\n";
  
 }
 catch (Derive& e)
 {
  std::cout << "Deriver exception is occurred.\n";
 }

输出为:

f3 exception is occurred.
main exception is occurred:1

两次输出表明在f3()捕获到异常后继续throw直到被main()继续捕获到异常。如果catch(...)在前但继续throw是可以编译通过的。

注意:在一个异常没有完成捕获并处理时抛出一个新的异常会导致程序崩溃!因此,不可以在析构函数或者operator delete函数中抛出异常。

通常来说,catch接收的异常类型为引用类型。这样做的目的是防止在拷贝初始化过程中出现抛出异常的操作。

异常与构造、析构函数的关系

假设我们想在类内捕获异常,自然会想到如下方式:

struct Str
{
  Str()
 {
  throw 100;
 }
};

class Cla
{
private:
 Str m_mem;
public:
   Cla()
 {
    try{}
  catch (int)
  {
   std::cout << "exception is catched in Cla.\n";
  }
 }

然而,这种办法无法在类内捕获异常。因为构造函数体内类已经经过了初始化(编译器会隐式构造一个m_mem()的初始化列表)。这个时候需要用function-try-block保护初始化逻辑。

class Cla
{
private:
 Str m_mem;
public:
 Cla()
  try :m_mem{}
 {

 }
 catch (int)
 {
  std::cout << "exception is catched at Cla\n";
  }
};

这样,就可以在类内捕获异常了。这里的try:m_mem{}里的m_mem{}可省略。

同时,假设我们也在main()内部也使用一次try-catch:

int main(int argc, char* argv[])
{
  try {
  Cla obj;
 }
 catch (int)
 {
  std::cout << "exception is catched at main.\n";
 }
  
}

最后输出为:

exception is catched at Cla. 
exception is catched at main.

这里main()也捕获到了异常。这是因为C++标准规定如果某个类的构造函数里出现了function-try-block,编译器会隐式在结尾加上一个throw让程序可以在后续继续接收到异常。

注意:如果在构造函数中抛出异常,已经构造的成员会被销毁,但类本身的析构函数不会被调用

本文作者:WYFC4

本文链接:https://www.cnblogs.com/wyfc4/p/17519816.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   WYFC4  阅读(65)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 404 not found REOL
404 not found - REOL
00:00 / 00:00
An audio error has occurred.