友元类、嵌套类、异常、运行阶段类型识别(RTTI)、类型转换运算符dynamic_cast, static_cast, const_cast, reiterpret_cast
1.友元类
1)格式 p489
friend class ClassName;
友元类的所有方法都可以访问原始类的私有成员和保护成员。
2)例子:Tv 类和 Remote (遥控器)类
将 Remote 类声明为 Tv 类的友元类;这同时意味着 Remote 类中有关于 Tv 类的代码,因此编译器必须先了解 Tv 类后,才能处理 Remote 类,因此先定义 Tv 类:
class Tv { public: friend class Remote; // Remote can access Tv private parts ... private: int channel; ... }; class Remote { public: void set_chan(Tv & t, int c) {t.channel = c;} // Remote 类中可以直接访问 Tv 类的私有成员 ... };
3)可以选择让特定的类成员成为另一个类的友元,而不必让整个类成为友元 -- 使用前向声明(forward declaration)p492
在上述例子中,唯一直接访问 Tv 成员的 Remote 方法是 Remote::set_chan(),因此它是唯一需要作为友元的方法;可以将该函数设置为 Tv 类的友元,但需要注意各种声明和定义的顺序:
让 Remote::set_chan() 成为 Tv 类的友元,就需要在 Tv 类中将其声明为友元:
class Tv { friend void Remote::set_chan()(Tv & t, int c); ... };
要是编译器能够处理这条语句,编译器必须知道 Remote 类的定义,因此 Remote 类的定义需要放在 Tv 类的前面;
但是 Remote 类中的 set_chan() 方法用到了 Tv 类中的成员 channel,因此 Tv 类也应在 Remote 类之前定义;
避开这种循环依赖的方法是使用前向声明,在 Remote 定义的前面插入下面的语句:
class Tv; //froware declaration class Remote {...}; class Tv {...};
注意,不能使用这样的顺序:
class Remote; //forward declaration class Tv {...}; class Remote {...};
因为编译器在 Tv 类的声明中看到 Remote 的 set_chan() 方法被声明为 Tv 类的友元之前,应该先看到 Remote 类的声明和 set_chan() 方法的声明。
使用正确的顺序后,还需要注意:
如果此时 Remote 类中有 内联方法代码:
class Remote { ... public: void onoff(Tv & t) {t.onoff();} void set_chan(Tv & t, int c) {t.channel = c;} ... };
这将调用 Tv 的 onoff() 方法以及使用 Tv 的私有成员 channel,所以此时编译器必须已经看到了 Tv 类的声明,这样才能知道 Tv 有哪些方法和成员,但是 Tv 类的声明在 Remote 类的声明的后面;
解决该问题的方法是,使 Remote 声明中只包含方法声明,并将实际的定义放在 Tv 类之后;这样,排列顺序如下:
class Tv; class Remote //仅包含方法的声明 { ... public: void onoff(Tv & t); void set_channel(Tv & t, int c); ... }; class Tv {...};
//将 Remote 类中方法的定义放在此处,通过在方法定义中使用 inline 关键字,仍然可以使其成为内联方法
注意,让整个 Remote 类成员友元并不需要前向声明,因为友元语句本身已经指出 Remote 是一个类:p493
friend class Remote;
4)两个类互为友元类 p494
5)某个成员为两个类共同的友元 p495
2.嵌套类 p495
C++ 中可以将类声明放在另一个类中;在另一个类中声明的类被称为嵌套类。
包含类 的成员函数可以创建和使用被嵌套类的对象;仅当声明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。
对类进行嵌套不创建类成员,仅仅是定义了一种类型,该类型(如果不是在共有部分声明)仅在包含嵌套类声明的类总有效。
3.异常
1)若出现了除以 0 等异常情况,编译器的处理方法:p500
- 调用 abort() 函数。abort() 函数原型位于头文件 csdlib(或 stdlib.h)中,其典型实现是向标准错误流(即 cerr 使用的错误流)发送消息 abnormal program termination,然后终止程序;调用 abort() 函数时将直接终止程序,而不是先返回到 main()。
- 返回错误码。
- 异常机制
2)异常机制
异常提供了将控制权从程序的一个部分传递到另一个部分的途径。对异常的处理有 3 个组成部分:
- 引发异常
- 使用处理程序捕获异常
- 使用 try 块
其中:
try 块标识后可跟一个多个 catch 块。 p502
throw 关键字表示引发异常,紧跟随其后的值(如字符串或对象)指出了异常的特征,如 p502
int main() { ... try { z = hmean(x, y); } catch (const char * s) // 将 throw 后的字符串的地址赋给指针 s { cout << s <<endl; ... } ... double hmean(double a, double b) { if (a == -b) throw "bad hmean() arguments: a = -b not allowed"; return 2.0 * a * b / (a + b); } ... }
异常类型可以是字符串,或者是其他 C++ 类型;通常为类类型。 p503
注意,throw 不是将控制权返回给调用函数,而是导致程序沿函数调用序列后退,直到找到包含 try 块的函数。在上述列子中,throw 将程序控制权返回给 main();程序将在 main() 中寻找与引发的异常类型匹配的异常处理程序(位于 try 块后面的 catch 块)。p503
如果函数引发了异常,但没有 try 块或没有匹配的处理程序时,程序最终将调用 abort() 函数。
3)栈解退 p506
在处理函数调用时,程序将调用函数的指令的地址(返回地址)放在栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始继续执行。
如果函数由于出现了异常而终止,程序将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,知道找到一个位于 try 块中的返回地址;随后,控制权将转到块尾的异常处理程序。这个过程被成为栈解退。
栈解退机制的一个非常重要的特性是,和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用。
函数返回仅仅处理该函数放在栈中的对象,throw 语句处理 try 块和 throw 之间整个函数调用序列放在栈中的对象。
4)throw 语句返回控制权 p510
throw 语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的 try-catch 组合。
此外
try { hm = hmean(a, b); ... } catch (bad_hmean & bg) { bg.mesg(); cout << "Cayght in means() \n"; throw; //rethrows the exception in hmean(a, b) }
catch 中的 throw; 语句用来将捕获到的 hmean(a, b) 中的异常再次向上传递。p509
5)
引发异常时编译器总是创建一个临时拷贝。p510
class problem {...}; ... void super() throw (problem) //异常规范 p506 { ... if (oh_no) { problem oops; throw oops; //引发异常,编译器创建一个临时拷贝 ... } ... try { super(); } catch (problem & p) // p 指向 oops 的副本而不是 oop 本身 { ... }
catch 后面使用 problem & p 而不是 problem p 的原因是,基类引用可以指向派生类对象。因此使用基类引用能够捕获任何异常对象。
6)exception 类 p511
exception 头文件定义了 exception 类,可以把它用作其他异常类的基类。exception 类中有一个名为 what() 的虚函数,它返回一个字符串,该字符串的特征随实现而异;因为它是一个虚函数,因此可以在从 exception 类派生而来的类中重新定义它:
#include <exception> ... class bad_hmean : public exception { public: const char * what() {return "bad arguments to hmean()";} ... }; class bad_gmean : public exception { public: const char * what() {return "bad arguments to gmean()";}\ ... };
基于 exception 的异常类型:
a. stdexcept 异常类 p512
头文件 stdexcept 定义了一些异常类;首先,该文件定义了 logic_error 和 runtime_error 类,它们都是以公有方式从 exception 派生而来的
class logic_error : public exception { public: explicit logic_error(const string & what_arg); ... }; class domain_error : public logic_error { public: expolicit domain_error(const string & what_arg); ... };
注意,这些类的构造函数都接受一个 string 对象作为参数,该参数提供了方法 what() 以 C 风格字符串方式返回的字符数据。p512
异常类 logic_error 描述了典型的逻辑错误。logic_error 派生的每个类的名称指出了它们用于报告的错误类型
- domain_error
- invalid_argument
- length_error
- out_of_bounds
每个类都有一个类似于 logic_error 的构造函数,使得我们能够提供一个供方法 what() 返回的字符串。
runtime_error 类派生的每个类描述了运行期间发生的错误
- range_error
- overflow_error
- underflow_error
b. bad_alloc 异常和 new p513
对于使用 new 导致的内存分配问题,C++ 的最新处理方式是让 new 引发 bad_alloc 异常。
c. 空指针 和 new p514
C++ 标准也提供了一种在 new 导致的内存分配问题 失败时返回空指针的 new。
7)异常、类和继承 p514
8)意外异常(带异常规范的函数产生的异常与规范列表中的异常不匹配)和未捕获异常 p517
9)动态分配和异常 p519
void test2(int n) { double * ar = new double(n); ... if(oh_no) throw exception(); ... delete [] ar; return; }
上述代码中,进行栈解退时,将删除栈中的变量 ar;但函数过早地终止意味着函数末尾的 delete[] 语句不会被执行,因此 ar 指向的内存块并未被释放,产生了内存泄漏。可以进行如下改进:
void test3(int n) { double * ar = new double(n); ... try { if (oh_no) throw exception(); } catch (exception & ex) { delete [] ar; throw; //重新 throw 原异常 } ... delete [] ar; return;
另一种方法是使用 智能指针模板(ch16)。
4.运行阶段类型识别(Runtime Type Identification)p520
C++ 中支持 RTTI 的 3 个元素:
- 如果可能的话,dynamic_cast 运算符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该运算符返回 0——空指针。
- typeid 运算府返回一个指出对象的类型的类型的值。
- type_info 结构存储了有关特定类型的信息。
只能将 RTTI 用于包含虚函数的类层次结构,因为只有对于这种类层次结构,才应该将派生对象的指针赋给基类指针。
1)dynamic_cast 运算符
dynamic_cast 运算符能够回答“是否可以安全地将对象的地址赋给特定类型的指针”。
语法:
Superb * bm = dynamic_cast<Superb *>(pg);
其中 pg 指向一个对象。
上述语句表明:指针 pg 的类型是否可以安全地被转换为 Superb *? 如果可以,运算符将返回对象的地址,否则返回一个空指针。
例如,对于该语句:
dynamic_cast<Type *>(pt);
通常,如果指向的对象(*pt)的类型为 Type 或者是从 Type 直接或间接派生而来的类型,则上面的表达式将指针pt转换为 Tpye 类型的指针;否则,结果为0,即空指针。
也可以将 dynamic_cast 用于引用,例如:
Superb & rs = dynamic_cast<Superb &>(rg);
但没有与空指针对应的引用值,因此无法使用特殊的引用值来指示失败。当请求不正确时,dynamic_cast 将引发类型为 bad_cast 的异常;该异常是从 exception 类派生而来的,它是在头文件 typeinfo 中被定义的。 p523
2)typeid 运算符和 type_info 类 p524
typeid 运算符能够确定两个对象是否为同种类型,它可以接受两种参数:
- 类名
- 结果为对象的表达式
typeid 运算符返回一个对 type_info 对象的引用,而 type_info 类重载了 == 和 != 运算符,因此可以使用这些运算符来对类型进行比较。
例如,如果 pg 指向的是一个 Magnificent 对象,则下述表达式的结果为 bool 值 true,否则为 false:
typeid(Magnificent) == typeid(*pg);
如果 pg 指向的是一个空指针,程序将引发 bad_typeid 异常。
type_info 类包含一个 name() 成员,该函数返回一个随实现而异的字符串:通常是类的名称。如,下面的语句显式指针 pg 指向的对象所属的类定义的字符串
cout << "Now processing type " << typeid(*pg).name() << ".\n";
5.类型转换运算符 p526
- dynamic_cast
- const_cast
- static_cast
- reinterpret_cast