1.2k likes | 1.39k Views
第四章 类与对象. 封装( Encapsulation ) 是面向对象程序设计最基本的特性,也就是把数据(属性)和函数(操作)合成一个整体,这是用类与对象实现的。 本章重点: 1. 引入 C++ 的类( class )和对象( object )的概念,建立 “ 函数也可以是数据类型的成员 ” 的思想。 2. 运算符重载。 . 第四章 类与对象. 4.1 类与对象. 4.6 友元. 4.2 从面向过程到面向对象. 4.7 静态成员. 4.3 构造函数和析构函数. 4.8 结构. 4.4 引用 与复制构造函数.
E N D
第四章 类与对象 封装(Encapsulation)是面向对象程序设计最基本的特性,也就是把数据(属性)和函数(操作)合成一个整体,这是用类与对象实现的。 本章重点: 1.引入C++的类(class)和对象(object)的概念,建立“函数也可以是数据类型的成员”的思想。 2.运算符重载。
第四章 类与对象 4.1 类与对象 4.6 友元 4.2 从面向过程到面向对象 4.7 静态成员 4.3 构造函数和析构函数 4.8 结构 4.4 引用与复制构造函数 4.9 名字空间域和类域(选读) 4.5 运算符的重载 4.10面向对象的程序设计和Windows编程
4.1类与对象 4.1.1 C++类的定义 4.1.2 成员函数的定义 4.1.3对象的创建与使用
4.1.1 C++类的定义 类的引入: 类是一种数据类型。 描述客观事物必须用不同的数据类型来描述不同的方面。如商品: 商品名称(用字符串描述),该商品数量(用整型数描述),该商品单价(用浮点数描述),该商品总价(用浮点数描述)。 这里用了属于三种不同数据类型的四个数据成员(data member)来描述一种商品。
4.1.1 C++类的定义 类的表述: class CGoods{ public: char Name[21] ; int Amount ; float Price ; float Total_value ; } ;//最后的分号不可少,这是一条说明语句 关键字class是数据类型说明符,指出下面说明的是类。标识符CGoods是商品这个类的类型名。花括号中是构成类体的系列成员,关键字public是一种访问限定符。
4.1.1 C++类的定义 访问限定符(access specifier): public(公共的)说明的成员能从外部进行访问。 private(私有的)和protected(保护的)说明的成员不能从外部进行访问。 每种说明符可在类体中使用多次。 访问限定符的作用域是从该说明符出现开始到下一个说明符之前或类体结束之前结束。 如果在类体起始点无访问说明符,系统默认定义为私有(private)。 访问限定符private(私有的)和protected(保护的)体现了类具有封装性(Encapsulation)。
4.1.1 C++类的定义 类的定义: class类名{ 《《private:》 成员表1;》 《public: 成员表2;》 《protected: 成员表3;》 }; //注意:所有说明都以分号结束 其中“class 类名”称为类头(class head)。花括号中的部分称为类体(class body),类体中定义了类成员表(class member list),包含数据和函数。
4.1.1 C++类的定义 成员函数(member function): class CGoods{ private : char Name[21] ; int Amount ; float Price ; float Total_value ; public : void RegisterGoods(char[],int,float); //输入数据 void CountTotal(void) ; //计算商品总价值 void GetName(char[]) ; //读取商品名 int GetAmount(void) ; //读取商品数量 float GetPrice(void) ; //读取商品单价 float GetTotal_value(void) ; };//读取商品总价值
4.1.1 C++类的定义 封装: 类把数据(事物的属性)和函数(事物的行为——操作)封装为一个整体。 接口: 通常数据成员被说明成私有的,函数成员被说明成公有的;从外部对数据成员进行操作,只能通过公有函数来完成,数据受到了良好的保护,不易受副作用的影响。公有函数集定义了类的接口(interface)。 成员函数可以直接使用类定义中的任一成员,可以处理数据成员,也可调用函数成员。 注意: 类是一种数据类型,定义时系统不为类分配存储空间,所以不能对类的数据成员初始化。类中的任何数据成员也不能使用关键字extern、auto或register限定其存储类型。
4.1.2 成员函数的定义 函数定义: 通常在类定义中,成员函数仅作声明。函数定义通常在类的说明之后进行,其格式如下: 返回值类型类名::函数名(参数表) {……}//函数体 其中运算符“::”称为作用域解析运算符(scope resolution operator),它指出该函数是属于哪一个类的成员函数。 类CGoods的函数定义
4.1.3对象的创建与使用 定义对象: 对象是类的实例(instance)。定义一种数据类型只是告诉编译系统该数据类型的构造,并没有预定内存。类只是一个样板,以此样板可以在内存中开辟出同样结构的实例——对象。格式如下: CGoods Car; 这个定义创建了CGoods类的一个对象Car,同时为它分配了属于它自己的存储块,用来存放数据和对这些数据实施操作的成员函数(代码)。对象只在定义它的域中有效。
对象1 对象2 数据区 代码区 数据区 代码区 ...... 对象n 数据区 代码区 对象存储: 图4.1 各对象完全独立地安排内存的方案 图4.1是系统为每一个对象分配了全套的内存。数据区安放成员数据,代码区安放成员函数。 注意:区别同一个类的各个不同的对象的属性是由数据成员决定的,不同对象的数据成员的内容是不一样的;而行为(操作)是用函数来描述的,这些操作的代码对所有对象都是一样的。
对象1 对象n 对象2 数据区 数据区 数据区 ...... 公共代码区 图4.2 各对象的代码区共用的方案 图4.2仅为每个对象分配一个数据区,代码区(放成员函数的区域)为各对象类共用。 图4.1对应的是在类说明中定义函数,而图4.2对应的是在类说明外部定义函数 。
4.1.3 对象的创建与使用 内联函数: 使用关键字inline,系统自动采用内联扩展方法实现,每个对象都有该函数一份独立的代码。 如RegisterGoods()函数可定义为: inlinevoid CGoods::RegisterGoods(char name[] , int amount , float price){ strcpy(Name,name) ; Amount=amount ; Price=price ; } 则每个对象都有RegisterGoods()函数一份独立的代码。 注意:inline只是一个建议,最后由编译器决定是否执行。对象的存储方式是物理的,这是由计算机来完成的,它并不影响类在逻辑上的封装性。
4.1.3 对象的创建与使用 【例4.1】商品类对象应用实例 对象使用规则: 只要在对象名后加点号(点操作符,成员访问运算符(member access oprator)之一),再加成员数据或成员函数名就可以了。但是这些成员必须是公有的成员,只有公有成员才能在对象的外面对它进行访问。 【例4.1】中对象car的4个数据成员全是私有的,如写: car.Name;car. Amount; car.Price;car.Total_value; 是错误的,必须用对象car所带的公有函数进行访问。
4.2 从面向过程到面向对象 (阅读) 结构化程序设计特点: 采用的是“自顶向下,逐步细化(divide and conquer,stepwise refinement)”的思想。具体操作方法是模块化。模块是按功能来分的,所以也称功能块。在C++中称为一个函数,一个函数解决一个问题,即实现一个功能或一个操作。 在模块化的思想中已经出现了封装的概念,这个封装是把数据封装到模块中,即局部变量。这是很不彻底的,因为模块是功能的抽象,而数据则是具有其个性的,一但发生变化,抽象的功能模块就不再适用了。可维护性差成了制约结构化程序设计的瓶颈。 面向过程程序设计缺点的根源在于数据与数据处理分离。
4.2 从面向过程到面向对象(阅读) 结构化程序设计弱点: 当软件规模过大,采用结构化程序设计,其开发和维护就越来越难控制。其根本的原因就在于面向过程的结构化程序设计的方法与现实世界(包括主观世界和客观世界)往往都不一致,结构化程序设计的思想往往很难贯彻到底。 对象概念: 对象的概念是面向对象技术的核心所在。面向对象技术中的对象就是现实世界中某个具体的物理实体在计算机逻辑中的映射和体现。
4.2 从面向过程到面向对象(阅读) 现实世界中的实体可以抽象出类别的概念。对应于计算机世界就有一个类(class)的概念。面向对象是计算机世界模拟现实世界。图4.3表达了计算机世界与现实世界之间的对应关系。 现实世界 客观世界 计算机世 界 对象 实体 实例化 映射 抽象 抽象 主观世界 抽象类别 类 图4.3对象、实体与类
4.2 从面向过程到面向对象(阅读) 对象、类与消息: • 面向对象程序设计模拟自然界认识和处理事物的方法,将数据和对数据的操作方法放在一起,形成一个相对独立的整体——对象(object),同类对象还可抽象出共性,形成类(class )。一个类中的数据通常只能通过本类提供的方法进行处理,这些方法成为该类与外部的接口。对象之间通过消息(message)进行通讯。
表针 旋钮 其他机械机构 属性 行为 4.2 从面向过程到面向对象(阅读) 对 象 调节旋钮
类的一个具体实现,称为实例 类 对象 描述这类对象共有的、本质的属性和行为 具体到一只圆形的或方形的手表 手表 一块手表 手表共有的属性(表针、旋钮、内部结构) 和行为(调节旋钮) 4.2 从面向过程到面向对象(阅读) 类 是一个抽象的概念,用来描述某一类对象所共有的、本质的属性和类行为。
发送消息 接收并响应消息 4.2 从面向过程到面向对象(阅读) 消 息 我们把对象之间产生相互作用所传递的信息称做消息。 启 动 转 向
内 外 机械零件 动作 调节旋钮 读表盘 4.2 从面向过程到面向对象(阅读) 面向对象程序设计的特点: 封装性 对象是一个封装体,在其中封装了该对象的属性和操作。通过限制对属性和操作的访问权限,可以将属性“隐藏”在对象内部,对外提供一定的接口,在对象之外只能通过接口对对象进行操作。 C++通过建立数据类型——类来支持封装和数据隐藏。封装性增加了对象的独立性,从而保证了数据的可靠性。一个定义完好的类可以作为独立模块使用。
汽车 载人 载货 客车 货车 小,速度快 大,速度慢 小轿车 大客车 4.2 从面向过程到面向对象(阅读) 继承与派生 以汽车为例看客观世界描述事物的方式: 面向对象程序设计提供了类似的机制: 当定义了一个类后,又需定义一个新类,这个新类与原来的类相比,只是增加或修改了部分属性和操作,这时可以用原来的类派生出新类,新类中只需描述自己所特有的属性和操作。 新类称为子类或派生类,原来的类称为基类。派生可以一直进行下去,形成一个派生树。 继承性大大简化了对问题的描述,大大提高了程序的可重用性,从而提高了程序设计、修改、扩充的效率。
4.2 从面向过程到面向对象(阅读) 多态性 多态性指,同一个消息被不同对象接收时,产生不同结果,即实现同一接口,不同方法。 高中生 大学生 计 算平均成绩 语文、数学、英语、政治、物理、化学、生物 高数、英语、计算机、线性代数
4.2 从面向过程到面向对象(阅读) 继承和多态性组合,可以生成很多相似但又独一无二的对象。继承性使得这些对象可以共享许多相似特性,而多态又使同一个操作对不同对象产生不同表现形式。这样不仅提高了程序设计的灵活性,而且减轻了分别设计的负担。
4.3 构造函数和析构函数 数据成员多为私有的,要对它们进行初始化,必须用一个公有函数来进行。同时这个函数应该在且仅在定义对象时自动执行一次。称为: 构造函数(constructor) 4.3.1 构造函数的定义与使用 4.3.2 析构函数的定义
4.3.1 构造函数的定义与使用 构造函数特征: 1.函数名与类名相同。 2.构造函数无函数返回类型说明。注意是什么也不写,也不可写void! 3.在程序运行时,当新的对象被建立,该对象所属的类的构造函数自动被调用,在该对象生存期中也只调用这一次。 4.构造函数可以重载。严格地讲,说明中可以有多个构造函数,它们由不同的参数表区分,系统在自动调用时按一般函数重载的规则选一个执行。
4.3.1构造函数的定义与使用 5.构造函数可以在类中定义,也可以在类外定义。 6. 如果类说明中没有给出构造函数,则C++编译器自动给出一个默认的构造函数: 类名(void) {} 但只要我们定义了一个构造函数,系统就不会自动生成默认的构造函数。 只要构造函数是无参的或各参数均有默认值的,C++编译器都认为是默认的构造函数,并且默认的构造函数只能有一个 。
4.3.1构造函数的定义与使用 CGoods的构造函数: 三参数: Cgoods (char* name , int amount , float price){ strcpy(Name,name) ;Amount=amount ;Price=price ; Total_value=price*amount ;} 两参数:货名和单价, Cgoods (char* name , float price){ strcpy(Name,name) ;Price=price;Amount=0 ; Total_value=0.0 ;} 默认的构造函数: CGoods(){ Name[0]=‘\0’ ; Price=0.0 ; Amount=0 ; Total_value=0.0 ;} 这三个构造函数同时被说明(重载)。
4.3.1构造函数的定义与使用 构造函数应用: CGoods Car1(“夏利2000”,30,98000.0); 调用了CGoods中的第一个构造函数,等效于: CGoods Car1= CGoods(“夏利2000”,30,98000.0); CGoods Car2(“桑塔那2000”,164000.0) ; 调用的是第二个构造函数,参数为两个。 CGoods Car3; 定义时调用不带参数的构造函数 但是定义对象时不能加括号。例如:CGoods Car4(); Car4()是不带参数的函数,它的返回值是类CGoods的对象。 【例4.1_1】完整商品类对象应用实例
4.3.2 析构函数的定义 析构函数(destructor)特征: 当一个对象的生命周期结束时,C++会自动调用析构函数(destructor)对该对象并进行善后工作, 1.构函数名与类名相同,但在前面加上字符‘~’,如 ~CGoods()。 2.析构函数无函数返回类型,与构造函数在这方面是一样的。但析构函数不带任何参数。 3. 一个类有一个也只有一个析构函数,这与构造函数不同。析构函数可以默认。 4. 对象注销时,系统自动调用析构函数。 【例4.2】定义一个矩形类
4.4引用与复制构造函数 4.4.1 引用 4.4.2 复制构造函数 4.4.3 成员对象与构造函数
4. 4.1引用 引用的导入: 参数传递的传值方式在函数域中为参数重新分配内存,而把实参的数值传递到新分配的内存中。它的优点是有效避免函数的副作用。 问题:如果要求改变实参的值,怎么办呢?如果实参是一个复杂的对象,重新分配内存会引起程序执行效率大大下降,怎么办呢? 有一种导出型数据类型—引用(reference)可以解决上面的难题。引用又称别名(alias)。
4.4.1引用 引用的定义: 引用是给一个已经定义的变量重新起一个别名,而不是定义一个新的变量,定义的格式为: 类型 &引用变量名=已定义过的变量名; 例如: double number ; double &newnum=number ; newnum是新定义的引用类型变量,它是变量number的别名。 引用主要用于函数之间的数据传递。
4.4.1引用 newnum是变量number的别名,C++系统不为引用类型变量分配内存空间。内存分配见下图: number称为引用newnum的关联变量。“&”(读作ampersand)在这里是引用的说明符。必须注意number和newnum都是double类型。如在程序中修改了newnum也就是修改了number,两位一体。 注意:对数组只能引用数组元素,不能引用数组(数组名本身为地址)。
4.4.1引用 【例4.3】引用作为函数的参数。 采用引用调用时,将对实参进行操作。 【例4.4】引用作为函数的返回值 一般函数返回值时,要生成一个临时变量作为返回值的副本,而用引用作为返回值时,不生成值的副本。 注意:采用引用返回方式时,返回的不能是函数中的局部变量,这时返回的局部变量地址已经失效。引用方式返回最常用的是由引用参数传递过来的变量(见例4.5),其次是全局变量,这样返回的变量地址是有效的。 【例4.5】返回值为引用的函数作为左值(选读)
4.4.2 复制构造函数 复制构造函数引入: 同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行复制是完全可行的。这个复制过程只需要复制数据成员,而函数成员是共用的(只有一份代码)。在建立对象时可用同一类的另一个对象来初始化该对象,这时所用的构造函数称为复制构造函数(Copy Constructor)。 CGoods类,复制构造函数为: CGoods (CGoods & cgd){ Strcpy (Name , cgd.Name); Price= cgd.price; Amount=cgd.Amount; Total_value=cgd.Total_value;}
4.4.2 复制构造函数 复制构造函数特征: 1.复制构造函数的参数必须采用引用。 * 在C++中按值传递一个参数时,会在函数中重新分配一块内存建立与参数同类型的变量或对象,再把参数的数据成员赋给新的变量或对象。在建立这个对象时,编译器就会自动为这个对象调用复制构造函数。如果其参数是真实的对象而不是引用,则又会引入新的一轮调用复制构造函数的过程,出现了无穷递归。
4.4.2 复制构造函数 2.系统会自动提供称为默认的按成员语义支持的复制构造函数,亦称为默认的按成员初始化。按成员作复制是通过依次复制每个数据成员实现的。 赋值运算符“=”称默认的按成员复制赋值操作符(Copy Assignment Operator),同类对象之间可以用“=”直接复制 。 3.通常按成员语义支持已经足够。但在某些情况下,它对类与对象的安全性和处理的正确性还不够,这时就要求提供特殊的复制构造函数和复制赋值操作符的定义。
4.4.2 复制构造函数 实例: CGood Car1(“夏利2000”,30,98000.00); //调用三个参数的构造函数 CGood Car2= Car1; //调用复制构造函数 CGood Car3 ( Car1); //调用复制构造函数,Car1为实参 这样三个对象的初始化结果完全一样。 注意: 在类定义中如果没有显式给出构造函数时,并不是不用构造函数,而是由系统自动调用默认的构造函数或默认的复制构造函数。如果有程序设计者定义的构造函数(包括复制构造函数),则按函数重载的规律,调用合适的构造函数。
4.4.2 复制构造函数 • 隐含的复制构造函数使用: • 当函数的形参是类对象,调用函数时,进行形参与实参结合时使用。这时要在内存新建立一个局部对象,并把实参复制到新的对象中。 • 2.当函数的返回值是类对象,函数执行完成返回调用者时使用。理由也是要建立一个临时对象,再返回调用者。 • 因为局部对象在离开建立它的函数时就消亡了,不可能在返回调用函数后继续生存,所以编译系统会在调用函数的表达式中创建一个无名临时对象,该临时对象的生存周期只在函数调用处的表达式中。所谓return 对象,实际上是调用复制构造函数把该对象的值拷入临时对象。如果返回的是变量,处理过程类似,只是不调用构造函数。
4.4.3 成员对象与构造函数 聚合(aggregation ) : 类中的成员,除了成员数据和成员函数外,还有成员对象,即用其他类的对象作为类的成员。使用成员对象的技术称为聚合。成员对象是实体,系统不仅为它分配内存,而且要进行初始化。
4.4.3 成员对象与构造函数 含对象成员的构造函数: 类名::构造函数名(参数总表):对象成员1(参数名表1),对象成员2(参数名表2),……对象成员n(参数名表n){……} 冒号后用逗号隔开的为要初始化的对象成员,附在后面的参数名表1,…,参数名表n依次为调用相应对象成员所属的构造函数时的实参表。这些表中的参数通常来自冒号前的参数总表,但没有类型说明。 含对象成员的类对象的初始化时,首先依次自动调用各成员对象的构造函数,再执行该类对象自己的构造函数的函数体部分。各成员对象的构造函数调用的次序与类定义中说明的顺序一致,而与它们在构造函数成员初始化列表中的顺序无关。
4.4.3 成员对象与构造函数 含对象成员的析构函数: 因为析构函数没有参数,所以包含成员对象的类的析构函数形式上并无特殊之处。但是撤销该类对象时,会首先调用自己的析构函数,再调用成员对象的析构函数,调用次序与初始化时的次序相反。 【例4.6】含有成员对象的类的构造函数
4.4.3 成员对象与构造函数 构造函数另一格式: 对于不含对象成员的类对象的初始化,也可以套用以上的格式,把部分需要直接赋初值的变量初始化写在冒号的右边: 类名::构造函数名(参数表):变量1(初值1),……,变量n(初值n){……} 当然也可以把一部分变量重新放回花括号中的函数体。冒号以后部分实际是函数体的一部分,所以在构造函数的声明中,冒号及冒号以后部分必须略去。
4.4.3 成员对象与构造函数 构造函数和析构函数的调用规则: 1. 对全局定义的对象,当程序进入入口函数main之前 对象就已经定义,那时要调用构造函数。整个程序结束时调用析构函数。 2. 对于局部定义的对象,每当程序控制流到达该对象定义处时,调用构造函数。当程序控制走出该局部域 时,则调用析构函数。 3. 对于静态局部定义的对象,在程序控制首次到达该对象定义处时,调用构造函数。当整个程序结束时调 用析构函数。
4.4.3 成员对象与构造函数 在正确定义了构造函数和析构函数的前提下,在一个健康的程序中,每个创建的对象必然有一个而且只有一个撤消动作。 【例4.7】演示对象创建和撤消的对应关系 注意:先建立的对象后撤销。
4.5 运算符的重载 运算符重载的概念: 运算符的重载是特殊的函数重载,必须定义一个函数,并通知C++编译器,当遇到该重载的运算符时调用此函数。这个函数叫做运算符重载函数,通常为类的成员函数。 运算符重载函数定义: 返回值类型 类名::operator重载的运算符(参数表) {……} operator是关键字,它与重载的运算符一起构成函数名。因函数名的特殊性,C++编译器可以将这类函数识别出来。
4.5 运算符的重载 细解运算符重载: 复数类+的重载: Complex Complex::operator+(Complex c){ //显式说明局部对象 Complex Temp(Real+c.Real , Image+c.Image) ; //注意:直接写对象c的私有成员,不用调c的公有函数处理 return Temp ;} 编译器把表达式c2+c3解释为: c2.operator+(c3) ; 函数c2.operator创建一个局部的Complex对象Temp,把出现在表达式中的两个Complex类对象c2和c3的实部之和及虚部之和暂存其内,然后把这个局部对象返回,赋给Complex类对象c(注意这里调用了复制构造函数生成一个无名临时对象过渡)。参见图4.8。