第5章类与对象 引言 面向对象的程序设计方法就是运用面向对象的观点对现实世界中的各种问题进行抽象,然后用计算机程序来描述并解决该问题,这种描述和处理是通过类与对象实现的。类与对象是C++程序设计中最重要的概念,在C++中如何描述类与对象?如何通过建立类与对象解决现实世界的问题?这些将是本章重点讨论的内容。 学习目标 (1) 掌握类的定义,会根据需求设计类; (2) 会根据类创建各种对象; (3) 掌握对象的各种成员的使用方法; (4) 会设计构造函数与拷贝构造函数初始化对象,理解其调用过程与顺序; (5) 理解浅拷贝与深拷贝的概念; (6) 掌握动态对象以及动态对象数组的建立与释放; (7) 理解类的静态成员的概念; (8) 理解友元函数与友元类的概念; (9) 掌握常对象与常成员的使用; (10) 了解对象在内存中的分布情况。 5.1类与对象的概念 5.1.1从面向过程到面向对象 在面向过程的结构化程序设计中,程序模块由函数构成,函数将进行数据处理的语句放在函数体内,完成特定的功能,数据则通过函数参数传递进入函数体。在前面的章节中使用C++编写的实际上是面向过程的程序。在面向对象的程序设计中,程序模块是由类构成的。类是对逻辑上相关的函数与数据的封装,它是对问题的抽象描述。为了对面向对象的程序设计方法有一个初步的认识,我们先举一个例子。 【例51】模拟时钟。 分析: 不管什么样的时钟,也不管各种时钟是如何运行的,它们都具有时、分、秒3个属性。除了运行、显示时间的基本功能外,还有设置(调整)时间、设置闹钟等功能。将时钟的这些属性和功能抽象出来,分别给出面向过程的程序与面向对象的程序实现对时钟的模拟。 时钟程序A时钟程序B 1/******************************* /********************************* 2*程序名: p5_1_a.cpp**程序名: p5_1_b.cpp* 3*功能: 面向过程的时钟程序**功能: 面向对象的时钟程序* 4*******************************/*********************************/ 5#include #include 6using namespace std;using namespace std; 7structClock {class Clock { 8int H,M,S;private: 9};int H,M,S; 10Clock MyClock;public: 11void SetTime(int H,int M,int S)void SetTime(int h,int m,int s) 12{{ 13MyClock.H=(H>=0&&H<24)?H:0;H=(h>=0&&h<24)?h:0; 14MyClock.M=(M>=0&&M<60)?M:0;M=(m>=0&&m<60)?m:0; 15MyClock.S=(S>=0&&S<60)?S:0;S=(s>=0&&s<60)?s:0; 16}} 17void ShowTime()void ShowTime() 18{{ 19cout<=0&&h<24)?h:0; M=(m>=0&&m<60)?m:0; S=(s>=0&&s<60)?s:0; } 执行: Clock MyClock; MyClock.ShowTime(); 显示结果为: 0:0:0 这是因为建立对象时调用了Clock(),各个形参被设成了默认值。 当执行: Clock MyClock(9,30,45); MyClock.ShowTime(); 显示结果为: 9:30:45 这是因为建立对象时调用了Clock(9,30,45)。 利用构造函数初始化新建立对象的方式还有: Clock MyClock=Clock(9,30,45); Clock(9,30,45)可看作是一个Clock类型的常量。 构造函数是类的一个成员函数,除了具有一般成员函数的特征外,还具有以下特殊性质: (1) 构造函数的函数名必须与定义它的类同名。 (2) 构造函数没有返回值,如果在构造函数前加void是错误的。 (3) 构造函数被声明定义为公有函数。 (4) 构造函数在建立对象时由系统自动调用。 ☆注意: 由于构造函数可以重载,可以定义多个构造函数,在建立对象时根据参数来调用相应的构造函数。如果相应的构造函数没有定义,则出错。例如,若将例p5_1_b中的构造函数定义成不带默认形参值的构造函数: Clock(int h, int m,int s) { H=(h>=0&&h<24)?h:0; M=(m>=0&&m<60)?m:0; S=(s>=0&&s<60)?s:0; } 在定义对象Clock MyClock时将自动调用Clock(),而前面在例p5_1_b中Clock类没有Clock()函数,因此出错。 5.2.2析构函数 自然界万物都是有生有灭,程序中的对象也是一样。对象在定义时诞生,不同生存期的对象在不同的时期消失。在对象要消失时,通常有一些善后工作需要做,例如构造对象时,通过构造函数动态申请了一些内存单元,在对象消失之前就要释放这些内存单元。C++用什么来保证这些善后清除工作的执行呢?答案是析构函数。 析构函数(destructor)也译作拆构函数,是在对象消失之前的瞬间自动调用的函数,其形式如下: ~构造函数名(); 析构函数与构造函数的作用几乎相反,相当于“逆构造函数”。析构函数也是类的一个特殊的公有函数成员,它具有以下特点: (1) 析构函数没有任何参数,不能被重载,但可以是虚函数,一个类只有一个析构函数。 (2) 析构函数没有返回值。 (3) 析构函数名在类名前加上一个逻辑非运算符“~”,以与构造函数相区别。 (4) 析构函数一般由用户自己定义,在对象消失时由系统自动调用,如果用户没有定义析构函数,系统将自动生成一个不做任何事的默认析构函数。 ☆注意: 对象消失时的清理工作并不是由析构函数完成,而是靠用户在析构函数中添加清理语句完成。 【例52】构造函数与析构函数。 为Clock类重新定义一个析构函数如下: 1/*********************************** 2*程序名: p5_2.cpp * 3*功能: 构造函数与析构函数 * 4***********************************/ 5#include 6using namespace std; 7class Clock { 8private: 9int H,M,S; 10public: 11Clock(int h=0,int m=0,int s=0) 12{ 13 H=h,M=m,S=s; 14 cout<<"constructor:"< 6using namespace std; 7class String { 8private: 9char *Str; 10int len; 11public: 12void ShowStr() 13{ 14 cout<<"string:"< 6using namespace std; 7class Clock { 8private: 9int H,M,S; 10public: 11Clock(int h=0,int m=0,int s=0) 12{ 13 H=h,M=m,S=s; 14 cout<<"constructor:"<”运算符访问公有数据成员和成员函数。其语法形式如下: 对象指针名->数据成员名 或 对象指针名->成员函数名(参数表) 例如: Clock C1(8,0,0); Clock *Cp; Cp=&C1; Cp->ShowTime(); 在C++中,对象指针可以作为成员函数的形参,一般而言,使用对象指针作为函数的参数要比使用对象作为函数的参数更普遍一些,因为使用对象指针作为函数的参数有以下两点好处。 (1) 实现地址传递: 通过在调用函数时将实参对象的地址传递给形参指针对象,使形参指针对象和实参对象指向同一内存地址,这样对象指针所指向对象的改变也将同样影响实参对象,从而实现信息的双向传递。 (2) 使用对象指针效率高: 使用对象指针传递的仅仅是对应实参对象的地址,并不需要实现对象之间的副本复制,这样就会减小时空开销,提高运行效率。 【例56】时间加法。 时间加法有两种,一种是时钟加秒数,另一种是时钟加时、分、秒,采用重载函数实现这两种加法。 1/************************************ 2*程序名: p5_6.cpp* 3*功能: 带时间加法的时钟类* 4************************************/ 5#include 6using namespace std; 7class Clock { 8private: 9int H,M,S; 10public: 11void SetTime(int h,int m,int s) 12{ 13H=h,M=m,S=s; 14} 15void ShowTime() 16{ 17cout<H+H+(Cp->M+M+(Cp->S+S)/60)/60)%24; 34M=(Cp->M+M+(Cp->S+S)/60)%60; 35S=(Cp->S+S)%60; 36} 37void Clock∷TimeAdd(int h,int m,int s) 38{ 39H=(h+H+(m+M+(s+S)/60)/60)%24; 40M=(m+M+(s+S)/60)%60; 41S=(s+S)%60; 42} 43void Clock∷TimeAdd(int s) 44{ 45H=(H+(M+(S+s)/60)/60)%24; 46M=(M+(S+s)/60)%60; 47S=(S+s)%60; 48} 49int main() 50{ 51Clock C1; 52Clock C2(8,20,20); 53C1.TimeAdd(4000); 54C1.ShowTime(); 55C2.TimeAdd(&C1); 56C2.ShowTime(); 57return 0; 58} 运行结果: 1:6:40 9:27:0 5.3.2对象引用 对象引用就是对某类对象定义一个引用,其实质是通过将被引用对象的地址赋给引用对象,使二者指向同一个内存空间,这样引用对象就成为了被引用对象的“别名”。 对象引用的定义方法与基本数据类型变量引用的定义是一样的。定义一个对象引用,并同时指向一个对象的格式如下: 类名&对象引用名=被引用对象; ☆注意: (1) 对象引用与被引用对象必须是同类型的。 (2) 除非是作为函数参数与函数返回值,对象引用在定义时必须要初始化。 (3) 定义一个对象引用并没有定义一个对象,所以不分配任何内存空间,不调用构造函数。 对象引用的使用格式如下: 对象引用名.数据成员名 或 对象引用名.成员函数名(参数表) 例如: Clock C1(8,20,20); Clock& Cr=C1;//定义了C1的对象引用Cr Cr.ShowTime();//通过对象引用使用对象的成员 运行结果为: 8:20:20 对象引用通常用作函数的参数,它不仅具有对象指针的优点,而且比对象指针更简洁、更方便、更直观。在p5_6.cpp中添加以下函数: void Clock∷TimeAdd(Clock & Cr) { H=(Cr.H+H+(Cr.M+M+(Cr.S+S)/60)/60)%24; M=(Cr.M+M+(Cr.S+S)/60)%60; S=(Cr.S+S)%60; } 将“C2.TimeAdd(&C1);”替换为“C2.TimeAdd(C1);”,运行结果与p5_6.cpp一样。 5.3.3对象数组 对象数组是以对象为元素的数组。对象数组的定义、赋值、引用与普通数组一样,只是数组元素与普通数组的数组元素不同。对象数组的定义格式如下: 类名对象数组名[常量表达式n],…,[常量表达式2][常量表达式1]; 其中,类名指出该数组元素所属的类,常量表达式给出某一维元素的个数。 与结构数组不同,对象数组的初始化需要使用构造函数完成,以一个大小为n的一维数组为例,对象数组的初始化格式如下: 数组名[n]={类名(数据成员1初值,数据成员2初值,…), 类名(数据成员1初值,数据成员2初值,…), … 类名(数据成员1初值,数据成员2初值,…)};n 不带初始化表的对象数组,其初始化靠调用不带参数的构造函数完成。 以一个m维数组为例,对象数组元素的存取格式如下: 对象数组名[下标表达式1][下标表达式2]…[下标表达式m].数据成员名 或 对象数组名[下标表达式1][下标表达式2]…[下标表达式m].成员函数名(参数表) 【例57】计算一个班学生某门功课的总评成绩。 分析: 首先设计一个类Score,这个类的数据成员为一个学生的学号、姓名、平时成绩、期末考试成绩,成员函数有求总评成绩、显示成绩,然后定义一个对象数组存储一个班学生的成绩,最后通过逐一调用数组元素的成员函数求每个学生的总评成绩。 1/************************************************* 2*程序名: p5_7.cpp * 3*功能: 求一个班学生某门功课的总评成绩 * 4*************************************************/ 5#include 6using namespace std; 7const int MaxN=100; 8const double Rate=0.6;//平时成绩比例 9class Score { 10private: 11long No;//学号 12char *Name;//姓名 13int Usual; //平时成绩 14int Final; //期末考试成绩 15int Total; //总评成绩 16public: 17Score(long=0,char* = NULL,int=0,int=0,int=0);//构造函数 18void Count();//计算总评成绩 19void ShowScore();//显示成绩 20}; 21Score∷Score(long no,char *name,int usual,int final,int total) 22{//构造函数 23 No=no; 24 Name=name; 25 Usual=usual; 26 Final=final; 27 Total=total; 28} 29void Score∷Count() 30{ 31Total=Usual*Rate+Final*(1-Rate)+0.5; 32} 33void Score∷ShowScore() 34{ 35cout<ShowTime();//结果为0:0:0 Cp=new Clock(8,0,0); //建立动态对象,调用构造函数Clock(int,int,int) Cp->ShowTime();//结果为8:0:0 在堆中建立的动态对象不能自动消失,需要使用delete语句删除对象,格式如下: delete 对象指针; 在删除动态对象时,释放堆中的内存空间,在对象消失时,调用析构函数。例如: delete Cp; //删除Cp指向的动态对象 动态对象的一个重要的使用方面是用动态对象组成动态对象数组。建立一个一维动态对象数组的格式如下: 对象指针=new 类名[数组大小]; 删除一个动态对象数组的格式如下: delete[]对象指针; 在建立动态对象数组时,要调用构造函数,调用的次数与数组的大小相同; 在删除对象数组时,要调用析构函数,调用次数与数组的大小相同。 将p5_7.cpp改为用动态对象数组实现,代码如下: Score∷SetScore(long no,char *name,int usual,int final,int total) { No=no; Name=name; Usual=usual; Final=final; Total=total; } SetScore()函数为动态数组设置初值。 int main() { Score * ClassScore; ClassScore=new Score [3]; ClassScore[0].SetScore(202007001,"Ye Xiaulu",80,79), ClassScore[1].SetScore(202007002,"Luo Zhangxiang",90,85), ClassScore[2].SetScore(202007003,"Wu Weixue",70,55); for(int i=0;i<3;i++) ClassScore[i].Count(); for(i=0;i<3;i++) ClassScore[i].ShowScore(); delete [] ClassScore; return 0; } 5.3.5this指针 在一个类的成员函数中,有时希望引用调用它的对象,对此,C++采用隐含的this指针来实现。this指针是一个系统预定义的特殊指针,指向当前对象,表示当前对象的地址。例如: void Clock∷SetTime(int h, int m, int s) { H=h, M=m, S=s; this->H=h, this->M=m, this->S=s; (* this).H=h, (* this).M=m, (* this).S=s; }此3句是等效的 起初,为了与类的数据成员H、M、S相区别,将SetTime的形参名设为h、m、s。如果使用this指针,就可以凭this指针区分本对象的数据成员与其他变量。使用this指针重新设计的SetTime()成员函数如下: void Clock∷SetTime (int H, int M, int S) { this->H=H, this->M=M, this->S=S; } 系统利用this指针明确指出成员函数当前操作的数据成员所属的对象。实际上,当一个对象调用其成员函数时,编译器先将该对象的地址赋给this指针,然后调用成员函数,这样成员函数对对象的数据成员进行操作时就隐含使用了this指针。 一般而言,通常不直接使用this指针来引用对象成员,但在某些少数情况下,可以使用this指针,例如重载某些运算符以实现对象的连续赋值等。 ☆注意: (1) this指针不是调用对象的名称,而是指向调用对象的指针的名称。 (2) this的值不能改变,它总是指向当前调用对象。 5.3.6组合对象 在现实生活中有很多“组合”的例子,例如,如果你想根据自己工作或学习的需要配置一台合适的计算机,首先要根据自己的资金预算设计符合自己需要的个性化装机方案,然后从市场上选购CPU、主板、内存、硬盘、显示器、光驱、机箱、键盘、鼠标等硬件和软件,最后将这些硬件和软件按规范的方式“组合”。这样,你就能得到一台为自己“量身定制”的计算机,这就是组合的概念。 组合概念体现的是一种包含与被包含的关系,在语义上表现为“is a part of”的关系,即在逻辑上A是B的一部分,例如眼(eye)、鼻(nose)、口(mouth)、耳(ear)是头(head)的一部分,如果将head、eye、nose、mouth、ear定义成类,类head应该由类eye、nose、mouth、ear组合而成。同样,CPU、主板、内存等都是计算机的一个组成部分,相对于计算机这个复杂类而言,它们是成员类,所以我们把这种组合关系称为“is part of”。 在C++程序设计中,类的组合用来描述一类复杂的对象,在类的定义中,它的某些属性往往是另一个类的对象,而不是像整型、浮点型之类的简单数据类型,也就是“一个类内嵌其他类的对象作为成员”,对于将对象嵌入到类中的这样一种描述复杂类的方法,我们称之为“类的组合”,一个含有其他类对象的类称为组合类,组合类的对象称为组合对象。 1. 组合类的定义 组合类定义的步骤为先定义成员类,再定义组合类。 【例58】计算某次火车的旅途时间。 分析: 某次火车有车次、起点站、终点站、出发时间、到达时间。前面定义的Clock类具有时间特性,因此可以利用Clock对象组合成一个火车旅途类TrainTrip。假定火车均在24小时内到达,旅途时间为到达时间减去出发时间。 若用空方框表示类,用灰框表示对象,组合类可以表示为空框包含灰框。设计TrainTrip类的示意图与成员构成图如图52所示。 TrainTrip StartTimeEndTime 类名成员名 TrainTrip Clock StartTime H、M、S SetTime()、ShowTime() Clock EndTime H、M、S SetTime(), ShowTime() char * TrainNO Clock TripTime() 图52类TrainTrip的构成与成员 1/***************************************** 2*程序名: p5_8.cpp * 3*功能: 计算火车旅途时间的组合类 * 4*****************************************/ 5#include 6using namespace std; 7class Clock { 8private: 9int H,M,S; 10public: 11void ShowTime() 12{ 13 cout<H=H, this->M=M, this->S=S; 18} 19Clock(int H=0,int M=0,int S=0) 20{ 21 this->H=H, this->M=M, this->S=S; 22} 23int GetH() 24{ 25return H; 26} 27int GetM() 28{ 29return M; 30} 31int GetS() 32{ 33return S; 34} 35}; 36class TrainTrip { 37private: 38 39char *TrainNo;//车次 40Clock StartTime;//出发时间 41Clock EndTime;//到达时间 42public: 43TrainTrip(char * TrainNo, Clock S, Clock E) 44{ 45this->TrainNo=TrainNo; 46StartTime=S; 47EndTime=E; 48} 49Clock TripTime() 50{ 51int tH,tM,tS; //临时存储小时、分、秒数 52int carry=0;//借位 53Clock tTime;//临时存储时间 54(tS=EndTime.GetS()-StartTime.GetS())>0?carry=0:tS+=60, carry=1; 55(tM=EndTime.GetM()-StartTime.GetM()-carry)>0?carry=0:tM+=60, carry=1; 56(tH=EndTime.GetH()-StartTime.GetH()-carry)>0?carry=0: tH+=24; 57tTime.SetTime(tH,tM,tS); 58return tTime; 59} 60}; 61int main() 62{ 63 Clock C1(8,10,10), C2(6,1,2); //定义Clock类的对象 64 Clock C3; //定义Clock类对象,存储结果 65 TrainTrip T1("K16",C1,C2); //定义TrainTrip对象 66 C3=T1.TripTime(); 67 C3.ShowTime(); 68 return 0; 69} 运行结果: 21:50:52 程序解释: (1) 时、分、秒值H、M、S是Clock类的私有成员,在Clock类外无法存取; 而在TrainTrip类中需要H、M、S值,因此在Clock类中提供了公有的存取H、M、S值的接口函数GetH()、GetM()、GetS()供TrainTrip类读取H、M、S。程序第23~34行GetH()、GetM()、GetS()为读取H、M、S值的函数。 (2) 旅途时间的结果是一个时间值,因此将函数TripTime()返回值的类型设置成Clock类型。 (3) TrainTrip是组合类,为了给各个成员提供初值,建立了两个Clock对象C1、C2,并设计了一个构造函数TrainTrip,将C1、C2的值赋给组合类的成员对象。程序第65行在建立对象T1时利用构造函数将初值赋给T1。 2. 组合对象的初始化 在程序p5_8.cpp中,为了初始化成员对象,建立了两个对象C1、C2,这两个对象除了用于初始化外,没有其他用处,而且在程序运行期间一直占据内存,造成额外的内存开销。 C++为组合对象提供了初始化机制: 在定义组合类的构造函数时可以携带初始化表。其格式如下: 组合类名(形参表): 成员对象1(子形参表1),成员对象2(子形参表2),… 成员对象初始化表 其中:  “成员对象1(子形参表1),成员对象2(子形参表2),…”称为初始化列表,该表放在构造函数的头部,各参数之间用逗号隔开。成员对象可以是类的普通数据成员。  形参表中的形参为该类的所有数据成员提供初值,子形参表中的形参为形参表中形参的一部分,子形参表n为成员对象n提供初值。 在为组合类定义了带初始化表的构造函数后,在建立组合对象时为对象提供初值的格式如下: 类名 对象名(实参表); 在建立对象时,调用组合类的构造函数; 在调用组合类的构造函数时,先调用各个成员对象的构造函数,成员对象的初值从初始化列表中取得。这样,实际上是通过成员类的构造函数对成员对象进行初始化,初始化值在初始化表中提供。 使用初始化列表,将p5_8.cpp修改成p5_8a.cpp,将其中的构造函数修改如下: TrainTrip(char * TrainNo, int SH,int SM, int SS, int EH, int EM,int ES): EndTime(EH,EM,ES), StartTime(SH,SM,SS) { this->TrainNo=TrainNo; } 将定义组合对象的程序修改如下: int main() { Clock C3; //定义Clock类对象,存储结果 TrainTrip T1("K16",8,10,10,6,1,2);//定义TrainTrip对象 C3=T1.TripTime(); C3.ShowTime(); return 0; } 程序运行结果与p5_8.cpp完全相同。 在建立对象T1时调用构造函数TrainTrip(),由于建立对象T1要先建立StartTime、EndTime两个成员对象,因此分别通过StartTime、EndTime调用构造函数Clock(),构造函数的参数从初始化列表中取得。在StartTime、EndTime构造完毕后,再执行TrainTrip()余下的部分。在构造StartTime、EndTime时如果没有初始化列表,则分别调用默认形式的构造函数Clock()。 ☆注意: 初始化列表既不能决定是否调用成员对象的构造函数,也不能决定调用构造函数的顺序,成员对象的调用顺序由成员对象定义的顺序决定,初始化列表只是提供调用成员对象构造函数的参数。 对于C++语言而言,类中的成员以其在类中声明的先后顺序依次构造,而不是根据构造函数初始化列表中成员对象构造函数声明的先后顺序来决定。如果构造函数中指定了一个特殊的构造顺序,那么析构函数将不得不去查询构造函数的定义,以获得如何对成员进行析构的顺序。由于构造函数和析构函数可以在不同的文件中定义,这就给编译器的实现者造成一个难题,而且由于构造函数可以重载,因此一个类可以有两个或者更多的构造函数。对于这些构造函数的定义,程序设计者并不能保证它们中的所有成员对象声明的先后顺序都是一致的,所以C++语言只能依据类中成员对象的声明顺序决定成员对象的构造和析构顺序。 在上述初始化列表中,初始化StartTime与EndTime采用的是用H、M、S分别初始化成员对象的方法,也可以采用Clock对象整体初始化成员对象。将p5_8a.cpp修改成p5_8b.cpp,对其中的构造函数修改如下: TrainTrip(char * TrainNo, Clock S, Clock E): StartTime(S),EndTime(E) { this->TrainNo=TrainNo; } 可采用与p5_8.cpp完全相同的方式定义组合对象,也可以将定义方式修改如下: int main() { Clock C3;//定义Clock类对象,存储结果 TrainTrip T1("K16",Clock(8,10,10),Clock(6,1,2)); //定义TrainTrip对象 C3=T1.TripTime(); C3.ShowTime(); return 0; } 与p5_8.cpp相比,发现分别使用常量Clock(8,10,10)、Clock(6,1,2)取代了C1、C2作为成员对象的初值。 3. 组合对象的构造函数 在定义一个组合类的对象时,不仅它自身的构造函数将被调用,还将调用其成员对象的构造函数,调用的先后顺序如下: (1) 成员对象按照在其组合类的声明中出现的次序依次调用各自的构造函数,而不是按初始化列表中的顺序。如果建立组合类的成员对象时没有指定对象的初始值,则自动调用默认的构造函数。 (2) 组合类对象调用组合类构造函数。 (3) 调用析构函数,析构函数的调用顺序与构造函数正好相反。 p5_8a.cpp的构造函数的调用顺序如下: 用对象初始化成员对象,在实参对象与形参结合时,函数返回对象时还会调用拷贝构造函数。程序p5_8.cpp、p5_8a.cpp和p5_8b.cpp的构造函数、拷贝构造函数、析构函数的详细调用过程留给读者作为练习。 4. 组合类成员对象的访问权限 组合类成员对象的成员有存取控制权限,成员对象作为组合类的成员后,又有存取控制权限。成员对象中的成员在组合类中的存取权限经过了两次限制,其最终权限见表52。 表52各种成员对象在组合类中的访问控制属性 组合类成 员 对 象 publicprotectedprivate publicpublic不可访问不可访问 protectedprotected不可访问不可访问 privateprivate不可访问不可访问 例如: 1class Clock {class Clock { 2protected:public: 3int H,M,S;int H,M,S; 4 5}; }; 6class TrainTrip {class TrainTrip { 7public:public: 8Clock StartTime;Clock StartTime; 9Clock EndTime;Clock EndTime; 10void ShowEndTime()void ShowEndTime() 11{{ 12cout< 6using namespace std; 7class Student { 8 private: 9 char *Name; 10 int No; 11 static int countS; 12 public: 13static int GetCount() 14 { 15 return countS; 16 } 17 Student(char* ="", int=0); 18 Student(Student &); 19 ~Student(); 20}; 21Student∷Student(char * Name, int No) 22{ 23 this->Name=new char [strlen(Name)+1]; 24 strcpy(this->Name, Name); 25 this->No=No; 26 ++countS; 27 cout<<"constructing:"< 6using namespace std; 7class Clock { 8private: 9int H,M,S; 10public: 11void ShowTime() 12{ 13 cout<H=H, this->M=M, this->S=S; 18} 19Clock(int H=0,int M=0,int S=0) 20{ 21 this->H=H, this->M=M, this->S=S; 22} 23friend Clock TripTime(Clock & StartTime, Clock & EndTime); 24}; 25Clock TripTime(Clock & StartTime, Clock & EndTime) 26{ 27int tH,tM,tS; //临时存储小时、分、秒数 28int carry=0;//借位 29Clock tTime;//临时存储时间 30(tS=EndTime.S-StartTime.S)>0?carry=0:tS+=60,carry=1; 31(tM=EndTime.M-StartTime.M-carry)>0?carry=0:tM+=60,carry=1; 32(tH=EndTime.H-StartTime.H-carry)>0?carry=0:tH+=24; 33tTime.SetTime(tH,tM,tS); 34return tTime; 35} 36int main() 37{ 38Clock C1(8,10,10), C2(6,1,2); //定义Clock类的对象 39Clock C3; //定义Clock类对象,存储结果 40C3=TripTime(C1,C2); 41C3.ShowTime(); 42return 0; 43} 运行结果: 21:50:52 程序解释: (1) 第25行,定义友元函数,如果前面加上Clock∷是错误的。 (2) 第40行,调用友元函数,如果通过对象调用C1.TripTime()是错误的。 在本例中,在Clock类体中设计了一个友元函数TripTime(),它不是类的成员函数。但是,我们可以看到友元函数中通过对象名StartTime和EndTime直接访问了它们的私有数据成员StartTime.H、StartTime.M、StartTime.S,这就是友元的关键所在。 使用友元成员的好处是两个类可以以某种方式相互合作、协同工作,共同完成某一个任务。 5.5.2友元类 友元除了可以是函数外,还可以是类,如果一个类声明为另一个类的友元,则该类称为另一个类的友元类。若A类为B类的友元类,则A类的所有成员函数都是B类的友元函数,都可以访问B类的任何数据成员。 友元类的声明是在类名之前加上关键字friend实现的。声明A类为B类的友元类的格式如下: class B { ... friend class A; }; 这里要注意一个问题,在声明A类为B类的友元类时,A类必须已经存在,但是如果A类又将B类声明为自己的友元类,又会出现B类不存在的错误。 显然,当遇到两个类相互引用的情况时,必然有一个类在定义之前就被引用,那么怎么办呢?对此,C++专门规定了前向引用声明,用于解决这类问题。前向引用声明是在引用未定义的类之前对该类进行声明,它只是为程序引入一个代表该类的标识符,类的具体定义可以在程序的其他地方进行。 例如下面的情况,类A的成员函数funA()的形式参数是类B的对象,同时类B的成员函数funB()以类A的对象为形参,这时就必须使用前向引用声明: classB; //前向引用声明 classA{ //A类的定义 public://外部接口 void funA(Bb); //以B类对象b为形参的成员函数 }; classB{ //B类的定义 public://外部接口 void funB(Aa);//以A类对象a为形参的成员函数 }; 【例511】使用友元类计算某次火车的旅途时间。 分析: 在p5_8.cpp中,定义了一个组合类TrainTrip,组合了Clock类对象表示某次火车的出发时间、到达时间。但是, TrainTrip中的成员函数无法直接存取出发时间、到达时间中的访问控制为private的H、M、S。如果将TrainTrip定义为Clock的友元类,则TrainTrip中的成员函数可以直接存取出发时间、到达时间中的数据成员。 1/******************************************** 2*程序名: p5_11.cpp* 3*功能: 计算火车旅途时间的友元类 * 4********************************************/ 5#include 6using namespace std; 7class TrainTrip; //前向引用声明 8class Clock { 9private: 10int H,M,S; 11public: 12void ShowTime() 13{ 14 cout<H=H, this->M=M, this->S=S; 19} 20Clock(int H=0,int M=0,int S=0) 21{ 22 this->H=H, this->M=M, this->S=S; 23} 24friend class TrainTrip; 25}; 26class TrainTrip { 27private: 28 char *TrainNo;//车次 29 Clock StartTime;//出发时间 30 Clock EndTime;//到达时间 31public: 32 TrainTrip(char * TrainNo, Clock S, Clock E) 33 { 34this->TrainNo=TrainNo; 35StartTime=S; 36EndTime=E; 37 } 38Clock TripTime() 39{ 40 int tH,tM,tS; //临时存储小时、分、秒数 41 int carry=0;//借位 42 Clock tTime;//临时存储时间 43(tS=EndTime.S-StartTime.S)>0?carry=0:tS+=60,carry=1; 44(tM=EndTime.M-StartTime.M-carry)>0?carry=0:tM+=60,carry=1; 45(tH=EndTime.H-StartTime.H-carry)>0?carry=0:tH+=24; 46tTime.SetTime(tH,tM,tS); 47return tTime; 48 } 49}; 50int main() 51{ 52Clock C1(8,10,10), C2(6,1,2); //定义Clock类的对象 53Clock C3;//定义Clock类对象,存储结果 54TrainTrip T1("K16",C1,C2); //定义TrainTrip对象 55C3=T1.TripTime(); 56C3.ShowTime(); 57return 0; 58} 运行结果: 21:50:52 友元关系具有以下性质: (1) 友元关系是不能传递的,B类是A类的友元,C类是B类的友元,在C类和A类之间,如果没有声明,就没有任何友元关系,就不能进行数据共享。 (2) 友元关系是单向的,如果声明B类是A类的友元,B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函数不能访问B类的私有和保护数据。 友元概念的引入提高了数据的共享性,加强了函数与函数之间、类与类之间的相互联系,大大提高了程序的效率,这是友元的优点,但友元也破坏了数据隐蔽和数据封装,导致程序的可维护性变差,给程序的重用和扩充埋下了深深的隐患,这是友元的缺点。 因此,在使用友元时必须慎重,要具体问题具体分析,要在提高效率和增加共享之间把握好一个“度”,要在共享和封装之间进行恰当的平衡。 5.6常成员与常对象 由于常量是不可改变的,因此我们将“常”广泛用在C++中表示不可改变的量,例如前面讲的常变量。不仅变量可以定义为常变量,函数的形参、类的成员、对象也可以使用const定义为“常”,以便对实参、类成员、类进行保护。 5.6.1函数实参的保护 函数的形参如果用const修饰,在函数体中该形参为只读变量,在函数体中不能对该形参变量进行修改。 如果形参变量是一个基本类型的变量,实参采用传值方式进行参数传递,无论在函数体中是否对形参变量进行了修改,都不会影响到实参。此时,const修饰形参的意义仅仅表明该形参是一个读入值,在函数体内没有对其进行修改。 但是,如果形参变量是指针型或引用型,参数传递是传地址与“传名”方式,函数体对实参的修改会影响到实参,为了对实参进行保护,需要用const对实参进行修饰。C++中大量系统函数的形参均用const进行修饰。例如 int atoi( const char *str ); //将数字串转换成整数 char *strcpy( char *to, const char *from );//将源字符串from复制到目标串to中,保护源串from string& insert( size_type index, const string& str );//在当前串的index位置插入串str, 保护串str 在自己设计的程序中,要养成将只读形参用const修饰的习惯。一方面保护实参,避免错误,使程序健壮; 另一方面,增强程序的可读性。 5.6.2常对象 在程序中,我们有时候不允许修改某些特定的对象。如果某个对象不允许被修改,则称该对象为常对象。在C++中用关键字const来定义常对象。 C++编译器对常对象(const对象)的使用是极为苛刻的,它不允许常对象调用任何类的成员函数,而且常对象一经定义,在其生存期内不允许改变,否则将导致编译错误。 常对象的定义格式如下: 类型const对象名; 或 const类型对象名; 例如: Clock const C1(9,9,9); 这里,我们就建立类Clock的一个const对象,并初始化该对象,该对象在程序运行过程中不能被修改。 既然const对象不能被任何对象修改,那么它又能否被其他对象访问呢? C++规定只有类的常成员函数(const成员函数)才能访问该类的常对象,当然,const成员函数依然不允许修改常对象。 下列程序段演示了常对象的使用。 int main() { constClockC1(9,9,9);//定义常对象C1 ClockconstC2(10,10,10);//定义常对象C2 ClockC3(11,11,11); //C1=C3;//错误!C1为常对象, 不能被赋值 //C1.ShowTime();//错误!C1为常对象, 不能访问非常成员函数 C3. ShowTime(); //C1.SetTime(0,0,0); //错误!C1为常对象, 不能被更新 return 0; } 程序运行说明: (1) const对象不能被赋值,所以必须在定义时由构造函数初始化; (2) const对象不能访问非常成员函数,只能访问常成员函数。 若将成员函数改为常成员函数,如下所示: void ShowTime() const {cout< 6using namespace std; 7class A { 8private: 9const int& r;//常引用数据成员 10const int a; //常数据成员 11static const int b; //静态常数据成员 12public: 13A(int i):a(i),r(a)//常数据成员只能通过初始化列表获得初值 14{ 15cout<<"constructor!"<,通过对象引用使用对象的成员要用操作符。 (7) 对象数组是以对象为元素的数组,对象数组的定义、赋值、引用与普通数组一样,建立一个对象数组相当于建立了多个对象,因此多次调用构造函数。对象数组的初始化需要使用构造函数完成。 (8) 建立动态对象使用语句new,动态对象一定要用语句delete删除。建立动态对象数组使用语句new[],删除动态对象数组使用语句delete[]。 (9) this指针是一个系统预定义的指向当前对象的指针,通过this指针可以访问对象的成员。 (10) 组合类是含有类对象的类,组合类对象称为组合对象。在定义组合对象时调用构造函数的顺序为类中成员对象定义的顺序,子对象在构造时初始值通过组合类构造函数的成员对象初始化表提供。 (11) 静态数据成员是类的数据成员,独立于类存在,在类内定义,在类外初始化。静态成员函数属于整个类,是该类的所有对象共享的成员函数,可通过类名、对象调用静态成员函数访问静态函数成员。 (12) 友元函数不是类的成员,但它可以访问类的任何成员。一个类的友元类可以访问该类的任何成员。 (13) 使用关键字const定义的对象称为常对象,常对象的成员不允许被修改。使用const定义的数据成员称为常数据成员,常数据成员不能被更新。在定义时使用const关键字修饰的成员函数称为常成员函数,用于访问类的常对象。 (14) 各类对象在内存中的分布以及生命周期与普通变量一样,一个类的所有对象共有该类的成员函数,独享各自的数据成员。 习题5 1. 填空题 (1) 类的只能被该类的成员函数或友元函数访问。 (2) 类的数据成员不能在定义的时候初始化,而应该通过初始化。 (3) 类成员默认的访问方式是。 (4) 类的是该类给外界提供的接口。 (5) 类的可以被类作用域内的任何对象访问。 (6) 为了能够访问某个类的私有成员,必须在该类中声明该类的。 (7) 为该类的所有对象共享。 (8) 每个对象都有一个指向自身的指针,称为指针,通过使用它来确定其自身的地址。 (9) 运算符自动建立一个大小合适的对象并返回一个具有正确类型的指针。 (10) C++禁止访问const对象。 (11) 在定义类的动态对象数组时,系统自动调用该类的函数对其进行初始化。 (12) C++中语句“ p="hello"; ”所定义的指针p和它所指的内容都不能被改变。 (13) 假定AB为一个类,则语句“”为该类拷贝构造函数的原型说明。 (14) 在C++中,访问一个对象的成员所用的运算符是,访问一个指针所指向对象的成员所用的运算符是。 (15) 析构函数在对象的时被自动调用,全局对象和静态对象的析构函数在调用。 (16) 设p是指向一个类动态对象的指针变量,则执行“delete p;”语句时将自动调用该类的。 2. 选择题 (1) 数据封装就是将一组数据和与这组数据有关的操作组装在一起,形成一个实体,这个实体也就是()。 A. 类 B. 对象 C. 函数体 D. 数据块 (2) 类的实例化是指()。 A. 定义类 B. 创建类的对象 C. 指明具体类 D. 调用类的成员 (3) 已知p是一个指向类Sample数据成员m的指针,s是类Sample中的一个对象。如果要给m赋值为5,正确的是()。 A. s.p=5; B. s->p=5; C. s.*p=5; D. *s.p=5; (4) 关于类和对象的说法不正确的是()。 A. 对象是类的一个实例 B. 一个类只能有一个对象 C. 一个对象只能属于一个具体的类 D. 类与对象的关系和数据类型与变量的关系是相似的 (5) 下列说法错误的是()。 A. 封装是将一组数据和这组数据有关的操作组装在一起 B. 封装使对象之间不需要确定的接口 C. 封装要求对象具有明确的功能 D. 封装使得一个对象可以像一个部件一样用在各种程序中 (6) 下面说法正确的是()。 A. 内联函数在运行时是将该函数的目标代码插入每个调用该函数的地方 B. 内联函数在编译时是将该函数的目标代码插入每个调用该函数的地方 C. 类的内联函数只能在类体内定义 D. 类的内联函数只能在类体外通过加关键字inline定义 (7) 下列说法正确的是()。 A. 类定义中只能说明函数成员的函数头,不能定义函数体 B. 类中的函数成员可以在类体中定义,也可以在类体之外定义 C. 类中的函数成员在类体之外定义时必须要与类声明在同一个文件中 D. 在类体之外定义的函数成员不能操作该类的私有数据成员 (8) 下面关于对象概念的描述错误的是()。 A. 对象就是C语言中的结构体变量 B. 对象代表着正在创建的系统中的一个实体 C. 对象是一个状态和操作(或方法)的封装体 D. 对象之间的信息传递是通过消息进行的 (9) 在建立类的对象时()。 A. 为每个对象分配用于保存数据成员的内存 B. 为每个对象分配用于保存函数成员的内存 C. 为所有对象的数据成员和函数成员分配一个共享的内存 D. 为每个对象的数据成员和函数成员同时分配不同内存 (10) 有以下类定义: class SAMPLE { int n; public: SAMPLE(int i=0):n(i){} void setValue(int n0); }; 下列关于setValue成员函数的实现正确的是()。 A. SAMPLE∷setValue(int n0){n=n0;} B. void SAMPLE∷setValue(int n0){n=n0;} C. void setValue(int n0){n=n0;} D. setValue(int n0){n=n0;} (11) 在下面的类定义中,错误的语句是()。 class sample { public: sample(int val);//① ~sample();//② private: int a=2.5;//③ sample();//④ } A. ①②③④ B. ② C. ③④ D. ①②③ (12) 对于任意一个类,析构函数的个数最多为()。 A. 0 B. 1 C. 2 D. 3 (13) 类的构造函数被自动调用执行的情况是在定义该类的()时。 A. 成员函数 B. 数据成员 C. 对象 D. 友元函数 (14) 有关构造函数的说法不正确的是()。 A. 构造函数的名字和类的名字一样 B. 构造函数在声明类变量时自动执行 C. 构造函数无任何函数类型 D. 构造函数有且只有一个 (15) ()是析构函数的特征。 A. 一个类中只能定义一个析构函数 B. 析构函数名与类名没任何关系 C. 析构函数的定义只能在类体内 D. 析构函数可以有一个或多个参数 (16) 下列()不是构造函数的特征。 A. 构造函数的函数名和类名相同 B. 构造函数可以重载 C. 构造函数可以设置默认参数 D. 构造函数必须指定类型说明 (17) 在下列函数原型中,可以作为类AA构造函数的是()。 A. void AA(int); B. int AA( ); C. AA(int)const; D. AA(int); (18) 下列关于成员函数特征的描述,()是错误的。 A. 成员函数一定是内联函数 B. 成员函数可以重载 C. 成员函数可以设置参数的默认值 D. 成员函数可以是静态的 (19) 不属于成员函数的是()。 A. 静态成员函数 B. 友元函数 C. 构造函数 D. 析构函数 (20) 已知类A是类B的友元,类B是类C的友元,则()。 A. 类A一定是类C的友元 B. 类C一定是类A的友元 C. 类C的成员函数可以访问类B的对象的任何成员 D. 类A的成员函数可以访问类B的对象的任何成员 (21) 关于动态存储分配,下列说法正确的是()。 A. new和delete是C++中用于动态内存分配和释放的函数 B. 动态分配的内存空间也可以被初始化 C. 当系统内存不够时,会自动回收不再使用的内存单元,因此程序中不必用delete释放内存空间 D. 当动态分配内存失败时,系统会立刻崩溃,因此一定用慎用new (22) 静态成员函数没有()。 A. 返回值 B. this指针 C. 指针参数 D. 返回类型 (23) 有以下类定义: class Foo { int bar; }; 则Foo类的成员bar是()。 A. 公有数据成员 B. 公有成员函数 C. 私有数据成员 D. 私有成员函数 (24) 下列关于this指针的叙述正确的是()。 A. 任何与类相关的函数都有this指针 B. 类的成员函数都有this指针 C. 类的友元函数都有this指针 D. 类的非静态成员函数才有this指针 (25) 下列程序的执行结果是()。 #include using namespace std; class Test { public: Test(){ n+=2; } ~Test(){ n-=3; } static int getNum() { return n; } private: static int n; }; int Test∷n = 1; int main() { Test* p = new Test; delete p; cout << "n=" << Test∷getNum() << endl; return 0; } A. n=0 B. n=1 C. n=2 D. n=3 (26) 下列程序执行后的输出结果是()。 #include using namespace std; class AA{ int n; public: AA(int k):n(k){} int get(){ return n;} int get()const{ return n+1;} }; int main() { AA a(5); const AA b(6); cout<a=a;}//② static int g(){ return a;} //③ void h(int b){Test∷b=b;}; //④ private: int a; static int b; const int c; }; int Test∷b=0; 在标注号码的行中,能被正确编译的是()。 A. ① B. ② C. ③ D. ④ (31) 若有以下类声明: class MyClass { public: MyClass(){cout<<1;} }; 执行下列语句: MyClass a,b[2],*P[2]; 程序的输出结果是()。 A. 11 B. 111 C. 1111 D. 11111 (32) 有以下程序: #include using namespace std; class A { public: static int a; void init(){a=1;} A(int a=2) { init(); a++; } }; int A∷a=0; A obj; int main() { cout< using namespace std; class MyClass { public: MyClass(){cout<<"A";} MyClass(char c) { cout< using namespace std; class MyClass { public: int number; void set(int i); }; int number=3; void MyClass∷set (int i) { number=i; } int main() { MyClass my1; int number=10; my1.set(5); cout< using namespace std; class Location{ public: int X,Y; void init (int initX,int initY) { X=initX, Y=initY; } int GetX() { return X; } int GetY() { return Y; } }; void display(Location& rL) { cout<init(7,8); for (int i=0;i<5;i++) display(*(rA++)); return 0; } (3) #include using namespace std; class Test{ private: static int val; int a; public: static int func(); void sfunc(Test &r); }; int Test∷val=200; int Test∷func() { return val++; } void Test∷sfunc(Test &r) { r.a=125; cout<<"Result3="< using namespace std; class ConstFun{ public: void ConstFun(){} const int f5()const{return 5;} int Obj() {return 45;} int val; int f8(); }; int ConstFun∷f8(){return val;} int main() { const ConstFun s; int i=s.f5(); cout<<"Value="<