程序运行时常会碰到一些异常情况,例如:

  • 做除法的时候除数为 0;
  • 用户输入年龄时输入了一个负数;
  • 用 new 运算符动态分配空间时,空间不够导致无法分配;
  • 访问数组元素时,下标越界;打开文件读取时,文件不存在。

这些异常情况,如果不能发现并加以处理,很可能会导致程序崩溃。

所谓“处理”,可以是给出错误提示信息,然后让程序沿一条不会出错的路径继续执行;也可能是不得不结束程序,但在结束前做一些必要的工作,如将内存中的数据写入文件、关闭打开的文件、释放动态分配的内存空间等。

一发现异常情况就立即处理未必妥当,因为在一个函数执行过程中发生的异常,在有的情况下由该函数的调用者决定如何处理更加合适。尤其像库函数这类提供给程序员调用,用以完成与具体应用无关的通用功能的函数,执行过程中贸然对异常进行处理,未必符合调用它的程序的需要。

此外,将异常分散在各处进行处理不利于代码的维护,尤其是对于在不同地方发生的同一种异常,都要编写相同的处理代码也是一种不必要的重复和冗余。如果能在发生各种异常时让程序都执行到同一个地方,这个地方能够对异常进行集中处理,则程序就会更容易编写、维护。

鉴于上述原因,C++ 引入了异常处理机制。其基本思想是:函数 A 在执行过程中发现异常时可以不加处理,而只是“拋出一个异常”给 A 的调用者,假定为函数 B。

拋出异常而不加处理会导致函数 A 立即中止,在这种情况下,函数 B 可以选择捕获 A 拋出的异常进行处理,也可以选择置之不理。如果置之不理,这个异常就会被拋给 B 的调用者,以此类推。

如果一层层的函数都不处理异常,异常最终会被拋给最外层的 main 函数。main 函数应该处理异常。如果main函数也不处理异常,那么程序就会立即异常地中止。

C++异常处理基本语法

C++ 通过 throw 语句和 try...catch 语句实现对异常的处理。throw 语句的语法如下:

throw  表达式;

该语句拋出一个异常。异常是一个表达式,其值的类型可以是基本类型,也可以是类。

try...catch 语句的语法如下:

try {
    语句组
}
catch(异常类型) {
    异常处理代码
}
...
catch(异常类型) {
    异常处理代码
}

catch 可以有多个,但至少要有一个。

不妨把 try 和其后{}中的内容称作“try块”,把 catch 和其后{}中的内容称作“catch块”。

try...catch 语句的执行过程是:

  • 执行 try 块中的语句,如果执行的过程中没有异常拋出,那么执行完后就执行最后一个 catch 块后面的语句,所有 catch 块中的语句都不会被执行;
  • 如果 try 块执行的过程中拋出了异常,那么拋出异常后立即跳转到第一个“异常类型”和拋出的异常类型匹配的 catch 块中执行(称作异常被该 catch 块“捕获”),执行完后再跳转到最后一个 catch 块后面继续执行。

例如下面的程序:

  1. #include <iostream>
  2. using namespace std;
  3. int main()
  4. {
  5. double m ,n;
  6. cin >> m >> n;
  7. try {
  8. cout << "before dividing." << endl;
  9. if( n == 0)
  10. throw -1; //抛出int类型异常
  11. else
  12. cout << m / n << endl;
  13. cout << "after dividing." << endl;
  14. }
  15. catch(double d) {
  16. cout << "catch(double) " << d << endl;
  17. }
  18. catch(int e) {
  19. cout << "catch(int) " << e << endl;
  20. }
  21. cout << "finished" << endl;
  22. return 0;
  23. }

程序的运行结果如下:
9 6
before dividing.
1.5
after dividing.
finished

说明当 n 不为 0 时,try 块中不会拋出异常。因此程序在 try 块正常执行完后,越过所有的 catch 块继续执行,catch 块一个也不会执行。

程序的运行结果也可能如下:
9 0
before dividing.
catch\(int) -1
finished

当 n 为 0 时,try 块中会拋出一个整型异常。拋出异常后,try 块立即停止执行。该整型异常会被类型匹配的第一个 catch 块捕获,即进入catch(int e)块执行,该 catch 块执行完毕后,程序继续往后执行,直到正常结束。

如果拋出的异常没有被 catch 块捕获,例如,将catch(int e),改为catch(char e),当输入的 n 为 0 时,拋出的整型异常就没有 catch 块能捕获,这个异常也就得不到处理,那么程序就会立即中止,try...catch 后面的内容都不会被执行。

能够捕获任何异常的 catch 语句

如果希望不论拋出哪种类型的异常都能捕获,可以编写如下 catch 块:

catch(...) {
    ...
}

这样的 catch 块能够捕获任何还没有被捕获的异常。例如下面的程序:

  1. #include <iostream>
  2. using namespace std;
  3. int main()
  4. {
  5. double m, n;
  6. cin >> m >> n;
  7. try {
  8. cout << "before dividing." << endl;
  9. if (n == 0)
  10. throw - 1; //抛出整型异常
  11. else if (m == 0)
  12. throw - 1.0; //拋出 double 型异常
  13. else
  14. cout << m / n << endl;
  15. cout << "after dividing." << endl;
  16. }
  17. catch (double d) {
  18. cout << "catch (double)" << d << endl;
  19. }
  20. catch (...) {
  21. cout << "catch (...)" << endl;
  22. }
  23. cout << "finished" << endl;
  24. return 0;
  25. }

程序的运行结果如下:
9 0
before dividing.
catch (...)
finished

当 n 为 0 时,拋出的整型异常被catchy(...)捕获。

程序的运行结果也可能如下:
0 6
before dividing.
catch (double) -1
finished

当 m 为 0 时,拋出一个 double 类型的异常。虽然catch (double)catch(...)都能匹配该异常,但是catch(double)是第一个能匹配的 catch 块,因此会执行它,而不会执行catch(...)块。

由于catch(...)能匹配任何类型的异常,它后面的 catch 块实际上就不起作用,因此不要将它写在其他 catch 块前面。

异常的再拋出

如果一个函数在执行过程中拋出的异常在本函数内就被 catch 块捕获并处理,那么该异常就不会拋给这个函数的调用者(也称为“上一层的函数”);如果异常在本函数中没有被处理,则它就会被拋给上一层的函数。例如下面的程序:

  1. #include <iostream>
  2. #include <string>
  3. using namespace std;
  4. class CException
  5. {
  6. public:
  7. string msg;
  8. CException(string s) : msg(s) {}
  9. };
  10. double Devide(double x, double y)
  11. {
  12. if (y == 0)
  13. throw CException("devided by zero");
  14. cout << "in Devide" << endl;
  15. return x / y;
  16. }
  17. int CountTax(int salary)
  18. {
  19. try {
  20. if (salary < 0)
  21. throw - 1;
  22. cout << "counting tax" << endl;
  23. }
  24. catch (int) {
  25. cout << "salary < 0" << endl;
  26. }
  27. cout << "tax counted" << endl;
  28. return salary * 0.15;
  29. }
  30. int main()
  31. {
  32. double f = 1.2;
  33. try {
  34. CountTax(-1);
  35. f = Devide(3, 0);
  36. cout << "end of try block" << endl;
  37. }
  38. catch (CException e) {
  39. cout << e.msg << endl;
  40. }
  41. cout << "f = " << f << endl;
  42. cout << "finished" << endl;
  43. return 0;
  44. }

程序的输出结果如下:
salary < 0
tax counted
devided by zero
f=1.2
finished

CountTa 函数拋出异常后自行处理,这个异常就不会继续被拋给调用者,即 main 函数。因此在 main 函数的 try 块中,CountTax 之后的语句还能正常执行,即会执行f = Devide(3, 0);

第 35 行,Devide 函数拋出了异常却不处理,该异常就会被拋给 Devide 函数的调用者,即 main 函数。拋出此异常后,Devide 函数立即结束,第 14 行不会被执行,函数也不会返回一个值,这从第 35 行 f 的值不会被修改可以看出。

Devide 函数中拋出的异常被 main 函数中类型匹配的 catch 块捕获。第 38 行中的 e 对象是用复制构造函数初始化的。

如果拋出的异常是派生类的对象,而 catch 块的异常类型是基类,那么这两者也能够匹配,因为派生类对象也是基类对象。

虽然函数也可以通过返回值或者传引用的参数通知调用者发生了异常,但采用这种方式的话,每次调用函数时都要判断是否发生了异常,这在函数被多处调用时比较麻烦。有了异常处理机制,可以将多处函数调用都写在一个 try 块中,任何一处调用发生异常都会被匹配的 catch 块捕获并处理,也就不需要每次调用后都判断是否发生了异常。

有时,虽然在函数中对异常进行了处理,但是还是希望能够通知调用者,以便让调用者知道发生了异常,从而可以作进一步的处理。在 catch 块中拋出异常可以满足这种需要。例如:

  1. #include <iostream>
  2. #include <string>
  3. using namespace std;
  4. int CountTax(int salary)
  5. {
  6. try {
  7. if( salary < 0 )
  8. throw string("zero salary");
  9. cout << "counting tax" << endl;
  10. }
  11. catch (string s ) {
  12. cout << "CountTax error : " << s << endl;
  13. throw; //继续抛出捕获的异常
  14. }
  15. cout << "tax counted" << endl;
  16. return salary * 0.15;
  17. }
  18. int main()
  19. {
  20. double f = 1.2;
  21. try {
  22. CountTax(-1);
  23. cout << "end of try block" << endl;
  24. }
  25. catch(string s) {
  26. cout << s << endl;
  27. }
  28. cout << "finished" << endl;
  29. return 0;
  30. }

程序的输出结果如下:
CountTax error:zero salary
zero salary
finished

第 14 行的throw;没有指明拋出什么样的异常,因此拋出的就是 catch 块捕获到的异常,即 string("zero salary")。这个异常会被 main 函数中的 catch 块捕获。

函数的异常声明列表

为了增强程序的可读性和可维护性,使程序员在使用一个函数时就能看出这个函数可能会拋出哪些异常,C++ 允许在函数声明和定义时,加上它所能拋出的异常的列表,具体写法如下:

void func() throw (int, double, A, B, C);

void func() throw (int, double, A, B, C){...}

上面的写法表明 func 可能拋出 int 型、double 型以及 A、B、C 三种类型的异常。异常声明列表可以在函数声明时写,也可以在函数定义时写。如果两处都写,则两处应一致。

如果异常声明列表如下编写:

void func() throw ();

则说明 func 函数不会拋出任何异常。

一个函数如果不交待能拋出哪些类型的异常,就可以拋出任何类型的异常。

函数如果拋出了其异常声明列表中没有的异常,在编译时不会引发错误,但在运行时, Dev C++ 编译出来的程序会出错;用 Visual Studio 2010 编译出来的程序则不会出错,异常声明列表不起实际作用。

C++标准异常类

C++ 标准库中有一些类代表异常,这些类都是从 exception 类派生而来的。常用的几个异常类如图 1 所示。


图1:常用的异常类

bad_typeid、bad_cast、bad_alloc、ios_base::failure、out_of_range 都是 exception 类的派生类。C++ 程序在碰到某些异常时,即使程序中没有写 throw 语句,也会自动拋出上述异常类的对象。这些异常类还都有名为 what 的成员函数,返回字符串形式的异常描述信息。使用这些异常类需要包含头文件 stdexcept。

下面分别介绍以上几个异常类。本节程序的输出以 Visual Studio 2010为准,Dev C++ 编译的程序输出有所不同。

1) bad_typeid

使用 typeid 运算符时,如果其操作数是一个多态类的指针,而该指针的值为 NULL,则会拋出此异常。

2) bad_cast

在用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常。程序示例如下:

  1. #include <iostream>
  2. #include <stdexcept>
  3. using namespace std;
  4. class Base
  5. {
  6. virtual void func() {}
  7. };
  8. class Derived : public Base
  9. {
  10. public:
  11. void Print() {}
  12. };
  13. void PrintObj(Base & b)
  14. {
  15. try {
  16. Derived & rd = dynamic_cast <Derived &>(b);
  17. //此转换若不安全,会拋出 bad_cast 异常
  18. rd.Print();
  19. }
  20. catch (bad_cast & e) {
  21. cerr << e.what() << endl;
  22. }
  23. }
  24. int main()
  25. {
  26. Base b;
  27. PrintObj(b);
  28. return 0;
  29. }

程序的输出结果如下:
Bad dynamic_cast!

在 PrintObj 函数中,通过 dynamic_cast 检测 b 是否引用的是一个 Derived 对象,如果是,就调用其 Print 成员函数;如果不是,就拋出异常,不会调用 Derived::Print。

3) bad_alloc

在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常。程序示例如下:

  1. #include <iostream>
  2. #include <stdexcept>
  3. using namespace std;
  4. int main()
  5. {
  6. try {
  7. char * p = new char[0x7fffffff]; //无法分配这么多空间,会抛出异常
  8. }
  9. catch (bad_alloc & e) {
  10. cerr << e.what() << endl;
  11. }
  12. return 0;
  13. }

程序的输出结果如下:
bad allocation
ios_base::failure

在默认状态下,输入输出流对象不会拋出此异常。如果用流对象的 exceptions 成员函数设置了一些标志位,则在出现打开文件出错、读到输入流的文件尾等情况时会拋出此异常。此处不再赘述。

4) out_of_range

用 vector 或 string 的 at 成员函数根据下标访问元素时,如果下标越界,则会拋出此异常。例如:

  1. #include <iostream>
  2. #include <stdexcept>
  3. #include <vector>
  4. #include <string>
  5. using namespace std;
  6. int main()
  7. {
  8. vector<int> v(10);
  9. try {
  10. v.at(100) = 100; //拋出 out_of_range 异常
  11. }
  12. catch (out_of_range & e) {
  13. cerr << e.what() << endl;
  14. }
  15. string s = "hello";
  16. try {
  17. char c = s.at(100); //拋出 out_of_range 异常
  18. }
  19. catch (out_of_range & e) {
  20. cerr << e.what() << endl;
  21. }
  22. return 0;
  23. }

程序的输出结果如下:
invalid vector <T> subscript
invalid string position

如果将v.at(100)换成v[100],将s.at(100)换成s[100],程序就不会引发异常(但可能导致程序崩溃)。因为 at 成员函数会检测下标越界并拋出异常,而 operator[] 则不会。operator [] 相比 at 的好处就是不用判断下标是否越界,因此执行速度更快。

C++异常处理(try catch throw)完全攻略的更多相关文章

  1. C++异常处理: try,catch,throw,finally的用法

    写在前面 所谓异常处理,即让一个程序运行时遇到自己无法处理的错误时抛出一个异常,希望调用者可以发现处理问题. 异常处理的基本思想是简化程序的错误代码,为程序键壮性提供一个标准检测机制. 也许我们已经使 ...

  2. 从零开始攻略PHP(9)——错误和异常处理

    1.Exception类 这个类是PHP为异常处理提供的内置类.构造函数的两个参数分别是错误消息和错误代码. 除了构造函数之外,该类还提供了如下的内置方法: · getCode() 返回传递给构造函数 ...

  3. [转贴]从零开始学C++之异常(一):C语言错误处理方法、C++异常处理方法(throw, try, catch)简介

    一.C语言错误处理方法 1.返回值(if … else语句判断错误) 2.errno(linux 系统调用) 3.goto语句(函数内局部跳转) 4.setjmp.longjmp(Do not use ...

  4. 从零开始学C++之异常(一):C语言错误处理方法、C++异常处理方法(throw, try, catch)简介

    一.C语言错误处理方法 1.返回值(if … else语句判断错误) 2.errno(linux 系统调用) 3.goto语句(函数内局部跳转) 4.setjmp.longjmp(Do not use ...

  5. Java 异常处理 try catch finally throws throw 的使用和解读(一)

    //最近的一个内部表决系统开发过程中,//发现对异常处理还存在一些模棱两可的地方,//所以想着整理一下//主要涉及到://1.try catch finally throws throw 的使用和解读 ...

  6. C语言错误处理方法、C++异常处理方法(throw, try, catch)简介

    一.C语言错误处理方法 1.返回值(if … else语句判断错误) 2.errno(linux 系统调用) 3.goto语句(函数内局部跳转) 4.setjmp.longjmp(Do not use ...

  7. Android使用XML全攻略(2)

    Android使用XML全攻略(2)   Android 是针对移动设备的一种新兴的开源操作系统和 SDK.借助它,您可以创建功能强大的移动应用程序.当您的应用程序可以访问 Web 服务时,其吸引力会 ...

  8. Android使用XML全攻略(1)

    Android使用XML全攻略(1)    Android 是针对移动设备的一种新兴的开源操作系统和 SDK.借助它,您可以创建功能强大的移动应用程序.当您的应用程序可以访问 Web 服务时,其吸引力 ...

  9. 关于Java高并发编程你需要知道的“升段攻略”

    关于Java高并发编程你需要知道的"升段攻略" 基础 Thread对象调用start()方法包含的步骤 通过jvm告诉操作系统创建Thread 操作系统开辟内存并使用Windows ...

随机推荐

  1. Spark基础:(五)Spark编程进阶

    共享变量 (1)累加器:是用来对信息进行聚合的,同时也是Spark中提供的一种分布式的变量机制,其原理类似于mapreduce,即分布式的改变,然后聚合这些改变.累加器的一个常见用途是在调试时对作业执 ...

  2. JAXB—Java类与XML文件之间转换

    JAXB-Java类与XML文件之间转换 简介         JAXB(Java Architecture for XML Binding) 是一个业界的标准,是一项可以根据XML Schema产生 ...

  3. swift 实现QQ好友列表功能

    最近项目中有类似QQ好友列表功能,整理了一下,话不多说,直接上代码 import UIKit class QQFriend: NSObject { var name: String? var intr ...

  4. Gradle入门及SpringBoot项目构建

    https://blog.csdn.net/qq_27520051/article/details/90384483 一.介绍 Gradle 是一种构建工具,它抛弃了基于XML的构建脚本,取而代之的是 ...

  5. 浅谈iptables与firewalld防火墙

    iptables基于包过滤的防火墙工具 ,Linux 内核集成的 IP 信息包过滤系统,对流入和流出服务器的数据包进行精细管理 规则是存储在专用信息包过滤表中 防火墙按照规则做出判断 而netfilt ...

  6. 【编程思想】【设计模式】【创建模式creational】Borg/Monostate

    Python版 https://github.com/faif/python-patterns/blob/master/creational/borg.py #!/usr/bin/env python ...

  7. ES6(模板字符串,三点运算符,Symbol,iterator接口)

    模板字符串 作用:简化字符串的拼接 模板字符串必须用``包含 变化的部分使用${xxx}包含 对象的简写方式 同名的属性可以省略不写 可以省略函数的function 箭头函数 箭头函数的特点 箭头函数 ...

  8. 学习 27 门编程语言的长处,提升你的 Python 代码水平

    Python猫注:Python 语言诞生 30 年了,如今的发展势头可谓如火如荼,这很大程度上得益于其易学易用的优秀设计,而不可否认的是,Python 从其它语言中偷师了不少.本文作者是一名资深的核心 ...

  9. Hadoop期末复习

    Hadoop期末复习 选择题 以下选项中,哪个程序负责HDFS数据存储. B A.NameNode B.DataNode C.Secondary NameNode D.ResourceManager ...

  10. 使用 Amazon S3 触发器创建缩略图

    使用 Amazon S3 触发器创建缩略图 环境 centos (注意,必须是Linux环境) node12.x 安装教程 curl -sL https://rpm.nodesource.com/se ...