本章先介绍C++语言最初就有的特性:友元类、友元成员函数和嵌套类,然后介绍C++语言新增的一些特性:异常、运行阶段类型识别(RTTI)和改进后的类型转换控制。
1. 友元
前面我们介绍了友元函数,它用于类的扩展接口。也可以将类作为友元,在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员。另外也可以只将特殊的成员函数指定为另一个类的友元。
1.1 友元类
假设我们需要定义电视机和遥控器的简单程序,我们可以定义两个类:Tv类和Remote类来表示电视机和遥控器。因为遥控器可以改变电视机的状态,因此Remote类可以作为Tv类的友元。
friend class Remote;
友元声明可以放在公有、私有或保护部分,其所在位置无关紧要。下面的代码是实现遥控和电视机的功能。虽然代码长,但是实现简单,只是为了说明它的作用,还可以扩展更多的遥控接口,做到一个遥控控制多台电视。
类声明部分(my_class.h):
#ifndef MY_CLASS_H
#define MY_CLASS_H
class Tv
{
public:
friend class Remote;
enum {
Off, On};
enum {
Minval, MaxVal = 20};//最小1,最大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;//On or off
int volume;//音量
int maxchannel;//maximum numbers of channels
int channel;
int mode;//broadcast or cable
int input;//TV or DVD
};
class Remote
{
private:
int mode;//TV or DVD
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 // MY_CLASS_H
类定义部分(my_class.cpp):
#include<iostream>
#include"my_class.h"
bool Tv::volUp()
{
if(volume < MaxVal)
{
volume++;
return true;
}
else
return false;
}
bool Tv::volDown()
{
if(volume > Minval)
{
volume--;
return true;
}
else
return false;
}
void Tv::chanDown()
{
if(channel > 1)
channel--;
else
channel = 1;
}
void Tv::chanUp()
{
if(channel < maxchannel)
channel++;
else
channel = maxchannel;
}
void Tv::settings()const
{
using std::cout;
using std::endl;
cout<<"TV is "<<(state == Off?"Off":"On")<<endl;
if(state == On)
{
cout<<"Volume setting = "<<volume<<endl;
cout<<"Channel setting = "<<channel<<endl;
cout<<"Mode = "<<(mode == Antenna? "antenna" : "cable")<<endl;
cout<<"Input = " <<(input == TV? "TV" : "DVD")<<endl;
}
}
main函数:
#include <iostream>
using namespace std;
int main()
{
char ch;
int count = 0;
cout<<"Enter characters;enter # quit:\n";
cin.get(ch);
while(ch != '#')
{
cout<<ch;
++count;
cin.get(ch);
}
cout<<endl<<count<<" characters read\n";
return 0;
}
输出:
Initial settings for 42" TV:
TV is Off
TV is On
Volume setting = 5
Channel setting = 5
Mode = cable
Input = TV
TV is On
Volume setting = 7
Channel setting = 10
Mode = cable
Input = TV
TV is On
Volume setting = 5
Channel setting = 28
Mode = antenna
Input = TV
1.2 友元成员函数
可以设置某个成员函数作为友元,比如:
class Tv
{
friend void Remote::set_chan(Tv& t, int c);
}
这样的话,Remote类必须在Tv类的前面,因为需要Remote已经定义。但是Remote的方法提到了Tv对象,而意味着Tv定义应当位于Remote定义之前。为了避开这种循环依赖,使用前向声明(foreard declaration)。为此,需要在Remote定义的前面插入下面的语句:
class Tv;
因此它们的顺序为(和前面介绍的类排列顺序不一样):
class Tv;//前向声明
class Remote {
... };
class Tv {
... };
但是不能像下面的方式排列!因为需要看到在Remote的方法被声明为友元之前,应该先看到Remote类的声明和set_chan()方法的声明。
class Remote;
class Tv{
... };
class Remote {
... };
还有一点,在Remote中的函数不能采用内联代码的形式(如果一定要使用内联,请仅仅在原型上加inline的形式!),因为还没看到Tv类的声明,不清楚Tv类函数有哪些。所以具体的排列顺序应该是这样的:
class Tv;//前向声明
class Remote {
... };//函数仅仅使用原型的形式
class Tv {
... };
//cpp文件进行Remote函数的实现
1.3 其他友元关系
类之间也可以进行交互,所以就出现了互为友元的类,即Remote是Tv的友元类,Tv也是Remote的友元类。它的实现如下:
class Tv
{
friend class Remote;
public:
void buzz(Remote& r);
}
class Remote
{
friend class Tv;
public:
void Bool volup(Tv& t){
t.volup(); }
}
inline void Tv::buzz(Remote& r)
{
...
}
由于Remote的声明位于Tv声明的后面,所以可以在类声明中定义Remote::volup(),但是Tv::buzz()方法必须在Tv声明的外部定义,使其位于Remote声明的后面。如果不希望buzz是内联,则应该在cpp文件中定义它。
1.4 共同的友元
一个函数可以同时作为两个类的友元。假设有两个类,希望两个类中的时钟同步,则可以使用下列代码:
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)
{
... }
前向声明使编译器看到Probe类声明中的友元声明时,知道Analyzer是一种类型。
2. 嵌套类
在另一个类中声明的类被称为嵌套类(nested class),
3. 异常
程序有时会遇到运行阶段错误,导致程序无法正常地运行下去。例如调和平均数的函数。$2.0 × x ×y / (x + y )
。如果x+y为0,则这种情况是不允许的运算。很多编译器通过生成一个无穷大的特殊浮点值来处理,cout将显示Inf、inf、INF或类似的东西;其他的编译器可能生成在发生被零除时崩溃的程序。最好编写在所有系统上都以相同的受控方式运行的代码。
3.1 调用abort()
如果遇到分母为0,则使用abort()终止程序,也可以使用exit()。abort的头文件存在cstdlib(或stdlib.h)中,代码如下:
#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<<"mean is: " <<z<<std::endl;
std::cout<<"Enter next set of numbers<q to quit>:";
}
std::cout<<"bye!\n";
return 0;
}
double hmean(double a, double b)
{
if(a == -b)
{
std::cout<<"untenable artgument to hmean()\n";
std::abort();//程序终止
//exit(0);
}
return 2.0 * a * b / (a + b);
}
3.2 返回错误码
可以根据程序的返回值来判断成功还是失败,但是本方法的任何返回值都是有效的返回值。本例采用istream族重载>>运算符技术变体,来告知调用程序是成功了还是失败了,使得程序可以采取除异常终止之外的其他措施。
即使遇到错误也不会退出程序,而是继续执行,只不过会告知调用失败还是成功。
#include<iostream>
#include<cfloat> //or float.h for DBL_MAX
bool hmean (double a, double b, double * ans);
int main()
{
double x, y, z;
std::cout<<"Enter two numbers: ";
while(std::cin>>x>>y)
{
if(hmean(x, y, &z) )
std::cout<<"mean is: " <<z<<std::endl;
else
std::cout<<"error!!!";
std::cout<<"Enter next set of numbers<q to quit>:";
}
std::cout<<"bye!\n";
return 0;
}
bool hmean(double a, double b, double *ans)
{
if(a == -b)
{
*ans = DBL_MAX;//double的最大值
return false;
}
else
{
*ans = 2.0 * a * b / (a + b);
return true;
}
}
另外需要说明DBL_MAX包含在cfloat头文件中。
DBL_MAX:double型的最大值bai
DBL_MIN:double型的最小值
FLT_MAX:float型的最大值
FLT_MIN:float型的最小值
3.3 异常机制
C++异常是对程序运行过程中发生的异常情况的一种响应。它有3个组成模块
- 引发异常(throw关键字):紧随其后的值(例如字符串或对象)指出了异常的特征
- 捕获异常(catch关键字):异常被引发后,程序应跳到这个位置。异常处理程序也成为catch块。可能有多个catch块。
- try块:标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch块。
一个简单程序来看看它的流程:
#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(const char* s)
{
std::cout<<s<<std::endl;
std::cout<<"Enter a new pair of numbers: ";
continue;
}
std::cout<<"mean: "<<z<<std::endl;
std::cout<<"Enter next set of numbers <q to quit>: ";
}
std::cout<<"bye!\n";
return 0;
}
double hmean(double a, double b)
{
if(a == -b)
throw "bad hmean() argument: a = -b not allowed";
return 2.0 * a * b / (a + b);
}
如果引发异常,匹配的引发 "bad hmean() argument: a = -b not allowed"将被赋给s。一旦异常与该处理程序匹配时,程序将执行catch块代码。如果没有引发异常,将跳过try块后面的catch块,直接执行处理程序后面的语句。
如果引发了异常,而没有try块或没有匹配的处理程序时,在默认情况下,程序最终将调用abort()函数。
3.4 将对象用作异常类型
通常,引发异常的函数将传递一个对象,可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。
假设需要实现两个功能,一个是调和平均数,另一个开根号。上面已经展示调和平均数,开根号需要满足被开根号的元素大于0,我们设置调和平均数可以循环测试(continue),而如果不满足开根号的运算规则,则跳过执行(break)。
#include<iostream>
class bad_hmean//调和平均数异常时,传递的类型
{
private:
double v1;
double v2;
public:
bad_hmean(double a = 0, double b = 0):v1(a),v2(b){
}
void mesg();
};
inline void bad_hmean::mesg()//开根号异常时,传递的类型
{
std::cout<<"hmean("<<v1<<", "<<v2<<"): "
<<"invalid arguments: a = -b\n";
}
class bad_gmean//
{
public:
double v1;
double v2;
bad_gmean(double a = 0, double b = 0):v1(a),v2(b){
}
const char * mesg();
};
inline const char* bad_gmean::mesg()
{
return "gmean() arguments should be >= 0\n";
}
#include<cmath> //or math.h
double hmean(double a, double b);
double gmean(double a, double b);
int main()
{
using std::cout;
using std::cin;
using std::endl;
double x, y, z;
std::cout<<"Enter two numbers: ";
while(std::cin>>x>>y)
{
try
{
z = hmean(x, y);
cout<<"hmean: "<<z<<endl;
cout<<"gmean: "<<gmean(x, y)<<endl;
}
catch(bad_hmean& bg)
{
bg.mesg();
cout<<"Try again.\n";
continue;
}
catch(bad_gmean& hg)
{
cout<<hg.mesg();
cout<<"Values used: "<<hg.v1<<", "
<<hg.v2<<endl;
cout<<"Sorry, you don't get to play any more.\n";
break;
}
}
std::cout<<"bye!\n";
return 0;
}
double hmean(double a, double b)
{
if(a == -b)
throw bad_hmean(a, b);
return 2.0 * a * b / (a + b);
}
double gmean(double a, double b)
{
if(a < 0 || b < 0)
throw bad_gmean(a, b);
return std::sqrt(a * b);
}
3.5 exception类
exception头文件定义了exception类,C++可以把它用作其他异常类的基类。exception有一个虚拟成员函数,它返回一个字符串,由于是虚函数,所以可从exception派生类中重新定义它。
#include<exception>
class bad_hmean:public std::exception
{
public:
const char *what() {
return "bad arguments to hmean()";}
};
使用
try{
...
}
catch(std::exception & e)
{
cout<<e.what()<<endl;
}
C++库定义了很多基于exception的异常类型。
(1) stdexcept异常类
头文件stdexcept定义了其他几个异常类。logic_error类和runtime_error类,他们都是公有方式从exception派生而来的。
class logic_error:public exception{
public:
explicit logic_error(const string& what_arg);
};
class domain_error:public logic_error{
public:
explicit domain_error(const string& what_arg);
};
类的构造函数接受一个string对象作为参数,干参数提供了方法what()以C-风格字符串方式返回的字符数据。每个类的名称指出了它用于报告的错误类型:
logic_error:
domain_error:域错误,比如sin大于1时,会报错。
invalid_argument:意料之外的值。比如只能接收0或1时,接收到其他字符报错。
length_error:指出没有足够空间来执行所需的操作。
out_of_bounds:索引错误runtime_error
underflow_error:下溢错误。
overflow_error:上溢错误。
range_error:上溢或下溢错误
(2) bad_alloc异常和new
对于new导致的内存分配问题,C++的最新处理方式是让new引发bad_alloc异常。头文件new包含bad_alloc类的声明,它是从exception类公有派生而来的。
#include<iostream>
#include<new>
#include<cstdlib> //for exit()
using namespace std;
struct Big
{
double stuff[20000];
};
int main()
{
Big* pb;
try{
cout<<"Trying to get a big block of memory:\n";
pb = new Big[100000];
cout<<"Got past the new request:\n";
}
catch(bad_alloc& ba)
{
cout<<"Caught the exception!\n";
cout<<ba.what()<<endl;
exit(EXIT_FAILURE);//exit(1);
}
cout<<"memory successfully allocated\n";
pb[0].stuff[0]=4;
cout<<pb[0].stuff[0]<<endl;
delete[]pb;
return 0;
}
不足分配空间,报错,输出:
(3)空指针和new
很多代码都是在new失败时,返回空指针。其用法如下:
int *pi = new (std::nothrow) int;
int *pa = new (std::nowthrow) int[500];
4. RTTI
RTTI(Runtime Type Identification)称为运行阶段类型识别,这是C++中新加入的功能。
4.1 RTTI的用途
一个基类有一个派生类。让基类指针指向其中任何一个派生类的对象。那么如何知道指针具体指向那种对象呢?也就是说具体运行时,调用基类还是派生类的函数。
4.2 RTTI的工作原理
C++有3个支持RTTI的元素。
- dynamic_cast运算符,用一个指向基类的指针来生成一个指向派生类的指针;
- typeid运算符返回一个指向对象的类型的值;
- type_info结构存储了有关特定类型的信息。
只能将RTTI用于包含虚函数的类层次结构
,原因在于只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针。
(1) dynamic_cast运算符
dynamic_cast运算符是最常用的RTTI组件,它不能回答“指针指向的是哪类对象”这样的问题,但是能够回答“是否可以安全地将对象的地址赋给特定类型的指针”。
Superb是一个类。pg是一个类对象。
Superb *pm = dynamic_cast<Superb *>(pg);//dynamic_cast<Type*>(pt)
指针pg的类型如果可以安全的转换为Super*,则返回对象的地址,否则返回一个空指针。
(2) typeid运算符和type_info类
typeid运算符使得能够确定两个对象是否为同种类型。它可以接受两种参数:类名和结果为对象的表达式。
typeid运算符返回一个对type_info对象的引用,type_info是头文件typeinfo中定义的一个类,它重载了== 和!=。
下列pg指向的是一个Magnificent对象,则下属表达式的结果为true,否则为false。
typeid(Magnificent) == typeid(*pg);
5. 类型转换运算符
在C++的创始人Bjarne Stroustrup看来,C语言中的类型转换运算符太过松散,在C中下列转换都是成立的,但是没有意义!
struct Data
{
double data[200];
};
struct Junk
{
int junk[100];
};
Data d = {
2.4e33, 3.5e-19, 20.2e32};
char* pch = (char*)(&d);
char ch = char(&d);
Junk * pj = (Junk*)(&d);
对于这种松散的情况,Stroustrop采取的措施是,更严格地限制允许的类型转换,并添加4个类型转换运算符,使转换过程更规范:
- dynamic_cast;
- const_cast;
- static_cast;
- reinterpret_cast。
(1) dynamic_cast
dynamic_cast运算符已经在上面介绍过了。假设High和Low是两个类,而ph和pl的类型分别为High和Low,则仅当Low是High的可访问基类(直接或间接)时,下面的语句才将一个Low*指针赋给pl:
pl = dynamic_cast<Low*>ph;//基类指针指向派生类
该运算符的用途是,使得能够在类层次结构中进行向上转换。
(2) const_cast
const_cast运算符用于执行只有一种用途的类型转换,即改变值为const或volatile,其语法与dynamic_cast运算符相同:
const_cast<type-name>(expression)
例如:
High bar;
const High* pbar = &bar;
High* pb = const_cast<High*>(pbar);//可以
const Low* pl = const_cast<const Low*>(pbar);//不可以
第一个类型转换使得*pb成为一个可用于修改bar对象值的指针,它删除const标签。第二个类型转换非法。(bar不是const类型)
(3) static_cast
static_cast运算符与其他与其他类型转换运算符相同:
static_cast<type-name>(expression)
仅当type_name可被隐式转换为expression所属的类型或expression可被隐式转换为type_name所属的类型时,上述转换才是合法的。假设High是Low的基类,而Pond是一个无关的类,则从High到Low的转换、从Low到High的转换都是合法的,而从Low到Pond的转换是不允许的。
High bar;
Low blow;
High* pb = static_cast<High*>(&blow);//可以
Low* pl = static_cast<Low*>(&bar);//可以
Pond* pmer = static_cast<Pond*>(&blow);//不可以
(4) reinterpret_cast
reinterpret_cast也与其他3个语法相同。如果作者不得不操作一些令人生厌的操作,可以使用reinterpret_cast运算符。(个人感觉也没啥意义呀!!!)
reinterpret_cast<type-name>(expression);
struct dat {
short a; short b;};
long value = 0xA224B118;
dat* pd = reinterpret_cast<dat*>(&value);
cout<<hex<<pd->a;