第9单元 异常处理
文章目录
- 第9单元 异常处理
- 单元导读
- 9.1 异常处理概览
- 9.1.1 异常处理概览
- 9.1.2 异常处理机制的优点
- 9.2 异常匹配与内建异常类
- 9.2.1 异常匹配与异常类
- 9.2.2 内建异常类
- 9.3 自定义异常类与多重捕获
- 9.3.1 自定义异常类
- 9.3.2 捕获多种无关异常
- 9.3.3 捕获派生异常
- 9.4 [C++11]noexcept与异常传播
- 9.4.1 C++11的noexcept
- 9.4.2 异常传播
- 9.5 重抛异常与异常的使用场景
- 9.5.1 重抛异常
- 9.5.2 何时应该使用异常?
- 9.5.3 变量/对象命名的2个编码规范
注:本部分内容主要来自中国大学MOOC北京邮电大学崔毅东的 《C++程序设计》课程。
注:94条 C++程序规范。
单元导读
本单元介绍异常的用法。异常是C++区别于C语言的一个重要特征。一些其它语言中的异常机制,都是借鉴C++的异常语法和处理结构的。比如,Java使用 try...throw(exception)...catch(exception)...finally
结构,Python使用 try...raise(exception)...except(exception)...else...finally
结构。掌握C++的异常处理方法,主要有以下几点:
- 理解“踹扔抓”三部曲的结构,尤其是
catch
是怎么匹配异常的。- 知道C++标准库中的异常类都是从
exception
继承下来的,并且支持what()
这个操作。- 了解l
ogic_error
、runtime_error
等一些常见的异常类的大致含义,以便于我们自己写异常类的时候拿来继承。- 理解多个
catch
语句的匹配原则,尤其是对于继承链上的异常类型,哪个在前哪个在后必须做到心里有数。- 掌握异常传播的原理:异常如果在当前函数中没有被捕获,它就会被抛向当前函数的调用者;抛异常的语句后面的语句都会被跳过,直到遇到对应的
catch
。
抓住上面所列举的5个要点,你再写几个异常处理的程序,就基本能够掌握异常的用法了。
9.1 异常处理概览
9.1.1 异常处理概览
首先来说明异常处理的必要性。使用“两数求商”作为例子,显然除数不能为0。若只是简单的直接读取两个数字,然后直接输出其商,那么就有可能会出现“运行时错误”。如下面的代码:
#include <iostream>
using namespace std;
int main() {
//读取两个数字
cout << "请输入两个数字:";
int number1, number2;
cin >> number1 >> number2;
//直接输出商
cout << number1 << " / " << number2 << " is ";
cout << (number1 / number2) << endl;//除数为0会导致程序崩溃!
return 0;
}
此时为了保证用户的输入不会导致被0除的错误,有下面两种处理方法:
- 使用
if
语句:#include<iostream> int main() { //输入两个数 int x{ 0 }, y{ 0 }; std::cin >> x >> y; //求x/y if (y == 0) { std::cout << "y is 0, quit." << std::endl; std::exit(0); }else{ std::cout << "x/y = " << (x/y); } }
- 使用
try-throw-catch
异常处理:#include<iostream> int main() { //输入两个数 int x{ 0 }, y{ 0 }; std::cin >> x >> y; //求x/y try { if (y == 0) { throw y; } std::cout << "x/y = " << (x/y); } catch(int& e){ cout << "the second number is: " << e << endl; } }
可以看到,上面的异常处理中使用了try-throw-catch
,核心思想是 “试试 扔点啥,看能否抓住”,下面是“踹扔抓”的代码块示例:
try {
//1. 被try块保护的语句
//...
//2. throw异常
throw an exception
//方法一:直接定义throw语句
//方法二:调用的函数中包含throw语句
//3. 若没有异常执行后续操作
//...
}
catch (type e) {
//处理异常
//注意只能捕获特定type e的异常
}
9.1.2 异常处理机制的优点
从上一小节两种处理异常的代码来看,异常处理三部曲“踹扔抓”显得比if...else...
更麻烦,这只是因为上面的例子太简单,要牢记“简单错误简单办,复杂错误异常办!”。那复杂错误为啥一定要使用“异常”处理呢?关键的一点在于异常处理机制可将异常信息从被调函数带回给主调函数,异常处理实际上是对程序执行流程的控制。下面对上一小节的代码进行改编:
//两数相除函数
int quotient(int number1,
int number2) {
if (number2 == 0)
throw number1;
return number1 / number2;
}
//主函数
int main() {
try {
int x = quotient(1, 0);
} catch (int) {
std::cout << "除数为0!";
}
}
上述代码将“两数相除”写成一个单独的函数quotient()
,并在主函数main()
中调用以完成功能。那若不用异常处理,quotient()
如何告诉main()
除数为0?
- 用返回值:显然不科学,因为返回值也可以是正常返回的结果。
- 用指针/引用类型的参数:如下面的代码,加一个参数用于指示是否有错误,但是非常不方便!进一步若采用嵌套结构
f(g(h(quotient(x,y))));
,使用这种方法将错误从quotient()
传到f()
就会非常麻烦!int quotient(int n1, int n2, int &s){ if(n2 == 0) s=-1; //求商,函数要3个参数? }
显然,这种情况下使用异常处理直接
throw
异常,就可以直接跳出最外层的try
块,显得非常方便、优雅!
9.2 异常匹配与内建异常类
9.2.1 异常匹配与异常类
本小节来介绍异常处理机制的实现原理——异常类型匹配,也就是catch块
。异常类型匹配的意思就是,若try{}
中所抛异常类型与catch()
的参数类型(ExceptionType)匹配,则进入catch
块。若对异常对象的内容不感兴趣,可省略catch
参数,只保留类型。下面给出catch
块的两种格式,以及两个例子:
/***********异常类型匹配格式***********/
catch(ExceptionType& parameter) {/*处理异常*/} //使用引用类型参数,也是为了提高程序效率
catch(ExceptionType&) {/*处理异常*/} //对参数不感兴趣时可以只保留类型
/***********示例1***********/
void f1() { throw 100;} //直接抛出int型异常
//对异常的值不感兴趣
try { f1(); }
catch (int) { //仅有类型
cout << "Error" << endl;
}
//对异常的值感兴趣
try { f1(); }
catch (int& e) { //类型+参数
cout << e << endl;
}
/***********示例2***********/
void f2() { for (int i = 1; i <= 100; i++) new int[70000000];} //申请28G内存
//bad_alloc是C++所有异常类的基类exception的派生
//对异常的值不感兴趣
try { f2(); }
catch (bad_alloc) {
cout << "new failed" << endl;
}
//对异常的值感兴趣
try { f2(); }
catch (bad_alloc &e) {
cout << "Exception: " << e.what() << endl; //输出异常对象的信息
//what()是exception类中的虚函数,是一个char*指针,指向一个字符串
}
通过上面两个例子可以看出,既可以使用“整数”作异常,也可以使用“类”作异常。那为什么要使用“类”作异常呢?直接把异常抛出来不就好了?这是因为:
- 使用“整数”作异常,能传递的信息量很少。
- 使用“类”作异常(异常类),则可以在类中定义很多信息,并且在捕获异常时接收这些信息。
9.2.2 内建异常类
本小节来介绍C++标准库中内建的异常类。 exception
是C++标准库中所有异常类的基类,要使用C++异常类时,需要包含头文件#include<exception>
。对于exception
类来说,包括:
- 无参构造函数:
exception();
。- 虚函数:
virtual const char* what();
, 返回字符指针char*
,指向一个解释性字符串。what()
返回的指针指向拥有解释信息的空终止字符串的指针。该指针保证在获取它的异常对象被销毁前,或在调用该异常对象的非静态成员函数前合法。
上图给出了C++标准库中的异常类,主要分为以下几类:
- 逻辑错误
logic_error
:domain_error
基本用不上,其他可以看看。- 运行时错误
runtime_error
:注意到overflow_error
、underflow_error
与C++数学标准库无关。bad_typeid
:交给typeid()
的参数为零或空指针时抛出,比如对0进行解引用typeid(*0)
。bad_cast
:当进行dynamic_cast
不成功时,就会抛出此异常。bad_alloc
:使用new
申请内存失败时会抛出此异常。
……下面注意:
- 使用任意一个异常类都需要添加
<exception>
头文件。- 使用
logic_error
和runtime_error
还需要包含头文件<stdexcept>
。- 使用
bad_typeid
、bad_cast
还需要包含头文件<type_info>
。- 使用
bad_alloc
还需要包含头文件<new>
。- 在使用所有标准库异常类的时候,都必须附加std名字空间。比如在使用
bad_alloc
异常类时,代码为:std::bad_alloc ex{"Out of memory"}; throw ex;
下面使用代码分别展示三种异常:out_of_range
、bad_alloc
、bad_cast
。首先是out_of_range
异常,注意选择Reslese
模式,而不是Debug
模式;另外,只有使用v.at()
才会抛出异常,具体原因可以查看cppreference的 std::vector::operator 说明:
源文件 out_of_range.cpp:
#include<iostream>
#include<vector>
using std::cout;
using std::endl;
int main() {
std::vector<char> v{ 'a','b','c','d','e' };//v.size()=5
//输出vector内容
try {
for (int i = 0; i < 6; i++) {
cout << v[i]; //不会做边界检查
cout << v.at(i) << endl;//会做边界检查
}
}
catch (std::out_of_range& e) {
cout << "异常:" << e.what() << endl;
}
}
输出结果:
aa
bb
cc
dd
ee
s异常:invalid vector subscript
下面使用代码展示内存分配失败时的bad_alloc
异常,这个异常可以随便选择模式Reslese
或者Debug
:
源文件 bad_alloc.cpp:
#include<iostream>
#include<exception>
#include<new>
using std::cout;
using std::endl;
int main() {
try {
for (int i = 0; i < 1000; i++) {
auto *p = new long long int[1000000];//8字节*10e6=8MB
cout << i << " array" << endl;
}
}
catch (std::bad_alloc& e) {
cout << "异常:" << e.what() << endl;
}
}
输出结果:
//前面依次有“0 array”到“255 array”
256 array
257 array
258 array
259 array
异常:bad allocation
下面使用代码展示侧向转换失败异常bad_cast
,这个也是随便选择模式:
源文件 bad_cast.cpp:
#include<iostream>
#include<exception>
#include<stdexcept>
using std::cout;
using std::endl;
//创建基类Student
class Student {
public:
Student() = default;//默认构造函数
virtual void foo() {};//添加虚函数以形成多态,才能进行动态转换
};
//创建派生类Undergraduate、Graduate
class Undergraduate:public Student{};
class Graduate:public Student{};
int main() {
Undergraduate u;
Graduate g;
Student *s1 = &u;//将Undergraduate转换成基类指针
Student *s2 = &g;//将Graduate转换成基类指针
//1. 先测试一下动态转换可以正常使用
Graduate *p = dynamic_cast<Graduate *>(s2);
long x = reinterpret_cast<long>(p);
cout << x << endl;//x非空就说明转换成功
//2. 侧向转换
//指针类型转换
Graduate *p2 = dynamic_cast<Graduate *>(s1);
if (p2 == nullptr) {//转换失败就是空指针
cout << "cast s1 to Graduate* failed" << endl;
}
else {
cout << "cast s1 to Graduate* succeeded" << endl;
}
//引用类型转换。只有引用转换异常时,才会抛出异常:bad_cast
try {
Graduate &r1 = dynamic_cast<Graduate &>(u);
}
catch (std::bad_cast &e) {
cout << "异常:" << e.what() << endl;
}
}
输出结果:
9435020
cast s1 to Graduate* failed
异常:Bad dynamic_cast!
9.3 自定义异常类与多重捕获
9.3.1 自定义异常类
有时候C++内建异常类可能不能满足需求,这时候就需要我们来自定义异常类。这个自定义的异常类与其它C++ class是类似的,并且通常由exception
或其后代类派生,而不是重新写一个,这样做的好处是可以使用exception
类的公共特征,比如使用what()
函数来查看异常信息。但注意,自定义异常类的原则 是:
- 自定义异常类应该优先考虑继承标准库中已有的异常类。
- 仅当预定义的异常类无法充分描述所遇到的问题时,才考虑自定义异常类。
下面以三维向量类Vec3D为例,介绍如何自定义一个向量索引越界的异常类。首先根据Vec3D的类图(图9-2),来介绍三维向量类Vec3D:
DIMENSION
:向量维度,本例为3。vec
:是array
数组,存放向量元素,注意维度是DIMENSION
。operator[]
:通过数组下标形式读取或者修改向量的元素。需检查下标是否属于区域[0, DIMENSION-1]
,如果越界,抛出异常。Vec3D()
、Vec3D(double x, double y, double z)
:无参/有参构造函数。
那operator[]
如何抛出向量索引越界的异常呢?思路如下图9-3:
- 继承自
exception
:因为所有的异常类都最好继承自exception
。- 想清楚索引越界异常的核心参数:数组越界只要说明当前向量范围,以及越界的索引即可。
- 自定义异常类:在自定义异常类中声明核心参数。
- 初始化基类:根据6.1.2节所学,派生类构造函数必须调用基类构造函数。
exception
作为所有异常类的基类,在构造时不接受参数,但是大多数异常类的派生类都会接受一个字符串类型参数,用于大致描述异常信息。于是就直接使用"Vec index error"
字符串来初始化out of range
对象。根据以上四步,基本上就可以比较完整的构造出自定义异常类了。
最后使用代码展示一下如何自定义异常类,有如下四项任务:
任务1:创建Vec3D类,用array保存向量成员。这是在平面向量类Vec2D的基础上扩展了一维,以后以此为基础可以创建真正的向量类。
任务2:创建RangeException类,定义构造函数 RangeException(std::size_ .t dimension, const int index)。
任务3:实现Vec3D::operator[](const int index),当index越界时,抛出VecException的对象。
任务4:在主函数中创建Vec3D对象并调用[ ]制造越界问题,捕获异常并输出异常中的信息。
头文件 Vec3D.h:
#pragma once
#include<array>
#include"RangeExpection.h"
//任务1:创建Vec3D类,用array保存向量成员。
//任务3:实现Vec3D::operator[](const int index),当index越界时,
// 抛出RangeException的对象。
class Vec3D {
private:
std::array<double ,3> v{ 1.0,1.0,1.0 };
public:
Vec3D() = default;
Vec3D(double x, double y, double z) {
v[0] = x;
v[1] = y;
v[2] = z;
}
//重载数组下标运算符,并判断是否越界
double& operator[](const int index) {
if (index >= 0 && index <= 2) {
return v[index];
}
else {
throw RangeException(3, index);//匿名对象
}
}
};
头文件 RangeException.h:
#pragma once
//任务2:创建RangeException类,定义构造函数
// RangeException(std::size_t dimension,const int index)
#include<iostream>//维度类型size_t
#include<exception>
class RangeException :public std::exception{
private:
std::size_t dimension{ 3 };//注意维度大小的类型是std::size_t
int index{ 0 };
public:
RangeException(std::size_t dimension, const int index) {
this->dimension = dimension;
this->index = index;
}
//注意下面一行加上const throw()就可以直接输出信息了
const char* what() const throw(){
return "Vec index error";
}
std::size_t getDimension() {
return dimension;
}
int getIndex() {
return index;
}
};
源文件 CustomEccept.cpp:
#include<iostream>
#include"Vec3D.h"
#include<exception>
using std::cout;
using std::endl;
//任务4:在主函数中创建Vec3D对象并调用[]制造越界问题
// 捕获异常并输出异常的信息。
int main() {
Vec3D v1{ 1.2,2.3,3.4 };
try {
cout << v1[4];//索引越界
}
catch (std::exception & e) {
//提示发生越界
cout << "Exception: " << e.what() << endl;
//输出详细的越界参数
if (typeid(e) == typeid(RangeException)) {
auto r = dynamic_cast<RangeException &>(e);
cout << "Vector dimension: " << r.getDimension() << endl;
cout << "Index: " << r.getIndex() << endl;
}
}
}
运行结果:
Exception: Vec index error
Vector dimension: 3
Index: 4
9.3.2 捕获多种无关异常
本小节非常简单,就是介绍一下当try
块中包含多个异常时,程序如何处理。显然根据一般逻辑,try
块会直接抛出遇到的第一个异常;而一个catch
块只能捕获一种异常,所以只需要在try
块后面定义多个catch
块,就可以处理所有希望处理的异常情况。
try {
throw excep1();//抛出异常excep1(),直接跳出try块
throw excep2();
throw excep3();
}
//下面三个catch块依次检测,并处理抛出的异常
catch (excep1& e) {
cout << e.what() << endl;
}
catch (excep2& e) {
cout << e.what() << endl;
}
catch (excep3& e) {
cout << e.what() << endl;
}
9.3.3 捕获派生异常
本小节介绍如何捕获派生异常。所谓“派生异常”就是,9.3.1小节介绍自定义异常时提到,自定义异常应该继承自一个基类异常,也就是说此时的自定义异常相当于一个派生异常。那当派生异常发生时,该如何进行catch
呢?答案是先catch
派生异常,后catch
基类异常。这是因为C++语法中,catch
在进行匹配时,不仅能捕获派生类对象,也能捕获基类对象。
也就是说,try
块后跟多个catch
块,并且捕获的异常类是继承链上的类,那么应该将派生类异常写在前面的catch
块中,基类异常写在后面的catch
块中。比如 MyException
异常类 是 logic_error
异常类 的派生,即class MyException:public logic_error{};
,那么如果catch
参数类型为基类异常类型“catch(logic_error)
”,则catch
块的书写顺序应该为:
/********不推荐的派生异常捕获********/
try {
throw MyException(); // 抛出派生异常对象
} catch (logic_error& e) { // catch参数为基类异常,但可以捕获所有派生类异常对象
MyException* p = dynamic_cast<MyException*>(&e); // 转指针失败不会再抛异常
if (p != nullptr)
cout << p->what() << endl;
else
cout << e.what() << endl;
}
//dynamic_cast<NewType>(obj)
//1.若转型失败且NewType是指针类型,则返回nullptr。
//2.若转型失败且NewType是引用类型,则抛出std::bad_cast类型的异常。
//综上,为了不增添新的异常,代码中使用指针类型转换。
/********推荐的派生异常捕获********/
try {
throw MyException(); // 抛出派生异常对象
} catch (MyException& e) {
cout << e.what() << endl;
} catch (logic_error& e) {
cout << e.what() << endl;
}
下面进行代码展示如何捕获多个不同类型的异常,上图9-4给出了代码中的异常继承链,首先来看代码任务:
1.基于Vec3D类、RangeException异常类修改。为了将Vec3D推广成一般向量做准备。
1.1将Vec3D的维数抽取出来,变成一个独立的常量,使可通过类的名字来引用。
1.2将RangeException改为继承out_of_range。
2.添加ZeroException,当向量除以一个数为0时抛该异常,该异常继承自runtime_error。
3.重载除法运算符operator/(),为Vec3D类添加标量除法(向量除以一个数),当除数为0.0时抛异常。注意,当比较除数divisor是否为0.0时,需要使用一些特殊技巧:
divisor的绝对值减去0.0是否小于std::numeric_ limits<doub1e>::epsilon()
IEEE 754 Rules:
X>0.0 : x/0.0 = INF
X<0.0 : X/0.0 = -INF
0.0/0.0 = NaN(Not a Number)
下面是判断除数divisor是否为0.0的参考代码:
//-->https://stackoverflow.com/a/37686/3242645
#include <cmath>
#include <limits>
bool AreSame(double a, double b) {
return std::fabs(a - b) < std::numeric_limits<double>::epsilon();
}
头文件RangeException.h:
#pragma once
#include<exception>
#include<stdexcept>
//任务1.2:将RangeException改为继承out_of_range。
class RangeException :public std::out_of_range {
private:
std::size_t dimension{ 0 };
int index{ 0 };
public:
//定义构造函数
RangeException(std::size_t dimension, int index)
: out_of_range("index exceed Vector dimension."){
this->dimension = dimension;
this->index = index;
}
std::size_t getDimension() {
return dimension;
}
int getIndex() {
return index;
}
};
头文件ZeroException.h:
#pragma once
#include<exception>
#include<stdexcept>
//任务2:添加ZeroException,当向量除以一个数为0时抛该异常,
// 该异常继承自runtime_error。
class ZeroException :public std::runtime_error {
public:
ZeroException() : runtime_error("Divided by 0.0"){}//无参构造函数
ZeroException(const char* msg):runtime_error(msg){}//有参构造函数
};
头文件Vec3D.h:
#pragma once
#include<array>
#include<cmath>//判断相等要用
#include<limits>//判断相等要用
#include"RangeException.h"//索引越界异常
#include"ZeroException.h"//0除异常
//任务1.1:将Vec3D的维数抽取出来,变成一个独立的常量。
//任务3:重载除法运算符operator/(),为Vec3D类添加标量除法
// (向量除以一个数),当除数为0.0时抛异常。
class Vec3D {
public:
constexpr static std::size_t DIMENSION = 3;//编译器静态常量,不能修改
private:
std::array<double, DIMENSION> vec{1.0, 1.0, 1.0};
bool isSame(double a, double b) {
return std::fabs(a - b) < std::numeric_limits<double>::epsilon();
}
public:
Vec3D() = default;
Vec3D(double x, double y, double z) {
vec[0] = x;
vec[1] = y;
vec[2] = z;
}
double &operator[](const int index) {
if (index >= 0 && index <DIMENSION) {
return vec[index];
}
else {
throw RangeException(DIMENSION, index); //索引越界异常
}
}
//重载除法运算符—标量除法
Vec3D operator /(const double divisor) {
Vec3D t(*this);//注意是返回了一个新的Vec3D对象
if (isSame(divisor, 0.0))
{
throw ZeroException(); //0除异常
}
else {
for (auto &i : t.vec) {
i /= divisor;
}
return t;
}
}
};
源文件MultipleCatch1.cpp:
#include<iostream>
#include<exception>
#include"Vec3D.h"
#include"RangeException.h"
using std::cout;
using std::endl;
int main() {
Vec3D v1{1.2, 2.3, 3.4};
try {
//cout << v1[3] << endl;//索引越界
cout << (v1/0.0)[0] << endl;//0除错误
}
//catch (std::exception& e) { //注意要先catch派生类错误
catch (RangeException & e) {
cout << "Exception: " << e.what() << endl;
cout << "Vector dimension: " << e.getDimension() << endl;
cout << "Index: " << e.getIndex() << endl;
}
catch (ZeroException & e) {
std::cout << "Exception: " << e.what() << std::endl;
}
return 0;
}
运行结果:
Exception: Divided by 0.0
9.4 [C++11]noexcept与异常传播
9.4.1 C++11的noexcept
本小姐介绍C++中的 noexcept
关键字,其放在函数声明中,作用是告诉编译器该函数不会抛出异常,可以做编译优化。这种指明函数是否会抛出异常的代码,最早起源于C++03将throw(ExceptionType)
放到函数声明中,说明函数会抛出什么类型的异常,也被称为“异常规约”;java也使用throws
关键字做同样的事情。但其实C++中基本没人用“异常规约” ,所以C++11剔除“异常规约”并使用noexcept
指明函数是否抛出异常,其作用和代码展示如下:
只关心“不抛异常”:
- 若函数不抛异常,则可做编译优化。
- 即便函数抛异常,也无需说明所抛异常类型(简化)。
下面展示
noexcept
声明符的用法://语法:noexcept或者noexcept(布尔表达式) //注意这个“布尔表达式”也可以是数字或逻辑判断,noexcept()会自动将其转换成布尔值! void foo() noexcept {} //正常形式 void foo() noexcept(true) {} //noexcept(true)等价于noexcept void foo() {} //可能会抛出异常 void foo() noexcept(false) {} //noexcept(false)等价于什么也不写,还是可能会抛出异常
那这个时候自然而然就会想到,如果我在函数声明中写了noexcept
说明不抛异常,但是我偏偏在函数中抛一个异常(有时候不是故意的,如下代码),那会怎么样呢?此时抛异常就相等于调用std::terminate()
,程序会终止运行:
void f() { /* 潜在抛出异常 */ }
void g() noexcept {
f(); // 合法,f()抛出异常就相当于调用std::terminate
throw 42; // 合法,抛出异常就等效于调用std::terminate,程序终止
}
要注意的是,函数名后面的noexcept
不能用于区分重载函数(如上图9-5),与之形成对比的是,函数名后面的const
可区分重载函数。
最后要注意的是,noexcept
还可以作为运算符使用:bool noexcept (expression)
。其作用是noexcept
运算符进行编译时检查,若表达式声明为不抛出任何异常则返回true
。
void may_throw();
void no_throw() noexcept;
int main() {
std::cout << std::boolalpha //使得布尔值按照字符输出
<< "Is may_throw() noexcept? "
<< noexcept (may_throw()) << '\n' //输出false
<< "Is no_throw() noexcept? "
<< noexcept (no_throw()) << '\n'; //输出true
}
下面使用代码展示noexcept
的用法:
源文件noexcept.cpp:
#include<iostream>
using std::cout;
using std::endl;
/*
任务1:编写函数,展示作为声明符的noexcept,noexcept(true),noexcept(false)的区别。
任务2:展示noexcept不能区分重载。
任务3:在noexcept函数中抛异常。
任务4:展示noexcept作为运算符的效果。
*/
void foo() noexcept { throw 1; }
//void foo(){}//不能区分重载
void tao() noexcept(1){} //noexcept()自动将1转换成true
void zen() noexcept(1-1){} //noexcept()自动将1-1转换成false
int main() {
cout << "foo() noexcept : " << noexcept(foo()) << "\n"
//<< "foo() : " << noexcept(foo()) << "\n"
<< "tao() noexcept(1) : " << noexcept(tao()) << "\n"
<< "zen() noexcept(1-1): " << noexcept(zen()) << "\n";
cout << "calling foo() noexcept {throw 1;}: ";
foo();
std::cin.get();
return 0;
}
输出结果:
foo() noexcept : 1
tao() noexcept(1) : 1
zen() noexcept(1-1): 0
calling foo() noexcept {throw 1;}:
//并弹出程序终止窗口,如下图9-6
9.4.2 异常传播
既然写程序时函数可以嵌套,那么try-throw
嵌套也时有发生,此时“异常”是如何传播、编译器如何处理的呢?这就是本节要介绍的“异常传播”。比如下图所示的函数嵌套,每个函数中都有 try-catch
块,此时最内层函数function3
抛出异常:
于是,function2
中try
块后面的代码都被跳过,直接寻找类型匹配的catch
块;若function2
中的catch
块都不匹配,那么就会将该异常返回给function1
,再寻找类型匹配的catch
块,不断回跳……;若寻找到匹配的catch
块,则正常执行代码,若直到主函数的catch
块都寻找完也不匹配,则程序崩溃。举个实际点的例子,若function3
抛出:
- 异常
Exception3
:执行Process ex3
->statement6
->statement3
->statement4
->statement1
->statement2
。- 异常
Exception2
:执行Process ex2
->statement4
->statement1
->statement2
。- 异常
Exception1
:执行Process ex1
->statement2
。- 异常
Exception0
:程序崩溃。
所以异常传播中的规则可以总结如下:
try
块中的异常:抛异常的语句后的块代码都被跳过,并开始找异常处理的代码。- 找异常处理的流程:沿函数调用的链反向寻找。按
catch
块的顺序,若找到则执行相应catch
块代码;若找不到,则退出当前函数,将异常传给调用当前函数的函数。
下面使用代码展示异常传播的过程,整个代码逻辑类似于上图9-7:
源文件Propagate.cpp
#include<iostream>
using std::cout;
using std::endl;
//任务1:编写异常传播的演示代码。定义3个异常类、3个函数(2个包含try-catch)。
//任务2:“单步调试”展示异常传播的流程。VS->“调试窗口”->“调用堆栈”,观察调用时堆栈变化。
class Exception1 {};
class Exception2 {};
class Exception3 {};
void f1();
void f2();
void f3();
int main() {
try {
f1();
cout << "main\n";
}
catch (const std::exception &e) {
cout << "catch f1()\n";
}
std::cin.get();
return 0;
}
void f1() {
try {
f2();
cout << "f1\n";
}
catch (const Exception1 &e) {
cout << "catch f2()\n";
}
}
void f2() {
try {
f3();
cout << "f2\n";
}
catch (const Exception2 &e) {
cout << "catch f3()\n";
}
}void f3() {
//throw Exception3();
throw Exception2();
//throw Exception1();
}
运行结果:
/****抛Exception3****/
//异常被操作系统截获
/****抛Exception2****/
catch f3()
f1
main
/****抛Exception1****/
catch f2()
main
最后需要强调一下,虽然C++引入的异常处理会为程序带来额外开销和代价,但是这种开销是必要的。用户编写的代码、目标文件(object files)、连接的程序库等任何一环使用到异常处理,其它部分必须也支持异常。否则在运行时程序就不可能提供正确的异常处理。
9.5 重抛异常与异常的使用场景
9.5.1 重抛异常
前面一直在介绍try
块中throw
异常,catch
块捕获并处理异常,那如果在catch
块中再throw
异常呢?这就是“重抛出异常”。当然正常情况下肯定不能这么干,只有当catch
无法处理其捕获到的异常、或想通知它的调用者发生了一个异常时,才可以“重抛出异常”。当然,这个重抛出的异常可以和捕获的异常类型不同。下面展示重抛异常的场景:
//定义派生类异常
class MyException: public logic_error {};
try{
try {
throw logic_error(); //抛出基类异常对象
}
//catch参数为基类异常,但可以捕获所有派生类异常对象
catch (logic_error& e) {
//MyException* p = dynamic_cast<MyException*>(&e); //转指针失败不会再抛异常
MyException& p = dynamic_cast< MyException&>(e); //引用转换失败会抛std::bad_cast异常
cout << p.what() << endl; //上面抛异常,本语句被跳过
}
}
catch(std::bad_cast& e){
//处理重抛异常
}
下面是展示重抛异常的代码示例:
源文件RethroeException.cpp:
#include<iostream>
#include<exception>
#include<stdexcept>
using std::cout;
using std::endl;
//展示在catch中用throw重抛异常的效果
void f();
int main() {
try {
f();//此函数中就包含一个try-throw块,但重抛异常
}
catch(const std::exception &e){
cout << "catched in main(): " << e.what() << endl;
}
}
void f() {
try {
throw std::logic_error("Throw in f()");
}catch(const std::exception & e){
cout << "catched in f(): " << e.what() << endl;
throw;//将捕获的异常重新抛出
}
}
运行结果:
catched in f(): Throw in f()
catched in main(): Throw in f()
9.5.2 何时应该使用异常?
作为本章的最后一节,本小节来讨论一下“何时使用异常机制”以及“何时不使用异常”。但实际上这种问题在各个论坛上争论不断,但老师结合“stack overflow”和“C++标准化组织”的FAQ,提取出了一些共同点:
何时使用异常:
- 当一个外部问题阻止你的程序运行时,抛异常。比如从服务器接收到非法数据、磁盘满了、宇宙射线阻止你查询数据库……
- 如果函数无法完成它所告知的功能并建立其正常的后置状态,抛异常。比如构造函数失败:vector的构造函数应创建一个对象,但对象占内存太大导致无法构建,那么应该抛异常。
何时不使用异常:
- 只发生在单独函数中的简单错误不要用异常处理。
- 不要用异常处理代码逻辑错误,可以用
assert()
中断程序执行然后调试。- 不要用异常来控制程序流程,比如不要用
throw
来结束循环,而应该使用if-else
、switch-case
、for
、do
、while
等。- 实时系统中不用异常,比如航天飞机控制程序、生命维持系统等。
更多资料:
- stack overflow文章:When and how should I use exception handling?
- C++标准委员会:Exceptions and Error Handling
关键:使用异常处理机制要适度,不可不用、不可多用。
9.5.3 变量/对象命名的2个编码规范
23:表示对象数量的变量名应加前缀n
。例如:nPoints
、nLines
。
24:代表实体编号的变量应加后缀No
,例如tableNo
、employeeNo
。另外一种比较优雅的方法是加上前缀i
,例如iTable
、iEmployee
,这样让他们的名字变成一个迭代器。