Chapter 15 友元异常和其他

本章内容包括:

  • 友元类
  • 友元类方法
  • 嵌套类
  • 引发异常、try块和catch块
  • 异常类
  • 运行阶段类型识别(RTTI)
  • dynamic_cast和typeid
  • static_cast、 const_cast、和reiterpret_cast

15.1 友元

15.1.1 友元类

友元类表示一种相互作用的关系,既不是is-a关系,也不是has-a关系。
友元类声明:friend class class-name;
友元声明可以位于公有、私有或保护部分,其所在的位置无关紧要。
如果两种状态值分别为true(1)和false(0),可以使用按位异或和赋值运算符来简化代码。
类友元是一种自然用语,用于表示一些关系。

15.1.2 友元成员函数

友元类的大部分方法使用公有接口可以实现,有一两个方法需要直接访问另一个类的成员。可以选择让特定的类成员称为另一个类的友元。
为了使编译器可以处理友元成员的语句,需要使用前向声明。
class class-name // forward declaration
称为友元类的方法只提供声明,在另一个类结尾处可以提供内联函数的定义,要提供非内联函数的定义,需要在另一个类的方法实现文件中。

15.1.3 其他友元关系

对于两个互相交互的类,可以让类彼此成员对方的友元来实现。

15.1.4 共同的友元

需要使用友元的另一种情况是,函数需要访问两个类的私有数据,要实现该方法,可以让该方法是一个类的成员,是另一个类的友元。

15.2 嵌套类

在C++中,可以将类声明放在另一个类中。在另一个类中声明的类被称为嵌套类(nested class)。包含类的成员函数可以创建和使用被嵌套类的对象。仅当嵌套类的声明位于公有部分时,才能在包含类的外面使用嵌套类。
对类进行嵌套与包含并不同。

  • 包含意味着将类对象作为另一个类的成员
  • 对类进行嵌套不创建类成员,而是定义了一种类型,该类型仅在包含嵌套类声明的类中有效。

15.2.1 嵌套类和访问权限

有两种权限适合于嵌套类,分别是作用域和访问控制:

  • 嵌套类的声明位置决定了嵌套类作用域,他决定包含类的外部或者派生类是否可以创建嵌套类的对象。
  • 访问控制,嵌套类的公有部分、保护部分和私有部分控制了对类成员的访问。

1.作用域
嵌套类在包含类的私有部分声明的,则只有包含类可以创建和使用该对象。派生和公有的情况按情况分析即可。
嵌套类、结构和枚举的作用域特征

声明位置 包含的类是否可以使用它 从包含它的类派生而来的类是否可以使用它 在外部是否可以使用
私有部分
保护部分
公有部分

2.访问控制
类可见后,起作用的是访问控制。嵌套类访问权的控制规则与常规类相同。

15.3 异常

程序有时运行阶段遇到错误,会导致程序无法正常的运行下去。C++异常为处理该情况提供了一种功能强大而灵活的工具。

15.3.1 调用abort()

abort()函数原型位于cstdlib头文件中。其典型实现是像标准错误流(cett使用的错误流)发送消息abnormal program termination(程序异常终止),终止该程序。
abort()是否刷新文件缓冲区(用于存储写到文件中的数据内存区域)取决于实现。
如果愿意,也可以使用exit(),该函数刷新文件缓冲区,但不显式消息。

15.3.2 返回错误码

使用函数的返回值来指出问题。将返回值重新定义bool,利用地址或引用接受答案。(或者使用一个全局变量)

15.3.3 异常机制

对异常的处理有三个组成部分:

  • 引发异常
  • 使用处理程序捕获异常
  • 使用try()块。

throw关键字表示引发异常,紧随其后的值(字符串或对象)指出了异常的特征。
程序使用异常处理程序表示捕获异常,处理程序以关键字catch开头,其后括号内的类型声明为异常标签,当异常标签与throw后的异常标签一致时,运行该catch块的程序。
try块表示其中特定的异常可能被激活的代码块,它后面跟有一个或多个catch块。

15.3.4 将对象用作异常类型

通常,引发异常的函数将传递一个对象。这样做的优点是可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。

15.3.5 异常规范和C++11

C++将异常规范摒弃了。
知道异常规范的样子如下所示:
double harm(double a) throw(bad_thing); // may throw bad_thing exception
double marm(double) throw(); // doesn't throw an exception
throw()部分就是异常规范,它可能出现在函数原型和函数定义中,可包含类型列表,也可不包含。
异常规范的作用之一是告知用户可能需要使用try块,然和这项工作也可使用注释轻松完成。
异常规范的另一个作用是让编译器添加执行运行阶段检查的代码,检查是否违反了异常规范,这很难检查。
C++11也建议您忽略异常规范。C++11支持一种特殊的异常规范,新增关键字noexcept指数函数不会引发异常,运算符noexcept()判断操作数是否会引发异常。

15.3.6 栈解退

假设try没有直接调用引发异常的函数,而是调用了对异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到包含try块和处理程序的函数。这涉及到栈解退。
现假设函数由于出现异常(而不是返回)终止,程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址。
程序进行栈解退以回到能够捕获异常的地方,将释放栈中的自动存储型变量。如果是类对象,将为该对象调用析构函数。

15.3.7 其他异常特性

try-catch机制类似于函数返回机制,不同之处是throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合。
另一个不同之处,引发异常时编译器总是创建一个临时拷贝,即使用异常规范和catch块中指定的是引用。
这里使用引用作为返回值利用了引用的另一个特征,基类引用可以执行派生类对象。假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它可以于任何对象匹配。
**如果有一个异常类继承层次结构,应该这样排列catch块;将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面。
方法是使用省略号来表示异常类型,从而捕获任何异常。
catch(...) { // statements } // catches any type exception

15.3.8 exception类

C++异常的主要目的是为设计容错程序提供语言级支持。C++在头文件exception中定义了exception类,C++可以把它用作其他异常类的基类。exception类有一个名为what()的虚拟成员函数,返回一个字符串,该字符串的特征随实现而异。
1.stdexcept异常类
头文件stdexcept定义了其他几个异常类。首先定义了logic_error和runtime_error类,它们都是以公有方式从exception派生而来。
这两个类的构造函数接受一个string对象作为参数,该参数提供了方法what()以C-风格字符串方式返回的字符数据。
logic_error描述了典型的逻辑错误,可以派生出以下类报告的错误类型:

  • domain_error
  • invalid_argument
  • length_error
  • out_of_bounds

数学函数有定义域domain和值域range,可以让不在定义域的参数引发domain_error异常。
给函数传递了一个意料之外的值,可以引发invalid_argument异常。
length_error异常之处没有足够空间来完成所需操作
out_of_bounds通常用于指示索引错误。

runtime_error异常系列描述了可能在运行期间发生难以预计和方法的错误。每个类的名称指出了它用于报告的错误类型:

  • range_error
  • overflow_error
  • underflow_error

下溢(underflow)错误在浮点计算中,存在浮点类型可以表示的最小非零值,计算结果比这个值还小时将导致下溢错误。整形和浮点型都可能发生上溢错误。计算结果可能不在函数允许的范围内,但没有发生上溢或下溢错误时用range_error。
一般而言,logic_error系列异常表明存在可以通过编程修复的问题,而runtime_error系列异常表明存在无法避免的问题。

2.bad_alloc异常和new
对于使用new导致的内存分配问题,C++的最新处理方式是让new引发bad_alloc异常。如果new超出了内存,what将会返回"std::bad_alloc"

3.空指针和new
在new失败是返回空指针时编写的,C++提供了一种失败时返回空指针的new,其用法如下:
int * pi = new (std::northrow) int

15.3.9 异常、类和继承

异常、类和继承以三种方式相互关联:

  • 从一个异常类派生出另一个异常类
  • 在类定义中嵌套异常类来组合异常
  • 嵌套声明本身可继承,还可以用作基类

在C++11中,exception的构造函数没有使用异常规范。

15.3.10 异常何时会迷失方向

异常引发后,在两种情况下会导致问题:

  • 如果在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配,否则称为意外异常。在默认情况下,这将导致程序异常终止。
  • 如果异常不是在函数中引发的(或者函数没有异常规范),则必须捕获它,则称为未被捕获异常,在默认情况下,这也将导致程序终止。

可以修改程序对意外异常和未捕获异常的反应。
未捕获异常发生时,程序首先调用terminate()函数,默认情况下调用abort()函数,程序终止,可以指定terminate()应调用的函数来修改terminate()的行为。使用set_terminate()函数,样例如下:

	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称为这样一种类型的名称:指向没有参数和返回值的函数的指针。直到应捕获哪些异常很有帮助,因此默认情况下,未捕获的异常将导致程序异常终止。

发生意外异常,程序将调用unexcept()函数。这个函数将调用terminate(),后者在默认情况下将调用abort()。存在一个可以修改unexcept()行为的函数。

	typedef void (*unexpected_handler) ();
	unexpected_handler set_unexpected(unexpected_handler f) throw();	// C++98
	unexpected_handler set_unexcepted(unexpected_handler f) noexcept;	// C++11
	void unexpected();				// C++98
	void unexpected() noexcept;		// C++11

set_unexcept()的函数的行为受到更严格的限制。unexcepted_handler函数可以:

  • 通过调用terminate()(默认行为)、abort()或exit()来终止程序;
  • 引发异常。

引发异常(第二种选择)的结果取决于unexcepted_handler函数所引发的异常以及引发意外异常的函数的代码规范:

  • 如果新引发的异常与原来的异常规范匹配,则程序将从那里开始进行正常处理。属于用语气的异常取代以为异常
  • 如果新引发的异常与原来的异常规范不匹配,且异常规范中没有包含std::bad_exception类型,则程序将调用terminate().
  • 如果新引发的异常与原来的异常规范不匹配,且原来的异常规范中包含了std::bad_exception类型,则不匹配异常用std::bad_exception取代。

如果要捕获所有异常(不管语气异常,还是意外异常),可以这样做:

  • 确保异常头文件可用
#include <exception>//</exception>
using namespace std;
  • 设计一个替代函数,将意外异常转换为bad_exception异常,函数的原型如下:
void myUnexcepted()
{
	throw std::bad_exception();			// or just throw;
}

仅使用throw,不指定异常会引发原来的异常。

  • 在程序的开始位置,将意外操作指定为调用该函数:
    set_unexcepted(myUnexcpeted);

15.3.11 有关异常的注意事项

应在设计程序使就加入异常处理功能,而不是以后再添加。这样做有些缺点,例如增加程序代码,降低程序的运行速度。
异常规范不适用于模板,模板函数引发的异常随特定具体化而异。
异常和动态内存分配并非总能协同工作。
异常处理对于某些项目极为重要,但它也会增加编程的工作量,增大程序、降低程序的速度。
异常处理
需要不断了解库的复杂性:什么异常将被引发,它们发生的原因和时间,如何处理它们等等。
通过库文档和源代码了解异常和错误处理细节。

15.4 RTTI

RTTI使运行阶段类型识别(Runtime Type Identification)的简称。这是新添加的C++中的特性之一。

15.4.1 RTTI的用途

假设有一个类层次结果,其中的类都是从同一个基类派生而来的,则可以让基类指针指向其中任何一个类的对象。如何知道指针指向的是哪种对象呢?

15.4.2 RTTI的工作原理

C++有3个支持RTTI的元素

  • dynamic_cast运算符使用一个指向基类的指针来生成一个指向派生类的指针,如果不能生成,返回0——空指针。
  • typeid运算符返回一个之处对象类型的值
  • typeinfo结构存储了有关特定类型的信息。

RTTI只适用于包含虚函数的类
1.dynamic_cast运算符
用法如下:

Superb * pm = dynamic_cast<Superb *> pg

如果pg可以被安全转换为Superb *,则返回对象的地址,否则返回空指针。
dynamic_cast用于引用,用法稍有不同:没有与空指针对应的引用值,因此无法使用特殊引用值来指示失败,请求不正确会引起bad_cast异常。因此,可以这样使用该运算符:

#include <typeinfo> // for bad_cast
...
try{
	Superb & rs = dynamic_cast<Superb &> (&rg);
	...
}
catch(bad_cast &){
...
}

2.typeid运算符和type_info类
typeid运算符使得能够确定两个对象是否为同种类型。可以接受两种参数:

  • 类名
  • 结果为对象的表达式

type_info类重载了==和!=运算符,以便可以用这些类型来比较。如:
typeid(Magnificent) == typeid(*pg)
如果pg是一个空指针,程序将引发bad_typeid异常。
该异常类型是从exception类派生而来。
type_info类的实现随厂商而异,但包含一个name()成员,显式指针pg指向所属类定义的字符串。

如果发现在拓展的if else语句系列中使用了typeid,则应考虑是否应该使用虚函数和dynamic_cast。

15.5 类型转换运算符

C语言的类型转换运算符太过松散,有好多类型转换没有意义,但C没有对其进行限制。
C++更严格地限制允许地类型转换,并添加4个类型转换运算符,使转换过程更规范:

  • dynamic_cast
  • const_cast
  • static_cast
  • reinterpret_cast

可以根据目的选择一个适合的运算符,而不是使用通用的类型转换。
const_cast可以改变值为const 或者 volatile。提供该运算符的原因使,有时候可能需要一个值在大多数时候是常量,在需要修改它时,使用const_cast。
const_cast可以修改指向一个值的指针,但修改const值的结果是不确定的。

static_cast运算符的语法与其他类型转换运算符相同,使用static_cast仅当type_name可被隐式转换为expression所属的类型或expression可被隐式转换为type_name的所属类型时,上述转换才合法。即可以进行单向隐式转换才合法。
static_cast可以将整形转换为枚举值。

reinterpret_cast运算符用于天生危险的类型转换,它不允许删除const,但可以执行一些依赖于实现的操作,例如将long转换为short,将指针类型转换成足以存储指针的整形。

15.6 复习题

1.下面建立友元的尝试有什么错误?
a.

class snap{
	friend clasp;
	...
};
class clasp {...};

class clasp的声明应该在snap前面,或者使用前向声明。
b.

class cuff{
	public:
		void snip(muff &) { ... }
		...
};
class muff {
	friend void cuff::snip(muff &);
	...
};

class cuff的函数不知道 class muff,需要使用前向声明class muff;
c.

class muff{
	friend void cuff::snip(muff &);
	};
class cuff{
public:
	void snip(muff &) { ... }
	...
	};

错误,class cuff的成员是class muff的友元,因此必须cuff在muff之前。且使用前向声明。

2.您知道了如何建立相互类友元的方法。能够创建一中更为严格的友情关系,即类B只有部分成员是类A的友元,而类A只有部分成员是类B的友元吗?请解释原因?
不可以,这样既要求类A的声明在类B前面,也要求类B的声明在类A前面,因此不可以。

3.下面的嵌套类声明可能存在什么问题?

class Ribs
{
private:
	class Sauce
	{
		int soy;
		int sugar;
	public:
		Sauce(int s1, int s2) : soy(s1), sugar(s2) { }
	};
	...
};

由于嵌套类成员是私有的,因此包含类无法访问嵌套类的成员,导致只能创建该类的对象,无法修改和使用。
4.throw和return之间的区别何在?
throw会利用栈解退找到一个包含引发异常的函数的try块部分地址,而return返回到调用该函数的地址。而且throw会保留一个拷贝异常对象,而return不会。
5.假设有一个从异常基类派生来的异常类层次结构,则应按什么样的顺序放置catch块?
基类引用的catch块放在最后面,而没有派生类的异常引用放在最前面,依次类推。
**应当从子孙到祖先的顺序排列catch语句块。
6.对于本章定义的Grand、Superb和Magnificent类,假设pg为Grand *指针,并将其中某个类的对象地址付给了它,而ps为Superb*指针,则下面两个代码示例的行为有什么不同?

if(ps = dynamic_cast<Superb *>(pg))
	ps->say();
if (typeid(*pg) == typeid(Superb))
	(Superb * pg) pg->sat();

第一个样例可以将Magnificent对象也转换成Superb类对象并进行输出,而第二个样例不可以,他只能转换Superb类对象。
7.static_cast运算符与dynamic_cast运算符有什么不同?
static_cast对于类型而言,只要可以进行单向隐式转换,就可以利用static_cast进行双向转换,而dynamic_cast只允许可以进行的隐式转换。static_cast可以进行枚举类型和整形之间以及数值类型之间的转换。

posted @ 2022-01-07 22:06  Fight!GO  阅读(161)  评论(0编辑  收藏  举报