1.友元
- 一个类成为另一个类的友元
例子:使用遥控器控制电视,也就是说遥控器中的方法可以改变电视的状态,然而遥控器和电视既不是is-a的关系同时也不是has-a的关系,所以这种情况适合使用友元类的方式来解决问题。
注意:友元声明可以位于公有,私有和保护部分
//友元类
//tv.h
#ifndef TV_H_
#define TV_H_
class Tv
{
public:
friend class Remote;
enum {Off ,On};
enum {MinVal,MaxVal = 20};
enum {Antenna,Cable};
enum {TV,DVD};
Tv(int s =Off, int mc = 125) :
state(s),volume(5),maxchannel(mc),channel(2),mode(Cable),input(TV) {}
void onoff() {state = (state == On)? Off : On};
bool ison() const {return state == On;}
bool volup();
bool voldown();
void chanup();
void chandown();
void set_mode(){mode = (mode == Antenna) ? Cable : Antenna}
void set_input(){input = (input==TV) ? DVD : TV;}
void settings() const;
private:
int state;
int volume;
int maxchannel;
int channel;
int input;
};
class Remote
{
private:
int mode;
public:
Remote(int m = Tv::TV) : mode(m);
bool volup(Tv & t){return t.volup();}
bool voldown(Tv & t){return t.voldown();}
void onoff(Tv & t){t.onoff();}
void chanup(Tv & t){t.chanup();}
void chandown(Tv & t){t.chandown();}
void set_chan(Tv & t,int c){t.channel = c;}
void set_mode(Tv & t){t.set_mode();}
void set_input(Tv & t){t.set_input();}
};
#endif
注意:类友元是一种自然用于,用于表示一种关系,友元声明也可以位于公有,私有或保护部分,其所在位置无关紧要,由于Remote类提到了TV类,所以编译器必须了解TV类后,才能处理Remote类,所以最简单的办法是先定义TV类,也可以使用前向声明。
- 友元成员函数
从上面的类定义可知,友元类Remote中唯一使用Tv类中私有成员的只有Remote::set_chan()函数,因此它是唯一一个需要作为友元的方法,所以我们也可以只将Remote类中唯一的这个需要作为友元的方法声明为友元。
class Tv
{
friend void Remote::set_chan(Tv & t,int c);
}
注意:想要执行这条语句,首先Tv类中要知道Remote的定义,否则他并不知道Remote是一个类,那么需在Tv类之前定义class Remote。而我们也知道Remote类中的各种方法均使用了Tv类对象,这就意味着Tv类定义应当位于Remote定义之前。避开这种循环依赖的方法是,使用前向声明。
class Tv;
class Remote {...};
class Tv {...};
还需要注意的一点:因为在Remote类中使用了内联代码,内敛代码会调用TV类中的方法,而Remote类定义的时候,Tv类的方法并没有被声明,所以需要将方法的声明放在Tv类声明之后,所以正确的排序顺序是:
class Tv;
class Remote {...};
class Tv {...};
//最后这部分是Remote类中使用Tv类中声明的方法而实现的方法
整体代码如下:
//只使用一个友元函数
//tv.h
#ifndef TV_H_
#define TV_H_
class Tv;
class Remote
{
public:
enum {Off ,On};
enum {MinVal,MaxVal = 20};
enum {Antenna,Cable};
enum {TV,DVD};
private:
int mode;
public:
Remote(int m = Tv::TV) : mode(m);
bool volup(Tv & t);
bool voldown(Tv & t);
void onoff(Tv & t);
void chanup(Tv & t));
void chandown(Tv & t);
void set_chan(Tv & t,int c);
void set_mode(Tv & t);
void set_input(Tv & t);
};
class Tv
{
public:
friend void Remote::set_chan(Tv & t,int c));
enum {Off ,On};
enum {MinVal,MaxVal = 20};
enum {Antenna,Cable};
enum {TV,DVD};
Tv(int s =Off, int mc = 125) :
state(s),volume(5),maxchannel(mc),channel(2),mode(Cable),input(TV) {}
void onoff() {state = (state == On)? Off : On};
bool ison() const {return state == On;}
bool volup();
bool voldown();
void chanup();
void chandown();
void set_mode(){mode = (mode == Antenna) ? Cable : Antenna}
void set_input(){input = (input==TV) ? DVD : TV;}
void settings() const;
private:
int state;
int volume;
int maxchannel;
int channel;
int input;
};
inline bool Remote::volup(Tv & t){return t.volup();}
inline bool Remote::voldown(Tv & t){return t.voldown();}
inline void Remote::onoff(Tv & t){t.onoff();}
inline void Remote::chanup(Tv & t){t.chanup();}
inline void Remote::chandown(Tv & t){t.chandown();}
inline void Remote::set_chan(Tv & t,int c){t.channel = c;}
inline void Remote::set_mode(Tv & t){t.set_mode();}
inline void Remote::set_input(Tv & t){t.set_input();}
#endif
注意:因为内联函数的链接性是内部的,这意味着内联函数的定义一定在使用函数的文件中,为了满足这个条件,所以我们将内联函数也放在头文件之中,确保内联函数定义放在正确的地方,当然也可以将函数放在实现函数文件之中,不过要去掉关键字inline,使之链接性为外部。
- 互相称为友元类
//互相成为友元类的方法
class Tv
{
friend class Remote;
public:
void buzz(Remote & r);
};
class Remote
{
friend class Tv;
public:
...
};
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)
{
...
}
2.嵌套类
嵌套类通常是为了帮助实现另一个类,并避免名称冲突,例如实现Queue类
//嵌套类
class Queue
{
class Node
{
public:
Item item;
Node * next;
Node(const Item * i) : item(i),next(0) { }
};
...
};
bool Queue::enqueue(const Item & item)
{
if(isfull())
return false;
Node * add = new Node(item);
}
//当然可以在实现文件中定义构造函数 不过要使用双作用域解析运算符
Queue::Node::Node(const Item & i) : item(i), next(0) {}
3.嵌套类和访问权限
- 作用域
如果嵌套类是在另一个类的私有部分声明的,则只有后者知道他,上述的Queue中的Node类就是这样,因为类的默认权限就是私有的,所以Queue成员可以使用Node对象和指向Node对象的指针,但是程序的其他部分甚至不知道存在Node类。对于从Queue派生出的类也不能访问Node,因为派生类不能直接访问基类的私有部分。
如果嵌套类是在另一个类的保护部分声明的,则他对于后者来说是可见的,但是对于外部对象还是不可见的,而且这种情况中,派生类将指导嵌套类,并且可以直接创建这种类型的对象。
如果嵌套类是在另一个类的公有部分声明的,则允许后者,后者的派生类以及外部世界使用它,不过使用的时候必须使用类限定符。
//公有部分声明嵌套类
class Team
{
public:
class Coach {...};
};
//如果要创建一个Coach对象
Team::Coach forhire;
- 访问控制
类可见后,起作用的就是访问控制。对嵌套类的访问权的控制规则与常规类相同,在Queue类声明中声明的Node类并没有赋予Queue类任何对Node类的访问特权,也没有赋予Node类任何对Queue类的访问特权。因此Queue类对象只能显式的访问Node类的公有成员,所以Node类的成员全部声明为公有的。但是Node类作为Queue的内部类,对于外部世界是不可见的,因为Node类是在private中声明的。所以,虽然Queue的方法可以直接访问Node成员,但使用Queue类的客户不能这样做。
4.模板中的嵌套
模板中的嵌套与正常类嵌套没有区别,就是使用正常的模板类和正常的嵌套方式。将Queue类定义转换为模板时,并不会给嵌套类带来问题。
5.异常
- abort()函数
// error1.cpp
#include<iostream>
#include<cstdlib>
double hmean(double a, double b);
int main()
{
double x,y,z;
std::cout << "Enter two numbers: ";
while (std::cin >> x >> y)
{
z = hmean(x,y);
std::cout << x << y << std::endl;
}
std::cout << "BYE" << std::endl;
return 0;
}
double hmean(double a, double b)
{
if(a == - b)
{
std::cout << "ERROR" << std::endl;
std::abort(); //会在标准流中输出 abnormal program termination
//然后直接将程序停止运行 并不会再回到主程序中
}
return 2.0 * a * b /(a+b);
}
- 返回错误码
#include<iostream>
#include<cfloat>
double hmean(double a, double b);
int main()
{
double x,y,z;
std::cout << "Enter two numbers: ";
while (std::cin >> x >> y)
{
if(hmean(x,y,&z))
std::cout << x << y << z << std::endl;
else
std::cout << "ERROR" << std::endl;
}
std::cout << "BYE" << std::endl;
return 0;
}
double hmean(double a, double b, double * ans)
{
if(a == - b)
{
*ans = DBL_MAX;
return false;
}
else
{
*ans = 2.0 * a * b /(a+b);
return true;
}
}
- 异常机制
对异常的处理有三个组成部分:
1.引发异常
2.使用处理冲虚捕获异常
3.使用try块
程序出现问题时将引发异常,使用throw语句跳转,命令程序跳到另一条语句。throw关键字表示引发异常,紧随其后的值指出了异常的特征。
程序使用异常处理程序来捕获异常,异常处理程序位于要处理问题的程序中。catch关键字表示捕获异常,处理程序以关键字catch开头,随后是位于括号中的类型声明,他指出异常处理程序要相应的异常类型;然后是一个用花括号括起的代码块,指出要采取的措施。catch关键字和异常类型用作标签,指出当异常被引发时,程序应调到这个位置指向。所以异常处理程序被称为catch块。
try块标识其中特定的异常可能被激活的代码块,他后面跟一个或者多个catch块。try块是由关键字try指示的,关键字try后面是一个由花括号括起的代码块,表明需要注意这些代码引发的异常。
//error3.cpp
#include<iostream>
double hmean(double a, double b);
int main()
{
double x,y,z;
std::cout << "Enter two numbers: ";
while (std::cin >> x >> y)
{
try
{
z = hmean(x,y);
}
//catch后面的参数表示需要获取的异常类型
catch(const char * s)
{
std::cout << s << std::endl;
continue;
}
std::cout << x << y << z << std::endl;
}
std::cout << "BYE" << std::endl;
return 0;
}
double hmean(double a, double b)
{
if(a == - b)
{
//发现异常的时候会导致程序沿函数调用序列后退
throw "Bad hmean() arguements :";
}
return 2.0 * a * b /(a+b);
}
- 将对象用作异常类型
如果引发异常的函数将传递一个对象,这样做的重要优点之一是,可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。另外,对象可以携带信息,程序员可以根据这些信息来确定引发异常的原因。同时,catch块可以根据这些信息来决定采取什么样的措施:
class Bad_hmean
{
private:
double v1;
double v2;
public:
bad_hmean(int a = 0, int b = 0) : v1(a),v2(b) {}
void mesg();
};
inline void bad_hmean::mesg()
{
std::cout << v1 << v2;
}
class Bad_gmean
{
private:
double v1;
double v2;
public:
bad_gmean(int a = 0, int b = 0) : v1(a),v2(b) {}
const char * mesg();
};
inline const char * bad_gmean::mesg()
{
return "gmean() arguement should be >= 0\n";
}
try
{
}
catch (bad_hmean & bg)
{
}
catch (bad_gmean & hg)
{
}
-
栈解退
假设函数由于异常而终止,则程序也将释放栈中的内存,但是不会释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句,这个过程称之为栈解退。
为什么需要栈解退:引发机制的一个非常重要的特性是,和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用,然而,函数返回仅仅处理该函数放在栈中的对象,然而throw函数则处理try块和throw之间整个函数调用序列放在栈中的对象。如果没有栈解退这种特性,则引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数不会调用。 -
其他异常特性
1.函数中的返回语句将控制权返回到调用fun()的函数,但是throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合
2.二者的另外一个不同之处是,引发异常时表引起总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。
class problem{...};
...
void super() throw (problem)
{
...
if(oh_no)
{
problem oops;
throw oops;
...
}
...
}
...
try
{
super();
}
catch(problem & p)
{
...
}
注意:p将指向oops的副本而非oops本身。这是一件好事,因为super()函数执行完毕之后,oops一件不复存在。
注意:直接将创建对象和引发异常的代码结合在一起可以让代码更加简洁。
throw problem();
注意:这里还使用引用的原因是:因为基类引用可以执行派生类对象,假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将与任何派生类对象匹配。
注意:如果您编写了一个调用另一个函数的函数,而您并不知道被调用的函数可能引发哪些异常,在这种情况下,仍可以捕获异常,即使不知道异常的类型,方法是使用省略号来表示异常类型:
catch(...)
{
...
}
可以将捕获所有异常的catch放在最后面,就好像使用switch中的default一样。
try
{
duper();
}
//顺序是3 2 1 是因为从小到大排序的话可以使3使用它本身的异常处理方法处理
catch(bad_3 &be)
{
...
}
catch(bad_2 &be)
{
...
}
catch(bad_1 &be)
{
...
}
catch(...)
{
...
}
- exception类
将异常合并到语言中,为了支持该语言,定义了exception类,C++可以把它用作其他异常类的基类,也就是说代码可以引发exception类异常,也可以将exception类用作基类,有一个名为what()的虚拟成员函数,他返回一个字符串,作为虚方法,可以在从exception派生而来的类中重新定义它。
#include<exception>
class bad_hmean : public std::exception
{
public:
const char * what() {return "bad arguement to hmean()";}
...
};
try{
...
}
catch(std::exception & e)
{
...
}
①stdexcept异常类
头文件stdexcept定义了其他几个异常类,首先是logic_error和runtime_error类,他们都是以公有方式从exception派生而来的。
异常类系列logic_error描述了典型的逻辑错误,下面是几个常见的错误:
- domain_error 定义域异常
- invalid_arguement 给函数传递一个意外的值异常
- length_error 用于指出没有足够的空间来执行所需操作
- out_of_bounds 用于指示索引错误
异常类系列runtime_error描述了可能在运行期间发生但难以预计和防范的错误。 - range_error 计算结果可能不在函数允许范围之内,但是没有发生上溢和下溢的时候可以使用
- overflow_error 上溢错误
- underflow_error 下溢错误
② bad_alloc异常和new
对于使用new导致的内存分配问题,C++的最新处理方式是让new引发bad_alloc异常。头文件new包含bad_alloc类的声明。
#include<iostream>
#include<new>
#include<cstdlib>
using namespace std;
struct Big
{
double stuff[20000];
};
int main()
{
Big * pb;
try
{
pb = new Big[10000];
}
catch(bad_alloc & ba)
{
cout << ba.what() << endl;
exit(EXIT_FAILURE);
}
pb[0].stuff[0] = 4;
cout << pb[0].stuff << endl;
delete [] pb;
return 0;
}
③空指针和new
int * pi = new(std::nothrow) int;
int * pa = new(std::nothrow) int[500];
- 异常何时会迷失方向
总之要捕获所有异常,包含预期的异常和意外异常,可以这么做:
#include<exception>
using namespace std;
void myUnexcepted()
{
throw std::bad_exception();
}
set_unexcepted(myUnexcepted);
double Argh(double,double) throw(out_of_bounds,bad_execption);
...
try{
x = Argh(a,b);
}
catch(out_of_bounds & ex)
{
...
}
catch(bad_exception & ex)
{
...
}
5.RTTI
RTTI是运行阶段类型识别的简称。
C++中三种支持RTTI的元素:
- 如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该运算符返回一个空指针
- typeid运算符返回一个指出对象的类型的值
- type_info结构存储了有关对象的类型的值
注意:RTTI只适用于包含虚函数的类
①dynamic_cast
//语法
Superb * pm = dynamic_cast<Superb *>(pg);
//dynamic_cast判断指针pg的类型是否可以被安全的转化为Superb *
//如果可以运算符就返回对象的地址
//否则返回空指针
②typeid运算符和type_info类
typeid运算符使得能够确定两个对象是否为同种类型,接受两种参数:
- 类名
- 结果为对象的表达式
typeid运算符返回一个对type_info对象的引用,其中type_info是在头文件typeinfo中定义的一个类。type_info类重载了== 以及 != 运算符。例如:
typeid(Magnificent) == typeid(*pg)
//如果pg指向的是一个Magnificent对象,则下述表达式的bool值为true
//否则为false
//如果pg是空指针,则程序引发bad_typeid异常
6.类型转化符
四种类型转换运算符
- dynamic_cast
前面已经介绍过了,总之,假设High和Low是两个类,而ph和pl的类型分别为High和Low,则仅当Low是High的可访问基类时,下一个语句才将一个Low*指针赋给pl:
pl = dynamic_cast<Low *> ph;
- const_cast
const_cast运算符用于执行只有一种用途的类型转换,即改变值为cosnt或volatile,语法方式和dynamic_cast相同:
const_cast <type-name> (expression)
//也就是说,如果类型的其他方面也被修改 上述的类型转换将出错
//除了const或者volatile特征可以不填意外,type_name和 expression的类型必须相同
//const_cast不是万能的,他可以修改指向一个值的指针,但修改const值的结果是不确定的。
- static_cast
- reinterpret_cast