C++ 程序抛出异常后执行顺序说明

1 析构函数中是否可以抛出异常

首先我们看一个常见的问题,析构函数中是否可以抛出异常。答案是C++标准指明析构函数不能、也不应该抛出异常!

C++异常处理模型是为C++语言量身设计的,更进一步的说,它实际上也是为C++语言中面向对象而服务的。

C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。

那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。

下面我们来看看析构函数中不能抛出异常的两个理由:

1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。

2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

那么当无法保证在析构函数中不发生异常时, 该怎么办?

其实还是有很好办法来解决的。那就是把异常完全封装在析构函数内部,决不让异常抛出函数之外。这是一种非常简单,也非常有效的方法。

//析构函数
~Class()
{
 try{
 }
 catch(){ //这里可以什么都不做,只是保证catch块的程序抛出的异常不会被扔出析构函数之外。
 }
}

2 程序抛出异常后会怎样

下面我们通过一个程序来观察当程序中抛出异常了是否会调用析构函数,异常抛出中throw()后面的语句是否还会执行。

程序如下,我们创建一个类,然后构造一个类对象,当抛出异常我们看程序是否会进入析构函数以及throw()抛出异常后面的程序:

#include<iostream>
using namespace std;
class setTry{
public:
 setTry(){ //构造函数
  cout << "start!" << endl; // 1
 }
 ~setTry(){ //析构函数
  cout << "end!" << endl; // 4
 }
 void dosomething(){
  cout << "do something!" << endl; //类方法
 }
};
int main(void)
{
 setTry newOne;
 try{
  throw("error!"); //直接抛出异常
  newOne.dosomething();
 }
 catch (char* one){ //接收char*类异常
  cout << one << endl; // 2
 }
 catch (...){   //接收其他类型异常
  cout << "..." << endl;
 }
 cout << "return 0!"<<endl; // 3
 return 0;
}

上面程序运行结就是按标注的1、2、3、4步骤输出的,结果如下图所示:

从运行结果就可以看出,抛出异常try内部的throw()后面程序不会再执行,而try外部后面的程序会继续执行。另外,析构函数在生存期结束也会被调用。

补充:C++异常捕获和处理

0. 写在前面

异常,让一个函数可以在发现自己无法处理的错误时抛出一个异常,希望它的调用者可以直接或者间接处理这个问题。而传统错误处理技术,检查到一个错误,返回退出码或者终止程序等等,此时我们只知道有错误,但不能更清楚的知道哪种错误,因此,使用异常,就把错误和处理分开来,由库函数抛出异常,由调用者捕获这个异常,调用者就可以知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。

1. 异常的抛出和处理

1. 异常处理的语句

try区段:这个区段中包含了可能发生异常的代码,在发生了异常之后,需要通过throw抛出。

throw子句:throw 子句用于抛出异常,被抛出的异常可以是C++的内置类型(例如: throw int(1);),也可以是自定义类型。

catch子句:每个catch子句都代表着一种异常的处理。catch子句用于处理特定类型的异常。

例2:

 #include <iostream>
 using namespace std;
 void Test1()
 {
  try
  {
   char* p = new char[0x7fffffff]; //抛出异常
  }
  catch (exception e)
  {
   cout << e.what() << endl; //捕获异常,然后程序结束
  }
 }
 int main()
 {
  Test1();
  system("pause");
  return 0;
 }

结果:

当使用new进行开空间时,申请内存失败,就会抛出异常,此时捕获到异常时,就可告诉使用者是哪里的错误,便于修改

2. 异常的处理规则

异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个处理代码。

被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。

抛出异常后会释放局部存储对象,所以被抛出的对象也就还给系统了,throw表达式会初始化一个抛出特殊的匿名对象,异常对象由编译管理,异常对象在传给对应的catch处理之后撤销。

例2:

class Exception//异常类
{
public:
 Exception(const string& msg, int id)
 {
  _msg = msg;
  _id = id;
 }
 const char* What() const
 {
  return _msg.c_str();
 }
protected:
 string _msg;
 int _id;
};
template<size_t N = 10>
class Array
{
public:
 int& operator[](size_t pos)
 {
  if (pos >= N)
  {
   Exception e("下标不合法", 1); //出了这个作用域,抛出的异常对象就销毁了,这时会生成一个匿名对象先接受这个对象,并传到外层栈帧。
   throw e;
  }
  return a[pos];
 }
protected:
 int a[N];
};
int f()
{
 try
 {
  Array<> a;
  a[11];
 }
 catch (exception& e)
 {
  cout << e.what() << endl; //类型不匹配,找离抛出异常位置最近且类型匹配的那个。
 }
 return 0;
}
int main()
{
 try
 {
  f();
 }
 catch (Exception& e)
 {
  cout << e.What() << endl;
 }
 system("pause");
 return 0;
}

结果:

f()函数中捕获的异常是标准库里面的异常,但抛出异常的对象是自己定义的异常类,故类型不匹配,找离抛出异常最近的且类型匹配的Exception

3. 异常处理栈展开

1.在try的语句块内声明的变量在外部是不可以访问的,即使是在catch子句内也不可以访问。  

2.栈展开(寻找异常处理(exception handling)代码)

栈展开会沿着嵌套函数的调用链不断查找,知道找到了已抛出的异常匹配的catch子句。如果在最后还是没有找到对应的catch子句的话,则退出主函数后查找过程终止,程序调用标准函数库的terminate()函数,终止该程序的执行

具体过程:

当一个exception被抛出的时候,控制权会从函数调用中释放出来,并需找一个可以处理的catch子句

对于一个抛出异常的try区段,程序会先检查与该try区段关联的catch子句,如果找到了匹配的catch子句,就使用这个catch子句处理这个异常。

没有找到匹配的catch子句,如果这个try区段嵌套在其他try区段中,则继续检查与外层try匹配的catch子句。如果仍然没有找到匹配的catch子句,则退出当前这个主调函数,并在调用了刚刚退出的这个函数的其他函数中寻找。

3. catch子句的查找:  

catch子句是按照出现的顺序进行匹配的(以例2来说,异常先会匹配catch(exception e)子句,然后在匹配 catch (Exception e)子句,一步一步的栈展开)。在寻找catch子句的过程中,抛出的异常可以进行类型转换,但是比较严格:

允许从非常量转换到常量的类型转换(权限缩小)

允许从派生类到基类的转换。

允许数组被转换成为指向数组(元素)类型的指针,函数被转换成指向该函数类型的指针(降级问题)

标准算术类型的转换(比如:把bool型和char型转换成int型)和类类型转换(使用类的类型转换运算符和转换构造函数)。

4. 异常处理中需要注意的问题

如果抛出的异常一直没有函数捕获(catch),则会一直上传到c++运行系统那里,导致整个程序的终止

一般在异常抛出后资源可以正常被释放,但注意如果在类的构造函数中抛出异常,系统是不会调用它的析构函数的,处理方法是:如果在构造函数中要抛出异常,则在抛出前要记得删除申请的资源。

异常处理仅仅通过类型而不是通过值来匹配的,所以catch块的参数可以没有参数名称,只需要参数类型。

函数原型中的异常说明要与实现中的异常说明一致,否则容易引起异常冲突。

应该在throw语句后写上异常对象时,throw先通过Copy构造函数构造一个新对象,再把该新对象传递给 catch.  

注:那么当异常抛出后新对象如何释放?

异常处理机制保证:异常抛出的新对象并非创建在函数栈上,而是创建在专用的异常栈上,因此它才可以跨接多个函数而传递到上层,否则在栈清空的过程中就会被销毁。所有从try到throw语句之间构造起来的对象的析构函数将被自动调用。但如果一直上溯到main函数后还没有找到匹配的catch块,那么系统调用terminate()终止整个程序,这种情况下不能保证所有局部对象会被正确地销毁。

catch块的参数推荐采用地址传递而不是值传递,不仅可以提高效率,还可以利用对象的多态性。另外,派生类的异常扑获要放到父类异常扑获的前面,否则,派生类的异常无法被扑获。

编写异常说明时,要确保派生类成员函数的异常说明和基类成员函数的异常说明一致,即派生类改写的虚函数的异常说明至少要和对应的基类虚函数的异常说明相同,甚至更加严格,更特殊。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。如有错误或未考虑完全的地方,望不吝赐教。

(0)

相关推荐

  • 详细分析C++ 异常处理

    异常是程序在执行期间产生的问题.C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作. 异常提供了一种转移程序控制权的方式.C++ 异常处理涉及到三个关键字:try.catch.throw. throw: 当问题出现时,程序会抛出一个异常.这是通过使用 throw 关键字来完成的. catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常. try: try 块中的代码标识将被激活的特定异常.它后面通常跟着一个或多个 catch 块. 如果有一个

  • C++常见异常处理原理及代码示例解析

    编程中常见的错误 程序的编译错误--比较好解决,主要是一些语法错误 程序的运行错误--产生因素较为复杂,如空间不够,下标越界,访问非法空间等. 异常是指程序运行时出现的不正常,可分为一下几类: CPU异常:如在计算过程中,出现除数为0的情况. 内存异常,如: 使用new或malloc申请动态内存但存储空间不够: 数组下标越界: 使用野指针.迷途指针读取内存: 设备异常,如: 无法打开文件,或文件损坏: 正在读取磁盘文件时挪动了文件或磁盘: 正在使用打印机但设备被断开: 正在使用的网络断线或阻塞:

  • C++构造函数抛出异常需要注意的地方

    从语法上来说,构造函数可以抛出异常.但从逻辑上和风险控制上,构造函数中尽量不要抛出异常.万不得已,一定要注意防止内存泄露. 1.构造函数抛出异常导致内存泄漏 在C++构造函数中,既需要分配内存,又需要抛出异常时要特别注意防止内存泄露的情况发生.因为在构造函数中抛出异常,在概念上将被视为该对象没有被成功构造,因此当前对象的析构函数就不会被调用.同时,由于构造函数本身也是一个函数,在函数体内抛出异常将导致当前函数运行结束,并释放已经构造的成员对象,包括其基类的成员,即执行直接基类和成员对象的析构函数

  • C++抛出和接收异常的顺序

    异常(exception)是C++语言引入的错误处理机制.它 采用了统一的方式对程序的运行时错误进行处理,具有标准化.安全和高效的特点.C++为了实现异常处理,引入了三个关键字:try.throw.catch.异常由throw抛出,格式为throw[expression],由catch捕捉.Try语句块是可能抛出异常的语句块,它通常和一个或多个catch语句块连续出现. try语句块和catch语句块必须相互配合,以下三种情况都会导致编译错误: (1)只有try语句块而没有catch语句块,或者

  • C++异常捕捉与处理的深入讲解

    前言 在阅读别人开发的项目中,也许你会经常看到了多处使用异常的代码,也许你也很少遇见使用异常处理的代码.那在什么时候该使用异常,又在什么时候不该使用异常呢?在学习完异常基本概念和语法之后,后面会有讲解. (1)异常抛出和捕捉语句 //1.抛出异常 throw 异常对象 //2.异常捕捉 try{ 可能会发生异常的代码 }catch(异常对象){ 异常处理代码 } throw子句:throw 子句用于抛出异常,被抛出的异常可以是C++的内置类型(例如: throw int(1);),也可以是自定义

  • C++ 程序抛出异常后执行顺序说明

    1 析构函数中是否可以抛出异常 首先我们看一个常见的问题,析构函数中是否可以抛出异常.答案是C++标准指明析构函数不能.也不应该抛出异常! C++异常处理模型是为C++语言量身设计的,更进一步的说,它实际上也是为C++语言中面向对象而服务的. C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持. 那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是

  • 浅谈Java文件执行顺序、main程序入口的理解

    在我们通过JVM编译Java后缀名的文件时,JVM首先寻找入口(main方法) public static void main(String[] args) 1.由于在入口时,未调用任何对象,该方法只能设置为static静态 2.JVM为Java的最底层,所以即使有返回结果,结果也无处可去,因此该方法必然是void无返回值 3.由于main方法是入口,被JVM自动调用,只有将该方法设置为public公有级别才能对JVM可见 综上,入口main方法只能写为 public static void m

  • 关于Java中try finally return语句的执行顺序浅析

    问题分析 finally语句块一定会执行吗? 可能很多人第一反应是肯定要执行的,但仔细一想,如果一定会执行的话 也就不会这么SB的问了. Demo1 public class Test { public static void main(String[] args) { System.out.println("return value of test(): " + test()); } public static int test() { int i = 1; // if (i ==

  • 关于C#执行顺序带来的一些潜在问题

    前言 编写程序的时候,人们的直观感觉通常认为,程序的执行顺序是按照语句的顺序进行的.然而,许多编程语言的规范是允许实际执行顺序与语句编写顺序不符的.实际上,编译器为了完成某种优化,常常会对一些操作进行适当的顺序调整,导致一些预料之外的现象. 实验现象 首先,通过一个例子来展示这个现象.在一个C# .NET Core 3.1命令行程序中,定义两个全局变量a和b,在线程1中,依次对b和a进行递增.这样,在任何时刻b应当等于a或a+1. static int a = 0; static int b =

  • Java.try catch finally 的执行顺序说明

    示例1: public static String hello() { String s = "商务"; try { return s; } catch (Exception e) { return "catch进来了"; } finally { s = "你好世界"; return s; } } 返回结果:你好世界,此时的返回顺序是 finally > try 示例2: public static String hello() { Str

  • Java拦截器Interceptor和过滤器Filte的执行顺序和区别

    目录 1.实现原理不同 2.使用范围不同 3.触发时机不同 4.拦截的请求范围不同 5.注入Bean情况不同 6.控制执行顺序不同 1.实现原理不同 过滤器和拦截器 底层实现方式大不相同,过滤器 是基于函数回调的,拦截器 则是基于Java的反射机制(动态代理)实现的. 1.拦截器是基于java的反射机制的,而过滤器是基于函数回调 2.过滤器依赖与servlet容器,而拦截器不依赖与servlet容器 3.拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用 4.拦截器可以访问

  • For循环中分号隔开的3部分的执行顺序探讨

    引发这个问题思考的是一段js程序的运行结果: 复制代码 代码如下: var i = 0; function a(){ for(i=0;i<20;i++){ } } function b(){ for(i=0;i<3;i++){ a(); } return i; } var Result = b(); 这段程序的运行结果是Result = 21: 从这段程序中我们可以看出,i在a函数返回的时候值是20这是没有问题的. 而在b函数返回的时候,i的值是20还是21就值得讨论了. 问题的本质即:先进行

  • JavaScript执行顺序详细介绍

    之前从JavaScript引擎的解析机制来探索JavaScript的工作原理,下面我们以更形象的示例来说明JavaScript代码在页面中的执行顺序.如果说,JavaScript引擎的工作机制比较深奥是因为它属于底层行为,那么JavaScript代码执行顺序就比较形象了,因为我们可以直观感觉到这种执行顺序,当然JavaScript代码的执行顺序是比较复杂的,所以在深入JavaScript语言之前也有必要对其进行剖析.1.1  按HTML文档流顺序执行JavaScript代码首先,读者应该清楚,H

  • java面试题之try中含return语句时代码的执行顺序详解

    前言 最近在刷java面试题偶然看到这类问题(try/finally中含有return时的执行顺序),觉得挺有意思于是小小的研究了一下,希望经过我添油加醋天马行空之后,能给你带来一定的帮助,下面来看看详细的介绍. 原题 try {} 里有一个return语句,那么紧跟在这个try后的finally {}里的代码会不会被执行?什么时候被执行?在return前还是后? 乍一看题目很简单嘛,java规范都说了,finally会在try代码块的return之前执行,你这文章写得没意义,不看了 你等等!(

  • 举例说明Java中代码块的执行顺序

    前言     今天在看Android ContentProvider实现的时候,突然想到了Java类在new的过程中,静态域.静态块.非静态域.非静态块.构造函数的执行顺序问题.其实这是一个很经典的问题,非常考察对Java基础知识的掌握程度.很多面试过程中相信也有这样的问题,趁着周末有时间复习一下. 结论     这里先把整理好的结论抛给大家,然后我在写个程序来验证我们的结论.在Java类被new的过程中,执行顺序如下: 实现自身的静态属性和静态代码块.(根据代码出现的顺序决定谁先执行) 实现自

随机推荐