[C++ Primer Plus] 友元、异常和其他

15.1.1

一,友元类

当一个类需要改变另一个类的状态时,可以类成为另一个类的友元。

友元的声明可以位于公有、私有或者保护部分,其所在的位置无关紧要。如将class Remote 作为 class TV 的友元,由于在类Remote 中肯定会提到类TV ,所以编译器必须在了解TV 类后才能处理Remote 类。最简单的方法就是首先定义TV 类,也可是使用前向声明。

首先定义TV类:

class TV
{
public:
	friend class Remote;
	//...
protected:
private:
};

class Remote
{
public:
protected:
private:
};

如果让Remote的某个方法称为TV类的友元,必须小心排列各种声明和定义的顺序。

使用前向声明:

class TV; // forward declaration

class Remote
{
public:
protected:
private:
};

class TV
{
public:
	friend class Remote;
	//...
protected:
private:
};

此处的前向声明可能还有一个问题 :如果Remote中调用TV的方法,所以编译器此时必须已经看到了TV类的声明,才能知道TV类有哪些方法,但上面样形式,TV在Remote之后声明的。解决方法是:是Remote的声明中只包含方法声明,将方达定义放在TV类只后。这样排序应如下:

class TV; // forward declaration

class Remote
{
};

class TV
{
};

// put Remote method definitions here

如果让整个Remote类成为友元,并不需要前向声明,因为友元语句本身已经指出Remote是一个类

friend class Remote;

15.2

二,嵌套类

在另一个类中声明的类被称为嵌套类。

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

15.2.1

三,嵌套类的访问权限

类的声明位置决定了类的作用域与可见性


15.3

四,异常

try->throw->catch

try块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch块。try关键字后面是一个由花括号括起的代码块,表明需要注意这些代码引发的异常。

throw关键字表示引发异常,紧随其后的值(通常为类类型)指出了异常的特征。

catch关键字表示捕获异常,随后是括号中的类型声明,它指出了异常处理程序要响应的异常类型;然后是一个用花括号括起的代码块,指出要采取的措施。

double hmean(double a, double b)
{
	if (a == -b)
	{
		throw "bad input";
	}
	return a*b/(a+b);
}
int main
{
	double x,y,z;
	try
	{
		z = hmean(x,y)
	}
	catch (const char* e)
	{
		std::cout<<"get out!!!"
		return 0;
	}
};

throw语句类似执行返回语句,因为它也将终止函数执行;但throw不是将控制权返回给调用程序,而是导致程序沿调函数调用序列后退,直到找到包含try块的函数(即throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合)。这将引发栈解退。

执行完try块中的语句后,如果没有引发异常,则跳过try块后面的catch块。

五,异常规范和C++11

double harm(double a) throw(bad_thing); //may throw bad_thing exception
double harm(double a) throw(); //doesn't throw an exception

其中的throw()部分就是异常规范,他可能出现在函数原型和函数定义中,可能包含类型列表,也可不包含。

然而C++11中,可以使用新增的关键字noexcept指出函数不会引发异常

double harm() noexcept; //harm() doesn't throw an exception

15.3.6

六,栈解退

如果函数发生异常而终止,则程序也将释放栈中内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,知道找到一个位于try块中的返回地址。随后控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退。

七,其他异常特性

throw-catch机制类似于函数参数和函数返回返回机制,但还是有些不同:

1,函数func()中的返回语句将控制权返回到调用func()的函数,但throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合。

2,引发异常时,编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。

class problem {...};
...
void super() throw(problem)
{
	...
	if (1)
	{
		problem opps; //construct object
		throw opps; // throw it
		//or use this func
		throw problem(); //construct and throw default problem object 
	}
	...
}

try
{
	super();
};
catch(problem & p)
{
	//statements
}

此处p将指向opp的副本,而不是p本身。这是件好事,因为函数super()执行完毕后,opps将不复存在。

同时此处catch捕捉异常对象的引用还有一个重要特征:基类引用可以执行派生类对象。假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将于任何派生类对象匹配。由于这个特性,当如果有一个异常类继承层次结构,应当这样排列catch块:将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面。

也可以创建捕获对象而不是引用的处理程序。在catch语句中使用基类对象时,将捕获所有的派生类对象,但派生特性将被剥去,因此将统一使用虚方法的基类版本。

可以在不知道异常类型的情况下,捕获异常,方法是:使用省略号来表示异常类型,从而捕获任何异常。

catch(...){//statements}

如果知道一些异常的类型,可以将捕获所有异常的catch块放在最后面,这一点优点类似于switch语句中的default:

try
{
	super();
};
catch(bad_case1 &be)
{//...}
catch(bad_case2 &be)
{//...}
catch(bad_case3 &be)
{//...}
catch(...)
{//statements}

15.3.8

八,exception类

exception头文件定义了exception类,C++可以把它用作其他异常的基类。代码可以引发exception异常,也可以将exception类作为基类。

有一个名为what()的虚拟成员函数,它返回一个字符串,该字符串的特征随实现而异。然而,由于这是一个虚方法,因此可以再exception派生类中重新定义它。

1,stdexcept异常类

定义了,logic_error和runtime_error类,他们都是从exception类公有派生出来的。

这些类的构造函数接受一个string对象作为参数,作为方法what()的返回数据。

logic_error公有派生出4个类:

domain_error;

invalid_argument;

length_error;

out_of_bounds;

runtime_error公有派生出3个类:

range_error;

overflow_error;

underflow_error;

他们之间的主要区别在于:可以根据不同的类名分别处理没种异常。另一方面,由于是继承关系也可以一起处理它们(如果愿意的话)。

2,bad_alloc异常和new

new导致的内存分配问题,C++的处理方式是让new引发bad_alloc异常。

3,空指针和new

15.3.9

九,异常、类和继承

异常、类和继承以3种方式向关联:

1,可以像C++库那样,从一个异常类派生出另一个;

2,可以在类定义中嵌套异常类声明,来组合异常;

3,这种嵌套声明本身可以被继承,还可以用作基类。

class Sales
{
public:

    class bad_index : public std::logic_error
    {
    private:
		///...
    public:
		///...
    };

	///...
private:
	//...
};

class LabeledSales : public Sales
{
  public:

    class nbad_index : public Sales::bad_index //继承基类中的公有成员类,bad_index
    {
    private:
        ///...
    public:
		///...
    };

	///...
private:
    ///...
};

15.3.10

十,异常何时会迷失方向

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

如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配,否则称为意外异常。

如果异常不是在函数中引发的(或者函数没有异常规范),则必须捕获它。如果没有捕获,则异常被称为未捕获异常。

总之,如果要捕获所有的异常(不管事预期异常还是意外异常),则可以这样做:

首先确保异常头文件的声明可用:

#include <exception>
using namespace std

然后设计一个替代函数,将异常转换为bad_exception异常,该函数的原型如下:

void myUnexpected()
{
	throw std::bad_exception();
}

接下来在程序的开始位置,将意外异常操作指定为调用该函数:

set_unexpected(myUnexpected);

最后,将bad_exception类型包括在异常规范中,并添加如下catch块序列:

double Argh(double,double)throw(out_of_range,bad_exception);
...
try
{
	x = Argh(a,b);
}
catch (out_of_range& e)
{
	...
}
catch (bad_exception& e)
{
	...
}

15.4 RTTI(Run-Time Type Identification),运行阶段类型识别

15.4.1

十一,RTTI的用途

假如,有一个类层次结构,其中的类都是从同一个基类派生而来的,利用多态性,则基类指针或者引用可以指向任意一个派生类对象。但如何知道指针指向的是哪种对象呢?可能有三种情况:

1,该类层次结构中所有的成员都拥有虚函数,则基类指针可以根据所指对象的类型,调用相应派生类的方法。

2,派生对象可能包含不是继承而来的方法,这种情况下,只有某些类型的对象可以使用该方法。

3,也可能是出于调试的目的,想跟踪生成的对象的类型。

对于后两种情况,RTTI提供解决方案。

15.4.2

,RTTI的工作原理

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

1,如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针;否则该运算符返回0---空指针。

2,typeid运算符返回一个指出对象的类型的值。

3,type_info结构存储了有关特定类型的信息。

只能将RTTI用于包含虚函数的类层次结构,原因在于只有对这种类层次结构,才应该将派生对象的地址赋给基类指针。

十三,dynamic_cast

dynamic_cast不能回答“指针指向的是哪类对象”,但能够回答“是否可以安全地将对象的地址赋给特定类型的指针”这样的问题。

通常,如果指向的对象(*pt)的类型为Type或者是从Type直接或者间接派生而来的类型,则下面的表达式将指针pt转换为类型为Type类型的指针:

dynamic_cast<Type *> (pt)

否则,结果为0,即空指针。

也可以将dynamic_cast用于引用,其用法稍微有点不同:没有与空指针对应的引用值,因此无法使用特殊的引用值来只是失败。当请求不准确时,dynamic_cast将引发类型为bad_cast的异常,这种异常是从exception类派生而来的,它是在头文件typeinfo中定义的。因此可以像下面这样使用该运算符,其中rg是对Grand对象的引用,Superb是从Grand派生出的对象:

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

十四,typeid运算符和type_info类

typeid运算符是的能够确定两个对象是否为同种类型。它与sizeof有些相似,可以接受两种参数:

1,类名

2,结果为对象的表达式

typeid运算符返回一个type_info对象的引用,其中,type_info是在头文件type_info中定义的一个类。type_info重载了==和!=运算符,以便可以使用这些运算符来对类型进行判断。例如,如果pg执行的是一个Superb对象,则下述表达式的结果为bool值true,否则为false:

typeid(Superb) == typeid(*pg)

如果pg是一个空指针,程序将引发bad_typeid异常。该异常类型是从exception类派生而来的,是在头文件typeinfo中声明的。

type_info类实现随厂商而异,但包含一个name()成员,该函数返回一个随实现而异的字符串:通常(但并非一定)是类名。例如,下面的语句显示指针pg指向的对象所属类定义的字符串:

cout::typeid(*pg).name()<<"\n";

十五,类型转换运算符

四种类型转换运算符,他们的语法相同:

1,dynamic_cast,语法:dynamic_cast<type-name> (expression)

2,const_cast,语法:const_cast<type-name> (expression)

3,static_cast,语法:static_cast<type-name> (expression)

4,reinterpret_cast,语法:reinterpret_cast<type-name> (expression)

dynamic_cast上面已经提到过,用途是,使得能够在类层次结构中进行向上转换(由于是is-a关系,这样的类型转换时安全的),而不允许其他转换。

const_cast运算符用于执行只有一种用途的类型转换,即改变值为const或volatile。如果类型的其他方面也被修改,则上述类型转换将出错。也就是说,除了const或volatile特征(有或无)可以不同外,type-name和expression的类型必须相同。

假设两个类High和Low:

High bar;
const High *pbar = &bar;
...
High *pb = const_cast<High *>(pbar); //valid
const Low *pl = const_cast<const Low *>(pbar); //invalid

第一个类型转换是的*pb成为一个可用于修改bar对象值的指针,它删除const标签。第二个类型转换是非法的,因为它同时尝试将类型从const High * 改为 const Low *。

需注意const_cast可以修改指向一个值的指针,但不可以修改const值。

void change(const int * pt)
{
	int * pc;
	pc = const_cast<int *>(pt);
	*pc = 100;
}

int main()
{
    int pop1 = 38383;
    const int pop2 = 2000;
    
    change(&pop1);
    change(&pop2);

    return 0;
}

在change()中,指针pt被声明为const int *,因此不能用来修改指向的int。指针pc删除了const特诊,因此可以用来修改指针指向的值,但仅当指向的不是const时才行,因此,pc可用于修改pop1的值,但不能修改pop2的值。

static_cast<type-name> (expression)仅当type-name可被隐式转换为expression所属的类型,或者expression可被隐式转换为type-name所属的类型时,上述转换才是合法的,否则将出错。假设Hige是Low的基类,而Pond是一个无关的类,则从Hige到Low的转换、从Low到High的转换都是合法的,而从Low到pond的转换是不允许的:

High bar;
Low blow;
...
High *pb = static_cast<High *>(&blow); //valid
Low *pl = static_cast<Low *>(&bar); //valid
Pond *pb = static_cast<Pond *>(&blow); //invalid,Pond unrelated

reinterpret_cast用于天生危险的类型转换(如将指针类型转换为足以存储指针表示的整型),使用reinterpret_cast运算符可以简化对这种行为的跟踪。

reinterpret_cast不支持所有类型的转换,如不能将指针类型转换为更小的整型或浮点型,不能将函数指针转换为数据指针,反之亦然。

猜你喜欢

转载自blog.csdn.net/u012481976/article/details/79507090