★ 熟悉C++中异常的处理方式
★ 理解栈解旋机制
★ 了解C++标准异常
★ 了解静态断言的作用
读者在编写程序的过程中,难免会出现一些错误,例如,除零错误、指针访问受保护空间、数组越界、访问的文件不存在等,这些错误会导致程序运行失败,像这些导致程序运行失败的错误,通常称为异常。为了确保程序的高容错性,开发者在编写程序过程中,需要对这些异常进行处理,防止系统崩溃。大多数常见的编程语言都提供了异常处理机制,C++也不例外,本章将针对C++的异常处理机制进行详细讲解。
在C++中,如果函数在调用时发生异常,异常通常会被传递给函数的调用者进行处理,而不在发生异常的函数内部处理。如果函数调用者也不能处理异常,则异常会继续向上一层调用者传递,直到异常被处理为止。如果最终异常没有被处理,则C++运行系统就会捕捉异常,终止程序运行。
C++的异常处理机制使得异常的引发和处理不必在同一函数中完成,函数的调用者可以在适当的位置对函数抛出的异常进行处理。这样,底层的函数可以着重解决具体的业务问题,而不必考虑对异常的处理。
C++的异常处理通过throw关键字和try…catch语句结构实现。通常情况下,被调用的函数如果发生异常,就通过throw关键字抛出异常,而函数的上层调用者通过try…catch语句检测、捕获异常,并对异常进行处理。
throw关键字抛出异常的语法格式如下所示:
throw 表达式;
在上述格式中,throw后面的表达式可以是常量、变量或对象。如果函数调用中出现异常,就可以通过throw将表示异常的表达式抛给它的调用者。
函数调用者通过try…catch语句捕获、处理异常,try…catch语句的语法格式如下所示:
try
{
… //可能会出现异常的代码
}
catch (异常类型1)
{
… //异常处理代码
}
catch (异常类型2)
{
… //异常处理代码
}
…
catch (异常类型n)
{
… //异常处理代码
}
catch (...)
{
… //异常处理代码
}
在上述语法格式中,try语句块用于检测可能发生异常的代码(函数调用),如果这段代码抛出了异常,则catch语句会依次对抛出的异常进行类型匹配,如果某个catch语句中的异常类型与抛出的异常类型相同,则该catch语句就捕获异常并对异常进行处理。
在使用try…catch语句时,有以下几点需要注意。
(1)一个try…catch语句中只能有一个try语句块,但可以有多个catch语句块,以便与不同的异常类型匹配。catch语句必须有参数,如果try语句块中的代码抛出了异常,无论抛出的异常的值是什么,只要异常的类型与catch语句的参数类型匹配,异常就会被catch语句捕获。最后一个catch语句参数为“…”符号,表示可以捕获任意类型的异常。
(2)一旦某个catch语句捕获到了异常,后面的catch语句将不再被执行,其用法类似switch…case语句。
(3)try和catch语句块中的代码必须使用大括号“{}”括起来,即使语句块中只有一行代码。
(4)try语句和catch语句不能单独使用,必须连起来一起使用。
在使用try…catch语句处理异常时,如果try语句块中的某一行代码抛出了异常,则无论异常是否被处理,抛出异常的语句后面的代码都不再被执行。例如,有如下代码:
try
{
func();
add();
cout << 3/0<< endl;
}
catch (int)
{
cout << "异常处理" << endl;
}
cout << "异常处理完毕,程序从此处开始向下执行" << endl;
在上述代码中,如果try语句块中的func()函数调用抛出了异常,并且catch语句成功捕获到了异常,则异常处理结束之后,程序会执行try…catch语句后面的代码,而不会执行try语句块中的add()函数。
下面通过案例演示C++的异常处理,如例9-1所示。
例9-1 tryCatch.cpp
1 #include<iostream>
2 #include<fstream>
3 using namespace std;
4 class AbstractException //定义抽象异常类AbstractException
5 {
6 public:
7 virtual void printErr() = 0; //纯虚函数printErr()
8 };
9 //定义文件异常类FileException公有继承AbstractException
10 class FileException : public AbstractException
11 {
12 public:
13 virtual void printErr() //实现printErr()函数
14 {
15 cout << "错误:文件不存在" << endl;
16 }
17 };
18 //定义整除异常类DivideException公有继承AbstractException
19 class DivideException:public AbstractException
20 {
21 public:
22 virtual void printErr() //实现printErr()函数
23 {
24 cout << "错误:除零异常" << endl;
25 }
26 };
27 void readFile() //定义readFile()函数
28 {
29 ifstream ifs("log.txt"); //创建文件输入流对象ifs并打开log.txt文件
30 if(!ifs) //如果文件打开失败
31 {
32 throw FileException(); //抛出异常
33 }
34 ifs.close(); //关闭文件
35 }
36 void divide() //定义divide()函数
37 {
38 int num1 = 100;
39 int num2 = 2;
40 if(num2 == 0) //如果除数num2为0
41 {
42 throw DivideException(); //抛出异常
43 }
44 int ret = num1/num2;
45 cout << "两个数相除结果:" << ret << endl;
46 }
47 int main()
48 {
49 try
50 {
51 readFile(); //检测readFile()函数调用
52 divide(); //检测divide()函数调用
53 }
54 catch(FileException& fex) //捕获FileException&类型异常
55 {
56 fex.printErr(); //调用相应函数输出异常信息
57 }
58 catch(DivideException& dex) //捕获DivideException&类型异常
59 {
60 dex.printErr();
61 }
62 catch(...) //捕获任意类型异常
63 {
64 cout << "处理其他异常" << endl;
65 }
66 cout << "程序执行结束" << endl;
67 return 0;
68 }
例9-1运行结果如图9-1所示。

图9-1 例9-1运行结果
在例9-1中,第4~8行代码定义了异常类AbstractException,AbstractException类是一个抽象类,该类声明了纯虚函数printErr()。第10~17行代码定义了文件异常类FileException,该类公有继承AbstractException类,并实现了printErr()函数,用于输出“文件不存在”的错误提示信息。第19~26行代码定义异常类D ivideException,该类公有继承AbstractException类,并实现了printErr()函数,用于输出除数为0的错误提示信息。
第27~35行代码定义了readFile()函数,在该函数中,创建了文件输入流对象ifs用于读取文件,如果文件不存在,就抛出FileException类型的异常。第36~46行代码定义了divide()函数,在该函数中,定义两个整数相除,如果除数为0,就抛出D ivideException类型的异常。
第49~65行代码在m ain()函数中使用try…catch语句检测并捕获异常。在try语句块中,检测readFile()函数和divide()函数调用,如果有异常抛出,则通过catch语句捕获异常。第一个catch语句捕获FileException&类型的异常,第二个catch语句捕获D ivideException&类型的异常,第三个catch语句捕获任意类型的异常。
由图9-1可知,程序输出了“文件不存在”的错误提示信息,这表明在调用readFile()函数时,由于文件log.txt不存在,readFile()抛出了异常,通过第54行代码的catch语句捕获了该异常,在catch语句块中通过对象fex调用printErr()函数输出了异常信息。同时,由图9-1还可知,catch语句捕获异常之后,程序直接执行了第66行代码,并没有返回执行第52行代码的divide()函数。
C++不仅能够处理各种不同类型的异常,还可以在异常处理前释放所有局部对象。从进入try语句块开始到异常被抛出之前,在栈上创建的所有对象都会被析构,析构的顺序与构造的顺序相反,这一过程称为栈解旋或栈自旋。
下面通过案例演示栈的解旋过程,如例9-2所示。
例9-2 stackUnwinding.cpp
1 #include<iostream>
2 using namespace std;
3 class Shape //定义形状类Shape
4 {
5 public:
6 Shape(); //构造函数
7 ~Shape(); //析构函数
8 static int count; //静态成员变量count
9 };
10 int Shape::count = 0; //count初始值为0
11 Shape::Shape() //实现构造函数
12 {
13 count++;
14 if(Shape::count == 3)
15 throw "纸张画不下啦!!";
16 cout << "Shape构造函数" << endl;
17 }
18 Shape::~Shape() //实现析构函数
19 {
20 cout << "Shape析构函数" << endl;
21 }
22 int main()
23 {
24 Shape circle; //画圆形
25 try //try语句块检测可能抛出异常的代码
26 {
27 int num = 2; //定义int类型变量num,表示纸张可画两个图形
28 cout << "纸张可画图形个数:" << num << endl;
29 Shape rectangle; //画长方形
30 Shape triangle; //画三角形
31 }
32 catch(const char* e) //捕获异常
33 {
34 cout << e << endl;
35 }
36 return 0;
37 }
例9-2运行结果如图9-2所示。
在例9-2中,第3~9行代码定义了形状类Shape,该类声明了一个静态成员变量count,用于记录Shape类对象的个数。此外,Shape类还声明了构造函数和析构函数。第10~21行代码,在类外初始化count的值为0,并实现类的构造函数与析构函数。在实现构造函数时,如果count值为3,就抛出一个异常,提示纸张画不下的异常信息。

图9-2 例9-2运行结果
第24行代码创建Shape类对象circle,表示画了一个圆形。第25~31行代码,在try语句块中检测可能抛出异常的代码。第27行代码定义int类型变量num为2,表示纸张可画两个图形;第29~30行代码,创建Shape类对象rectangle和triangle。第32~35行代码通过catch语句捕获char*类型异常并输出异常信息。
由图9-2可知,程序抛出了异常,catch语句捕获并输出了异常提示信息:“纸张画不下啦!!”程序在运行时,首先调用Shape类的构造函数创建了对象circle;然后进入try语句块,定义num变量并输出;最后创建Shape类对象rectangle,对象rectangle创建成功之后,内存中有两个Shape类对象,当程序再创建对象triangle时,count值为3,就抛出了异常,对象triangle并没有创建成功。
在抛出异常之前,程序会将try语句块中创建的对象(num和rectangle)都释放。因此,在图9-2中,异常信息输出之前调用了一次Shape析构函数,用来析构对象rectangle。在try语句块之外创建的对象circle,待异常处理完成之后才析构。因此,在图9-2中,异常信息输出之后又调用了一次Shape析构函数。
需要注意的是,栈解旋只能析构栈上的对象,不会析构动态对象。
C++提供了一组标准异常类,这些类以exception为根基类,程序中抛出的所有标准异常都继承自exception类。exception类定义在exception头文件中,C++11标准对exception类的定义如下所示:
class exception{
public:
exception () noexcept;
exception (const exception&) noexcept;
exception& operator=(const exception&) noexcept;
virtual ~exception();
virtual const char* what() const noexcept;
};
由上述定义可知,exception类提供了多个函数,其中,what()函数用于描述异常相关信息。what()函数是一个虚函数,exception类的派生类可以对what()函数重新定义,以便更好地描述派生类的异常信息。
exception类派生了多个类,这些派生类又作为基类派生出了新的类,因此,以exception为根基类的标准异常类是一个庞大的异常类库。标准异常类的继承关系如图9-3所示。
在图9-3中,logic_error类与runtim e_error类是exception类主要的两个派生类,它们都定义在stdexcept头文件中。logic_error类表示那些可以在程序中被预先检测到的异常,即通过检查或编译可以发现的错误。runtim e_error则表示运行时的异常,这类错误只有在程序运行时才能被检测到。logic_error类和runtim e_error类都有一个带有const string&类型参数的构造函数,构造异常对象时,可以将错误信息传递给该参数,通过调用异常对象的what()函数,可以得到构造时提供的异常信息。

图9-3 标准异常类的继承关系
标准异常类被定义在不同的头文件中,读者可通过查阅附录Ⅱ查看标准异常类所属的头文件及含义。下面通过案例演示标准异常类的使用,如例9-3所示。
例9-3 stdexception.cpp
1 #include<iostream>
2 using namespace std;
3 class Animal //定义动物类Animal
4 {
5 public:
6 virtual void speak(); //声明虚函数speak()
7 };
8 void Animal::speak() //类外实现speak()函数
9 {
10 cout << "动物叫声" << endl;
11 }
12 class Cat:public Animal //猫类Cat公有继承Animal类
13 {
14 public:
15 virtual void speak(); //声明虚函数speak()
16 };
17 void Cat::speak() //类外实现speak()函数
18 {
19 cout << "小猫喵喵叫" << endl;
20 }
21 int main()
22 {
23 Animal animal; //创建Animal类对象animal
24 Animal& ref = animal; //定义Animal类引用ref
25 ref.speak(); //通过Animal的引用ref调用speak()函数
26 try
27 {
28 //将引用ref强制转换为Cat&类型
29 Cat& cat = dynamic_cast<Cat&>(ref);
30 cat.speak();
31 }
32 catch(bad_cast& ex) //捕获异常,bad_cast标准异常
33 {
34 cout << ex.what() << endl;
35 }
36 return 0;
37 }
例9-3运行结果如图9-4所示。

图9-4 例9-3运行结果
在例9-3中,第3~7行代码定义了动物类Anim al,该类声明了一个虚函数speak()。第12~16行代码定义了猫类Cat公有继承Anim al类,Cat类声明了虚函数speak()。第23~25行代码创建了Anim al类对象anim al;定义了Anim al类引用ref,使用对象anim al为ref初始化,并通过引用ref调用speak()函数。第26~31行代码检测第29~30行代码是否抛出异常。第29行代码通过dynam ic_cast转换运算符将Anim al类型的引用ref强制转换为Cat类型的引用,并赋值给Cat类型的引用cat。由于引用ref指向的是基类对象anim al,因此在将其转换为派生类引用时会发生bad_cast异常。第32~35行代码通过catch语句捕获bad_cast类型的标准异常,如果捕获到异常,就调用what()函数输出异常信息。由图9-4可知,程序在运行时抛出了bad_cast标准异常,提示异常信息:“Bad dynam ic_cast!”
在exception类定义中,noexcept关键字表示函数不抛出异常。noexcept关键字是C++11新增的关键字,在C++11之前,如果一个函数不抛出异常,则在函数后面添加throw()函数,示例代码如下所示:
void func() throw();
相比于throw(),noexcept关键字有利于编译器优化代码,但它并不适用于任何地方。在指定函数是否抛出异常时,如果读者对noexcept关键字不是很熟悉,尽量使用throw(),而不要轻易使用noexcept关键字。
断言是编程中常用的一种调试手段。在C++11之前,C++使用assert()宏进行断言。但是,assert()宏只能在程序运行时期执行,这意味着不运行程序将无法检测到断言错误。如果每次断言都要执行一遍程序,则检测效率就会降低。另外,对于C++中使用较多的模板来说,模板实例化是在编译阶段完成的,assert()断言不能在编译阶段完成对模板实例化的检测。
为此,C++11引入了静态断言static_assert,用于实现编译时断言。静态断言的语法格式如下所示:
static_assert(常量表达式, 提示字符串);
在上述语法格式中,static_assert有两个参数:第一个参数是一个常量表达式,即断言表达式;第二个参数是一个字符串。在执行断言时,编译器首先检测“常量表达式”的值,若常量表达式的值为真,则static_assert()不做任何操作,程序继续完成编译;若常量表达式的值为假,则static_assert()产生一条编译错误提示,错误提示的内容就是第二个参数。
下面通过案例演示静态断言的用法,如例9-4所示。
例9-4 staticAssert.cpp
1 #include<iostream>
2 using namespace std;
3 template<typename T,typename U>
4 void func(T& t, U& u) //定义函数模板func()
5 {
6 static_assert(sizeof(t) == sizeof(u), //静态断言
7 "the parameters must be the same width.");
8 cout << t << "与 << u << "字节大小相同" << endl;
9 }
10 int main()
11 {
12 int x = 100; //定义变量
13 int y = 20; //定义变量
14 char ch = 'a'; //定义变量
15 func(x, y); //调用func()函数
16 func(x, ch); //调用func()函数
17 return 0;
18 }
编译例9-4,结果如图9-5所示。

图9-5 例9-4编译结果
在例9-4中,第3~9行代码定义了函数模板func(),在函数内部,使用static_assert()判断两个参数的字节大小是否相同,如果不相同,即静态断言失败,则输出提示信息“the param eters must be the sam e width”。第12~14行代码,定义了两个int类型的变量x、y和一个char类型的变量ch。第15行代码调用func()函数,传入x与y作为参数,由于传入的参数都为int类型,因此程序编译不会出错。第16行代码调用func()函数,传入x与ch作为参数,由于x为int类型,ch为char类型,因此,程序在编译时静态断言失败,编译器报错。由图9-5可知,编译错误就是由于第16行代码断言失败。如果注释掉第16行代码,则程序编译通过。
需要注意的是,static_assert()断言表达式的结果必须在编译阶段就可以计算出来,即必须是常量表达式。如果使用了变量,则会导致编译错误,示例代码如下:
void set_age(const int n)
{
static_assert(n > 0, "The age should be greater than zero!");
}
上述代码中,在static_assert()中使用了参数变量n,编译时报错“表达式的计算结果不是常数”。
本章主要讲解了C++中的异常处理机制。异常处理机制通过抛出异常与处理异常的分离,使程序每个部分只完成自己的本职工作而互不干扰,保证了程序的高容错性。静态断言是C++程序常用的程序调试手段,使用静态断言可以在编译阶段查找出程序中存在的逻辑错误,从而提高程序的编译效率。学习完本章,读者可以了解C++中异常处理的方式和规则。
一、填空题
1.C++中,抛出异常的关键字是___。
2.C++标准异常库以___类为根基类。
3.C++标准异常类中,___类表示运行时异常。
4. 在C++中,宏定义___表示静态断言。
二、判断题
1.try…catch语句中可以有多个try语句。( )
2.try…catch语句可以分开,单独使用。( )
3.try语句块中代码抛出异常后,如果异常被正确处理,抛出异常代码后面的程序会继续执行。( )
4. 栈解旋会把try语句块中的所有对象都释放,包括堆内存上的对象。( )
5.C++标准异常类都定义了what()函数,用于描述异常信息。( )
6. 使用静态断言,程序可以在编译时检测错误。( )
三、选择题
1. 关于函数声明“float func(int a,int b)throw;”,下列描述中正确的是( )。
A.表明函数抛出float类型异常
B.表明函数可抛出任何类型异常
C.表明函数不抛出任何类型异常
D.表明函数可能抛出异常
2. 关于C++异常处理的流程,下列说法中错误的是( )。
A.对某段可能产生异常的代码或函数使用try结构进行检测
B.如果在执行try结构期间没有引起异常,则跟在try后面的catch结构不会执行
C.如果在执行try结构期间发生异常,在异常发生的位置使用throw抛出异常,一个异常对象将被创建
D.本层try语句抛出了异常,只能由本层的catch语句处理
3. 关于栈解旋,下列说法中正确的是( )。
A.栈解旋时,对象的析构顺序与构造顺序相同
B.栈解旋只能释放栈上的对象
C.栈解旋可以释放堆上的对象
D.try语句块之外的对象也可以通过栈解旋释放
4. 关于标准库异常,下列说法中错误的是( )。
A.logic_error类表示那些可以在程序中被预先检测到的异常
B.异常基类exception定义在头文件exception中
C.exception类接口中的函数都有一个noexcept关键字,这表示exception类成员函数不会抛出任何异常
D.runtim e_error类不能被继承
5. 关于断言,下列说法中错误的是( )。
A.断言是调试程序的一种手段
B.static_assert是静态断言,即在程序编译时期检测错误
C.宏assert()用来在运行阶段实现断言
D.static_assert可以使用变量作为参数
四、简答题
1. 简述C++中的异常处理过程。
2. 简述static_assert与assert的区别。
五、编程题
请按照下列要求编写程序。
(1)定义一个异常类Cexception,有成员函数reason(),用来显示异常的类型。
(2)定义一个函数fun()触发异常,在主函数try语句块中调用fun(),在catch语句块中捕获异常,观察程序执行流程。