第11章 继承
(本资料qq讨论群112133686)
面向对象的程序设计提供了类的继承机制,允许程序员在保持原有类特性的基础上,进行更具体、更详细的类的定义。以原有的类为基础产生新的类,新类继承了原来类的特性,也可以说是从原有类派生出新类。类的派生好处在于代码的重用性和可扩展性。通过继承可以充分利用别人做过的一些类似的的研究和已有的一些分析、解决方案。重用这些代码,便使自己的开发工作站在巨人的肩膀上。
本章围绕派生与继承,通过各种案例演示不同继承方式下的基类成员的访问控制问题,添加构造函数和析构函数,围绕派生过程,着重讨论不同继承方式下的基类成员的访问控制问题,讨论了较为复杂的继承关系中,派生类成员的惟一标识和访问问题,最后讨论派生类对象的使用场合和范围问题。掌握这些特性,为下一章学习的类的多态性奠定继承。
11.1 继承与派生概述
为提高软件的生产率,C++提供了类的继承机制,在保持类特性的基础上,进行更具体、更详细的说明。继承是新的类从已有的类得到基类已有的特征。
案例11-1 动物对象进化
【案例描述】
本实例中家鸡从红色原鸡继承,具有红色原鸡的特征,同时新增了自己的特征,这就是继承特性。本例效果如图11-1所示。
图11-1 动物对象进化
【实现过程】
程序定义红色原鸡类Red_Jungle_fowl,私有成员外形、羽色,为字符串waixing、yuse;定义家鸡类Chicken,从Red_Jungle_fowl类继承而来。代码如下:
#include <iostream>
#include<string.h>
using namespace std;
class Red_Jungle_fowl //红色原鸡类定义
{
private: //私有成员
char *waixing; //外形
char *yuse; //羽色
public:
//构造函数,带缺省参数
Red_Jungle_fowl(char *x="",char *y="")
{
waixing=x;
yuse=y;
}
void disp() //显示红色原鸡的信息
{
cout<<"( "<<waixing<<" , "<<yuse<<" )"<<endl;
}
char *GetX() //读取私有成员waixing
{
return waixing;
}
char* GetY() //读取私有成员yuse
{
return yuse;
}
};
class Chicken:public Red_Jungle_fowl //家鸡Chicken,从Red_Jungle_fowl类继承而来
{
private:
char* mingsheng; //在Red_Jungle_fowl类基础上增加了鸣声信息
public:
//派生类构造函数,初始化表中调用基类构造函数
Chicken(char* x,char* y,char* z):Red_Jungle_fowl(x,y)
{
mingsheng=z;
}
void disp() //隐藏了基类中的同名函数disp
{
cout<<"( "<<GetX()<<" , "<<GetY()<<" , "<<mingsheng<<" )"<<endl;
}
char* calcSum() //增添了计算3个数据成员和的函数
{
char *retval=new char[50]; //申请空间
strcpy(retval,GetX()); //复制字符串
strcat(retval,","); //连接字符串
strcat(retval,GetY()); strcat(retval,",");
strcat(retval,mingsheng);
return retval;
}
};
char* main()
{
//建立Red_Jungle_fowl类对象pt1
Red_Jungle_fowl pt1("头具肉冠,喉侧有一对肉垂","红色");
pt1.disp(); //显示pt1的信息
//建立Chicken类对象pt2
Chicken pt2("头具肉冠,喉侧有一对肉垂","红色","两杂茶花");
pt2.disp(); //显示pt2的信息
char *retval=new char[50];
retval=pt2.calcSum(); //计算pt2中3个坐标信息的和
cout<<retval<<endl; //输出结果
delete[] retval;
system("pause"); return 0;
}
【案例分析】
(1)类的一个重要特征是继承,这就可以基于一个类生成另一个类的对象,以便使后者拥有前者的某些成员,再加上自己的一些成员。代码中声明家鸡Chicken的基类Red_Jungle_fowl,它们有一些共同的特征,比如都可以用外形和羽色两个变量来描述。
(2)理论上说,子类是可以继承基类的所有成员,但是构造函数和析构函数、operator=()成员friends除外。虽然基类的构造函数和析构函数没有被继承,但是当一个子类的object被生成或销毁的时候,其基类的默认构造函数(即没有任何参数的构造函数)和析构函数总是被自动调用的。
注意:派生类实现派生的过程:吸收基类成员、改造基类成员和添加新的成员。
11.2 派生类
案例11-2 错误的模糊引用
【案例描述】
类是可以继承的,继承就是在一个已存在的类的基础上建立一个新的类。已存在的类称为基类或父类,新建立的类称为派生类或子类。继承必须清晰,否则会引起编译错误。本例效果如图11-2所示。
图11-2 模糊引用(类继承问题)
【实现过程】
定义基类A,类B和类C是类A的派生类,类D是类B和类C的派生类,在主函数中定义类D的新类d1,求d1变量x的值。代码如下:
#include <iostream>
using namespace std;
class A{ //定义类A
public: int x;
A(int a=0) { x=a;}
};
class B:public A{ //类B是类A的派生类
public: int y;
B(int a=0,int b=0):A(a) { y=b;}
};
class C:public A{ //类C是类A的派生类
public: int z;
C(int a,int c):A(a){ z=c; }
};
class D:public B,public C{ //类D是类B和类C的派生类
public: int dx;
D(int a1,int b,int c,int d,int a2):B(a1,b),C(a2,c)
{ dx=d;}
};
void main(void)
{ D d1(11,22,33,44,55); //定义d1为类D的类型
cout<<d1.x<<endl; //模糊引用,错误
system("pause");
}
【案例分析】
这种同一个公共的基类在派生类中产生多个拷贝的应用,不仅会造成多占用存储空间,而且可能会造成多个拷贝中的数据不一致和模糊的引用,这时候就会出现编译错误。
案例11-3 MP3录音类
【案例描述】
日常使用软件进行录音(如MP3录音),产生的文件占用容量比其他没有压缩的软件要小,本实例和下面的实例介绍录音的例子,本例定义录音类并写采集录音信息到文件中,效果如图11-3所示。
图16-9 MP3录音类)
【实现过程】
定义录音类mp3Writer继承基类Ireceiver,定义打开文件和写文件功能的函数,定义显示使用帮助函数,枚举设备名称的函数。其代码如下:
// IReceiver类实例化
class mp3Writer: public IReceiver {
private:
CMP3Simple m_mp3Enc;
FILE *f;
public:
mp3Writer(unsigned int bitrate = 128, unsigned int finalSimpleRate = 0):
m_mp3Enc(bitrate, 44100, finalSimpleRate) {
f = fopen("music.mp3", "wb"); //打开文件
if (f == NULL) throw "Can't create MP3 file.";
};
~mp3Writer() { //析构函数
fclose(f);
};
virtual void ReceiveBuffer(LPSTR lpData, DWORD dwBytesRecorded) {
BYTE mp3Out[44100 * 4];
DWORD dwOut;
m_mp3Enc.Encode((PSHORT) lpData, dwBytesRecorded/2, mp3Out, &dwOut);
fwrite(mp3Out, dwOut, 1, f); //写
};
};
//显示帮助信息
void printHelp(char *progname) {
printf("%s -devices\n\tWill list WaveIN devices.\n\n", progname);
//代码略
}
//列出WaveIN 设备名称
void printWaveINDevices() {
//向量
const vector<CWaveINSimple*>& wInDevices = CWaveINSimple::GetDevices();
UINT i;
for (i = 0; i < wInDevices.size(); i++) { //枚举设备名称
printf("%s\n", wInDevices[i]->GetName());
}
}
【案例分析】
上面代码定义了录音类,类中代码实现了打开文件、写入文件内容的功能。Ireceiver类接收采集的声音信息PCM,并且通过CWaveINSimple实例处理存储在缓冲区的声音。CWaveINSimple为输入声音设备向量容器,可以枚举设备名称。printLines()函数显示输入声音设备名称。
案例11-4 MP3录音类
【案例描述】
继续上个实例话题,代码见实例11-3。上个实例定义好了类,现在在主函数中调用定义好的类、控制音频输入设备进行录音操作,并把录音的内容写入MP3文件中。本例效果如图11-4所示。
图16-10 MP3录音类
【实现过程】
主函数定义变量并初始化,显示使用帮助,打开设备,并开始录音。其头文件代码如下:
int main(int argc, char* argv[])
{
mp3Writer *mp3Wr; //接收PCM的类 "IReceiver"
char *strDeviceName = NULL; //定义初始化字符串
char *strLineName = NULL;
char *strTemp = NULL;
UINT nVolume = 0; //音量
UINT nBitRate = 128; //Bit
UINT nFSimpleRate = 0; //采样速率
setlocale( LC_ALL, ".866"); //配置地域化信息函数
try {
if (argc < 2) printHelp(argv[0]); //显示帮助
else if (argc == 2) {
if (::strcmp(argv[1],"-devices") == 0) printWaveINDevices();
else if ((strTemp = ::strstr(argv[1],"-device=")) == argv[1]) {
strDeviceName = &strTemp[8];
CWaveINSimple& device = CWaveINSimple::GetDevice(strDeviceName);
printLines(device);
}
else printHelp(argv[0]);
}
else { //设备
if ((strTemp = ::strstr(argv[1],"-device=")) == argv[1]) {
strDeviceName = &strTemp[8];
}
if ((strTemp = ::strstr(argv[2],"-line=")) == argv[2]) {
strLineName = &strTemp[6];
}
if ((strDeviceName == NULL) || (strLineName == NULL)) {
printHelp(argv[0]); //显示使用帮助
return 0;
}
for (int i = 3; i < argc; i ++) {
if ((strTemp = ::strstr(argv[i],"-v=")) == argv[i]) {
strTemp = &strTemp[3];
nVolume = (UINT) atoi(strTemp); //音量
}
else if ((strTemp = ::strstr(argv[i],"-br=")) == argv[i]) {
strTemp = &strTemp[4];
nBitRate = (UINT) atoi(strTemp); //比特率
}
else if ((strTemp = ::strstr(argv[i],"-sr=")) == argv[i]) {
strTemp = &strTemp[4];
nFSimpleRate = (UINT) atoi(strTemp); //采样速率
}
else {
printHelp(argv[0]);
return 0;
}
}
printf("\nRecording at %dKbps, ", nBitRate);
if (nFSimpleRate == 0) printf("44100Hz\n");
else printf("%dHz\n", nFSimpleRate);
printf("from %s (%s).\n", strLineName, strDeviceName);
printf("Volume %d%%.\n\n", nVolume); //显示音量
CWaveINSimple& device = CWaveINSimple::GetDevice(strDeviceName);
CMixer& mixer = device.OpenMixer(); //Mixer类
CMixerLine& mixerline = mixer.GetLine(strLineName);
mixerline.UnMute();
mixerline.SetVolume(nVolume);
mixerline.Select();
mixer.Close();
mp3Wr = new mp3Writer(nBitRate, nFSimpleRate);
device.Start((IReceiver *) mp3Wr); //开始录音
printf("hit <ENTER> to stop ...\n");
while( !_kbhit() ) ::Sleep(100);
device.Stop();
delete mp3Wr;
}
}
catch (const char *err) {
printf("%s\n",err); //显示错误
}
CWaveINSimple::CleanUp();
return 0;
}
【案例分析】
(1)代码首先进行类mp3Wr初始化,定义变量:音量、Bit、采样速率,通过主函数变量argv读取输入变量参数的值,并且显示程序处理的信息。定义混音器类mixer并初始化,定义类mixerline实现声音输入并初始化;然后开始录音,按任何键退出程序。
(2)Setlocale()函数用来配置地域的信息,设置当前程序使用本地化信息,其包含在头文件locale.h中。
11.3 多基派生
案例11-5 判断一个点是否在正方体内
【案例描述】
本实例紧接实例12-4话题,一个抽象类允许多次继承也就是多重继承;多重继承后,继承类的成员函数和成员变量,相对基类有什么变化规律呢。本实例正方体类继承矩形类,矩形类继承图形基类,实现多重继承。本例效果如图11-5所示。
图11-5 判断一个点是否在正方体内
【实现过程】
设计抽象基类的图形基类Figure,具有长、宽和高三种属性,定义个结构Rect,纯虚函数inRect();定义个矩形类Rectangle继承基类,再定义个正方体类box继承矩形类,提供成员函数inputRect()和inRec()分别输入正方体参数,判断点是否在正方体区域内。代码实现如下:
#include <iostream>
#include <cmath>
#include<stdlib.h>
using namespace std;
class Figure //图形基类定义
{
public:
float x; //两个边长x和y z
float y;
float z;
public:
typedef struct{
int x; //左上角x坐标
int y; //左上角Y坐标
int z; //左上角z坐标
int l; //图形的长
int w; //图形的宽
int h; //图形的高
}Rect;
public:
virtual void DispName() =0;
virtual bool inRect(int x ,int y ,Rect const & a) =0;//纯虚函数,因此Figure类是抽象类,无法声明其对象
};
class Rectangle:public Figure //在抽象类Figure的基础上派生Rectangle矩形类
{
public:
Rectangle(float xp=0,float yp=0) //构造函数
{
x=xp;
y=yp;
}
virtual void DispName() //覆盖实现了虚函数DispName,此处virtual去掉没有影响
{
cout<<"矩形:"<<endl;
}
virtual bool inRect(int x ,int y ,Rect const & a)
{return true;}
};
class box:public Rectangle
{
public:
box(float l,float w,float h) //开方体类的构造函数声明
{ };
void inputRect(Rect &a,char const *prompt); //输入a 的 x y h winline
bool inRect(int x ,int y ,int z ,Rect const & a);
};
void box::inputRect(Rect &a,char const *prompt) //输入立方体参数
{
cout<<prompt<<endl;cout<<"x:"; //prompt为提示
cin>>a.x; //输入a.x、a.y、a.z
cout<<"y:";
cin>>a.y;
cout<<"z:";
cin>>a.z;
h_w:
cout<<"h:";
cin>>a.h; //输入a.h、a.wa.l
cout<<"w:";
cin>>a.w;
cout<<"l:";
cin>>a.l;
if (a.h<=0||a.w<=0||a.l<=0)
{
cout<<"h and w and l must >0\n";
goto h_w;
}}
bool box::inRect(int x ,int y ,int z,Rect const & a)
{ //判断点是否在正方体区域内,原理就是点的x和y同时在正方体的x、y、z 范围内。
int a_x2=a.x+a.w;
int a_y2=a.y+a.h;
int a_z2=a.z+a.l;
return (x<a_x2 && x>a.x)&&(y<a_y2 && y>a.y)&&(z<a_z2 && z>a.z);
}
int main()
{
box::Rect a;
Figure *pF=NULL; //虽然不能创建Figure类对象,但可声明Figure型的指针
box r(1.2f,3.6f,2.3f); //声明一个正方体对象,其边长分别为1.2和3.6
pF=&r; //用正方体对象r的地址为pF赋值
cout<<"已有个点(12,23,33)\n";
r.inputRect(a,"输入正方体a");
if(r.inRect(12,23,33,a))
{
cout<<"点(12,23,33)在正方体范围内\n";
}
else{cout<<"点(12,23,33)超出正方体范围\n";
}
system("pause");
return 0;
}
【案例分析】
(1)代码中Rectangle继承Figure,box又继承Rectangle。在C++中有两种继承:单一继承和多重继承。当一个派生类仅由一个基类派生时,称为单一继承;而当一个派生类由两个或更多个基类所派生时,称为多重继承。
(2)当从已有的类中派生出新的类时,可以对派生类作以下几种变化:
— 可以继承基类的成员数据或成员函数;
— 可以增加新的成员变量;
— 可以增加新的成员函数;
— 可以重新定义已有的成员函数;
— 可以改变现有的成员属性。
注意:一个函数如果被定义成虚函数,则不管经历多少次派生,仍将保持其虚特性,以实现“一个接口,多个形态“的特征。
11.4 虚基类
案例11-6 判断一个矩形是否成立
【案例描述】
本例在实例12-4基础上,根据功能需要,在抽象类减去和增加纯虚函数。利用面向对象多态技术,很容易编写个虚函数实现判断矩形成立的功能。本例效果如图11-6所示。
图11-6 判断一个矩形是否成立
【实现过程】
定义抽象基类的图形基类Figure,和实例12-4一样的,增加定义纯虚函数justify_Re();再定义个矩形类Rectangle继承基类,增加继承基类虚成员函数justify_Re()判断矩形成立,其中调用justify_RT()函数判断任意三个点能否组成直角三角形。其代码如下:
#include <iostream>
using namespace std;
class Figure //图形基类定义
{
public:
virtual void DispName() =0; //纯虚函数,因此Figure类是抽象类,无法声明其对象
virtual int justify_Re() =0;
};
class Rectangle:public Figure //在抽象类Figure的基础上派生Rectangle矩形类
{
private:
float x; //两个边长x和y
float y;
public:
typedef struct
{ int x; //左上角x坐标
int y; //左上角Y坐标
int h; //矩形的高
int w; //矩形的宽
}Rect;
public:
Rectangle(float xp=0,float yp=0) //构造函数
{
x=xp;
y=yp;
}
virtual void DispName() //覆盖实现了虚函数DispName,此处virtual去掉没有影响
{
cout<<"矩形:"<<endl;
}
int justify_Re();
};
int Rectangle::justify_Re() //判断矩形成立
{
int justify_RT(float,float,float,float,float,float);
int flag_RT1,flag_RT2, flag_RT3;
float position_x1, position_y1,position_x2,
float position_y2, position_x3, position_y3;
float position_x4, position_y4;
cout<< "请依次输入四个坐标:" << endl;
cout<< "请输入第一个坐标" << endl;
cin>> position_x1 ;
cin >> position_y1;
cout<< "请输入第二个坐标" << endl;
cin>> position_x2 ;
cin >> position_y2;
cout<< "请输入第三个坐标" << endl;
cin>> position_x3;
cin >> position_y3;
cout<< "请输入第四个坐标" << endl;
cin>> position_x4 ;
cin >> position_y4;
flag_RT1=justify_RT(position_x1,position_y1,position_x2,position_y2, position_x3,position_y3);
flag_RT2=justify_RT(position_x2,position_y2,position_x3,position_y3,position_x4,position_y4);
flag_RT3=justify_RT(position_x3,position_y3,position_x4,position_y4,position_x1,position_y1);
if(flag_RT1 && flag_RT2 && flag_RT3)
{
cout<< "输入的四个点能组成矩形" << endl;
return 0;
}
else
{
cout<< "输入的四个点无法组成矩形!" <<endl;
return 1; }
}
//判断任意三个点能否组成直角三角形
int justify_RT(float x1,float y1,float x2,float y2,float x3,float y3)
{
if( ( (x1-x2)*(x1-x2)+ (y1-y2)*(y1-y2) ) + ( (x2-x3)*(x2-x3) + (y2-y3)*(y2-y3) )== ( (x1-x3)*(x1-x3) + (y1-y3)*(y1-y3) ) )
return 1;
else
return 0;
}
int main()
{
Rectangle::Rect a,b;
Figure *pF=NULL; //虽然不能创建Figure类对象,但可声明Figure型的指针
Rectangle r(1.2f,3.6f); //声明一个矩形对象,其边长分别为1.2和3.6
pF=&r; //用矩形对象r的地址为pF赋值
r.justify_Re(); //判断一个矩形是否成立
system("pause");
return 0;
}
【案例分析】
(1)判断四个点能否组成矩形,可以根据“有三个直角的四边形为矩形”的命题来判断,判断的命题可以转化为任意三个点能否组成直角三角形。
(2)将justify_Re ()判断矩形成立声明为虚函数,编译器对其进行动态聚束,调用了Rectangle中的函数justify_Re ()判断任意三个点能否组成直角三角形。
案例11-7 判断两个矩形是否存在重合
【案例描述】
在实例11-6的基础上增加一个成员函数判断重合,该函数嵌套调用。也就是继承的虚函数调用a函数,在a函数中又调用b函数,b函数又调用c函数。本例举函数嵌套调用,效果如图11-7所示。
图11-7 判断两个矩形是否存在重合
【实现过程】
定义的抽象基类图形基类Figure,和实例121一样的;增加定义纯虚函数xiangjiao()判断矩阵是否重合;再定义个矩形类Rectangle继承基类,增加继承基类虚成员函数xiangjiao(),函数中调用intersection()判断矩形是否重合,再在函数中调用inRect()判断点是否在矩形区域。代码实现如下:
#include <iostream>
#include <cmath>
#include<stdlib.h>
using namespace std;
class Figure //图形基类定义
{
public:
float x; //两个边长x和y
float y;
public:
typedef struct
{ int x; //图形左上角x坐标
int y; //图形左上角Y坐标
int h; //图形的高
int w; //图形的宽
}Rect;
virtual bool xiangjiao(Rect const &a,Rect const &b ) =0;//判断重合
virtual void DispName() =0; //纯虚函数,因此Figure类是抽象类,无法声明其对象
};class Rectangle:public Figure //在抽象类Figure的基础上派生Rectangle矩形类
{
public:
Rectangle(float xp=0,float yp=0) //构造函数
{
x=xp;
y=yp;
}
virtual void DispName() //覆盖实现了虚函数DispName,此处virtual去掉没有影响
{
cout<<"矩形:"<<endl;
}
void inputRect(Rect &a,char const *prompt);//输入a 的x、y、h、winline
bool xiangjiao(Rect const &a,Rect const &b );//判断相交
bool intersection(Rect const &a,Rect const &b );
bool inRect(int x ,int y ,Rect const & a);
};
//输入矩形参数
void Rectangle::inputRect(Rect &a,char const *prompt)
{
cout<<prompt<<endl; cout<<"x:"; //prompt为提示
cin>>a.x; //图形左上角x坐标
cout<<"y:"; //图形左上角y坐标
cin>>a.y;
h_w:
cout<<"h:"; //图形的高
cin>>a.h;cout<<"w:";
cin>>a.w; //图形的宽
if (a.h<=0 || a.w<=0)
{
cout<<"h and w must >0\n";
goto h_w;
}}
bool Rectangle::inRect(int x ,int y ,Rect const & a)
{//判断点是否在矩形区域内,原理就是点的x和y同时在矩形的x、y范围内。
int a_x2=a.x+a.w;
int a_y2=a.y+a.h;
return (x<a_x2 && x>a.x) && (y<a_y2 && y>a.y);
}
//bool intersection(Rect const &a,Rect const &b );
inline bool Rectangle::xiangjiao(Rect const &a,Rect const &b )
{//分别判断a、b和b、a的顶点包含关系,有一个成立就重合
return intersection(a,b)||intersection(b,a);
}
bool Rectangle::intersection(Rectangle::Rect const &a,Rectangle::Rect const &b )
{//判断矩形是否重合的办法就是判断:第二个矩形的4个顶点有无任何一个落在第一个矩形内。此函数判断b是否被a包含。
int b_x2=b.x+b.w;
int b_y2=b.y+b.h;
return inRect(b.x,b.y,a)||inRect(b.x,b_y2,a)||inRect(b_x2,b.y,a)||inRect(b_x2,b_y2,a);
}
int main()
{
Rectangle::Rect a,b;
Figure *pF=NULL; //虽然不能创建Figure类对象,但可声明Figure型的指针
Rectangle r(1.2f,3.6f); //声明一个矩形对象,其边长分别为1.2和3.6
pF=&r; //用矩形对象r的地址为pF赋值
r.inputRect(a,"input a");
r.inputRect(b,"input b");
if(r.xiangjiao(a,b))
{
cout<<"两个矩形存在重合\n";
}
else{cout<<"两个矩形不重合\n";
}
system("pause");
return 0;
}
【案例分析】
(1)函数xiangjiao ()分别判断矩形a、b 和 b、a 的顶点包含关系,有一个成立就重合。函数intersection()判断矩形是否重合的办法,就是判断第二个矩形的4个顶点有无任何一个落在第一个矩形内,此函数判断b是否被a包含。函数inRect()判断点是否在矩形区域内,原理就是点的x和y坐标值同时在矩形的x、y坐标范围内。
(2)代码return inRect(b.x,b.y,a) ||…很长,调用函数inRect()中布尔表达式有一个为真就返回真,否则返回假。函数inline bool Rectangle,可以在类体外定义类的内联成员函数,前面加上关键字inline,实质是用存储空间来换取时间。
注意:C++语言中,所有函数都是平行独立的,无主次、相互包含之分。函数可以嵌套调用,不可嵌套定义,定义的虚函数嵌套也遵循这个原则。
案例11-8 补充代码使正方体不重合且Z轴距离为
【案例描述】
在实例11-5的基础上增加基类的纯虚函数;从已有的类中派生出新的类时,通过对成员函数删除、增加和修改,很方便实现增加和改进功能。在正方体类增加定义纯虚函数具体实现功能,多重继承纯虚函数,判断重合程序。本例最终得出如图11-8所示结果。
图11-8 补充代码使正方体不重合且Z轴距离为10
【实现过程】
定义的抽象基类图形基类Figure,和实例11-5一样的,在正方体类box增加定义纯虚函数xiangjiao()判断重合;增加继承基类虚成员函数xiangjiao(),xiangjiao中调用intersection()判断矩形是否重合,intersection中调用inRect()判断点是否在矩形区域。代码实现如下:
#include <iostream>
#include <cmath>
#include<stdlib.h>
using namespace std;
class Figure //图形基类定义
{
public:
float x; //两个边长x、y和z
float y;
float z;
public:
typedef struct{
int x; //左上角x坐标
int y; //左上角Y坐标
int z; //左上角z坐标
int l; //图形的长
int w; //图形的宽
int h; //图形的高
}Rect;
public:
virtual void DispName() =0;
virtual bool xiangjiao(Rect const &a,Rect const &b ) =0;//判断相交
};
class Rectangle:public Figure //在抽象类Figure的基础上派生Rectangle矩形类
{
public:
Rectangle(float xp=0,float yp=0) //构造函数
{
x=xp;
y=yp;
}
virtual void DispName() //覆盖实现了虚函数DispName,此处virtual去掉没有影响
{
cout<<"矩形:"<<endl;
}
virtual bool xiangjiao(Rect const &a,Rect const &b )//判断相交
{ return true; }
};
class box:public Rectangle
{
public:
box(float l,float w,float h) //开方体类的构造函数声明
{ };
void inputRect(Rect &a,char const *prompt);//输入a
bool inRect(int x ,int y ,int z ,Rect const & a);
bool intersection(Rect const &a,Rect const &b );
bool xiangjiao(Rect const &a,Rect const &b );//判断相交
};
void box::inputRect(Rect &a,char const *prompt) //输入立方体参数
{
cout<<prompt<<endl;cout<<"x:"; //prompt为提示
cin>>a.x; //输入a.x、a.y、a.z
cout<<"y:";
cin>>a.y;
cout<<"z:";
cin>>a.z;
h_w:
cout<<"h:";
cin>>a.h; //输入a.h、a.w、a.l
cout<<"w:";
cin>>a.w;
cout<<"l:";
cin>>a.l;
if (a.h<=0||a.w<=0||a.l<=0)
{
cout<<"h and w and l must >0\n";
goto h_w;
}}
bool box::inRect(int x ,int y ,int z,Rect const & a)
{ //判断点是否在正方体区域内,原理就是点的x、 y和z坐标的值同时在正方体的x、 y、z坐标的范围内
int a_x2=a.x+a.w;
int a_y2=a.y+a.h;
int a_z2=a.z+a.l;
return (x<a_x2 && x>a.x)&&(y<a_y2 && y>a.y)&&(z<a_z2 && z>a.z);
}
bool box::intersection(box::Rect const &a,box::Rect const &b )
{ //判断正方体是否相交的办法就是判断:第二个矩形的8个顶点是否有一个落在第一个正方体内,此函数判断b是否被a包含
int b_x2=b.x+b.w;
int b_y2=b.y+b.h;
int b_z2=b.z+b.l;
return inRect(b.x,b.y,b.z,a)||inRect(b.x,b_y2,b.z,a)||inRect(b_x2,b.y,b.z,a) ||inRect(b_x2,b_y2,b.z,a)||
inRect(b.x,b.y,b_z2,a)||inRect(b.x,b_y2,b_z2,a)||inRect(b_x2,b.y, b_z2,a)||inRect(b_x2,b_y2,b_z2,a)
;
}
inline bool box::xiangjiao(Rect const &a,Rect const &b )
{ //分别判断a、b和b、a的顶点包含关系,有一个成立就重合
return intersection(a,b)||intersection(b,a);
}
int main()
{
box::Rect a,b;
Figure *pF=NULL; //虽然不能创建Figure类对象,但可声明Figure型的指针
box r(1.2f,3.6f,2.3f); //声明一个正方体对象,其边长分别为1.2和3.6
pF=&r; //用正方体对象r的地址为pF赋值
r.inputRect(a,"输入正方体 a");
r.inputRect(b,"输入正方体 b");
if(r.xiangjiao(a,b))
{ cout<<"两个正方体存在重合\n";}
else{ cout<<"两个正方体不重合\n"; }
system("pause");
return 0;
}
【案例分析】
当从已有的类中派生出新的类时,如代码中Rectangle派生出box,可以对派生类做以下几种变化:
— 可以继承基类的成员数据或成员函数。
— 可以增加新的成员变量。
— 可以增加新的成员函数。
— 可以重新定义已有的成员函数。
— 可以改变现有的成员属性。
注意:用虚基类进行多重派生时,若虚基类没有缺省的构造函数,则在每一个派生类的构造函数中都必须有对虚基类构造函数的调用。
11.5 派生类的构造函数和析构函数
编程中有时需要实现自行复制本类对象的能力的函数,这就是拷贝构造函数,其作用使用一个已经存在的对象,该对象由拷贝对象参数指定,去初始化同类的一个新对象。
案例11-9 卫星定位街道系统(三维)
【案例描述】
默认拷贝构造函数功能是,把初始化的值都复制到新建立的对象中。本例定义三维向量类,定义矢量之间运算,求矢量的长度等。本例效果如图11-9所示。
【实现过程】
定义三维向量类Vector3D,成员函数有定义矢量之间的'+'法,'-'法、'*'法、'\'法,求矢量的长度等功能。代码实现如下:
#include <stdio.h>
#include <assert.h>
#include <math.h>
#include<iostream.h>
#include<iostream>
#define VECTOR3D_H
#define PI 3.14159265358979323846f
class Vector3D //三维向量类
{
public:
Vector3D() : m_X(0.0f), m_Y(0.0f), m_Z(0.0f){ }
Vector3D(const Vector3D& V) : m_X(V.m_X), m_Y(V.m_Y), m_Z(V.m_Z) { }
Vector3D(float X, float Y, float Z) : m_X(X), m_Y(Y), m_Z(Z) { }
~Vector3D() { }
float X() const { return(m_X); }
void X(const float X) { m_X = X; }
float Y() const { return(m_Y); }
void Y(const float Y) { m_Y = Y; }
float Z() const { return(m_Z);}
void Z(const float Z) { m_Z = Z; }
float& operator[](unsigned int i) //访问数组元素
{
switch(i){
case 0 : return(m_X);
case 1 : return(m_Y);
case 2 : return(m_Z);
default : return(m_Z);
} }
Vector3D& operator=(const Vector3D& V) //分配
{ m_X = V.m_X; m_Y = V.m_Y; m_Z = V.m_Z;
return(*this);
}
Vector3D operator-() const //定义矢量之间的'-'法
{
return(Vector3D(-m_X, -m_Y, -m_Z));
}
Vector3D operator+(const Vector3D& V) const //定义矢量之间的'+'法
{
return(Vector3D(m_X + V.m_X, m_Y + V.m_Y, m_Z + V.m_Z)); }
Vector3D& operator+=(const Vector3D& V)
{
m_X += V.m_X; m_Y += V.m_Y; m_Z += V.m_Z;
return(*this);
}
Vector3D operator-(const Vector3D& V) const //不同
{
return(Vector3D(m_X - V.m_X, m_Y - V.m_Y, m_Z - V.m_Z));
}
Vector3D& operator-=(const Vector3D& V)
{
m_X -= V.m_X; m_Y -= V.m_Y; m_Z -= V.m_Z;
return(*this);
}
float operator*(const Vector3D& V) const //两个向量的点乘积
{
return(m_X * V.m_X + m_Y * V.m_Y + m_Z * V.m_Z);
}
Vector3D operator*(const float Scale) const
//由缩放的相同的组件对应乘以这个矢量的每个组件Scale
{
return(Vector3D(m_X * Scale, m_Y * Scale, m_Z * Scale));
}
Vector3D operator/(const float Scale) const
{ return(operator*(1.0f / Scale)); }
Vector3D& operator*=(const float Scale) //表
{ m_X *= Scale; m_Y *= Scale; m_Z *= Scale;
return(*this);
}
Vector3D& operator/=(const float Scale)
{
return(operator*=(1.0f / Scale));
}
Vector3D& operator*=(const Vector3D& Scale) //元素表
{ m_X *= Scale.m_X; m_Y *= Scale.m_Y; m_Z *= Scale.m_Z;
return(*this); }
float Magnitude2() const //向量长度length^2
{ return((m_X * m_X) + (m_Y * m_Y) + (m_Z * m_Z));
} float Magnitude() const //向量长度
{ return(static_cast<float>(sqrt(Magnitude2())));
}
void fabs() //计算元素+向量
{
if(m_X < 0.0f) m_X = -m_X;
if(m_Y < 0.0f) m_Y = -m_Y;
if(m_Z < 0.0f) m_Z = -m_Z;
printf("[%.3f, %.3f, %.3f]\n", m_X, m_Y, m_Z);
}
void Unit() //获得矢量的长度
{ float len = Magnitude();
if(len == 0.0f)
{
m_X = len;
m_Y = len;
m_Z = len;
}
else
{
len = 1.0f / len;
m_X *= len;
m_Y *= len;
m_Z *= len;
}
}
int operator==(const Vector3D& Cmp) const //向量等式
{
return((m_X == Cmp.m_X) && (m_Y == Cmp.m_Y) && (m_Z == Cmp.m_Z));
}
int operator!=(const Vector3D& Cmp) const //向量不等
{
return((m_X != Cmp.m_X) || (m_Y != Cmp.m_Y) || (m_Z != Cmp.m_Z)); }
void Debug(FILE* fd)
const { fprintf(fd, "[%.3f, %.3f, %.3f]\n", m_X, m_Y, m_Z); }
protected: float m_X; //矢量元素
float m_Y;
float m_Z;
};
void main()
{
cout<<"为A的模\n";
cout<<"为B的模\n";
Vector3D A(3.0,4.0,5.0); //第一个对象
Vector3D B(A); //用A初始化B,调用拷贝构造函数
A.fabs(); //计算元素+向量
B.fabs();
system("pause");
}
【案例分析】
(1)可以在定义一个对象的时候用另一个对象对其初始化,即构造函数的参数是另一个对象的引用,这种构造函数常为完成拷贝功能的构造函数称为拷贝构造函数。如代码Vector3D& operator=(const Vector3D& V)。float X() const { return(m_X);,为常对象成员。
(2)虚析构函数~Vector3D(),作用是在对象撤销之前做必要的“清理现场”的工作。当派生类的对象从内存中撤销时,一般先调用派生类的析构函数,然后再调用基类的析构函数。
案例11-10 传气球
【案例描述】
编程中经常需产生随机数,比如游戏中打牌打麻将,随机把牌打乱后每次玩家抓到的牌不一样。本实例和下面实例都是产生随机数的例子。本例程序产生随机数模拟击鼓传花游戏,效果如图11-10所示。
图11-10 传气球
【实现过程】
定义2个类RandomNumber和Vector,为随机数类和队伍类。主函数main()输入参加游戏人数,要传的花从哪个人开始,传的花落在哪个人身上那个人就出列,再判断是否结束游戏。头文件代码如下:
class RandomNumber //随机类
{
private:
//私有成员包含随机种子
unsigned long randSeed;
public:
//构造函数,默认0自动为随机种子
RandomNumber(unsigned long s = 0);
//产生随机整数0<=value<=n-1
unsigned short Random(unsigned long n);
//生成随机数0<=value<1.0
double fRandom(void);
};
//随机种子发生器
RandomNumber::RandomNumber (unsigned long s)
{
if (s == 0)
randSeed = time(0); //自动使用系统实际
else
randSeed = s; //用户使用随机种子
}
//返回随机整数0<=value<=n-1<65536
unsigned short RandomNumber::Random (unsigned long n)
{
randSeed = multiplier * randSeed + adder;
return (unsigned short)((randSeed >> 16) % n);
}
//返回(value in range 0..65535) / 65536
double RandomNumber::fRandom (void)
{
return Random(maxshort)/double(maxshort);
}
#endif //随机数发生器变量RANDOM_NUMBER_GENERATOR
class Vector //队伍类
{
private:
int * elements; //存储队列元素
int Arraysize,VectorLength; //最多人数,队伍人数
public:
Vector(int Arraysize=Defaultsize); //构造函数
~Vector(void) { delete [] elements; } //析构函数
int getlength(void) { return VectorLength; }
int getnode(int i) //取得节点
{
return (i<0||i>=VectorLength)? NULL:elements[i];
}
int find(int & x) //查找花位置
{
for(int i=0;i<VectorLength;i++)
if(elements[i]==x) return i;
return -1;
}
int insert(int & x,int i); //插入花
int remove(int i); //移开花
};
【案例分析】
(1)十几人或几十人围成回味、圆圈坐下,其中一人拿花,一人背着大家或蒙眼击鼓,鼓响传花,鼓停花止。花在谁手中,谁就摸彩,如果花束正好在两人手中,则两人可通过猜拳或其他方式决定负者。
(2)随机数类RandomNumber成员函数Random()和fRandom(),产生0到65535范围的数;游戏队伍类Vector成员函数getnode()和find(),取得节点也就是花落在这个人身上所要查找花的位置。
11.6 分清继承还是组合
案例11-11 继承和组合区别
【案例描述】
C++的继承特性可以提高程序的可复用性。正因为继承很有用和容易用,才要防止乱用继承。特别是要分清继承和组合,本案例演示组合,效果如图11-11所示。
图11-11 继承和组合区别
【实现过程】
例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是头(Head)的一部分,所以类Head 应该由类Eye、Nose、Mouth、Ear 组合而成,不是派生而成。演示代码如下:
#include <iostream>
#include <typeinfo>
using namespace std;
class Eye
{
public:
void Look(void);
};
class Nose
{
public:
void Smell(void);
};
class Mouth
{
public:
void Eat(void);
};
class Ear
{
public:
void Listen(void);
};
// 正确的设计,冗长的程序
class Head
{
public:
void Look(void) { m_eye.Look(); }
void Smell(void) { m_nose.Smell(); }
void Eat(void) { m_mouth.Eat(); }
void Listen(void) { m_ear.Listen(); }
private:
Eye m_eye;
Nose m_nose;
Mouth m_mouth;
Ear m_ear;
};
int main()
{
Head myHead;
myHead.Look();
return 1;
}
【案例分析】
(1)若在逻辑上B 是A 的“一种”(a kind of ),则允许B 继承A 的功能。如男人(Man)是人(Human)的一种,男孩(Boy)是男人的一种。那么类Man 可以从类Human 派生,类Boy 可以从类Man 派生。
(2)若在逻辑上A 是B 的“一部分”(a part of),则不允许B 继承A 的功能,而是要用A和其它东西组合出B。
11.7 基类与派生类对象间的相互转换
案例11-12 基类与派生类之间对象指针转化
【案例描述】
对于基类与派生类对象之间:派生类对象可以用来当做基类对象使用,完成基类的功能,不需要类型转化。反之则不成立,需要一个派生类对象的地方,不能填入一个基类对象,效果如图11-12所示。
图11-12 基类与派生类之间对象指针转化
【实现过程】
定义2个类RandomNumber和Vector,为随机数类和队伍类。主函数main()输入参加游戏人数,要传的花从哪个人开始,传的花落在哪个人身上那个人就出列,再判断是否结束游戏。头文件代码如下:
#include <iostream>
using namespace std;
//没有使用虚函数的继承派生关系
class Base
{
public:
Base(int i = 0):ival(i){}
void getVal()
{
cout<<ival<<endl;
}
private:
int ival;
};
class Derived:public Base
{
public:
Derived(int i = 0, int j = 1):Base(i),ival(j){}
void getVal()
{
cout<<ival<<endl;
}
private:
int ival;
};
//使用了虚函数的继承派生关系
class Base1
{
public:
Base1(int i = 0):ival(i){}
virtual void getVal()
{
cout<<ival<<endl;
}
private:
int ival;
};
class Derived1:public Base1
{
public:
Derived1(int i = 0, int j = 1):Base1(i),ival(j){}
void getVal()
{
cout<<ival<<endl;
}
private:
int ival;
};
void useBaseObj(Base b)
{
b.getVal();
}
void useDerivedObj(Derived d)
{
d.getVal();
}
void useBasePtr(Base *pb)
{
pb->getVal();
}
void useDerivedPtr(Derived *pd)
{
pd->getVal();
}
void useBase1Obj(Base1 b)
{
b.getVal();
}
void useDerived1Obj(Derived1 d)
{
d.getVal();
}
void useBase1Ptr(Base1 *pb)
{
pb->getVal();
}
void useDerived1Ptr(Derived1 *pd)
{
pd->getVal();
}
int main()
{
Base b;
Derived d;
Base *pb = &b;
Derived *pd = &d;
useBaseObj(b);//基类实参,基类形参,调用基类函数
useBaseObj(d);//派生类实参,基类形参,自动转化,调用基类函数
// useDerivedObj((Derived)b);//无法用基类(自动的)构造出一个派生类
useDerivedObj(d);//派生类实参,派生类形参,调用派生类函数
cout<<endl;
useBasePtr(pb);//指向基类形参,指向基类实参,调用基类函数
useBasePtr(pd);//指向基类形参,指向派生类实参,调用静态类型-基类函数
// useDerivedPtr(pb);//指向基类实参,指向派生类形参,无法自动转化
useDerivedPtr((Derived*)pb);//强制类型转化,打印结果为随机数
//为什么?
//因为程序试图访问一个自己并没有的成员
useDerivedPtr(pd);//指向派生类实参,指向派生类形参,调用派生类
cout<<endl;
pb = new Derived;//静态类型为指向基类,动态类型为指向派生类
useBasePtr(pb);//形参为指向基类指针,实参静态类型为指向基类指针,调用基类函数
useDerivedPtr((Derived*)pb);//形参类型为指向派生类指针,实参静态类型为指向基类的指针
//但是由于内存中的确有这个数(因为我们实际new的是一个派生类)
//所以结果为派生类函数
cout<<endl;
Base1 b1;
Derived1 d1;
useBase1Obj(b1);//基类实参-基类形参,调用基类函数
useBase1Obj(d1);//派生类实参-基类形参,发生派生类到基类的转化,调用基类
// useDerivedObj((Derived)b);//基类实参,派生类形参,无法类型转化,函数报错
useDerived1Obj(d1);//派生类实参-派生类形参,调用派生类函数
cout<<endl;
Base1 *pb1 = &b1; //b1的实际类型为基类,它的虚函数表指明了如果通过指针或者引用调用自己,使用基类的方法
Derived1 *pd1 = &d1;//d1的实际类型为派生类,它的虚函数表指明了如果通过指针或者引用调用自己,使用派生类方法
useBase1Ptr(pb1); //通过虚函数表查得,对于pb1应该调用基类的方法
useBase1Ptr(pd1); //通过虚函数表查得,对于pd1应该调用基类的方法
// useDerivedPtr(pb1); //指向基类的实参,指向派生类形参,不匹配
useDerived1Ptr((Derived1*)pb1); //通过强制类型转换使其匹配,调用基类函数
//强制类型转化并没有修改虚函数表的内容,所以还是调用基类的方法
useDerived1Ptr(pd1);//通过虚函数表查得,调用派生类的方法
cout<<endl;
pb1 = new Derived1; //形参静态类型为基类,动态类型为派生类,虚函数表中指明了遇见它调用派生类函数
useBase1Ptr(pb1); //使用派生类方法
useDerived1Ptr((Derived1*)pb1);//使用派生类方法
return 0;
}
【案例分析】
对于指针和引用,分为两种情况:
(1)如无没有虚函数,调用基类还是派生类完全取决于函数形参的静态类型。如果形参与实参类型不符,需要类型转化:派生类指针转化为基类指针,由编译器自动完成;而基类指针转化为派生类,则必须强制完成。
(2)有虚函数,函数的调用是通过虚函数表来完成的。每次通过指针或者引用调用它时,都调用的是它实际类型的函数。
11.8 本章练习