C++基础——异常

转自:https://blog.csdn.net/sky453589103/article/details/49862949

异常是指程序在运行时存在异常行为,这些异常的行为让函数不能正常执行。异常应该捕获的应该是你能够处理的错误,比如:不能连接服务器,不能连接数据库等,但异常不应该是你修复代码的bug的手段,比如:数组访问越界。死锁等情况。在异常处理中,你需要的做的是尽可能的修补你错误,比如:释放内存。解锁互斥量。


c++的异常包括下面的三个部分:
1.    throw子句:throw 子句用于抛出异常,被抛出的异常可以是C++的内置类型(例如: throw int(1);),也可以是自定义类型。
2.    catch子句:每个catch子句都代表着一种异常的处理。catch子句用于处理特定类型的异常。
3.    try区段:这个区段中包含了可能发生异常的代码,在发生了异常之后,需要通过throw抛出。

编写异常:
通常的形式是:

try {  
    // do something  
    throw exception;  
} catch (exception declaration) {  
    //  excepetion handling code   
    // throw or no  
} catch (exception declaration) {  
    //  another excepetion handling code   
    // throw or no  
}  
示例:
//  定义自己的异常类,public 继承C++标准库的exception
class myexception : public std::exception {  
public :  
  myexception(std::string s) : exception(s.c_str()) {  
  
  }  
  virtual const char *what() const {  
    return exception::what();  
  }  
};  
  
void Func() {  
  try{  
    int *p = NULL;  
    if (p == NULL) {  
      throw myexception("p is NULL");  
    }  
  }  
  catch(myexception e) {  
    printf("%s\n", e.what());  
    throw;    //重新抛出异常,异常类型为myexception  
  }  
  catch (std::exception e) {  
    printf("%s", e.what());  
  }  
}  
在try的语句块内声明的变量在外部是不可以访问的,即使是在catch子句内也不可以访问

寻找异常处理(exception handling)代码:
当一个exception被抛出的时候,控制权会从函数调用中释放出来,并需找一个可以处理的catch子句。对于一个抛出异常的try区段,程序会先检查与该try区段关联的catch子句,如果找到了匹配的catch子句,就使用这个catch子句处理这个异常。
否则,如果这个try区段嵌套在其他try区段中,则继续检查与外层try匹配的catch子句。如果仍然没有找到匹配的catch子句,则退出当前这个主调函数,并在调用了刚刚退出的这个函数的其他函数中寻找。这个过程就是所谓的 栈展开,栈展开会沿着嵌套函数的调用链不断查找,知道找到了已抛出的异常匹配的catch子句。如果在 最后还是没有找到对应的catch子句的话,则退出主函数后查找过程终止,程序调用标准函数库的terminate()函数,终止该程序的执行

catch子句的查找:
catch子句是 按照出现的顺序进行匹配的(以上面的代码为例,异常先会匹配catch(myexception e)子句,然后在匹配  catch (std::exception e)子句,一步一步的栈展开)。在寻找catch子句的过程中,抛出的异常可以进行 类型转换,但是比较严格:
1.    允许从非常量转换到常量的类型转换。
2.    允许从派生类到基类的转换。
3.    允许数组被转换成为指向数组(元素)类型的指针,函数被转换成指向该函数类型的指针。
4.    标准算术类型的转换(比如:把bool型和char型转换成int型)和类类型转换(使用类的类型转换运算符和转换构造函数)。

如果你想一次性 捕获全部的异常的话,C++也提供了一种简便的做法,在catch子句的异常声明中使用 省略号来作为异常声明:

void Func() {  
    try {  
        //  your code  
    }  
    catch(...) {  
        // special operation for handling exception  
        throw;  
    }  
}  
决定一个throw是否发生在发生在try区段中,我们修改一下上面的例子,新增两个类A和类B :
class myexception : public std::exception {  
public :  
  myexception(std::string s) : exception(s.c_str()) {  
  
  }  
  virtual const char *what() const {  
    return exception::what();  
  }  
};  
  
class A {  
public :  
  ~A() {  
    printf("A's destructor\n");  
  }  
  
};  
  
class B {  
public :  
  ~B() {  
    printf("B's destructor\n");  
  }  
  
};  
  
void Func() {  
  A a;  
  throw myexception("p is NULL");  
  try{  
    int *p = NULL;  
    if (p == NULL) {  
      //throw myexception("p is NULL");  
    }  
  }  
  catch(myexception e) {  
    printf("%s\n", e.what());  
    throw;  
  }  
  catch (std::exception e) {  
    printf("%s", e.what());  
  }  
  
  B b;  
}  
  
  
void Func2() {  
  Func();  
}  

我们可以很明显的看到,  throw myexception("p is NULL")子句出现在了try区段之外,而注释了的  throw myexception("p is NULL")在try区段内。那么 编译器是如何知道,一个throw子句在不在try区段之内的呢?这个做法C++并没有规定,但是有一个很好的做法就是 使用program couter-range表格。program couter(程序计数器)中存放着下一个即将执行的指令。为了标志出某一段代码是否在try区段内,可以 将程序计数器的起始值和结束值存放在一个表格中。当一个异常被throw时,我们可以 比较当前的程序计数器的值是否在对应try语句的范围表格中,来决定这个throw子句是否在try区段中。如果异常不能在主调函数中被处理(或者catch之后继续抛出),程序计数器就会被设置为调用端地址,然后继续比较对应的try区段范围表格,以此往复,直到终止程序(调用terminate()函数)。

关于活动的local object:
在上面的例子,我们可以看到,在throw myexception("p is NULL")子句出现之前,a对象已经被创建在函数的堆栈中,因此它是一个 活动的local object,但是b对象还没有被创建,因此b不是活动的local object。 在栈展开的过程中,a对象将会被自动销毁(调用析构函数,如果是内置类型的话则什么都不做)。有一点需要注意的是,如果你是动态分配的内存,这个动态分配的内存是不会被释放的。

从上面的信息可以得出,下面这个流程图:


从函数堆栈的角度来看待栈展开的行为的话。还是一上面的代码为例,我们可以知道,在调用函数Func的过程中,a对象在throw子句之前,所以,在调用throw之前,程序会在Func1对应的堆栈上调用类A的构造函数,建立a对象。在调用throw之后,因为此时a对象已经在堆栈中了,而throw子句并没有与之关联的catch子句处理,因此,对象a在堆栈中的空间被释放,异常传播到Func2 中。

异常对象在发生异常而被抛出的过程中,异常对象的行为:
    异常对象是一种特殊的对象,之所以说它特殊,不是因为它的类型特殊,而是因为它所存在的空间特殊。 异常对象位于编译器管理的空间中,编译器确保无论最终调用的是哪个catch子句都能访问这个空间
    当抛出异常时, 编译器使用异常抛出表达式来对异常对象进行拷贝初始化,因此,throw语句中的 表达式必须要用完整的类型(也即是要看到这个类型完整的定义,而不可以是前置声明这类声明),而且如果这个类型是类类型的话,必须还要有一个可以访问的析构函数和复制构造函数(或者移动构造函数);如果是数组类型,则表达式会被转换成为与之对应的指针类型(指针退化)。
    当执行到抛出异常的语句(也就是throw子句), 如果throw子句是在try区段或者是不在try区段内,也不在catch子句被的话,这个 异常对象会被复制一次,无论在匹配到的catch子句中的异常声明是否是引用类型。此时,被复制的对象会被 加入到编译器的特殊空间中。如果这个时候catch子句继续抛出异常,那么就会有两种情况需要考虑:
    1.匹配到的是引用类型:编译器不会生成一个副本,而是直接将catch子句中的引用绑定到上一次throw子句中的对象。
    2. 匹配到的不是引用类型:编译器将会产生一个副本,而这个副本就是catch子句中声明的那个变量。
考虑下面一段代码:

#include <cstdio>  
#include <cstdlib>  
#include <exception>  
#include <string>  
  
class myexception : public std::exception {  
public :  
  myexception(std::string s) : exception(s.c_str()) {  
  
  }  
  virtual ~myexception() {  
    printf("myexception's destructor\n");  
  }  
  virtual const char *what() const {  
    return exception::what();  
  }  
public :  
  std::string _s;  
};  
  
class A {  
public :  
  ~A() {  
    printf("A's destructor\n");  
  }  
  
};  
  
class B {  
public :  
  ~B() {  
    printf("B's destructor\n");  
  }  
  
};  
  
void Func() {  
  A a;  
   printf("dasd\n");  
   myexception e("p is NULL");  
   e._s = "haha";  
  try{  
    int *p = NULL;  
    if (p == NULL) {  
      throw e;  
    }  
  }  
  catch(myexception &e) {  
    e._s = "heiheihei";  
    printf("%s\n", e.what());   // 这里输出 p is NULL
    throw;  
  }  
  
  printf("%s\n", e._s.c_str());  
  
  B b;  
}  
  
void Func2() {  
  try {  
    Func();  
  }  
  catch (myexception &e) {  
    printf("Func2 : %s\n", e._s.c_str());  
    throw;  
  }  
}  
  
int main() {  
  try {  
    Func2();  
  }  
  catch (myexception &e) {  
    printf("main: %s\n", e._s.c_str());  
  }  
  printf ("hahah\n");  
  
  system("PAUSE");  
  return 0;  
}  

输出的是:

我们可以清楚的看到,当执行到Func()函数的try区段中的throw的时候,立即执行了myexception的复制构造函数,然后在查找匹配的catch子句的时候,因为 使用的都是引用类型,因此,这个被复制出来的变量不用继续被复制。而 这个异常对象会在catch子句评估确定不再抛出异常或者下一个匹配的catch不是引用类型(包括指针么?)的时候被析构。需要注意的是,如果是指针类型的话,被复制的也只是这个指针,而不是指针所指的对象。因此,如果你返回一个指向局部对象的指针,在这个局部对象被析构之后解引用这个指针的话,是会出错的。

最后,加上一个C++标准库的异常类型结构图:



异常类型

说明

exception

最常见的问题

runtime_error

只有在运行时才能检查的错误

range_error

运行时错误:生成的结果超出了有意义的范围

overflow_error

运行时错误:计算上溢

underflow_error

运行时错误:计算下溢

logic_error

程序逻辑错误

domain_error

逻辑错误:参数对应的结果值不存在

invalid_error

逻辑错误:无效参数

length_error

逻辑错误:试图创建一个超出该类型最大长度的对象

out_of_range

逻辑错误:使用一个超出有效范围的值


也参考:

C++ 异常机制分析








  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++著名的基础书籍潘嘉杰著,很适合初学者~~口碑很好的哦~~ 适合基础看的,大牛飞过~~~ 第一篇 过程化的程序设计   第1章 良好的学习开端 1   1.1 软件与程序 1   1.2 程序设计要做什么 1   1.3 选好一种语言 2   1.4 C++能够做些什么 2   1.5 C语言、C++和VisualC++的关系 2   1.6 学习程序设计的方法和必要准备 3   1.7 总结 3   第2章 Hello,World 4   第3章 各种各样的“箱子”——变量 12   第4章 要走哪条路——条件语句 20   第5章 有个圈儿的程序——循环语句 36   第6章 好用的“工具”——函数 51   第7章 好大的“仓库”——数组 69   第8章 内存里的快捷方式——指针 84   第9章 自己设计的箱子——枚举和结构 98   第二篇 实战程序设计   第10章 高效阅读程序代码 119   第11章 调试程序代码技巧 127   第12章 编写程序技巧 150   第三篇 面向对象的程序设计   第13章 初识对象 163   第14章 再识对象 169   第15章 造物者与毁灭者——对象生灭 178   第16章 共有财产·好朋友·操作符 206   第17章父与子——继承 228   第18章 再谈输入与输出 273   第19章 万用的模板 285   第20章 异常的处理 297   附录A 常用保留字列表 305   附录B 常见编译错误和解决方法 307   附录C 参考答案 310   附录D 参考文献 356
贪心算法是一种问题求解方法,它在每一步总是做出当前情况下的最优选择,以期望获得最优解。而"最大整数"同样可以使用贪心算法来求解。 对于"最大整数"的问题,我们可以考虑如下的贪心策略:从高位开始,尽可能选择较大的数字。具体步骤如下: 1. 对于给定的整数,我们首先将其转化为一个数组,其中每个元素表示整数的一个位数。 2. 从最高位(最左侧)开始,遍历数组。 3. 对于当前位上的数字,从9开始递减,找到第一个小于等于当前数字的最大数字。 4. 如果找到了符合条件的最大数字,将其放在当前位。否则,不做任何操作。 5. 继续向下遍历,重复步骤3-4。 6. 最终,得到的数组即为满足条件的最大整数。 以一个具体的例子说明上述算法:假设给定的整数为5372。 1. 将整数转化为数组[5, 3, 7, 2]。 2. 从最高位开始遍历。 3. 对于第一位5,从9开始递减,找到第一个小于等于5的数字,为7。 4. 将7放在第一位,得到[7, 3, 7, 2]。 5. 对于第二位3,从9开始递减,找到第一个小于等于3的数字,为3(与当前数字相等)。 6. 不做任何操作,得到[7, 3, 7, 2]。 7. 对于第三位7,从9开始递减,找到第一个小于等于7的数字,为7。 8. 将7放在第三位,得到[7, 3, 7, 2]。 9. 对于第四位2,从9开始递减,找到第一个小于等于2的数字,为2。 10. 将2放在第四位,得到[7, 3, 7, 2]。 11. 遍历结束,最终得到的数组为[7, 3, 7, 2],转化为整数为7372。 通过上述贪心算法,我们得到了满足条件的最大整数7372。证明了贪心算法在"最大整数"问题中的有效性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值