类和对象及其封装性_第1页
类和对象及其封装性_第2页
类和对象及其封装性_第3页
类和对象及其封装性_第4页
类和对象及其封装性_第5页
已阅读5页,还剩138页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

类和对象及其封装性第一页,共一百四十三页,2022年,8月28日本章要点

1类的定义及其类对象的封装性

2类成员函数的定义

3类对象的定义和使用

4类的构造函数和析构函数

5类对象的动态创建和释放

6类对象的赋值与复制

7与类对象有关的指针

8类的静态成员

9类对象成员、类对象数组和类对象参数

10友元(友元函数、友元成员和友元类)

11类的只读成员函数定义第二页,共一百四十三页,2022年,8月28日3.1类的定义及其类对象的封装性无论采用哪种程序设计范型所设计的程序都是由数据处理这些数据的操作组成的。程序的运行就是按照特定的结构和流程将操作施加在相应的数据上,完成程序需要实现的功能。在传统设计范型中,数据是使用语言所提供的简单数据类型和构造数据类型(例如C语言中的结构类型

struct)定义生成的;而操作是通过过程或函数的形式定义提供的。第三页,共一百四十三页,2022年,8月28日在面向对象设计范型中,使用了数据抽象的概念,即数据总是与允许施加它们的操作绑定在一起的。这就要求编程语言能够提供符合数据抽象的预定义数据类型,特别需要提供能构造符合数据抽象用户自定义类型的构造数据类型(例如C++

和Java

语言中的类类型class)。程序中的数据和操作都是由按数据抽象封装起来的对象提供的。第四页,共一百四十三页,2022年,8月28日3.1.1C++的类类型定义

在C++中,用户可以使用类类型关键字

class定义自己的抽象数据类型。这种定义方法和形式与使用结构体类型关键字struct定义数据结构类型十分相似。例如,可以用struct定义描述学生基本信息的数据结构类型Student:

struct

Student { intnum; charname[20]; charsex; };第五页,共一百四十三页,2022年,8月28日同样,可以用class定义描述学生基本信息和基本操作的数据类型Student:

class

Student

{ intnum; charname[20]; charsex;

public: voiddisplay() { cout<<“num:”<<num<<endl; cout<<“name:”<<name<<endl; cout<<“sex:”<<sex<<endl; } };第六页,共一百四十三页,2022年,8月28日比较两种用户自定义类型,它们的共同之处表现在:⑴类型定义首行的格式相同,由类型关键字(struct

class)与自定义类名组成。例如:“structStudent”和“classStudent”。⑵类型定义体都使用左花括号“{”表示开始,使用右花括号“}”表示结束,并用分号“;”表示整个自定义类型定义工作完成。⑶使用自定义类型(结构或类)定义类型实体(结构

变量或类对象)的格式相同。在面向对象的程序设计中通常将所有的类型(包括系统预定义的简单数据类型)实体可以统一称为对象。例如,可使用自定义结构类型Student定义结构对象:

Studentstud1,stud2;

也可使用自定义类类型Student定义类对象:

Studentstud1,stud2;第七页,共一百四十三页,2022年,8月28日在面向对象的程序设计中通常将所有的类型(包括系统预定义的简单数据类型)实体可以统一称为对象。例如,可使用自定义结构类型Student定义结构对象:

Studentstud1,stud2;

也可使用自定义类类型Student定义类对象:

Studentstud1,stud2;第八页,共一百四十三页,2022年,8月28日二者的不同之处表现在:⑴使用C

语言的struct

定义的结构类型的定义体中只包含数据成员,而使用class定义类类型的定义体中既包含数据成员,还包含了操作这些数据的成员函数。注意,在C++

中,struct

定义能力被扩展,也可以class定义一样包含操作数据成员的成员函数。⑵结构类型的成员的缺省访问权限均为公有,即可以从结构对象外直接访问。例如:

Studentstud1; cout<<“num:”<<stud1.num<<endl;第九页,共一百四十三页,2022年,8月28日

cout<<“name:”<<<<endl; cout<<“sex:”<<stud1.sex<<endl;

而类类型的成员的缺省访问权限均为私有,例如,类Student

中的数据成员就不能从类外直接访问,而显示这些数据只能通过调用成员函数

display实现。

Studentstud1; cout<<“name:”<<<<endl;//非法

stud1.display();第十页,共一百四十三页,2022年,8月28日3.1.2类类型成员的访问权限为了实现类对象的封装性(数据隐藏和提供访问接口)类类型定义为类成员提供了私有和公有两种基本访问权限供用户选择。1私有成员①访问权限:只限于类成员访问。②关键字:private声明或从定义体开始的缺省声明。例如,下面的Student定义与前面的Student

定义等价的:第十一页,共一百四十三页,2022年,8月28日

class

Student {

private: intnum; charname[20]; charsex;

public: voiddisplay(){ cout<<“num:”<<num<<endl; cout<<“name:”<<name<<endl; cout<<“sex:”<<sex<<endl; } };第十二页,共一百四十三页,2022年,8月28日③私有段:从private:

开始至其它访问权限声明之间 所有成员组成的代码段。例如Student

定义中从

private:

开始到public:

之间的所数据成员。④成员种类:数据成员和成员函数。2公有成员①访问权限:允许类成员和类外的任何访问。②关键字:public。③公有段:从public:

至其它成员声明之间所有成员 组成的代码段。④成员种类:数据成员和成员函数。第十三页,共一百四十三页,2022年,8月28日使用私有成员来隐藏由类对象操作的数据,然后提供相应的公有成员函数来访问和操作这些数据,而访问和操作这些数据实现细节通常是被隐藏起来的。除了私有和公有两种基本访问权限外,类类型定义还提供了允许类成员和派生类成员访问,而不允许类外访问的保护成员访问权限(protected),以满足实现继承性的需要。第十四页,共一百四十三页,2022年,8月28日为了使C++

语言所设计程序中的数据都能实现数据抽象,并能与C

语言设计的程序中的数据兼容,C++

仿照类类型定义的功能,对struct

定义的结构体类型功能进行如下扩展:⑴定义体中也可以包括对数据成员进行处理和操作的

成员函数。⑵添加了与类类型定义相同的成员访问权限声明功能,但仍然保留了缺省声明表示成员的访问权限为

公用的基本特点。扩展后的struct

可以定义与类类型效果相同的结构类型,例如:第十五页,共一百四十三页,2022年,8月28日

struct

Student {

private: intnum; charname[20]; charsex;

public: voiddisplay(){ cout<<“num:”<<num<<endl; cout<<“name:”<<name<<endl; cout<<“sex:”<<sex<<endl; } };第十六页,共一百四十三页,2022年,8月28日该结构类型与先前使用class

定义的Student

类型的效果完全相同。请注意,这并不意味着可以用struct

替代class,因为使用class定义的类类型的缺省私有性质能更方便、安全地实现面向对象程序设计对类对象的要求,因此强烈建议使用class建立数据类型;而只有希望所建立类型的全部成员都是公有访问权限时,使用struct建立结构类型比较方便。第十七页,共一百四十三页,2022年,8月28日3.1.3类类型的构造

类类型定义为所定义的数据类型建立了一个明确的边界,类定义体中的私有成员(数据成员和成员函数)和公有成员函数的实现细节均被封装在此边界内,使得这些类成员和实现细节无法从类对象外被访问,从而受到保护。同时对类公有成员(数据成员和成员函数)的访问和调用又为类对象之间的通讯提供了接口,使得类对象成为一个既访问安全又操作方便的抽象数据实体。下面以一个简单机器人类为例说明类类型的构造:第十八页,共一百四十三页,2022年,8月28日①确定机器人状态的属性:是位置和面对的方向②改变、访问和显示机器人状态的操作:有定位、

转向、前进、显示状态等。

classHominoid { intdirction; //机器人的方向

pointlocation; //机器人的位置

public: voidturnLeft(); //向左转90度

voidturnRight(); //向右转90度

booladvance(); //前进一步

pointlocation(pointloc); //定位

boolfacingWall(); //判断是否面对墙壁

voiddisplay(); //显示当前位置

… };第十九页,共一百四十三页,2022年,8月28日

booladvance(); //前进一步

pointlocation(pointloc); //定位

boolfacingWall(); //判断是否面对墙壁

voiddisplay(); //显示当前位置

… };

该定义所建立的机器人类的构造可以形象地用下图 表示,它很象一个封装好的器件。第二十页,共一百四十三页,2022年,8月28日directionlocation...turnLeftdislayturnRightfacingWalladvancelocation属性操作边界第二十一页,共一百四十三页,2022年,8月28日

Java

的类定义与C++

的类定义在格式上基本相同,但有两点是不一样的:⑴成员的访问权限必须逐个显式说明;⑵类定义结束不要分号“;”。例如:

classUser

{

privateStringname;

privateintage;

publicUser(Stringstrintyy){name=str;age=yy;} } 返回第二十二页,共一百四十三页,2022年,8月28日3.2类成员函数的定义1成员函数的性质类的成员函数在声明和定义的格式上以及用法和作用上与一般函数基本一致。但由于成员函数是属于某一个类的成员,因此它与一般函数的区别表现:⑴作用域在类定义体所确定的边界内,即可以访问本类的任何成员(私有和公有的数据和函数)。⑵需要根据功能和作用指定成员函数的访问权限,一般情况下,将向类外提供操作功能的成员函数指定为public,将只为类内提供服务功能的成员函数指定为private,将只为类内和派生类提供服务功能的成员函数指定为protected。第二十三页,共一百四十三页,2022年,8月28日2成员函数的声明和定义形式

⑴在类定义体内定义成员函数的实现代码。这种形 式下,函数定义的首部将起到函数原型的作用, 因此无须成员函数定义之前的原型声明。例如: classpoint

{ intx,y; public: voidsetpoint(intvx,intvy) {x=vx;y=vy;} };第二十四页,共一百四十三页,2022年,8月28日

⑵在类定义体内声明成员函数,而在类定义体外定 义成员函数的实现代码。采用这种定义形式的 时,类定义体外的定义代码必须满足:

①在成员函数名之前应缀上所属的类名::,“::”是 作用域运算符,以便说明函数的作用域。②成员函数定义的首部(函数的返回类型、函数 名和参数表列)必须与在类定义体中声明的该 函数的原型一致。例如:第二十五页,共一百四十三页,2022年,8月28日 classpoint{ intx,y; public: voidsetpoint(int,int); }; voidpoint::setpoint(intvx,intvy) { x=vx; y=vy; }由于第⑵种形式不仅可以减少类定义体的代码长度,使类定义体清晰、可读性好;更重要的是有助 于类的操作接口与操作实现细节相分离,并隐藏细 节。因此,提倡采用该形式定义类成员函数。第二十六页,共一百四十三页,2022年,8月28日3类的内置(内联)成员函数定义方式:

⑴隐式定义——函数定义在类定义体中,此时只要函数的实现代码符合内置函数的定义要求,该成员函数就会自动被定义内置函数,而说明内置函数的关键字inline可以忽略。例如: classpoint

{ intx,y; public: voidsetpoint(intvx,intvy) //内置函数 {x=vx;y=vy;} };其中成员函数setpoint定义的首部与加缀inline的首部“inlinevoidsetpoint(intvx,intvy)”等效。第二十七页,共一百四十三页,2022年,8月28日

⑵显式定义——函数声明在类定义体中,而函数定义在类定义体外,此时函数定义的首部必须冠以关键字inline说明此函数是内置的。例如: classpoint { intx,y; public:

inlinevoidsetpoint(int,int); //内置函数声明 };

inlinevoidpoint::setpoint(intvx,intvy)//内置函数定义 { x=vx; y=vy; }第二十八页,共一百四十三页,2022年,8月28日需要特别注意的是:由于调用内置函数需要将内置函数的目标代码复制到它被调用的位置,因此编译器在进行内置函数的调用编译时,必须能获得被调内置函数的目标代码。这就需要在调用内置函数的源代码文件中必须包含被调用内置函数的定义代码的源代码文件。也就是说,如果某个被调用的内置成员函数定义在类定义体中,该类定义体代码被保存在一个头文件中,例如“student.h”,则调用该内置函数的源文件应添加预编译命令#include“student.h”。如果该内置函数定义在类定义体外,代码包含在类实现文件中,例如“student.cpp”,则调用该内置函数的源文件应添加预编译命令#include“student.cpp”。第二十九页,共一百四十三页,2022年,8月28日4成员函数的存储空间从类类型的定义不难看出,类的数据成员(对象属性)中保存的数据值代表了类对象的状态,决定了该类的不同对象的差别,因此当类对象创建时,每个类对象都必须独占一份(个数相同、类型相同)

数据成员存储空间,用于保存区别于其他对象的状态;而类的成员函数描述了该类所有对象的统一行为操作,而操作结果(行为表现)的差异取决于不同对象的状态,因此,成员函数的运行代码被存储在与数据成员存储空间不同的代码空间中,被该类的所有对象共享。第三十页,共一百四十三页,2022年,8月28日例如:由于语句Studentstud1,stud2,stud3;

执行所创建的3

个Student

对象在程序运行空间中占用内存的大小和位置的状态示意如下:返回程序数据区程序代码区Student::Student()的运行代码Student::~Student()的运行代码Student::display()的运行代码numnamesexstud1numnamesexstud2numnamesexstud3第三十一页,共一百四十三页,2022年,8月28日3.3类对象的定义和使用3.3.1类与对象的关系⑴类是一组具有相同属性和行为的对象的抽象,是创建对象的模板,是用户使用class

创建的自定义类型。类一旦创建,其作用可与系统预定义类型(例如,int、double等)类比。⑵

对象是类的实例,创建类的对象可以与创建预定义类型的变量(例如,int

x;、doubled;等)类比。⑶类只是提供了该类对象的创建和使用的方法和规则,因此类本身不占用内存。创建类对象时将按类定义提供的方法和规则,在内存中为类对象分配空间,因此,封装是对类对象而言的。第三十二页,共一百四十三页,2022年,8月28日3.3.2类对象的定义方法:1先创建类类型,使用时再定义对象

大多数情况均采用该方法定义类对象。这样创建的类对象的生存周期取决于创建的位置。例如:

classStudent{…}; voidmain() {

StudentZhang,Li; //创建Student局部对象

… }

虽然C++

也允许将上述的类对象定义语句写成:

classStudentZhang,Li;

(C

风格)但不能体现C++

面向对象的设计风格,且不方便简洁,所以不提倡。第三十三页,共一百四十三页,2022年,8月28日2在创建类类型的同时定义对象。使用这种方法定义的类对象的生存周期取决于类类型的创建位置。例如: classStudent { intnum; charname[20]; charsex; public: voiddisplay(){ cout<<“num:”<<num<<endl; cout<<“name:”<<name<<endl; cout<<“sex:”<<sex<<endl; } }Zhang,Li;第三十四页,共一百四十三页,2022年,8月28日3不出现类名,直接定义对象。使用这种方法定义类对象的类类型一般在程序中只 出现和使用一次。例如,创建一个描述人员信息的 类类型person

中包含了一个联系信息属性成员

touchInfo,该属性是一个由人员的通讯地址、邮政编 码、电话号码、e-mail等信息和相应的处理操作构 成的类类型对象,而该类类型只在定义touchInfo

属 性出现和使用一次。此时就可以采用本方法在类类 型person

中定义touchInfo

属性:第三十五页,共一百四十三页,2022年,8月28日 classperson

{ … class //无须类名 { stringAddess; stringpostCode; stringponeNum; stringE-mail; public: … }touchInfo; … };第三十六页,共一百四十三页,2022年,8月28日3.3.3类对象的使用

类的使用是通过首先创建类对象,然后引用类对象的公有成员(访问数据成员或调用成员函数)达到对该类的使用。依据类对象的创建方式不同,类的使用形式可以分为三种:1通过对象名和成员运算符访问对象成员 一般形式:对象名.成员名;

其中成员名必须是对象名所指明对象的所属类的公 有数据成员名或公有成员函数名。例如:第三十七页,共一百四十三页,2022年,8月28日 classpoint{ intx,y; public: setpoint(intvx,intvy){x=vx;y=vy;} }; voidfun(){

pointpt; //创建point类对象pt

pt.setpoint(10,10); //给point对象pt的坐标x,y赋值

pt.x=50;

//错误,不能访问私有成员 ... }

代码pt.setpoint(10,10)实际是pt.point::setpoint(10,10)

的缩写,表明通过对象访问对象名所指示的类成员。第三十八页,共一百四十三页,2022年,8月28日2通过指向对象的指针访问对象成员 一般形式:指针名->成员名; 其中成员名必须是指针所指对象的所属类的公有数 据成员名或公有成员函数名。例如: classpoint{ intx,y; public: setpoint(intvx,intvy){x=vx;y=vy;} }; voidfun(){

point*p=newpoint; //p指向动态创建的point对象

p->setpoint(10,10);//给p所指对象的坐标x,y赋值 ... }第三十九页,共一百四十三页,2022年,8月28日3通过对象的引用访问对象成员 一般形式:对象引用名.成员名; 其中成员名必须是对象引用名所引用对象的所属类

的公有数据成员名或公有成员函数名。例如: classpoint{ intx,y; public: setpoint(intvx,intvy){x=vx;y=vy;} }; voidfun(){

pointpt,&p=pt; //p引用point对象pt

p.setpoint(10,10); //给p引用的对象的坐标x,y赋值 ... }第四十页,共一百四十三页,2022年,8月28日3.3.4成员名解析

由于类成员作用域在该类定义体所限定的边界内,因此,不同类中具有同名的成员是不会产生二义性。例如: classrealSet

//定义一个实数集合类 { … public: voidprint(); }; classintSet

//定义一个整数集合类 { … public: voidprint(); };第四十一页,共一百四十三页,2022年,8月28日 voidfun() {

intSet

is;

realSet

rs; …

is.print(); //调用intSet类中的print()函数

rs.print(); //调用realSet类中的print()函数 } 显然不会引起二义性错误。返回

第四十二页,共一百四十三页,2022年,8月28日3.4构造函数 使用类定义对象时,需要一种操作,使所定义的对象与类的定义域相关。实现这一操作的成员函数称为构造函数,该函数要完成的操作包括:⑴依据类数据成员的个数和类型为对象分配内存;⑵根据需要为对象的数据成员进行必要的初始化。 构造函数是类必须拥有的特殊成员函数,该函数从定义形式到使用场合和方法上都与一般成员函数有所区别,这些差异表现在以下几个方面:第四十三页,共一百四十三页,2022年,8月28日⑴构造函数名必须与类名相同,否则编译器将会把它当作一般成员函数对待。例如:

classStudent { public:

Student(); … };

Student::Student() { … }第四十四页,共一百四十三页,2022年,8月28日⑵构造函数没有返回值,因此,声明和定义构造函数 时都不能说明它的返回类型;⑶构造函数的功能是将对象初始化,因此构造函数一 般只对数据成员进行初始化和必要的辅助操作,而 不提倡做与初始化无关的操作。⑷系统总会为类提供一个隐含的缺省构造函数。该构 造函数实际上是一个空定义体函数,因此只能为对 象分配空间而不能为数据成员进行初始化。 在大多数情况下,数据成员的初始化操作是十分必 要的,因此通常需要显式定义构造函数。构造函数 一旦显式定义,缺省构造函数将被覆盖。第四十五页,共一百四十三页,2022年,8月28日⑸在程序运行过程中,类对象是在进入其作用域时才 被创建的。也就是说,此时类对象的构造函数被调 用。⑹构造函数在类对象创建时由系统自动执行,不需要 用户调用,也不能由用户调用。例如:

Studentstud1; //系统调用构造函数创建stud1 stud1.Student(); //企图用一般成员函数的调用方法

//调用构造函数,因此是错误的。⑺不能为构造函数定义函数指针,也不能获取构造函 数的调用地址。⑻基类的构造函数不能被派生类继承。⑼构造函数不能声明为虚函数。第四十六页,共一百四十三页,2022年,8月28日例3-1

定义一个整数队列类,使用由系统隐含提供的缺 省构造函数创建整数队列,并测试队列功能。1问题分析

⑴用例分析

⑵类图描述向队列中装入整数从队列中取出整数用户qurue-q[2..*]:int-head:int-tail:int+qput(ini:int)+qget():int第四十七页,共一百四十三页,2022年,8月28日

2详细设计

⑴类设计①qurue类

ⅰ类定义 classqueue { intq[100]; //队列空间 inthead,tail; //队列头、尾指示 public: voidqput(inti); //队列插入操作 intqget(); //队列取出操作 };第四十八页,共一百四十三页,2022年,8月28日

ⅱ算法描述 成员函数qput

的N-S

流程图: 成员函数qget

的N-S

流程图:

tail==100&&tail-head==100?

真假显示"队列满"

trail=trail+1,数据插入队尾

head==tail?真假显示"队列空"

head=head+1

从队头取数据

tail==100?真 假移动队列数据,修改队列指针tail=tail–head;head=0;第四十九页,共一百四十三页,2022年,8月28日

⑵类对象创建和使用

main函数的N-S流程图:使用queue类创建实例a,b向队列实例a,b中分别插入:10,20和20,19从队列实例a,b中分别顺序取出数据,并显示第五十页,共一百四十三页,2022年,8月28日3.4.1参数化的构造函数

与其他成员函数一样,构造函数也可以有参数。通过这些参数为类对象的数据成员传递初值。例如: classpoint { intx,y; public:

point(intvx,intvy); //声明带参数的构造函数 voidoffset(intax,intay); };第五十一页,共一百四十三页,2022年,8月28日

point::point(intvx,intvy) { x=vx; //用传递来的实参对x,y赋初值

y=vy; } main() {

pointp(10,20); //定义对象,并传递初值

//… }注意,不要将使用参数创建类对象的代码写成:

pointp=point(10,20);第五十二页,共一百四十三页,2022年,8月28日3.4.2构造函数的重载 在一个类中允许定义多个参数不同构造函数,即构造函数重载。这样就为在不同情况下创建对象的特定初始化需要提供了实现手段。也就是说,在类对象定义时,编译器可以依据创建对象所需要的参数差异确定调用构造函数的哪一个版本来创建类对象。例3-1-1

定义一个有两个构造函数的类,并使用不同构 造函数定义对象。第五十三页,共一百四十三页,2022年,8月28日3.4.3使用缺省参数值的构造函数与其他函数一样,构造函数的参数也可以具有缺省值,表示类对象的某些属性在大多数情况下是预先可以确定的缺省状态,例如计数器的初值一般为“0”、战士的性别多数为“男”、大学教师的学位一般为“硕士”等。构造函数的缺省参数值的定义和使用规则与其他带缺省参数值的函数相同。例3-1-2

描述了如何声明,定义和使用带有缺省参数值 的类构造函数。第五十四页,共一百四十三页,2022年,8月28日归纳构造函数使用缺省参数值的编程要点是:⑴指定缺省参数值只能在构造函数的声明中,而不能 出现在构造函数定义的首部。构造函数定义在类定 义体中的情况除外。⑵函数声明中的参数可以省略参数名,此时指定缺省参数值的格式为:类型名=缺省值。例如:

Box(int=10,int=10,int=10);⑶如果构造函数的全部参数都指定了缺省值,应该避 免再定义一个无参数的构造函数。因为在定义构造函数时,编译器会认为可能是重复定义。例如:

Box();

Box(int=10,int=10,int=10);第五十五页,共一百四十三页,2022年,8月28日 更重要的是定义类对象时,遇到如下情况:

Boxbox1;

编译器无法确定调用哪一个构造函数版本来创建类 对象。⑷如果构造函数的全部参数都指定了缺省值,就容易 在重载构造函数时造成二义性。例如:

Box(int=10,int=10,int=10);

Box();

Box(int,int);

因此,应避免全部参数都指定了缺省值。例如:

Box();

Box(int,int=10,int=10);

Box(int,int);第五十六页,共一百四十三页,2022年,8月28日3.4.4用参数初始化表对数据成员初始化所谓参数初始化是指系统在为类对象的各个数据成员分配内存空间的同时能按照用户通过参数指定的值为数据成员赋值,而不是在各个数据成员的内存空间分配完成后,再对它们进行赋值。这就需要通过一种语法格式,即参数初始化表,使编译器能按照上述要求实现对类对象的各个数据成员的初始化。构造函数参数初始化表的一般形式为:

:[基类初始化列表],[属性初始化列表]第五十七页,共一百四十三页,2022年,8月28日其中基类初始化列表只有在派生类的构造函数初始化表才会存在,这一部分将在第五章中介绍。属性初始化列表由若干个属性初始化项组成,项间用“,”隔开:

属性初始化项1,

属性初始化项2,…

属性初始化项n每个属性初始化项的一般格式为:

属性名(参数列表)不难看出,属性初始化项的含义是调用相应的属性类的具有参数的构造函数用于属性对象的创建和赋初值操作。第五十八页,共一百四十三页,2022年,8月28日 这从另一个角度告诉我们,一个构造函数的定义中没有出现初始化表意味着使用了隐含的初始化表,该表的功能是分别调用了相应类的无参数(或有缺省参数值)构造函数完成类对象的基类部分和各个属性对象创建和赋初值。例3-1-3

是将例3-1-1中的类构造函数改写为使用参数初 始化表实现类对象各数据成员的初始化。 虽然两个实例中对类对象的数据成员初始化的结果是完全相同的,但两种初始化方法对数据成员的赋值的时间和方法是完全不同的。第五十九页,共一百四十三页,2022年,8月28日 在构造函数定义中使用初始化表另一个非常重要的原因就是对于类对象的常数据成员、引用数据成员的初始化就必须在创建的同时进行赋值操作,而不能在函数体中进行赋值。例如:

classA{ public:

A(inti);

constint&ref; //常引用数据成员

private:

constinta; //常数据成员

};

A::A(inti):a(i),ref(a){}第六十页,共一百四十三页,2022年,8月28日3.4.5拷贝构造函数

拷贝构造函数是一个特定的构造函数。该构造函数与其他构造函数在形式上的差别仅在于函数的参数必须是同类型对象的常引用。拷贝构造函数的原型格式如下:

类型名(const

类型名&

引用名);

拷贝构造函数的功能是创建一个新对象,并将参数所引用对象的各个数据成员值复制到新对象的对应的数据成员域中。第六十一页,共一百四十三页,2022年,8月28日系统会为每个类缺省定义一个拷贝构造函数,也允许用户定义一个拷贝构造函数,用以取代缺省的拷贝构造函数。一般情况下,使用系统定义的缺省拷贝构造函数就可以满足类对象的复制操作,但在有些情况下,用户必须定义自己的拷贝构造函数。例如:

classstring

{ intlength; char*str; //指针数据成员

public:

string(intlen); … };第六十二页,共一百四十三页,2022年,8月28日 string::string(intlen) { length=len;

str=newchar[len+1];

//指针数据成员指向动态分配的内存空间

} main() { strings1(10); //创建一个string对象s1 strings2(s1); //复制s1到新string对象s2 … }第六十三页,共一百四十三页,2022年,8月28日在这种情况下,s1

和s2

的指针数据成员str

指向了同一内存空间,使得通过str对该内存空间的任何操作都不能保持应有的独立性。更严重的是当s1和s2

之中有一个被撤消时,在堆中分配的内存空间被撤消回收,使得另一个string

对象的指针数据成员str

成为无效指针,任何通过该指针的操作均为非法操作,会导致严重的运行错误。造成这一问题的原因是系统提供的缺省拷贝构造函数不能复制string对象。因此,在这种情况下必须定义自己的拷贝构造函数:lengthstrs1lengthstrs2第六十四页,共一百四十三页,2022年,8月28日

string::string(conststring&s) { length=s.length; str=

newchar[length+1]; strcpy(str,s.str); }

Java

没有C++

那种含义的指针,也没有拷贝构造函数。同时,对象的撤消是由垃圾收集器完成的,因此也不会产生像C++

中那样的问题。当然在Java

中也可以用赋值运算符“=”来进行对象赋值,但是,这并不意味着一个简单赋值操作所具有的直觉含义。例如:第六十五页,共一百四十三页,2022年,8月28日

classUser { publicstringname; … } classTest { publicstaticvoidmain(String[]args){

Useru1=newUser(“ariel”,112);();

//ariel

Useru2=u1; =“muriel”;();

//muriel } }第六十六页,共一百四十三页,2022年,8月28日显然,这里的u2=u1

只是对对象引用的复制。由于u1

和u2

引用同一个User

对象,所以才会导致修改

实际上等价于对

的修改(这与C++

中两个指针指向同一个对象的情况类似)。如果要完成直觉意义上的复制,就必须通过实现User

类的克隆接口Cloneable后,调用逐字节复制的克隆函数

clone()

完成。例如:第六十七页,共一百四十三页,2022年,8月28日

classUserimplementsCloneable

{ publicstringname; … } classTest { publicstaticvoidmain(String[]args){

Useru1=newUser(“ariel”,112); System.out.println();

//ariel

Useru2=(User)u1.clone(); =“muriel”;

System.out.println();

//ariel

} }第六十八页,共一百四十三页,2022年,8月28日3.5析构函数

对象撤消时,也需要一种操作,使被撤消的对象从程序的数据区中合法消失。实现这一操作的成员函数称为析构函数,该函数要完成的操作包括:⑴回收被撤消对象数据成员所占用的内存;⑵根据需要完成回收被撤消对象数据成员所占内存之 前的必要操作。 析构函数也是类必须拥有的特殊成员函数,该函数从定义形式到使用场合和方法上都与构造函数相似,主要特点表现在以下几个方面:第六十九页,共一百四十三页,2022年,8月28日⑴析构函数名必须是类名加字符“~”前缀,否则编译器将会把它当作一般成员函数对待。例如:

classStudent { public:

~Student(); … };

Student::~Student() { … }第七十页,共一百四十三页,2022年,8月28日⑵析构函数没有返回值,因此,声明和定义析构函数 时都不能说明它的返回类型;⑶系统总会为类提供一个隐含的缺省析构函数。该析 构函数实际上是一个空定义体函数,因此只能撤消 回收对象所占用的空间。 如果在对象被撤消之前无须做必要的预处理操作, 则可以放心使用缺省析构函数。但在有些情况下, 则必须定义自己析构函数。析构函数一旦显式定义,缺省析构函数将被覆盖。例如:第七十一页,共一百四十三页,2022年,8月28日classstring

{ intlength;

char*contents;public: string(char*s); //声明构造函数 ~string(); //声明析构函数};类string的对象在撤消之前需要先检查指针类型属性contents

是否指向有效的内存空间,如果是,则应回收所占用的内存空间。因此,必须重新定义析构函数,取代系统隐含定义的缺省析构函数。string类的构造函数和析构函数的操作可以按如下定义:第七十二页,共一百四十三页,2022年,8月28日

string::string(char*s) //定义构造函数

{ if(s) {length=strlen(s);

contents=newchar[length+1]; //分配存储

strcpy(contents,s); //字串赋值

} else {length=0;

contents=0; //设置指针数据成员为空

}}第七十三页,共一百四十三页,2022年,8月28日

string::~string() //定义析构函数

{ if(contents)delete[]contents; //释放contents指向的内存空间

}⑷在程序运行过程中,类对象是在退出其作用域时才 被析构的。也就是说,此时类对象的析构函数被调 用。⑸析构函数在类对象撤消时由系统自动执行,不需要 用户调用,也不能由用户直接调用。第七十四页,共一百四十三页,2022年,8月28日⑹不能为析构函数定义函数指针,也不能获取析构函 数的调用地址。⑺基类的析构函数不能被派生类继承。⑻析构函数可以声明为虚函数,并且提倡声明为虚函 数(详细原因在第六章运行多态性中讲述)。返回第七十五页,共一百四十三页,2022年,8月28日3.6对象的动态创建和释放与预定义类型一样,自定义类型也可以使用运算符new

动态创建对象和使用运算符delete

撤消类对象。例3-2

描述了动态创建、撤消和使用point

类对象。注意:1使用无参数或具有缺省参数值的构造函数动态创建 类对象(即创建对象时不传递初始值)的格式为:

new

类型名;

例如,point*p=newpoint;

而不应写成:

new

类型名();

例如,point*p=newpoint();

上述格式与动态创建系统预定义类型变量的格式完 全一致(预定义类型无缺省初始值)。例如:

int*p=newint;第七十六页,共一百四十三页,2022年,8月28日2使用有参数的构造函数动态创建类对象(即创建对 象时传递初始值)的格式为:

new

类型名(初始值…);

例如,point*p=newpoint(10,20);

上述格式与动态创建系统预定义类型变量并传递初 始值的格式完全一致。例如:

int*p=newint(10);第七十七页,共一百四十三页,2022年,8月28日例3-3

通过构造函数对对象数组进行初始化。对象数组初始化的方法一般有两种:1使用缺省构造函数创建对象数组后,调用一个专门 用于初始化的成员函数对数组中的每个对象分别进 行初始化。该方法虽然必须分两步完成对象数组的 创建和初始化,但可以将对象数组中的元素初始化 为任意值。2定义一个带缺省值参数的构造函数。使得在创建对 象数组的同时,由构造函数的缺省参数值完成数组 中的每个对象对象的初始化。使用该方法创建和初 始化对象数组简单、方便,但只能将对象数组中的 每个元素初始化为固定的缺省值。 返回第七十八页,共一百四十三页,2022年,8月28日3.7对象的赋值与复制1对象的赋值 对象的赋值只能发生在同类型对象之间的,这与系统预定义类型变量的赋值是一致的。对象赋值的一般格式为:

对象名1

=

对象名2;

赋值操作是由赋值运算符“=”完成的,该运算符(函 数)的功能是将对象名2

所指示对象的各个数据成员 值依次传递给对象名1

所指示对象的各个数据成员, 使对象1

与对象2

完全相同。第七十九页,共一百四十三页,2022年,8月28日 系统会为每一个自定义类型自动添加一个隐含的缺 省赋值运算符,因此一般的自定义类型不必显式定 义赋值运算符。但如果类定义中包含有指针类属性 (

拷贝构造函数中已经讨论这种情况),则必 须显式定义赋值运算符用于取代隐含的缺省赋值运 算符。如何定义赋值运算符将在第五章中讨论。

例3-2-1

描述了使用缺省赋值运算符完成对象的赋值 操作。第八十页,共一百四十三页,2022年,8月28日2对象的复制 对象的复制是指按照一个已经存在的对象创建一个与该对象完全相同的新对象。显然对象的复制操作是由类的拷贝构造函数完成的,复制一个已有对象的一般形式为:

类型名

对象2(对象1);

例如,Boxbox2(box1);

在C++中上述复制也适用于预定义类型,即每个预 定义类型都有一个隐含的拷贝构造函数。 例如,int

a(10),b(a);第八十一页,共一百四十三页,2022年,8月28日

C++

还提供了另外一种方便用户的复制表达式,即用赋值运算符代替括号调用类的拷贝构造函数:

类型名

对象2=对象1;

例如,Box

box2=box1;

显然,这种复制形式与预定义类型变量赋值定义形式是一致的。 例如,inta=10,b=a;

程序中需要对象复制操作的情况有三种: ⑴用户需要定义与已有对象完全一致的新对象。 ⑵函数调用时,系统需要复制被传递的实参对象。 ⑶函数返回时,系统会复制被返回的操作结果。返回第八十二页,共一百四十三页,2022年,8月28日3.8与对象有关的指针 指针变量可以用于指向任何预定义类型变量,当然也可以指向自定义类型对象。不仅如此,指针变量还可以指向对象的类成员。1指向对象的指针 定义指向对象的指针变量的一般形式:

类型名

*对象指针名; 例如,Box*pt;

显然,这与定义预定义类型指针的形式是完全一致的。同样,指针的使用形式也是相同的。例如:

pt=newBox;

pt->volume(); //与(*pt).volume();等价第八十三页,共一百四十三页,2022年,8月28日2指向对象成员的指针 对象的成员有数据成员和成员函数两种,因此指向 对象成员的指针也有两种。 ⑴指向数据成员的指针 定义指向数据成员的指针变量的一般形式:

类型名

*指针变量名;

显然,这与定义预定义类型指针是完全一致的。 同样,指针的使用形式也是相同的。例如:

classTime{ public: inthour,minute,sec; voidshow(); };第八十四页,共一百四十三页,2022年,8月28日

voidTime::show() { cout<<hour<<“:”<<minute<<“:”<<sec<<endl; }

Timetime;

int*p;

p=&time.hour;

而不能按如下表达式为指针赋值:

p=&Time::hour;

注意,指针只能指向公有数据成员。第八十五页,共一百四十三页,2022年,8月28日 ⑵指向对象成员函数的指针 定义指向成员函数的指针变量的一般表达式:

类型名

(类名::*函数指针名)(参数列表);

例如,void(Time::*pf)();

为函数指针赋值,指向成员函数的一般表达式:

函数指针名

=&类名::成员函数名;

例如,pf=&Time::Show;

通过成员函数指针调用成员函数的一般表达式:

(对象名.*成员函数指针)(实参列表);

例如,Timetime; (time.*pf)();

或者(对象指针->*成员函数指针)(实参列表);

例如,Time*pt=&time; (pt->*pf)();第八十六页,共一百四十三页,2022年,8月28日3this

指针

this指针是类成员函数拥有的一个隐含指针,它指向该成员函数被调用时,操作所施加的类对象地址,实现不同对象的相同行为的表现差异。如图所示:成员函数{………}this数据成员…类对象1数据成员…类对象2数据成员…类对象n第八十七页,共一百四十三页,2022年,8月28日 成员函数通过this

指针可以访问所指对象中的类成 员,数据成员的访问格式可写为:

this->数据成员名 成员函数的访问格式可写成:

this->成员函数名(参数列表)

例如:第八十八页,共一百四十三页,2022年,8月28日

classexth{ inti; public: voidload(intval) {this->i=val;} //与i=val;等价

intget() {returnthis->i;} //与returni;等价

}; voidmain() {

exthobj; obj.load(100); cout<<obj.get(); }第八十九页,共一百四十三页,2022年,8月28日 显然,一般情况下,上述书写格式是繁琐的,并且 是不必要的。但在成员函数的形参名与某个数据成 员名相同时,使用this

指针是区分这两个数据变量 的好方法。例如:

point::point(intx,inty) {

this->x=x; this->y=y; }

当然,这种问题也可以用如下方法解决:

point::point(intx,inty) {

point::x=x; point::y=y; }第九十页,共一百四十三页,2022年,8月28日

为了进一步了解成员函数被调用时this

指针是如何在“幕后”工作的,我们再来分析一段程序:

classabc{private: chara; intb;public: voidinit(charma,intmb){a=ma;b=mb;}};intmain(){

abcob; ob.init(‘x’,12);

return0;}第九十一页,共一百四十三页,2022年,8月28日分析:在main()

中,abc

类成员函数init通过类对象

ob

被调用的:

ob.init(‘x’,12);

C++

是如何使this

指针指向对象ob

的呢?编译器在编译过程中对上述语句经过如下的转换:①

首先将成员运算符“.”之前的对象ob

的地址作为 实参传给函数init,则语句变为:

init(&ob,‘x’,12);

第九十二页,共一百四十三页,2022年,8月28日 ②

虽然成员函数init

的原型中没有相应的形参,但 程序编译时,编译器将该函数的原型和定义隐含 转换成以下形式:

init(abc*this,charma,intmb)

{this->a=ma;this->b=mb;}

显然,该成员函数被调用时,第一个形参应是调 用该成员函数的对象地址,因此当对象

ob

调用 成员函数init

时,形参this

=

&ob。确保成员函数

init

的操作施加于对象ob。 返回第九十三页,共一百四十三页,2022年,8月28日3.9静态成员 前面我们所涉及到的类成员都是由每个对象独享,即每个对象拥有独立的数据成员空间,成员函数的调用都依赖某个特定对象,成员函数的操作结果取决于该对象的状态,并仅服务于该对象。这些由对象独享的成员被称为非静态成员,是类的主要组成成员。此外,类中还需要包括一些由类的所有对象共享的成员,即类的所有对象共同拥有的数据成员和调用不依赖于某个特定对象,操作结果为类的所有对象服务的成员函数。这些由类的所有对象共享的成员被称为静态成员。在类中声明静态成员的方法就是用关键字

static声明数据成员和成员函数;第九十四页,共一百四十三页,2022年,8月28日3.9.1静态数据成员 与非静态数据成员不同,静态数据成员属于类的所有对象,因此无论创建多少个该类的对象,静态数据成员在内存中都只有一个。也就是说,静态数据成员所占的内存空间不在任何一个类对象空间中,因此类的构造函数不会为静态数据成员分配空间的初始化。第九十五页,共一百四十三页,2022年,8月28日例3-4

中Student

类中的数据成员count

用于存放学生总 数。显然,不管有多少学生,学生总数只有一 个。也就是说,学生总数

count

属于

Student

类的 所有对象,因此应声明为静态数据成员。而数据 成员StudentNo

用于存放每个学生的学号,因此 该数据成员必须是非静态数据成员。下图描述了 这两种数据成员与Student

类对象的关系:Student1:StudentNoStudent2:StudentNoStudent4:StudentNocount第九十六页,共一百四十三页,2022年,8月28日使用静态数据成员的四点说明:⑴静态数据成员属于类的所有对象,而不像非静态数据成员那样属于某一个对象,是整个类的属性。因此访问静态数据成员的形式通常采用:

类名::数据成员名。

例如,Student::count

C++

编译器也允许采用通过对象名访问静态数据成员的形式:

对象名.数据成员名。

例如,Student1.count

后一种形式等价与Student1.Student::count,显然对象名是多余的。第九十七页,共一百四十三页,2022年,8月28日⑵类中声明的静态数据成员既不能随着对象创建被定义和初始化,也不能在类定义中进行定义和初始化,而必须在类定义外,并且在该类的任何对象创建之前,提供初始化定义。一般形式为:

数据类型

类名::静态数据成员名

=初值;

例如本例中为Student::count

提供初始化定义语句:

intStudent::count=0;

如果只定义不赋初值,则被缺省设置为零。注意,对于类的常静态(staticconst)数据成员,也需要和一般静态数据成员一样在类外初始化。第九十八页,共一百四十三页,2022年,8月28日⑶静态数据成员与其他静态变量一样,是在编译时创建并被初始化的。它在该类的任何对象被建立之前就存在,因此它可以在程序中不依赖对象被访问。⑷

C++

使用静态数据成员的一个主要原因是可以避免使用全局变量。使用全局变量的类都是违反面向对象的程序设计的封装原则的。

Java

中静态数据成员的声明、定义和用途与C++

中的静态数据成员基本一致。但应注意表示上的不同

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论