导图社区 C++ 第十五章:友元、异常和其他
初步介绍异常模块,作为入门这块了解并且会简单地使用即可。此外还介绍了友元,主要是友元类,还有RTTI的讲解。内容较多,很详细。
编辑于2021-01-02 19:53:39第15章 友元、异常和其他
15.3 异常
概述
异常是相对较新的C++功能,有些老式编译器可能没有实现。另外,有些编译器默认关闭这种特性,您可能需要使用编译器选项来启用它
作为试验,以一个计算两个数的调和平均数的函数为例。两个数的调和平均数的定义是:这两个数字倒数的平均值的倒数,因此表达式为: 2.0 * x * y / (x + y)
15.3.1 调用abort()
1. 一种解决方案
如果其中一个参数是另一个参数的负值,则调用abort()函数
2. abort()函数
(1)abort()函数的原型位于头文件cstdlib(或stdlib.h)中,其典型实现是向标准错误流(即cerr使用的错误流)发送消息abnormal program termination(程序异常终止),然后终止程序
(2)abort()函数返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。abort()是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。如果愿意,也可以使用exit(),该函数刷新文件缓冲区,但不显示消息
15.3.2 返回错误码
1. 另一种解决方案
一种比异常终止更灵活的方法是,使用函数的返回值来指出问题。例如,ostream类的get(void)成员通常返回下一个输入字符的ASCII码,但到达文件尾时,将返回特殊值EOF
2. 特殊方案
有时候,任何数值都是有效的返回值,因此不存在可用于指出问题的特殊值。在这种情况下,可使用指针参数或引用参数来将值返回给调用程序,并使用函数的返回值来指出成功还是失败
3. 说明
另一种在某个地方存储返回条件的方法是使用一个全局变量。可能问题的函数可以在出现问题时将该全局变量设置为特定的值,而调用程序可以检查该变量。传统的C语言数学库使用的就是这种方法,它使用的全局变量名为errno。当然,必须确保其他函数没有将该全局变量用于其他目的
bool hmean(double a, double b, double * ans) { if (a == -b) { *ans = DBL_MAX; return false; } else { *ans = 2.0 * a * b / (a + b); return true; } }
15.3.3 异常机制
1. 3个组成部分
· 引发异常
· 使用处理程序捕获异常
· 使用try块
2. throw语句
throw语句实际上是跳转,即命令程序跳到另一条语句。throw关键字表示引发异常,紧随其后的值(例如字符串或对象)指出了异常的特征
3. catch关键字
catch关键字表示捕获异常。处理程序以关键字catch开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型;然后是一个用花括号括起的代码块,指出要采取的措施。catch关键字和异常类型用作标签,指出当异常被引发时,程序应跳到这个位置执行。异常处理程序也被称为catch块
4. try关键字
try块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch块。try块是由关键字try指示的,关键字try的后面是一个由花括号括起的代码块,表明需要注意这些代码引发的异常
5. 程序示例
// error3.cpp -- using an exception #include <iostream> double hmean(double a, double b); int main() { double x, y, z; std::cout << "Enter two numbers: "; while (std::cin >> x >> y) { try { // start of try block z = hmean(x,y); } // end of try block catch (const char * s) // start of exception handler { std::cout << s << std::endl; std::cout << "Enter a new pair of numbers: "; continue; } // end of handler std::cout << "Harmonic mean of " << x << " and " << y << " is " << z << std::endl; std::cout << "Enter next set of numbers <q to quit>: "; } std::cout << "Bye!\n"; return 0; } double hmean(double a, double b) { if (a == -b) throw "bad hmean() arguments: a = -b not allowed"; return 2.0 * a * b / (a + b); }
6. 程序说明
(1)try块与下面类似: try { // start of try block z = hmean(x,y); } // end of try block 如果其中的某条语句导致异常被引发,则后面的catch块将对异常进行处理。如果程序在try块的外面调用hmean(),将无法处理异常
(2)引发异常的代码与下面类似: if (a == -b) thow "bad hmean() arguments: a = -b not allowed"; 其中被引发的异常是字符串“bad hmean() arguments: a = -b not allowed”。异常类型可以是字符串(就像这个例子中那样)或其他C++类型;但通常为类类型
(3)执行throw语句类似于执行返回语句,因为它也将终止函数的执行;但throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数。在这个例子中,throw将程序控制权返回给main()。程序将在main()中寻找与引发的异常类型匹配的异常处理程序(位于try块的后面)
(4)处理程序(或catch块)与下面类似: catch (const char * s) // start of exception handler { std::cout << s << std::endl; std::cout << "Enter a new pair of numbers: "; continue; } // end of handler 关键字catch表明这是一个处理程序,而char*s则表明该处理程序与字符串异常匹配。s与函数参数定义极其类似,因为匹配的引发将被赋给s。另外,当异常与该处理程序匹配时,程序将执行括号中的代码。执行完try块中的语句后,如果没有引发任何异常,则程序跳过try块后面的catch块,直接执行处理程序后面的第一条语句
15.3.4 将对象用作异常类型
1. 选择对象的好处
(1)可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常
(2)对象可以携带信息,程序员可以根据这些信息来确定引发异常的原因
2. 方法
1. 代码示例
class bad_hmean { private: double v1, v2; public: bad_hmean(double a = 0, double b = 0) : v1(a), v2(b){} void mesg(); }; inline void bad_hmean::mesg() { std::cout << "hmean(" << v1 << ", " << v2 <<"): " << "invalid arguments: a = -b\n"; } class bad_gmean { public: double v1, v2; bad_gmean(double a = 0, double b = 0) : v1(a), v2(b){} const char * mesg(); }; inline const char * bad_gmean::mesg() { return "gmean() arguments should be >= 0\n"; }
int main() { using std::cout; using std::cin; using std::endl; double x, y, z; cout << "Enter two numbers: "; while (cin >> x >> y) { try { // start of try block z = hmean(x,y); cout << "Harmonic mean of " << x << " and " << y << " is " << z << endl; cout << "Geometric mean of " << x << " and " << y << " is " << gmean(x,y) << endl; cout << "Enter next set of numbers <q to quit>: "; }// end of try block catch (bad_hmean & bg) { // start of catch block bg.mesg(); cout << "Try again.\n"; continue; } catch (bad_gmean & hg) { cout << hg.mesg() << "Values used: " << hg.v1 << ", " << hg.v2 << endl; cout << "Sorry, you don't get to play any more.\n"; break; } // end of catch block } cout << "Bye!\n"; // cin.get(); // cin.get(); return 0; } double hmean(double a, double b) { if (a == -b) throw bad_hmean(a,b); return 2.0 * a * b / (a + b); } double gmean(double a, double b) { if (a < 0 || b < 0) throw bad_gmean(a,b); return std::sqrt(a * b); }
2. 代码说明
可以将一个bad_hmean对象初始化为传递给函数hmean()的值,而方法mesg()可用于报告问题(包括传递给函数hmena()的值)。函数hmean()可以使用下面这样的代码: if (a == -b) throw bad_hmean(a,b); 上述代码调用构造函数bad_hmean(),以初始化对象,使其存储参数值。 如果函数hmean()引发bad_hmean异常,第一个catch块将捕获该异常;如果gmean()引发bad_gmean异常,异常将逃过第一个catch块,被第二个catch块捕获
15.3.5 异常规范和C++11
1. 介绍
异常规范(exception specification)是C++98新增的一项功能,但C++11却将其摒弃了。这意味着C++11仍然处于标准之中,但以后可能会从标准中剔除,因此不建议您使用它
2. 用法
double harm(double a) throw(bad_thing); // may throw bad_thing exception double marm(double) throw(); // doesn't throw an exception 其中的throw()部分就是异常规范,它可能出现在函数原型和函数定义中,可包含类型列表,也可不包含
3. 作用
(2)让编译器添加执行运行阶段检查的代码,检查是否违反了异常规范。但是这很难检查。例如,marm()可能不会引发异常,但它可能调用一个函数,而这个函数调用的另一个函数引发了异常。另外,您给函数编写代码时它不会引发异常,但库更新后它却会引发异常
(1)告诉用户可能需要使用try块。然而,这项工作也可使用注释轻松地完成
4. 建议
编程社区(尤其是尽力编 写安全代码的开发人员)达成的一致意见是,最好不要使用这项功能。而C++11也建议您忽略异常规范
5. 特殊的异常规范
C++11确实支持一种特殊的异常规范:您可使用新增的关键字noexcept指出函数不会引发异常: double marm() noexcept; // marm() doesn't throw an exception 有关这种异常规范是否必要和有用存在一些争议,有些人认为最好不要使用它(至少在大多数情况下如此);而有些人认为引入这个新关键字很有必要,理由是知道函数不会引发异常有助于编译器优化代码。通过使用这个关键字,编写函数的程序员相当于做出了承诺
15.3.6 栈解退
1. 作用机制
现在假设函数由于出现异常(而不是由于返回)而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退
2. 特性
和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用。然而,函数返回仅仅处理该函数放在栈中的对象,而throw语句则处理try块和throw之间整个函数调用序列放在栈中的对象
3. 图示
4. 程序代码
double hmean(double a, double b) { if (a == -b) throw bad_hmean(a,b); return 2.0 * a * b / (a + b); } double gmean(double a, double b) { if (a < 0 || b < 0) throw bad_gmean(a,b); return std::sqrt(a * b); } double means(double a, double b) { double am, hm, gm; demo d2("found in means()"); am = (a + b) / 2.0; // arithmetic mean try { hm = hmean(a,b); gm = gmean(a,b); } catch (bad_hmean & bg) // start of catch block { bg.mesg(); std::cout << "Caught in means()\n"; throw; // rethrows the exception } d2.show(); return (am + hm + gm) / 3.0; }
int main() { using std::cout; using std::cin; using std::endl; double x, y, z; { demo d1("found in block in main()"); cout << "Enter two numbers: "; while (cin >> x >> y) { try { // start of try block z = means(x,y); cout << "The mean mean of " << x << " and " << y << " is " << z << endl << "Enter next pair: "; } // end of try block catch (bad_hmean & bg) // start of catch block { bg.mesg(); cout << "Try again.\n"; continue; } catch (bad_gmean & hg) { cout << hg.mesg() <<"Values used: " << hg.v1 << ", " << hg.v2 << endl; cout << "Sorry, you don't get to play any more.\n"; break; } // end of catch block } d1.show(); } cout << "Bye!\n"; return 0; }
5. 代码说明
main()调用了means(),而means()又调用了hmean()和gmean()。函数means()计算算术平均数、调和平均数和几何平均数。main()和means()都创建demo类型的对象(demo是一个喋喋不休的类,指出什么时候构造函数和析构函数被调用),以便您知道发生异常时这些对象将被如何处理。函数main()中的try块能够捕获bad_hmean和badgmean异常,而函数means()中的try块只能捕获bad_hmean异常。catch块的代码如下: catch (bad_hmean & bg) // start of catch block { bg.mesg(); std::cout << "Caught in means()\n"; throw; // rethrows the exception } 上述代码显示消息后,重新引发异常,这将向上把异常发送给 main()函数。一般而言,重新引发的异常将由下一个捕获这种异常的try-catch块组合进行处理,如果没有找到这样的处理程序,默认情况下程序将异常终止
15.3.7 其他异常特性
1. 特性
(1)throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合
(2)引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用
2. 相关说明
(1)为何还需要使用引用? 引用还有另一个重要特征:基类引用可以执行派生类对象。假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将与任何派生类对象匹配。如果有一个异常类继承层次结构,应这样排列catch块:将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面。但派生特性将被剥去,因此将使用虚方法的基类版本
(2)如何处理未知的异常? 方法是使用省略号来表示异常类型,从而捕获任何异常: catch(...) { // statements } // catches any type exception 如果知道一些可能会引发的异常,可以将上述捕获所有异常的catch块放在最后面,这有点类似于switch语句中的default
15.3.8 exception类
概述
为支持该语言,exception头文件(以前为exception.h或except.h)定义了exception类,C++可以把它用作其他异常类的基类。代码可以引发exception异常,也可以将exception类用作基类。有一个名为what()的虚拟成员函数,它返回一个字符串,该字符串的特征随实现而异。然而,由于这是一个虚方法,因此可以在从exception派生而来的类中重新定义它 #include <exception> class bad_hmean : public std::exception { public: const char* what() { return "bad arguments to hmean()"; } ... };
1. stdexcrpt异常类
1. 派生关系
exception类
logic_error类
· domain_error
数学函数有定义域(domain)和值域(range)。如果您编写一个函数,该函数 将一个参数传递给函数std::asin(),则可以让该函数在参数不在定义域−1到+1之间时引发domain_error异常
· invalid_argument
异常invalid_argument指出给函数传递了一个意料外的值。例如,如果函数希望接受一个这样的字符串:其中每个字符要么是'0'要么是'1',则当传递的字符串中包含其他字符时,该函数将引发invalid_argument异常
· length_error
异常length_error用于指出没有足够的空间来执行所需的操作。例如,string类的append()方法在合并得到的字符串长度超过最大允许长度时,将引发length_error异常
· out_of_bounds
异常out_of_bounds通常用于指示索引错误。例如,您可以定义一个类似于数组的类,其operator() []在使用的索引无效时引发out_of_bounds异常
runtime_error类
· range_error
计算结果可能不再函数允许的范围之内,但没有发生上溢或下溢错误,在这种情况下,可以使用 range_error异常
· overflow_error
下溢(underflow)错误在浮点数计算中。一般而言,存在浮点类型可以表示的最小非零值,计算结果比这个值还小时将导致下溢错误
· underflow_error
整型和浮点型都可能发生上溢错误,当计算结果超过了某种类型能够表示的最大数量级时,将发生上溢错误
2. 说明
(1)一般而言,logic_error系列异常表明存在可以通过编程修复的问题,而runtime_error系列异常表明存在无法避免的问题
(2)所有这些错误类有相同的常规特征,它们之间的主要区别在于:不同的类名让您能够分别处理每种异常
2. bad_allo异常和new
对于使用new导致的内存分配问题,C++的最新处理方式是让new引发bad_alloc异常。头文件new包含bad_alloc类的声明,它是从exception类公有派生而来的。但在以前,当无法分配请求的内存量时,new返回一个空指针: #include <iostream> #include <new> #include <cstdlib> // for exit(), EXIT_FAILURE using namespace std; int main() { ... try { cout << "Trying to get a big block of memory:\n"; pb = new Big[10000]; // 1,600,000,000 bytes cout << "Got past the new request:\n"; } catch (bad_alloc & ba) { cout << "Caught the exception!\n"; cout << ba.what() << endl; exit(EXIT_FAILURE); } ... }
3. 空指针和new
很多代码都是在new在失败时返回空指针时编写的。为处理new的变化,有些编译器提供了一个标记(开关),让用户选择所需的行为。当前,C++标准提供了一种在失败时返回空指针的new,其用法如下: int* pi = new (std::nothrow) int; int* pa = new (std::nowthrow) int[500];
15.3.9 异常、类和继承
示例
// sales.h -- exceptions and inheritance #include <stdexcept> #include <string> class Sales { public: enum {MONTHS = 12}; // could be a static const class bad_index : public std::logic_error { private: int bi; // bad index value public: explicit bad_index(int ix, const std::string & s = "Index error in Sales object\n"); int bi_val() const {return bi;} virtual ~bad_index() throw() {} }; explicit Sales(int yy = 0); Sales(int yy, const double * gr, int n); virtual ~Sales() { } int Year() const { return year; } virtual double operator[](int i) const; virtual double & operator[](int i); private: double gross[MONTHS]; int year; }; class LabeledSales : public Sales { public: class nbad_index : public Sales::bad_index { private: std::string lbl; public: nbad_index(const std::string & lb, int ix, const std::string & s = "Index error in LabeledSales object\n"); const std::string & label_val() const {return lbl;} virtual ~nbad_index() throw() {} }; explicit LabeledSales(const std::string & lb = "none", int yy = 0); LabeledSales(const std::string & lb, int yy, const double * gr, int n); virtual ~LabeledSales() { } const std::string & Label() const {return label;} virtual double operator[](int i) const; virtual double & operator[](int i); private: std::string label; };
// sales.cpp -- Sales implementation #include "sales.h" using std::string; Sales::bad_index::bad_index(int ix, const string & s ) : std::logic_error(s), bi(ix) { } Sales::Sales(int yy) { year = yy; for (int i = 0; i < MONTHS; ++i) gross[i] = 0; } Sales::Sales(int yy, const double * gr, int n) { year = yy; int lim = (n < MONTHS)? n : MONTHS; int i; for (i = 0; i < lim; ++i) gross[i] = gr[i]; // for i > n and i < MONTHS for ( ; i < MONTHS; ++i) gross[i] = 0; } double Sales::operator[](int i) const { if(i < 0 || i >= MONTHS) throw bad_index(i); return gross[i]; } double & Sales::operator[](int i) { if(i < 0 || i >= MONTHS) throw bad_index(i); return gross[i]; } LabeledSales::nbad_index::nbad_index(const string & lb, int ix, const string & s ) : Sales::bad_index(ix, s) { lbl = lb; } LabeledSales::LabeledSales(const string & lb, int yy) : Sales(yy) { label = lb; } LabeledSales::LabeledSales(const string & lb, int yy, const double * gr, int n) : Sales(yy, gr, n) { label = lb; } double LabeledSales::operator[](int i) const { if(i < 0 || i >= MONTHS) throw nbad_index(Label(), i); return Sales::operator[](i); } double & LabeledSales::operator[](int i) { if(i < 0 || i >= MONTHS) throw nbad_index(Label(), i); return Sales::operator[](i); }
// use_sales.cpp -- nested exceptions #include <iostream> #include "sales.h" int main() { using std::cout; using std::cin; using std::endl; double vals1[12] = { 1220, 1100, 1122, 2212, 1232, 2334, 2884, 2393, 3302, 2922, 3002, 3544 }; double vals2[12] = { 12, 11, 22, 21, 32, 34, 28, 29, 33, 29, 32, 35 }; Sales sales1(2011, vals1, 12); LabeledSales sales2("Blogstar",2012, vals2, 12 ); cout << "First try block:\n"; try { int i; cout << "Year = " << sales1.Year() << endl; for (i = 0; i < 12; ++i) { cout << sales1[i] << ' '; if (i % 6 == 5) cout << endl; } cout << "Year = " << sales2.Year() << endl; cout << "Label = " << sales2.Label() << endl; for (i = 0; i <= 12; ++i) { cout << sales2[i] << ' '; if (i % 6 == 5) cout << endl; } cout << "End of try block 1.\n"; } catch(LabeledSales::nbad_index & bad) { cout << bad.what(); cout << "Company: " << bad.label_val() << endl; cout << "bad index: " << bad.bi_val() << endl; } catch(Sales::bad_index & bad) { cout << bad.what(); cout << "bad index: " << bad.bi_val() << endl; } cout << "\nNext try block:\n"; try { sales2[2] = 37.5; sales1[20] = 23345; cout << "End of try block 2.\n"; } catch(LabeledSales::nbad_index & bad) { cout << bad.what(); cout << "Company: " << bad.label_val() << endl; cout << "bad index: " << bad.bi_val() << endl; } catch(Sales::bad_index & bad) { cout << bad.what(); cout << "bad index: " << bad.bi_val() << endl; } cout << "done\n"; return 0; }
15.3.10 异常何时会迷失方向
1. 导致问题的两种情况
(1)如果异常是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类及其派生类的对象匹配),否则称为意 外异常(unexpected exception)。在默认情况下,这将导致程序异常终止(虽然C++11摒弃了异常规范,但仍支持它,且有些现有的代码使用了它)
(2)如果异常不是在函数中引发的(或者函数没有异常规范),则必须捕获它。如果没被捕获(在没有try块或没有匹配的catch块时,将出现这种情况),则异常被称为未捕获异常(uncaught exception)。在默认情况下,这将导致程序异常终止
2. 修改办法
(1)未捕获异常
· 介绍
未捕获异常不会导致程序立刻异常终止。相反,程序将首先调用函数terminate()。在默认情况下,terminate()调用abort()函数。可以指定terminate()应调用的函数(而不是abort())来修改terminate()的这种行为————set_terminate()
set_terminate()和terminate()都是在头文件exception中声明的: typedef void (*terminate_handler) {}; terminate_handler set_terminate(terminate_handler f) throw(); // C++98 terminate_handler set_terminate(terminate_handler f) noexcept; // C++11 void terminate(); // C++98 void terminate() noexcept; // C++11 其中的typedef使terminate_handler成为这样一种类型的名称:指向没有参数和返回值的函数的指针。 set_terminate()函数将不带任何参数且返回类型为void的函数的名称(地址)作为参数,并返回该函数的地址。如果调用了set_terminate()函数多次,则terminate()将调用最后一次set_terminate()调用设置的函数
· 示例
假设希望未捕获的异常导致程序打印一条消息,然后调用exit()函数,将退出状态值设置为5: #include <exception> using namespace std; 然后,设计一个完成上述两种操作所需的函数,其原型如下: void myQuit() { cout << "Terminating due to uncaught excrption\n"; exit(5); } 最后,在程序的开头,将终止操作指定为调用该函数: set_terminate(myQuit);
(2)意外异常
· 介绍
假设函数的原型如下: double Argh(double, double) throw(out_of_bounds); 则可以这样使用该函数: try { x = Argh(a, b); } catch (out_of_bounds& ex) { ... } 原则上,异常规范应包含函数调用的其他函数引发的异常。例如,如果Argh()调用了Duh()函数,而后者可能引发retort对象异常,则Argh()和Duh()的异常规范中都应包含retort。除非自己编写所有的函数,并且特别仔细,否则无法保证上述工作都已正确完成
在这种情况下,行为与未捕获的异常极其类似。如果发生意外异常,程序将调用unexpected()函数(您没有想到是unexpected()函数吧?谁也想不到!)。这个函数将调用terminate(),后者在默认情况下将调用abort()。正如有一个可用于修改terminate()的行为的set_terminate()函数一样,也有一个可用于修改unexpected( )的行为的函数————set_unexpected()
这些新函数也是在头文件exception中声明的: typedef coid (*unexprcted_handler) {}; unexprcted_handler set_unexpected(unexprcted_handler f) throw(); // C++98 unexprcted_handler set_unexpected(unexprcted_handler f) noexpect; //C++11 void unexpected(); // C++98 void unexpected() noexpect; // C++11
· 说明
然而,与提供给set_terminate()的函数的行为相比,提供给 set_unexpected()的函数的行为受到更严格的限制。具体地说, unexpected_handler函数可以: · 通过调用terminate()(默认行为)、abort()或exit()来终止程序 · 引发异常 引发异常(第二种选择)的结果取决于unexpected_handler函数所引发的异常以及引发意外异常的函数的异常规范: · 如果新引发的异常与原来的异常规范匹配,则程序将从那里开始进行正常处理,即寻找与新引发的异常匹配的catch块。基本上,这种方法将用预期的异常取代意外异常 · 如果新引发的异常与原来的异常规范不匹配,且异常规范中没有包括std ::bad_exception类型,则程序将调用terminate()。bad_exception是从exception派生而来的,其声明位于头文件exce ption中 · 如果新引发的异常与原来的异常规范不匹配,且原来的异常规范中包含了std::bad_exception类型,则不匹配的异常将被std::bad_exception异常所取代
· 捕获所有异常的方法
首先确保异常头文件的声明可用: #include <exception> using namespace std;
然后,设计一个替代函数,将意外异常转换为bad_exception异常,该函数的原型如下: void myUnexpected() { throw std::bad_exception(); // or just throw } 仅使用throw,而不指定异常将导致重新引发原来的异常。然而,如果异常规范中包含了这种类型,则该异常将被bad_exception对象所取代
接下来在程序的开始位置,将意外异常操作指定为调用该函数: set_unexpected(myUnexpected);
最后,将bad_exception类型包括在异常规范中,并添加如下catch块序列: double Argh(double, double) throw(out_oof_bounds, bad_exception); ... try { x = Argh(a, b); } catch (out_of_bounds& ex) { ... } catch (bad_exception& ex) { ... }
15.3.11 有关异常的注意事项
1. 异常与程序设计
从前面关于如何使用异常的讨论可知,应在设计程序时就加入异常处理功能,而不是以后再添加
缺点:使用异常会增加程序代码,降低程序的运行速度
2. 异常与模板
异常规范不适用于模板,因为模板函数引发的异常可能随特定的具体化而异
3. 异常与动态内存分配
异常和动态内存分配并非总能协同工作
案例一
void test1(int n) { string mesg("I'm trapped in an endless loop"); ... if (oh_no) throw exception(); ... return; } string类采用动态内存分配。通常,当函数结束时,将为mesg调用string的析构函数。虽然throw语句过早地终止了函数,但它仍然使得析构函数被调用,这要归功于栈解退。因此在这里,内存被正确地管理
案例二
void test2(int n) { double* ar = new double[n]; ... if (oh_no) throw exception(); ... delete[] ar; return; } 这里有个问题。解退栈时,将删除栈中的变量ar。但函数过早的终止意味着函数末尾的delete[]语句被忽略。指针消失了,但它指向的内存块未被释放,并且不可访问。总之,这些内存被泄漏了.
解决方案
方案一
在引发异常的函数中捕获该异常,在catch块中包含一些清理代码,然后重新引发异常: void test3(int n) { double* ar = new double[n]; ... try { if (oh_no) throw exception(); } catch(exception& ex) { delete[] ar; throw; } ... delete[] ar; return; } 然而,这将增加疏忽和产生其他错误的机会
方案二
使用智能指针模板之一
4. 总结
总之,虽然异常处理对于某些项目极为重要,但它也会增加编程的工作量、增大程序、降低程序的速度。另一方面,不进行错误检查的代价可能非常高
15.5 类型转换运算符
1. dynamic_cast
(1)语法
dynamic_cast < type-name > (expresion)
(2)用处
假设High和Low是两个类,而ph和pl的类型分别为High *和Low *,则仅当Low是High的可访问基类(直接或间接)时,下面的语句才将一个Low*指针赋给pl: pl = dynamic_cast<Low*> ph; 否则,该语句将空指针赋给pl
2. const_cast
(1)语法
const_cast < type-name > (expresion)
(2)用处
const_cast运算符用于执行只有一种用途的类型转换,即改变值为const或volatile。它只是修改指针或引用的const权限,若用来修改const值,则结果是不确定的(未定义行为,取决于编译器)
3. static_cast
(1)语法
static_cast < type-name > (expresion)
(2)用处
仅当type_name可被隐式转换为expression所属的类型或expression可被隐式转换为type_name所属的类型时,上述转换才是合法的,否则将出错
(3)说明
假设High是Low的基类,而Pond是一个无关的类,则从High到Low的转换、从Low到High的转换都是合法的,而从Low到Pond的转换是不允许的: High bar; Low blow; ... High* pb = static_cast<High*>(&blow); // valid upcast Low* pl = static_cast<Low*>(&bar); // valid downcast Pond* pmer = static_cast<Pond*>(&blow); // invalid, Pond unrelated 第一种转换是合法的,因为向上转换可以显示地进行。第二种转换是从基类指针到派生类指针,在不进行显示类型转换的情况下,将无法进行。但由于无需进行类型转换,便可以进行另一个方向的类型转换,因此使用static_cast来进行向下转换是合法的。 同理,由于无需进行类型转换,枚举值就可以被转换为整型,所以可以用static_cast将整型转换为枚举值。同样,可以使用static_cast将double转换为int、将float转换为long以及其他各种数值转换
4. reinterpret_cast
(1)语法
reinterpret_cast < type-name > (expresion)
(2)用处
reinterpret_cast运算符用于天生危险的类型转换。它不允许删除const,但会执行其他令人生厌的操作。有时程序员必须做一些依赖于实现的、令人生厌的操作,使用reinterpret_cast运算符可以简化对这种行为的跟踪工作。 下面是一个使用示例: struct dat {short a; short b;}; long value = 0xA224B118; dat* pd = reinterpret_cast<dat*>(&value); cout << hex << pd->a; // display first 2 bytes of value 通常,这样的转换适用于依赖于实现的底层编程技术,是不可移植的。例如,不同系统在存储多字节整型时,可能以不同的顺序存储其中的字节
(3)说明
reinterprete_cast运算符并不支持所有的类型转换。例如,可以将指针类型转换为足以存储指针表示的整型,但不能将指针转换为更小的整型或浮点型。另一个限制是,不能将函数指针转换为数据指针,反之亦然
在C++中,普通类型转换也受到限制。基本上,可以执行其他类型转换可执行的操作,加上一些组合,如static_cast或reinterpret_cast后跟const_cast,但不能执行其他转换。因此,下面的类型转换在C语言中是允许的,但在C++中通常不允许,因为对于大多数C++实现,char类型都太小,不能存储指针: char ch = char(&d); // type cast #2 - convert address to a char 这些限制是合理的,如果您觉得这种限制难以忍受,可以使用C语言
15.4 RTTI
15.4.1 RTTI的用途
RTTI是运行阶段类型识别(Runtime Type Identification)的简称。RTTI旨在为程序在运行阶段确定对象的类型提供一种标准方式
15.4.2 RTTI的工作原理
概述
C++有3个支持RTTI的元素
· dynamic_cast运算符
如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该运算符返回0——空指针
· typeid运算符
typeid运算符返回一个指出对象的类型的值
· type_info
type_info结构存储了有关特定类型的信息
只能将RTTI用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针
1. dynamic_cast运算符
(1)用于指针
该运算符的用法如下,其中pg指向一个对象: Superb* pm = dynamic_cast<Superb*>(pg); 这提出了这样的问题:指针pg的类型是否可被安全地转换为Superb*?如果可以,运算符将返回对象的地址,否则返回一个空指针
(2)用于引用
没有与空指针对应的引用值,因此无法使用特殊的引用值来指示失败。当请求不正确时,dynamic_cast将引发类型为bad_cast的异常,这种异常是从exception类派生而来的,它是在头文件typeinfo中定义的。因此,可以像下面这样使用该运算符,其中rg是对Grand对象的引用: #include <typeinfo> // for bad_cast ... try { Superb& rs = dynamic_cast<Superb&>(rg); ... } catch (bad_cast&) { ... };
(3)注意
通常,如果指向的对象(*pt)的类型为Type或者是从Type直接或间接派生而来的类型,则下面的表达式将指针pt转换为Type类型的指针: dynamic_cast<Type*>(pt) 否则,结果为0,即空指针
即使编译器支持RTTI,在默认情况下,它也可能关闭该特性。如果该特性被关闭,程序可能仍能够通过编译,但将出现运行阶段错误。在这种情况下,您应查看文档或菜单选项
2. typeid运算符和type_info类
(1)typeid介绍
typeid运算符使得能够确定两个对象是否为同种类型。它与sizeof有些相像,可以接受两种参数: · 类名 · 结果为对象的表达式
(2)typeid用法
typeid运算符返回一个对type_info对象的引用,其中,type_info是在头文件typeinfo(以前为typeinfo.h)中定义的一个类。type_info类重载了==和!=运算符,以便可以使用这些运算符来对类型进行比较。例如,如果pg指向的是一个Magnificent对象,则下述表达式的结果为bool值true,否则为false: typeid(Magnificent) == typeid(*pg) 如果pg是一个空指针,程序将引发bad_typeid异常。该异常类型是从exception类派生而来的,是在头文件typeinfo中声明的
(3)type_info
type_info类的实现随厂商而异,但包含一个name()成员,该函数返回一个随实现而异的字符串:通常(但并非一定)是类的名称。例如,下面的语句显示指针pg指向的对象所属的类定义的字符串: cout << "Now processing type " << typeid(*pg).name() << ".\n";
3. 误用RTTI的例子
1. 举例
Grand * pg; Superb * ps; for (int i = 0; i < 5; i++) { pg = GetOne(); pg->Speak(); if( ps = dynamic_cast<Superb *>(pg)) ps->Say(); } 通过放弃dynamic_cast和虚函数,而使用typeid,可以将上述代码重新编写为: Grand * pg; Superb * ps; for (int i = 0; i < 5; i++) { pg = GetOne(); if (typeid(Magnificent) == typeid(*pg)) { pm = (Magnificent*) pg; pm->Speak(); pm->Say(); } else if (typeid(Superb) == typeid(*pg)) { pm = (Superb*) pg; pm->Speak(); pm->Say(); } else pg->Speak(); } 上述代码不仅比原来的更难看、更长,而且显式地指定各个类存在 严重的缺陷.下面的语句适用于所有从Grand派生而来的类: pg->Speak(); 而下面的语句适用于所有从Superb派生而来的类: if (ps == dynamic_cast<Superb*>(pg)) ps->Say();
2. 提示
如果发现在扩展的if else语句系列中使用了typeid,则应考虑是否应该使用虚函数和dynamic_cast
15.2 嵌套类
概述
在另一个类中声明的类被称为嵌套类(nested class),它通过提供新的类型类作用域来避免名称混乱。包含类的成员函数可以创建和使用被嵌套类的对象;而仅当声明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符(然而,旧版本的C++不允许嵌套类或无法完全实现这种概念)
由于结构是一种其成员在默认情况下为公有的类,所以Node实际上是一个嵌套类,但该定义并没有充分利用类的功能。具体地说,它没有显式构造函数
15.2.1 嵌套类的访问权限(*)
15.2.2 模板中的嵌套(*)
15.1 友元
15.1.1 友元类
概述
类并非只能拥有友元函数,也可以将类作为友元。在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员
1. 案例
假定需要编写一个模拟电视机和遥控器的简单程序。决定定义一个Tv类和一个Remote类,来分别表示电视机和遥控器。很明显,这两个类之间应当存在某种关系,但是什么样的关系呢?遥控器并非电视机,反之亦然,所以公有继承的is-a关系并不适用。遥控器也非电视机的一部分,反之亦然,因此包含或私有继承和保护继承的has-a关系也不适用。事实上,遥控器可以改变电视机的状态,这表明应将Romote类作为Tv类的一个友元
2. 友元声明可以位于公有、私有或保护部分,其所在的位置无关紧要
3. 说明
(1)由于Remote类提到了Tv类,所以编译器必须了解Tv类后,才能处理Remote类,为此,最简单的方法是首先定义Tv类。也可以使用前向声明(forward delaration)
(2)很多方法都使用条件运算符在两种状态之间切换: void onoff() { state = (state == On)? Off : On; } 如果两种状态值分别为true(1)和false(0),则可以结合使用按位异或和赋值运算符(^=)来简化上述代码: void onoff() { state ^= 1; }
class Tv { public: friend class Remote; // Remote can access Tv private parts enum {Off, On}; enum {MinVal,MaxVal = 20}; enum {Antenna, Cable}; enum {TV, DVD}; Tv(int s = Off, int mc = 125) : state(s), volume(5), maxchannel(mc), channel(2), mode(Cable), input(TV) {} void onoff() {state = (state == On)? Off : On;} bool ison() const {return state == On;} bool volup(); bool voldown(); void chanup(); void chandown(); void set_mode() {mode = (mode == Antenna)? Cable : Antenna;} void set_input() {input = (input == TV)? DVD : TV;} void settings() const; // display all settings private: int state; // on or off int volume; // assumed to be digitized int maxchannel; // maximum number of channels int channel; // current channel setting int mode; // broadcast or cable int input; // TV or DVD }; class Remote { private: int mode; // controls TV or DVD public: Remote(int m = Tv::TV) : mode(m) {} bool volup(Tv & t) { return t.volup(); } bool voldown(Tv & t) { return t.voldown(); } void onoff(Tv & t) { t.onoff(); } void chanup(Tv & t) { t.chanup(); } void chandown(Tv & t) { t.chandown(); } void set_chan(Tv & t, int c) { t.channel = c; } void set_mode(Tv & t) { t.set_mode(); } void set_input(Tv & t) { t.set_input(); } };
15.1.2 友元成员函数
1. 可以选择仅让特定的类成员成为另一个类的友元,而不必让整个类成为友元,但这样做稍微有点麻烦,必须小心排列各种声明和定义的顺序。让Remote::set_chan()成为Tv类的友元的方法是,在Tv类声明中将其声明为友元: class Tv { friend void Remote::set_chan(Tv& t, int c); ... }; 然而,要使编译器能够处理这条语句,它必须知道Remote的定义。否则,它无法知道Remote是一个类。而set_chan是这个类的方法。这意味着应将Remote的定义放到Tv的定义前面。Remote的方法提到了Tv对象,而这意味着Tv定义应当位于Remote定义之前。避开这种循环依赖的方法是,使用前向声明(forward declaration)。为此,需要在Remote定义的前面插入下面的语句: class Tv; // forward declaration
2. 还有一个麻烦。程序清单15.1的Remote声明包含了内联代码,例如: void onoff(Tv& t) { t.onoff(); } 由于这将调用Tv的一个方法,所以编译器此时必须已经看到了Tv类的声明,这样才能知道Tv有哪些方法,但正如看到的,该声明位于Remote声明的后面。这种问题的解决方法是,使Remote声明中只包含方法声明,并将实际的定义放在Tv类之后。这样,排列顺序将如下: class Tv; // forward declaration class Remote { ... }; // Tv-using methods as prototypes only class Tv { ... }; // put Remote method definitions here 通过在方法定义中使用inline关键字,仍然可以使其成为内联方法
3. 内联函数的链接性是内部的,这意味着函数定义必须在使用函数的文件中。在这个例子中,内联定义位于头文件中,因此在使用函数的文件中包含头文件可确保将定义放在正确的地方。也可以将定义放在实现文件中,但必须删除关键字inline,这样函数的链接性将是外部的. 顺便说一句,让整个Remote类成为友元并不需要前向声明,因为友元语句本身已经指出Remote是一个类: friend class Remote;
15.1.3 其他友元关系
需要记住的一点是,对于使用Remote对象的Tv方法,其原型可在Remote类声明之前声明,但必须在Remote类声明之后定义,以便编译器有足够的信息来编译该方法
15.1.4 共同的友元
需要使用友元的另一种情况是,函数需要访问两个类的私有数据。从逻辑上看,这样的函数应是每个类的成员函数,但这是不可能的。它可以是一个类的成员,同时是另一个类的友元,但有时将函数作为两个类的友元更合理