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不能回答“指针指向的是哪类对象”,但能够回答“是否可以安全地将对象的地址赋给特定类型的指针”这样的问题。
通常,如果指向的对象(*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不支持所有类型的转换,如不能将指针类型转换为更小的整型或浮点型,不能将函数指针转换为数据指针,反之亦然。