详解C++异常处理(try catch throw)完全攻略

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

  • 做除法的时候除数为 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 块后面继续执行。

例如下面的程序:

#include <iostream>
using namespace std;
int main()
{
  double m ,n;
  cin >> m >> n;
  try {
    cout << "before dividing." << endl;
    if( n == 0)
      throw -1; //抛出int类型异常
    else
      cout << m / n << endl;
    cout << "after dividing." << endl;
  }
  catch(double d) {
    cout << "catch(double) " << d << endl;
  }
  catch(int e) {
    cout << "catch(int) " << e << endl;
  }
  cout << "finished" << endl;
  return 0;
}

程序的运行结果如下:
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 块能够捕获任何还没有被捕获的异常。例如下面的程序:

#include <iostream>
using namespace std;
int main()
{
  double m, n;
  cin >> m >> n;
  try {
    cout << "before dividing." << endl;
    if (n == 0)
      throw - 1; //抛出整型异常
    else if (m == 0)
      throw - 1.0; //拋出 double 型异常
    else
      cout << m / n << endl;
    cout << "after dividing." << endl;
  }
  catch (double d) {
    cout << "catch (double)" << d << endl;
  }
  catch (...) {
    cout << "catch (...)" << endl;
  }
  cout << "finished" << endl;
  return 0;
}

程序的运行结果如下:
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 块捕获并处理,那么该异常就不会拋给这个函数的调用者(也称为“上一层的函数”);如果异常在本函数中没有被处理,则它就会被拋给上一层的函数。例如下面的程序:

#include <iostream>
#include <string>
using namespace std;
class CException
{
public:
  string msg;
  CException(string s) : msg(s) {}
};
double Devide(double x, double y)
{
  if (y == 0)
    throw CException("devided by zero");
  cout << "in Devide" << endl;
  return x / y;
}
int CountTax(int salary)
{
  try {
    if (salary < 0)
      throw - 1;
    cout << "counting tax" << endl;
  }
  catch (int) {
    cout << "salary < 0" << endl;
  }
  cout << "tax counted" << endl;
  return salary * 0.15;
}
int main()
{
  double f = 1.2;
  try {
    CountTax(-1);
    f = Devide(3, 0);
    cout << "end of try block" << endl;
  }
  catch (CException e) {
    cout << e.msg << endl;
  }
  cout << "f = " << f << endl;
  cout << "finished" << endl;
  return 0;
}

程序的输出结果如下:
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 块中拋出异常可以满足这种需要。例如:

#include <iostream>
#include <string>
using namespace std;
int CountTax(int salary)
{
  try {
    if( salary < 0 )
      throw string("zero salary");
    cout << "counting tax" << endl;
  }
  catch (string s ) {
    cout << "CountTax error : " << s << endl;
    throw; //继续抛出捕获的异常
  }
  cout << "tax counted" << endl;
  return salary * 0.15;
}
int main()
{
  double f = 1.2;
  try {
    CountTax(-1);
    cout << "end of try block" << endl;
  }
  catch(string s) {
    cout << s << endl;
  }
  cout << "finished" << endl;
  return 0;
}

程序的输出结果如下:
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 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常。程序示例如下:

#include <iostream>
#include <stdexcept>
using namespace std;
class Base
{
  virtual void func() {}
};
class Derived : public Base
{
public:
  void Print() {}
};
void PrintObj(Base & b)
{
  try {
    Derived & rd = dynamic_cast <Derived &>(b);
    //此转换若不安全,会拋出 bad_cast 异常
    rd.Print();
  }
  catch (bad_cast & e) {
    cerr << e.what() << endl;
  }
}
int main()
{
  Base b;
  PrintObj(b);
  return 0;
}

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

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

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

#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
  try {
    char * p = new char[0x7fffffff]; //无法分配这么多空间,会抛出异常
  }
  catch (bad_alloc & e) {
    cerr << e.what() << endl;
  }
  return 0;
}

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

在默认状态下,输入输出流对象不会拋出此异常。如果用流对象的 exceptions 成员函数设置了一些标志位,则在出现打开文件出错、读到输入流的文件尾等情况时会拋出此异常。此处不再赘述。
4) out_of_range
用 vector 或 string 的 at 成员函数根据下标访问元素时,如果下标越界,则会拋出此异常。例如:

#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
using namespace std;
int main()
{
  vector<int> v(10);
  try {
    v.at(100) = 100; //拋出 out_of_range 异常
  }
  catch (out_of_range & e) {
    cerr << e.what() << endl;
  }
  string s = "hello";
  try {
    char c = s.at(100); //拋出 out_of_range 异常
  }
  catch (out_of_range & e) {
    cerr << e.what() << endl;
  }
  return 0;
}

程序的输出结果如下:
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)完全攻略的文章就介绍到这了,更多相关C++异常内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++异常处理 try,catch,throw,finally的用法

    写在前面 所谓异常处理,即让一个程序运行时遇到自己无法处理的错误时抛出一个异常,希望调用者可以发现处理问题. 异常处理的基本思想是简化程序的错误代码,为程序键壮性提供一个标准检测机制. 也许我们已经使用过异常,但是你习惯使用异常了吗? 现在很多软件都是n*365*24小时运行,软件的健壮性至关重要. 内容导读 本文包括2个大的异常实现概念:C++的标准异常和SEH异常. C++标准异常: 也许你很高兴看到错误之后的Heap/Stack中对象被释放,可是如果没有呢? 又或者试想一下一个能解决的错误

  • C++编程异常处理中try和throw以及catch语句的用法

    若要在 C++ 中实现异常处理,你可以使用 try.throw 和 catch 表达式. 首先,使用 try 块将可能引发异常的一个或多个语句封闭起来. throw 表达式发出信号,异常条件(通常是错误)已在 try 块中发生.你可以使用任何类型的对象作为 throw 表达式的操作数.该对象一般用于传达有关错误的信息.大多数情况下,建议你使用 std::exception 类或标准库中定义的派生类之一.如果其中的类不合适,建议你从 std::exception 派生自己的异常类. 若要处理可能引

  • C++中try throw catch异常处理的用法示例

    前言 今天在开发过程中调用一个库函数结果库函数有throw操作,当前代码没有对throw进行捕获操作,导致进程在main 函数中捕获到异常导致进程crash.所以借此记录下c++关于try,throw,catch的用法. 程序运行时常会碰到一些异常情况,例如: 做除法的时候除数为 0: 用户输入年龄时输入了一个负数: 用 new 运算符动态分配空间时,空间不够导致无法分配: 访问数组元素时,下标越界:打开文件读取时,文件不存在. 这些异常情况,如果不能发现并加以处理,很可能会导致程序崩溃. 所谓

  • 详解C++异常处理(try catch throw)完全攻略

    程序运行时常会碰到一些异常情况,例如: 做除法的时候除数为 0: 用户输入年龄时输入了一个负数: 用 new 运算符动态分配空间时,空间不够导致无法分配: 访问数组元素时,下标越界:打开文件读取时,文件不存在. 这些异常情况,如果不能发现并加以处理,很可能会导致程序崩溃. 所谓"处理",可以是给出错误提示信息,然后让程序沿一条不会出错的路径继续执行:也可能是不得不结束程序,但在结束前做一些必要的工作,如将内存中的数据写入文件.关闭打开的文件.释放动态分配的内存空间等. 一发现异常情况就

  • 详解将Django部署到Centos7全攻略

    Django部署到Cenos7需要安装大量的依赖包, 有很多坑需要踩, 这里是踩坑后探索出的标准化步骤 实验环境: 腾讯云centos7 用centos7.5镜像创建容器(这步操作按自己需求来, 不使用docker的可以跳过这步) docker run -t -i --name fangyuanxiaozhan-com -p 9000:9000 centos:7.5.1804 /bin/bash 创建名为fangyuanxiaozhan-com的容器 将容器的9000端口映射到物理机的9000端

  • 详解C++异常处理机制示例介绍

    目录 1.C++异常处理的套路 2.C++标准异常库中常用的几个异常处理类 2.1异常类的例子 2.2封装一个自定义异常类 1.C++异常处理的套路 C++异常处理机制之抛出异常关键字:throw throw关键字的使用: 在哪可能出现异常就在哪里使用throw关键字抛出异常. 这个异常可以使用一个常量,字符串,或类对象,都可以来抛出. throw + 常量.字符串.数对象 :用表示抛出一个异常. throw抛出的异常将层层向外抛出,至少有try....catch...语句来捕获这个异常并处理这

  • 详解SpringBoot异常处理流程及原理

    异常处理流程 执行目标方法,目标方法运行期间有任何异常都会被catch捕获,并标志当前请求结束,dispatchException抛出异常 进入视图解析流程,并渲染页面,发生异常时,参数mv为空,传入捕获的异常dispatchException 处理handler发生的异常,处理完成返回ModelAndView (1)遍历所有的HandlerExceptionResolvers,找到可以处理当前异常的解析器来解析异常 (2)调用resolveException解析异常,传入request和res

  • 详解Java异常处理最佳实践及陷阱防范

    前言 不管在我们的工作还是生活中,总会出现各种"错误",各种突发的"异常".无论我们做了多少准备,多少测试,这些异常总会在某个时间点出现,如果处理不当或是不及时,往往还会导致其他新的问题出现.所以我们要时刻注意这些陷阱以及需要一套"最佳实践"来建立起一个完善的异常处理机制. 异常分类 首先,这里我画了一个异常分类的结构图. 在JDK中,Throwable是所有异常的父类,其下分为"Error"和"Exception&

  • 详解Java异常处理中throw与throws关键字的用法区别

    抛出异常 抛出异常有三种形式,一是throw,一个throws,还有一种系统自动抛异常.下面它们之间的异同. 系统自动抛异常 当程序语句出现一些逻辑错误.主义错误或类型转换错误时,系统会自动抛出异常.如: public static void main(String[] args) { int a = 5, b =0; System.out.println(5/b); //function(); } 系统会自动抛出ArithmeticException异常: Exception in threa

  • 详解Python异常处理中的Finally else的功能

    Python使用Try Exception来处理异常机制 若Exception中有Try对应的异常处理,则Try - exception之后的代码将被执行,但若Try - exception中没有对应的代码,则程序抛出Traceback停止运行 那么else finally就是针对这两种情况带来的后果分别相应的关键字 else 如果一个Try - exception中,没有发生异常,即exception没有执行,那么将会执行else语句的内容 反之,如果触发了Try - exception(异常

  • Python 错误和异常代码详解

    程序中的错误一般被称为 Bug,无可否认,这几乎总是程序员的错... 程序员的一生,始终伴随着一件事 - 调试(错误检测.异常处理).反反复复,最可怕的是:不仅自己的要改,别人的也要改...一万头草泥马奔腾而过! 错误 程序错误,主要分为三类: 语法错误 逻辑错误 运行时错误 语法错误 语法错误(也称:解析错误):是指不遵循语言的语法结构引起的错误(程序无法正常编译/运行). 在编译语言(例如:C++)中,语法错误只在编译期出现,编译器要求所有的语法都正确,才能正常编译.不过对于直译语言(例如:

  • java异常处理执行顺序详解try catch finally

    目录 不含return的执行顺序 finally子句 含return的执行顺序 返回类型是对象类型时值的变化 结论 不含return的执行顺序 执行顺序为执行try中代码,如果没有异常,然后执行try catch后续的代码.如: public static void main(String[] args) { try { int j = 10 / 2; System.out.println("执行try中代码!"); } catch (Exception e) { e.printSta

随机推荐