第四章 类与对象.docx
- 文档编号:12105203
- 上传时间:2023-06-04
- 格式:DOCX
- 页数:66
- 大小:187.14KB
第四章 类与对象.docx
《第四章 类与对象.docx》由会员分享,可在线阅读,更多相关《第四章 类与对象.docx(66页珍藏版)》请在冰点文库上搜索。
第四章类与对象
第四章类与对象
前几章初步介绍了面向过程的程序设计方法。
介绍了什么是数据,怎样定义数据、怎样按一定的算法编写函数来对数据进行操作,以及怎样用结构化程序设计的思想来编写一个小程序。
这为学习面向对象的程序设计打下了基础,因为C++不是纯面向对象程序设计的语言,这一点尤其重要。
从本章起将进入面向对象程序设计学习的实质阶段。
封装(Encapsulation)是面向对象程序设计最基本的特性,把数据(属性)和函数(操作)合成一个整体,这在计算机世界中是用类与对象实现的。
本章将引入C++的类(Class)和对象(Object)的概念,建立“函数也可以是数据类型的成员”的思想。
本章另一个重点是用运算符重载来体现类和对象封装的实用性和重要性。
4.1类与对象
本小节引入类与对象的最基本的概念:
类的定义、对象的创建与使用。
4.1.1C++类的定义
在前面几章中讨论了基本数据类型、数组和枚举类型。
后两者是导出的数据类型,是由用户自己定义,自己按规则构造的,但其基本组成单位都是同一种数据类型。
然而客观事物是复杂的,要描述它们必须从多方面进行,需要用不同的数据类型来描述不同的方面。
如商场中的商品可以描述为:
商品名称(用字符串描述),该商品数量(用整型数描述),该商品单价(用浮点数描述),该商品总价(用浮点数描述)。
这里用了属于3种不同数据类型的4个数据成员(DataMember)来描述一种商品。
在C++中可以这样表述:
classCGoods{
public:
charName[21];//对于中文名称可用wchar_tName[11]
intAmount;
floatPrice;
floatTotal_value;
};//最后的分号不可少
以上表述中,关键字class是数据类型说明符,指出下面说明的是类。
标识符CGoods是商品类的类型名。
“{}”中是构成类体的一系列成员,关键字public是一种访问限定符,表示其后所列为公有成员。
在类的外部只能对公有成员进行访问。
访问限定符(Accessspecifier)有3种:
public(公有的),private(私有的)和protected(保护的),由后两种说明的成员是不能从外部对它进行访问的。
这三种说明符的作用域是从该说明符出现开始到下一个说明符之前或类体结束之前。
每种说明符可以在类体中使用多次。
如果在类体起始点没有访问说明符,则系统默认定义为私有(Private)。
访问限定符private(私有的)和protected(保护的)体现了类具有封装性(Encapsulation)。
定义一个类的一般格式为:
class类名{
《《private:
》
成员表1;》
《public:
成员表2;》
《protected:
成员表3;》
};
其中“class类名”称为类头(Classhead)。
“{}”中的部分称为类体(Classbody),类体中定义了类成员表(Classmemberlist)。
成员包括数据成员和函数成员(FunctionMember)。
现已定义的商品类还没有包括类定义的更关键部分:
对数据成员的操作。
商品类定义的操作部分可以包括:
输入商品的名称、数量和单价,计算总价值,取得有关数据。
这可以用6个函数来完成:
voidRegisterGoods(char[],int,float);//输入数据
voidCountTotal(void);//计算商品总价值
voidGetName(char[]);//读取商品名
intGetAmount(void);//读取商品数量
floatGetPrice(void);//读取商品单价
floatGetTotal_value(void);//读取商品总价值
这些函数实现对数据的操作,可以把它们与数据成员封装在一起,于是得到新的商品类:
classCGoods{
private:
charName[21];
intAmount;
floatPrice;
floatTotal_value;
public:
voidRegisterGoods(char[],int,float);
voidCountTotal(void);
voidGetName(char[]);
intGetAmount(void);
floatGetPrice(void);
floatGetTotal_value(void);
};
在类中引进了函数成员或称成员函数(MemberFunction),即函数也就成了数据(类)中的一员。
从逻辑上完成了类的封装。
类把数据(事物的属性)和函数(事物的行为——操作)封装为一个整体。
注意:
4个数据成员被说明成私有的,而6个函数成员被说明成公有的;如果从外部对4个数据成员进行操作,则只能通过六个公有函数来完成,数据受到了良好的保护,不易产生副作用。
公有函数集定义了类的接口(Interface)。
在定义一个类时请注意以下两点:
(1)类是一种数据类型,定义时系统并不为类分配存储空间,所以不能对类的数据成员初始化。
当然类中的任何数据成员也不能使用关键字extern、auto或register限定其存储类型。
(2)成员函数可以直接使用类定义中的任一成员,可以处理数据成员,也可调用函数成员。
私有的数据成员只有通过类的接口(公有函数),才能从外部对其进行处理。
由于历史的原因,类定义也称类声明,是一个说明语句,最后必须加分号。
本教材中凡是用户自定义类型的说明,一律称定义。
只有对函数格式的说明才称声明或函数声明。
习惯上类定义中术语的使用视强调哪一方面来定:
强调是成员,称数据成员和函数成员;强调是数据或函数,称成员数据或成员函数。
其实含义是相同的。
4.1.2成员函数的定义
在4.1.1节中,只对成员函数作了一个声明,并没有对函数进行定义。
函数定义通常在类定义的外部进行,其格式如下:
返回值类型类名:
:
函数名(参数表){……}
其中运算符“:
:
”称为域解析运算符(Scoperesolutionoperator),它指出该函数是属于某个类的成员函数。
类CGoods的函数可以如下定义:
voidCGoods:
:
RegisterGoods(charname[],intamount,floatprice){
strcpy(Name,name);//字符串复制函数
Amount=amount;Price=price;
}
voidCGoods:
:
CountTotal(void){
Total_value=Price*Amount;
}
voidCGoods:
:
GetName(charname[]){
strcpy(name,Name);
}
intCGoods:
:
GetAmount(void){
return(Amount);
}
floatCGoods:
:
GetPrice(void){
return(Price);
}
floatCGoods:
:
GetTotal_value(void){
return(Total_value);
}
也可以在类定义中直接定义函数,但是系统在处理上是不同的。
它们的差异就如同普通函数和内联函数的差异一样,体现在代码的存储上,参见4.1.3节。
但是在定义类时,并不分配内存,要到创建类的对象时才会按不同的方式分配内存。
4.1.3对象的创建与使用
对象是类的实例(Instance),正如在前几章称变量是数据类型的实例一样。
定义一种数据类型只是告诉编译器该数据类型的结构形式,并没有预定内存,或者说并没有创建可用来存放数据的变量。
类只是一个样板,以此样板可以在内存中建立一个个同样结构的实例——对象。
创建类的对象有两种常用方法。
第一种是直接定义类的实例——对象:
CGoodsCar;
这个定义创建了CGoods类的一个对象Car,同时为它分配了属于它自己的存储空间,用来存放数据和对这些数据实施操作的成员函数(代码)。
与定义变量一样,一个对象只在定义它的域中有效。
第二种是采用动态创建类的对象的方法。
将在第7章中介绍。
所谓动态是指在程序运行时建立对象。
而第1种方法则是在编译时(程序运行前)建立。
一个样板可以制造出无数相同的物品,同样,一个类可以定义出无数同样构造的对象。
存储对象有两种方法。
图4.1所示是系统为每一个对象分配了全套的内存,包括存放成员数据的数据区和存放成员函数的代码区。
仔细分析后会发现:
区别同一个类的各个不同的对象的属性是由数据成员决定的,不同对象的数据成员的内容是不一样的;而行为(操作)是用函数来描述的,这些操作的代码对所有的对象都是一样的。
如果每个对象的数据占内存1KB,而代码占50KB,共占51KB;100个对象共占5100KB,其中重复的代码占了4950KB。
若按图4.2方式安排,仅为每个对象分配一个数据区,代码区为各对象共用,同样100个对象,则只要150KB。
图4.1对应的是在类定义中定义函数,而图4.2对应的是在类定义外部定义函数。
所以在绝大多数场合,总是在类定义外部定义函数,而仅在类定义中给出函数的声明。
如果在类定义外部定义函数时,使用关键字inline,则系统也会采用内联扩展方法实现,这时每个对象都有该函数一份独立的代码。
如RegisterGoods()函数可定义为:
inlinevoidCGoods:
:
RegisterGoods(charname[],intamount,floatprice){
strcpy(Name,name);Amount=amount;Price=price;
}
则所定义的每个对象都有RegisterGoods()函数一份独立的代码。
第三章指出,内联对编译器只是一个建议。
正因为图4.1的内存分配方法明显不合理,C++标准规定仅当类的成员函数很小并不包括循环等复杂结构,且这些成员函数的函数体在类定义内部直接定义或在外部定义为内联时,才用内联扩展方式实现。
在具体的C++平台中也许根本不理睬内联建议。
上面所述对象的存储方式是物理的,是由计算机来完成的,它并不影响类在逻辑上的封装性。
程序设计是一个逻辑的概念,是由人来完成的。
从程序员的角度上看,逻辑上各对象是完全独立的,不必去管物理上是怎样存储的,即类的封装在逻辑上是完善的。
但若知道物理存储方式,可加深对类与对象的理解,这也是程序员必备的知识。
【例4.1】商品类对象应用实例。
#include
#include
#include
usingnamespacestd;
//省略了类定义
intmain(){
CGoodscar;
charstr[21];
intnumber;
floatpr;
cout<<"请输入汽车型号:
";
cin.getline(str,20);//输入串长必须小于20
cout<<"请依次输入汽车数量与单价:
";
cin>>number>>pr;
car.RegisterGoods(str,number,pr);
car.CountTotal();
str[0]='\0';//字符串str清0
car.GetName(str);//给str赋值car.Name
cout< cout< return0; } 对象使用的规则很简单,只要在对象名后加点号(点操作符,成员访问运算符(MemberAccessOprator)之一),再加成员数据或成员函数名就可以了。 但是这些成员必须是公有的成员,只有公有成员才能在对象的外面对它进行访问。 在上面的例子中的如果将A行和B行写成: cout< cout< 是错误的,因为这里对象car的4个数据成员全是私有的,在外部是不能直接访问的,必须用对象car所带的公有函数进行访问。 *4.2从面向过程到面向对象 在4.1节中,介绍了C++的类和对象,在那里类是作为一种广义数据类型来理解,而把对象看作类的实例,也就是作为一种变量来理解。 从语言的角度来看,这是严格的。 但从面向对象的程序设计的角度看,则只是表面现象,而不是本质。 4.2.1传统的面向过程的结构化程序设计 上世纪七十年代,面向过程(Procedure-Oriented)的结构化程序设计(StructuredProgramming,SP)逐步成为程序设计的主流。 结构化程序设计的提出与发展是伴随软件日益庞大和复杂进行的,但是当软件复杂到一定的程度后,结构化程序设计也不能满足需要。 当软件规模超过一定的尺度后,采用结构化程序设计,其开发和维护就越来越难控制。 根本原因在于面向过程的结构化程序设计的方法与现实世界(包括主观世界和客观世界)往往不一致,结构化程序设计的思想很难贯彻到底。 结构化程序设计提出“自顶向下,逐步细化(Top-down,StepwiseRefinement)”的思想,是完全正确的。 通常处理一件事时,总是先分析事物,抓住其主要矛盾,然后逐层分解,把复杂的事物分解成一个个相对简单的事物,逐一进行处理,彻底解决问题。 这里的具体操作方法——模块化,是按功能来分的,称功能块,或者说是从事物中抽象出来的一般操作,在C++中称为一个函数解决一个问题,一个函数实现一个功能或一个操作。 对这样的功能块的具体要求是独立性要强,即模块内部的联系要紧密,模块间的联系要弱。 模块内部各功能块之间的联系称为内聚度,内聚度最高的模块是功能组合模块,该模块的各个组成部分全部为执行同一功能而存在,并且只执行一个功能。 影响模块之间联系的因素有两个: 一是模块之间的联结形式,二是模块之间接口的复杂性。 最好的是数据联结,这是指两个模块之间的通信信息是若干数据的联结形式。 由于两者之间没有控制信号的交换,相互影响最小。 同时模块之间传递的信息要尽可能少。 全局变量会加强模块间的联系,所以要求尽量使用局部变量,基本不用全局变量。 但是以功能抽象为基础的结构化程序设计,当程序规模和复杂性达到一定程度时不可避免地引入大量的全局变量,一个数万行的程序,全局变量所占内存很轻易就达到数十KB,也就是说,优良的模块化不能坚持到底。 结果是程序的开发,特别是维护越来越难控制。 即使坚持了优良的模块化也不行,比如一个实时的管理系统,当管理的规则发生大的变化,程序的维护往往相当困难,为程序某一处修改通用函数往往会影响程序其它部分,牵一发而动全身。 可维护性差成了制约结构化程序设计应用的“瓶颈”。 在模块化的思想中已经出现了封装的概念,这个封装是把数据封装到模块中,即局部变量。 但这是很不彻底的,因为模块是功能的抽象,而数据则是具有其个性的,一但发生那怕是一点点变化,抽象的功能模块就不再适用了。 正如在前面说的管理规则变化了,则管理模块以及所有与之有联系的模块都必须更改,必须重新进行功能抽象,必须重新建立模块间联系的规则。 基于以上的原因,使得面向对象程序设计替代结构化程序设计成为主流的程序设计方法。 4.2.2面向对象的程序设计 对象(Object)的概念是面向对象技术的核心所在。 面向对象技术中的对象就是现实世界中某个具体的物理实体在计算机世界中的映射和体现。 比如一部移动电话,它是现实世界中的一个实体。 它由天线、发射部件、接收部件、显示屏、按键、专用集成电路芯片及外壳组成;它有着实在的功能,可以打电话,可以发短消息,可以存储、输入和编辑各种个人信息,甚至可以上网。 这样一个实体可以在计算机世界中映射为一个计算机可以理解、可以操纵、具有前面所叙述的属性和操作的对象。 又如一辆自行车,它由车架、车轮、脚踏和传动机构、变速机构等组成,它具有代步功能,它可以进行变速骑行,特别要强调的是它有一些特征可以把这辆自行车与其他自行车区分开来,其中最重要的是钢印号。 这些都可以在面向对象的程序中用对象及其属性和操作模拟出来。 现实世界中的实体可以抽象出类别的概念。 比如在草原上有很多马匹,它们之间有不同,也有相同的特性,据此可以抽象出马这样一个概念。 人们只能看到某一匹具体的马,而不能见到抽象的马。 对应于计算机世界就有一个类的概念,因为类是一个抽象概念的对应体,所以计算机不给它分配内存,只给对象分配内存。 图4.3表达了计算机世界与现实世界之间的对应关系。 类是一个抽象的概念,用来描述这类对象所共有的、本质的属性和行为。 任何一个对象都是这个类的一个具体实现,称为实例(Instance)。 同类对象之间具有相同的属性和行为。 自然界是由各种各样的对象组成的,这些对象之间通过信息传递产生相互作用,构成富有生机的世界。 对象之间产生相互作用所传递的信息称做消息(Message)。 比如汽车和人是两个对象,人启动汽车,就是向汽车发送消息,转动方向盘,也是发送消息,其中转动的角度是消息中的参数。 汽车接收到消息后,按照消息及其参数执行相应的操作。 在面向对象设计的程序中,对象之间的相互作用也是通过消息机制实现的。 在传统的面向过程的结构化程序设计中采用的是把现实世界的问题抽象成计算机可以理解和处理的数据结构的方法,或者说使现实世界向计算机世界靠拢。 而面向对象的程序设计的思想是要用计算机逻辑来模拟现实世界的存在,是让计算机世界向现实世界靠拢。 这一思路使程序员可以用更接近人的自然思维模式和更接近现实世界本来面目的方法来进行程序设计,使日后的程序维护更加顺利,避免了面向过程设计所遇到的困难。 面向过程追求统一的算法,而面向对象对具体问题作具体分析。 如前面所举例子,管理规则变化了,对面向对象的程序设计而言,只影响具体的对象,只需修改该对象,而不会影响程序其他部分。 面向对象的程序设计思想也带来了另一个问题,程序会变的规模更大,占用的内存更多,运行也慢下来了。 但是随着计算机的速度越来越快,内存越来越大,而价格却越来越低,这一缺点已经是越来越无足轻重了。 面向对象程序设计具有以下几个特点: (1)封装性(Ecapsulation) 类对象作为独立的基本单元,实现了将数据和数据处理相结合的思想。 此外,封装特性还体现在可以限制从外部对类对象中数据和操作的访问权限,从而将属性“隐藏”在对象内部,对外只呈现一定的外部特性和功能。 就像手表,大量的零件和动作被封装在外壳中,并被隐藏起来,提供给用户的只能是读表盘和旋旋钮。 同样,在对象之外,不能直接引用其中的数据,只能通过接口达到间接使用数据的目的。 定义完好的类一旦建立,就可作为一个整体单元使用,用户不需要知道这个类是如何工作的,而只需要知道如何使用就行。 (2)继承(Inheritance)和派生(Derivation)性 以汽车为例,如果已经定义了汽车类,现在需要定义小汽车,通常不必重复描述属于汽车的那些共有特征,而是在继承汽车类特性的基础上,描述出属于小汽车的新的特征。 称小汽车继承了汽车,也可以称是由汽车派生出来的。 面向对象程序设计提供了类似的机制。 当定义了一个类后,又需定义一个新类,这个新类与原来类相比,只是增加了或修改了部分属性和操作,这样,在定义新类时只需说明新类继承原来类,然后描述出新类所特有的属性和操作即可。 称原来类为基类,由它派生出来的类称为子类或派生类。 由基类派生出子类,子类还可继续派生它的子类,如此下去,可以形成树状派生关系,称为派生树或继承树。 继承性可以简化人们对问题的认识和描述,同时还可以在开发新程序和修改源程序时最大限度利用已有的程序,提高了程序的可重用性,从而提高了程序修改、扩充和设计的效率。 (3)多态性(Polymorphism) 多态性指的是,同样一个消息,被不同对象接收时,产生不同的结果。 系统提供的这种机制主要用在具有继承关系的类体系中。 一个类体系中的不同对象可以用不同方式响应同一消息,并产生不同结果,实现“同一接口,多种方法”。 例如,定义了中学生类,再由中学生派生出大学生类。 对于“计算平均成绩”这样一个消息,对中学生对象,计算的是语文、数学、英语等课程;而对大学生对象,则计算高等数学、英语、线性代数等课程。 继承和多态性的组合,可以很容易地生成一系列虽然类似但独一无二的类。 继承性使这些类共享许多相似特征,而多态性使同样的操作对不同的类对象有不同的表现方式。 这样既提高了程序的灵活性,又减轻了分别逐个设计的负担。 面向对象程序设计着眼点是对象,程序设计的核心是从问题中抽象出合适的对象,即首先解决“做什么”的问题。 至于“怎么做”,则由操作方法的设计完成,并封装在对象内部。 而对于操作方法的设计,核心仍然是算法的设计,完全吸收了结构化程序设计的思想。 正如使用结构化程序设计语言编写的程序并不一定具有结构化程序的特点一样,不能说采用面向对象的程序设计语言编出来的就是面向对象的程序,只有从实际问题中,采用面向对象的系统分析和设计,抽象出真正能体现实际问题中各实体本质的类和对象来,才能说编出来的程序是面向对象的。 第1步将从最简单的开始,即从怎样编码开始。 4.3构造函数和析构函数 定义对象时,按现在已学过的知识无法进行初始化,即无法对数据成员进行赋初值的过程。 在定义类时是不分配内存的,这时谈不到初始化。 如果想在类类型的定义中给出该类所有对象统一的数据成员的初始值,是概念性的错误,初始化只能在定义对象时进行。 而在定义对象时,书中还没有给出怎样初始化数据成员的方法。 从封装的目的出发,数据成员应该多为私有的,要对它们进行初始化,必须用一个公有函数来进行。 同时该函数应该在且仅在定义对象时自动执行一次。 在C++程序设计语言中这个函数称为构造函数(Constructor)。 必须指出: 调用构造函数进行初始化是C++的标准方法。 4.3.1构造函数的定义与使用 对于对象的初始化,采用构造函数是标准的方法,在C++中,始终给构造函数留有位置,即使不编构造函数,C++编译器也会自动产生一个默认的构造函数,不过什么具体初始化工作也不做。 所以当需要对对象进行初始化时,总是编写一个或一组构造函数。 构造函数是特殊的公有成员函数,其特征如下: (1)函数名与类名相同。 (2)构造函数无函数返回类型说明。 注意是没有而不是void,即什么也不写,也不可写void! 因为构造函数是隐式(自动)调用的,有一个固定格式,编译器才可以正确判断哪些是构造函数。 (3)在程序运行时,当新的对象被建立,该对象所属的类的构造函数自动被调用,在该对象生命期中也只调用这一次。 (4)构造函数可以重载。 类定义中可以有多个构造函数,它们由不同的参数表区分,系统在自动调用时按一般函数重载的规则选一个执行。 (5)构造函数可以在类中定义,也可以在类外定义。 (6)如果类定义中没有给出任何构造函数,则C++编译器自动给出一个默认的构造函数: 类名(void){} 该函数只是一种标准的形式,并不做具体初始化工作。 (7)为了与C语言兼容,如果类的数据成员全为公有的,也可以不用构造函数做具体的初始化,
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第四章 类与对象 第四 对象