友元
可以将类作为友元。这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员。
现在有一个tv类和一个remote类(遥控器),我们就可以把remote作为tv的友元。
friend class Remote;
友元声明可以位于公有、私有或保护部分,位置无关紧要。由于remote类提到了tv类,所以编译器必须了解tv类后才能处理remote类。为此,我们需要首先定义tv类。
所有的remote方法都将一个tv对象引用作为引用,因此可以对任何一个tv对象进行操作。
class Tv{
public:
friend class Remote;
...
private:
int channel;
...
}
class Remote{
public:
void set_chan(Tv & t,int c){t.channel=c;}//这个方法可以调用tv的私有成员
...
}
当然也可以不把remote类设置成tv的友元,而是仅仅把setchan这个方法设置成tv的友元,但是这需要小心排列各种声明和定义的顺序。
class Tv{
friend void Remote::set_chan(Tv &t, int c);
...
}
这意味着应将remote放到tv之前,但是remote中的方法也依靠tv来完成,因此tv应该在remote之前,这就导致了循环。此时就应当使用前向声明。
class Tv;
class Remote{...};
class Tv{...};
如果是这样:
class Remote;
class Tv{...};
class Remote{...};
这不可行。因为编译器在tv类中看到remote的一个方法被声明为tv类的友元之前,应该先看到remote类的声明。
如果使用了上面的形式,那么remote类中只能包含声明,而不能包含定义,因为编译器那时还不知道tv中的声明。
class Tv;
class Remote{...};//声明
class Tv{...};
//remote中方法的定义
还可以让tv和remote类互为友元。只不过也有一定的顺序问题。
class Tv{
friend class Remote;
public:
void buzz(Remote &r);//不能有定义,只能声明
...
};
class Remote{
friend class Tv;
public:
void volup(Tv & t){t.vol++;}
...
};
inline void Tv::buzz(Remote &r){
...
}
有时候一个函数能成为两个类的友元。
class Analyzer;
class Probe{
friend void sync(Analyzer &a,const Probe &p);
friend void sync(Probe &p,const Analyzer &a);
...
};
class Analyzer{
friend void sync(Analyzer &a,const Probe &p);
friend void sync(Probe &p,const Analyzer &a);
};
inline void sync(Analyzer &a,const Probe &p){
...
}
inline void sync(Probe &p,const Analyzer &a){
...
}
嵌套类
在另一个类中声明的类被称为嵌套类,它通过提供新的类型类作用域来避免名称混乱。包含类的成员函数可以创建和使用被嵌套类的对象;而仅当声明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。
对类进行嵌套与包含并不同。包含意味着将类对象作为另一个类的成员,而对类进行嵌套不创建类成员,而是定义了一种类型,该类型仅在包含嵌套类声明的类中有效。
template <class T>
class Queue{
private:
class Node{
public:
T item;
Node * next;
Node(const T & i):item(i),next(0){}
};
Node * front;
Node * rear;
int items;
const int qsize;
Queue(const Queue & q):qsize(0){}
Queue & operator=(const Queue &q){return *this;}
public:
Queue(int qs=10);
~Queue();
bool enqueue(const T &item);
bool deuqe(T &item);
};
template<class T>
Queue<T>::Queue(int qs):qsize(qs){
front=rear=0; //设置为空指针
items=0;
}
template<class T>
Queue<T>::~Queue(){
Node * temp;
while (front!=0){
temp=front;
front=front->next;
delete front;
}
}
template<class T>
bool Queue<T>::enqueue(const T &item){
Node * add = new Node(item);
items++;
if (front=0) front=add;else rear->next=add;
rear=add;
return true;
}
template<class T>
bool Queue<T>::dequeue(T & item){
item=front->item;
items--;
Node * temp=front;
front=front->next;
delete temp;
if (items==0) rear=0;
return true;
}
这是一个在模板类中使用嵌套类的例子。
嵌套类声明的位置、以及嵌套类自身的访问限制符控制对类成员的访问。
声明位置 | 包含它的类是否可以使用 | 从包含它的类派生而来的类是否可以使用 | 在外部是否可以使用 |
---|---|---|---|
私有部分 | 是 | 否 | 否 |
保护部分 | 是 | 是 | 否 |
公有部分 | 是 | 是 | 是,通过类限定符 |
当嵌套类可见之后,访问控制规则仍然和平常一致。(即被嵌套类可以访问public部分,不能访问private部分等等)
异常
当程序想要直接停止时,可以使用std::abort()方法。此时不管在哪个函数中,都将直接结束程序。
当程序运行出现错误时,我们应该使用异常机制来处理而不是使用abort()。
C++的try-catch机制大致和Java相同,却也有所不同:
try{
cin>>x>>y;
z=hmean(x,y);
}catch(const char *s){
cout<<s<<endl;
}
...
double hmean(double a,double b){
if (b==0) throw "divided by zero";
return a/b;
}
当try中的某句语句出现异常就会导致catch被引发。抛出的异常类型需要与捕获的异常类型一致。一个try后面可以跟多个catch。
异常类型可以为字符串,也可以为其他C++类型。一般传递对象,因为对象可以携带信息。
如果抛出异常之后找不到合适的catch,程序最终会调用abort()。
如果函数出现异常而终止,程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址。随后,控制权将跳转到块尾的异常处理程序,这个过程被称为栈解退。栈解退时仍然会调用析构函数。
引发异常时,编译器会创建一个临时拷贝,即使catch块中指定的是一个引用。
当返回的类有继承关系时,应该合理安排catch的顺序。
使用catch(…)可以捕获所有会引发的异常。
C++编译器将异常合并到语言中。exception头文件定义了exception类,C++可以把它用作其他异常类的基类。代码可以引发exception异常,也可以将exception作为基类。有一个名为what()的虚拟成员函数,它返回一个字符串。由于这是一个虚方法,因此可以在派生类中重新定义。
#include<exception>
class bad_hmean:public std::exception{
public:
const char * what(){return "bad arguments";}
...
};
try{
...
}catch(std::exception &e){
...
}
stdexexcept异常类定义了logic_error和runtime_error类,它们都是以公有方式从exception派生而来的。
logic_error包含以下几个异常:
domain_error; //参数不在定义域中
invalid_argument; //参数出乎意料
length_error; //长度过长
out_of_bounds. //索引错误
runtime_error包含以下几个异常:
range_error; //计算结果溢出
overflow_error; //上溢
underflow_error; //下溢
logic_error表明存在可以通过编程修复的问题,而runtime_error表明存在无法避免的问题。
对于new导致的内存分配问题,new会引发bad_alloc异常,包含在头文件new中,从exception类公有派生而来。
也可以使用下面的用法,使得new不抛出异常,而是返回一个空指针:
Big * pb;
pb=new (std::nothrow) Big[10000];
if (pb==0){
cout<<"too big";
}
还有一个注意点:
void test(int n){
double *ar=new double[n];
...
if (oh_no)
throw exception();
...
delete [] ar;
return;
}
抛出异常会导致ar没有被释放,应该这样设计:
void test(int n){
double *ar=new double[n];
...
try{
if (oh_no)
throw exception();
}catch(exception & ex){
delete [] ar;
throw;
}
...
delete [] ar;
return;
}
RTTI
RTTI是运行阶段类型识别的简称。通过RTTI,我们可以得知引用或指针具体所指的类型。
C++有3个支持RTTI的元素。
1.dynamic_cast运算符
dynamic_cast能够回答“类型转换是否安全”这个问题。
Superb * pm=dynamic_cast<Superb *>(pg);
如果pg的类型可以被安全地转换为Superb*,则返回对象地址,否则返回空指针。
dynamic_cast<Type *>(pt);
如果指向的对象(*pt)的类型为Type或者Type直接或间接派生而来的类型,则pt则会转换为Type类型的指针,否则返回空指针。
也可以将其运用于引用,只不过是当引用不正确时,会抛出bad_cast异常,这个异常在头文件typeinfo中定义。
#include<typeinfo>
...
try{
Superb & rs=dynamic_cast<Superb &>(reg);
...
}catch(bad_cast &){
...
};
2.typeid运算符和type_info类
typeid运算符使得能够确定两个对象是否为同种类型,它可以接受两种参数:(1)类名(2)结果为对象的表达式
typeid运算符返回一个type_info对象的引用,type_info是在头文件typeinfo中定义的一个类,它重载了==和!=运算符,以便用来比较。
typeid(Superb)==typeid(*pg)
如果pg是一个空指针,程序将进引发bad_typeid异常,该异常也是在typeinfo中声明的。
typeid(*pg).name()
这将返回一个字符串,通常是类的名称。
如果发现在扩展的ifelse语句系列中使用了typeid,应考虑是否应该使用虚函数和dynamic_cast。
类型转换运算符
C++添加了四个类型转换运算符,以便更严格地限制类型转换。
dynamic_cast;
const_cast;
static_cast;
reinterpret_cast.
const_cast只用于改变值为const或volatile,不能用于改变类型的其他方面,否则会出错。
const int* const_p = &constant;
int* modifier = const_cast<int*>(const_p);
*modifier = 7;//但是const_p指向的值仍然不会改变
这样就把const标签去除了。
static_cast可以用于显式类型转换。
Father bar;
Son *pl=static_cast<Son *>(& bar);//ok
int i;
float f = 166.71;
i = static_cast<int>(f);//ok
reinterpret_cast主要是将数据从一种类型的转换为另一种类型。所谓“通常为操作数的位模式提供较低层的重新解释”也就是说将数据以二进制存在形式的重新解释。
int i;
char *p = "This is an example.";
i = reinterpret_cast<int>(p);//i和p完全相同