




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
类与对象本教案改变了教学次序第一页,共一百一十页,2022年,8月28日第五章类与对象5.1类与对象
5.5运算符的重载
5.4构造函数和析构函数(下)
5.3引用
5.2从面向过程到面向对象
5.9全局对象与类接口
5.10面向对象的程序设计和Windows编程
5.8结构和联合
5.7静态成员
5.6友元
5.11对象与类的识别
5.4构造函数和析构函数(上)第二页,共一百一十页,2022年,8月28日5.1
类与对象
对象的创建与使用名字空间域和类域5.1.1C++类的定义5.1.2 成员函数的定义
第三页,共一百一十页,2022年,8月28日5.1.1C++类的定义
在C++中,类是一种数据类型。数组和枚举类型是由用户自己定义,自己按规则构造出来的,但其基本组成单位(数据成员)都是同一种数据类型。然而客观事物是复杂的,要描述它必须从多方面进行,也就是用不同的数据类型来描述不同的方面。如商场中的商品可以这样描述:
商品名称(用字符串描述),该商品数量(用整型数描述),该商品单价(用浮点数描述),该商品总价(用浮点数描述)。这里用了属于三种不同数据类型的四个数据成员(datamember)来描述一种商品。第四页,共一百一十页,2022年,8月28日5.1.1C++类的定义在C++中可以这样表述:classCGoods{public:charName[21]; //对于中文可用wchar_tname[11] intAmount; floatPrice; floatTotal_value;};
//最后的分号不可少,这是一条说明语句上面的表述中,关键字class是数据类型说明符,指出下面说明的是类。标识符CGoods是商品这个类的类型名。花括号中是构成类体的一系列的成员,关键字public是一种访问限定符,表示其后所列为公共成员,就是说可以在外部对这些成员进行访问。第五页,共一百一十页,2022年,8月28日5.1.1C++类的定义访问限定符(accessspecifier)有三种:public(公共的),private(私有的)和protected(保护的),其中后两种说明的成员是不能从外部进行访问的。每种说明符可在类体中使用多次。它们的作用域是从该说明符出现开始到下一个说明符之前或类体结束之前结束。如果在类体起始点无访问说明符,系统默认定义为私有(private)。访问说明符private(私有的)和protected(保护的)体现了类具有封装性(Encapsulation)。第六页,共一百一十页,2022年,8月28日5.1.1C++类的定义
定义一个类的一般格式为:class类名{《《private:》
成员表1;》《public:
成员表2;》《protected:
成员表3;》};//最后的分号不可少;注意:所有说明都有分号其中“class类名”称为类头(classhead)。花括号中的部分称为类体(classbody),类体中定义了类成员表(classmemberlist)。类定义的更关键部分是对数据成员的操作。这可以用函数来完成。
第七页,共一百一十页,2022年,8月28日5.1.1C++类的定义这样描述一种商品的完整方式如下:classCGoods{private:charName[21];int Amount;floatPrice;float Total_value;public:voidRegisterGoods(char*,int,float);//输入数据
void CountTotal(void);//计算商品总价值
void GetName(char*);//读取商品名
int GetAmount(void);//读取商品数量
float GetPrice(void);//读取商品单价
float GetTotal_value(void);//读取商品总价值};第八页,共一百一十页,2022年,8月28日5.1.1C++类的定义
这样在类中引进了成员函数(memberfunction)或函数成员,也就是函数也成了数据(类)中的一员。类把数据(事物的属性)和函数(事物的行为——操作)封装为一个整体。还应注意到:四个数据成员被说明成私有的,而六个函数成员被说明成公有的;这就是说如果从外部对四个数据成员进行操作的话,只能通过六个公有函数来完成,数据受到了良好的保护,不易受副作用的影响。公有函数集定义了类的接口(interface)。类是一种数据类型,定义时系统并不为类分配存储空间,所以不能对类的数据成员初始化。当然类中的任何数据成员也不能使用关键字extern、auto或register限定其存储类型。成员函数可以直接使用类定义中的任一成员,可以处理数据成员,也可调用函数成员。
第九页,共一百一十页,2022年,8月28日5.1.2 成员函数的定义
在前面的小结中,只对成员函数作了一个声明,或者讲只给出了函数的原型,并没有对函数进行定义。函数定义通常在类的说明之后进行,其格式如下:返回值类型
类名::函数名(参数表){……}//函数体其中运算符“::”称为作用域解析运算符(scoperesolutionoperator),它指出该函数是属于哪一个类的成员函数。当然也可以在类的定义中直接定义函数。但系统处理方法不一样。第十页,共一百一十页,2022年,8月28日5.1.2 成员函数的定义类CGoods的函数可以如下定义:voidCGoods::RegisterGoods(char*name,intamount,floatprice){
//char*是指向字符的指针类型说明,name现可理解为字符串strcpy(Name,name);//字符串拷贝函数Amount=amount;Price=price;}voidCGoods::CountTotal(void){Total_value=Price*Amount;}voidCGoods::GetName(char*name){strcpy(name,Name);}intCGoods::GetAmount(void){return(Amount);}floatCGoods::GetPrice(void){return(Price);}floatCGoods::GetTotal_value(void){return(Total_value);}第十一页,共一百一十页,2022年,8月28日
对象是类的实例(instance),正如在前几章称变量是数据类型的实例一样。声明一种数据类型只是告诉编译系统该数据类型的结构形式,并没有预定内存,或者讲并没有创建了可用来存放数据的变量。类只是一个样板,以此样板可以在内存中开辟出一个个同样结构的实例——对象。创建类的对象可以有两种常用方法。第一种是直接定义类的实例——对象:CGoodsCar;这个定义创建了CGoods类的一个对象Car,同时为它分配了属于它自己的存储块,用来存放数据和对这些数据实施操作的成员函数(代码)。与变量定义一样,一个对象只在定义它的域中有效。第二种是采用动态创建类的对象的方法,将在第七章中学习。所谓动态指在程序运行时建立对象。而前一种是在编译时(程序运行前)建立。
对象的创建与使用第十二页,共一百一十页,2022年,8月28日有两种方法可存储对象。
数据区代码区对象1数据区代码区对象2数据区代码区对象n......图5.1各对象完全独立地安排内存的方案数据区对象1数据区对象2数据区对象n......图5.2各对象的代码区共用的方案公共代码区图5.1是系统为每一个对象分配了全套的内存,包括安放成员数据的数据区和安放成员函数的代码区。但区别同一个类的各个不同的对象的属性是由数据成员决定的,不同对象的数据成员的内容是不一样的;而行为(操作)是用函数来描述的,这些操作的代码对所有的对象都是一样的。图5.2仅为每个对象分配一个数据区,代码区(放成员函数的区域)为各对象类共用。图5.1对应的是在类说明中定义函数,而图5.2对应的是在类说明外部定义函数。第十三页,共一百一十页,2022年,8月28日5.1.3 对象的创建与使用下例给出对象使用的规则【例5.1】商品类对象应用实例:#include<iostream.h>#include<iomanip.h>#include<string.h>//省略了类定义voidmain(){CGoods car;charstring[21];int number;floatpr;如果通过使用关键字inline,则系统也会自动采用内联扩展方法实现,这时每个对象都有该函数一份独立的拷贝。如RegisterGoods()函数可定义为:inlinevoidCGoods::RegisterGoods(char*name,intamount,floatprice){strcpy(Name,name);Amount=amount;Price=price;}则每个对象都有RegisterGoods()函数一份独立的拷贝。第十四页,共一百一十页,2022年,8月28日成员名Name[21];Amount;Price;Total_value;10minicar5210minicar52string[21]numberPrminicar52minicarcout<<“请输入汽车型号:”
;cin.getline(string,20);//输入串长必须小于20cout<<“请依次输入汽车数量与单价:”
;cin>>number>>pr;car.RegisterGoods(string,number,pr);car.CountTotal();string[0]=’\0’;//字符串string清零car.GetName(string);//string赋值car.Namecout<<setw(20)<<string<<setw(5)<<car.GetAmount();//Acout<<setw(10)<<car.GetPrice()<<setw(20)<<car.GetTotal_value()<<endl;//B}第十五页,共一百一十页,2022年,8月28日5.1.3 对象的创建与使用注意:
因为图5.1的内存分配方法明显不合理,在具体的C++平台中也仅当类的成员函数不包括循环等复杂结构,并这些成员函数的函数体在类定义内部直接定义时,才用内联扩展方式实现。上面所述对象的存储方式是物理的,这是由计算机来完成的,它并不影响类在逻辑上的封装性。程序设计是一个逻辑的概念,是由人来完成的。从程序员的角度看,逻辑上各对象是完全独立的,不必去管物理上是怎样存储的,所以类的封装在逻辑上是完善的。但若知道物理存储方式,可加深对类与对象的理解,这也是程序员必备的知识。第十六页,共一百一十页,2022年,8月28日5.1.3 对象的创建与使用对象使用的规则很简单,只要在对象名后加点号(点操作符,成员访问运算符(memberaccessoprator)之一),再加成员数据或成员函数名就可以了。但是这些成员必须是公有的成员,只有公有成员才能在对象的外面对它进行访问。如果在上面的例子中的A行和B行写成:cout<<setw(20)<<car.Name<<setw(5)<<car.Amount;cout<<setw(10)<<car.Price<<setw(20)<<car.Total_value<<endl;那就错了,因为这里对象car的4个数据成员全是私有的,在外部是不能访问的,必须用对象car所带的公有函数进行访问。第十七页,共一百一十页,2022年,8月28日*5.2 从面向过程到面向对象*供学生阅读:
上世纪六十年代中后期软件危机发生之后,面向过程(procedure-oriented)的结构化程序设计(structuredprogramming,SP)成为主流。结构化程序设计的提出与发展是伴随软件日益庞大和复杂进行的,但是当软件复杂到一定的程度后,结构化程序设计也不能满足需要。当软件规模超过一定的尺度后,采用结构化程序设计,其开发和维护就越来越难控制。其根本的原因就在于面向过程的结构化程序设计的方法与现实世界(包括主观世界和客观世界)往往都不一致,结构化程序设计的思想往往很难贯彻到底。第十八页,共一百一十页,2022年,8月28日5.2 从面向过程到面向对象
在结构化程序设计中,采用的是“自顶向下,逐步细化(divideandconquer,stepwiserefinement)”的思想。它的具体操作方法是模块化,是按功能来分的,所以也称功能块。也就是从一般事物中抽象出来的操作,在C++中称为一个函数,一个函数解决一个问题,即实现一个功能或一个操作。当程序规模和复杂性达到一定程度时不可避免地引入大量的全局变量,优良的模块化没法坚持到底。在模块化的思想中已经出现了封装的概念,这个封装是把数据封装到模块中,即局部变量。但这是很不彻底的,因为模块是功能的抽象,而数据则是具有其个性的,一但发生那怕是一点变化,抽象的功能模块就不再适用了。如一个管理软件,管理规则变化了,则管理模块以及所有与之有联系的模块都必须更改,必须重新进行功能抽象,必须重新建立模块间联系的规则。可维护性差成了制约结构化程序设计应用的瓶颈。第十九页,共一百一十页,2022年,8月28日5.2 从面向过程到面向对象
对象的概念是面向对象技术的核心所在。面向对象技术中的对象就是现实世界中某个具体的物理实体在计算机逻辑中的映射和体现。比如你所拥有的一部移动电话,它是现实世界中的一个实体。它由天线、发射部件、接收部件、显示屏、按键、专用集成电路芯片及外壳组成;它有着其实在的功能,可以打电话,可以发短消息,可以存储、输入和编辑各种个人信息,甚至可以上网。这样一个实体可以在计算机世界中映射为一个计算机可以理解、可以操纵、具有前面所叙述的属性和操作的对象。又如你们所拥有的一辆自行车,它由车架、车轮、脚踏和传动机构、变速机构等组成,它具有代步功能,它可以进行变速骑行,特别要强调的是它有一些特征可以把你的这辆自行车与其他自行车区分开来,其中最重要的是钢印号。这些都可以在面向对象的程序中用对象及其属性和操作模拟出来。第二十页,共一百一十页,2022年,8月28日5.2 从面向过程到面向对象对象类计算机世界实体抽象类别现实世界客观世界抽象抽象实例化映射主观世界图5.3对象、实体与类
现实世界中的实体可以抽象出类别的概念。对应于计算机世界就有一个类(class)的概念,因为类是一个抽象的概念的对应体,所以计算机不给它分配内存,只给对象分配内存。图5.3表达了计算机世界与现实世界之间的对应关系。第二十一页,共一百一十页,2022年,8月28日5.4 构造函数和析构函数
定义对象时,按现在已学过的知识无法进行初始化,即无法对数据成员进行赋初值的过程。数据成员,从封装的目的出发,应该多为私有的,要对它们进行初始化,看来必须用一个公有函数来进行。同时这个函数应该在且仅在定义对象时自动执行一次,否则就不是初始化了。在C++程序设计语言中这个函数称为构造函数。必须指出:调用构造函数也是建立对象的唯一方法(联合例外,见5.8)。第二十二页,共一百一十页,2022年,8月28日
5.4 构造函数和析构函数
5.
4.
1 构造函数的定义与使用
5.
4.
3 析构函数的定义5.
4.
4 成员对象与构造函数5.
4.
2 拷贝构造函数
第二十三页,共一百一十页,2022年,8月28日
对于对象的初始化,采用构造函数(constructor)。当需要对对象进行初始化时,总是编写一个或一组构造函数。构造函数是特殊的公有成员函数,其特征如下:1.函数名与类名相同。2.构造函数无函数返回类型说明。注意是没有而不是void,即什么也不写,也不可写void!实际上构造函数有返回值,返回的就是构造函数所创建的对象,见5.5节。3.在程序运行时,当新的对象被建立,该对象所属的类的构造函数自动被调用,在该对象生存期中也只调用这一次。4.构造函数可以重载。严格地讲,说明中可以有多个构造函数,它们由不同的参数表区分,系统在自动调用时按一般函数重载的规则选一个执行。构造函数的定义与使用
第二十四页,共一百一十页,2022年,8月28日
构造函数的定义与使用5.构造函数可以在类中定义,也可以在类外定义。6.如果类说明中没有给出构造函数,则C++编译器自动给出一个缺省的构造函数:
类名(void){}但只要我们定义了一个构造函数,系统就不会自动生成缺省的构造函数。缺省的构造函数,也可以由程序员自己来编,只要构造函数是无参的或者只要各参数均有缺省值的,C++编译器都认为是缺省的构造函数,并且缺省的构造函数只能有一个。如果对象的数据成员全为公有的,也可以在对象名后加“=”加“{}”,在花括号中顺序填入全体数据成员的初始值.第二十五页,共一百一十页,2022年,8月28日
构造函数的定义与使用
下面编写5.1节中商品类CGoods的构造函数。可以用三个参数来实现对4个数据成员的初始化:Cgoods(char*name,intamount,floatprice){strcpy(Name,name);Amount=amount;Price=price;Total_value=price*amount;
}在实际应用时,也可用两个参数:货名和单价,这时构造函数为:Cgoods(char*name,floatprice){strcpy(Name,name);Price=price;Amount=0;Total_value=0.0;}这两个构造函数同时被说明(重载)。如果定义对象时的格式为:CGoodsCar1(“夏利2000”,30,98000.0);则调用了CGoods中的第一个构造函数,相当于自动调用:CGoods(“夏利2000”,30,98000.0);
第二十六页,共一百一十页,2022年,8月28日
构造函数的定义与使用如果定义对象时的格式为:CGoodsCar2(“桑塔那2000”,164000.0);则调用的是第二个构造函数,参数为两个。定义对象初始化时也可以把构造函数显式表示出来如:CGoodsCar1=CGoods(“夏利2000”,30,98000.0);如果还希望初始化时,不带任何参数,可作如下定义:CGoods(){Name[0]=‘\0’;Price=0.0; Amount=0;Total_value=0.0;}但是定义对象时不能加括号。例如:CGoodsCar3,Car4();Car3是类CGoods的对象,定义时调用不带参数的构造函数。但是Car4()不是一个对象,而是一个不带参数的函数,它的返回值是类CGoods的对象。
第二十七页,共一百一十页,2022年,8月28日5.4.3 析构函数的定义
当一个对象定义时,C++自动调用构造函数建立该对象并进行初始化,那么当一个对象的生命周期结束时,C++也会自动调用一个函数注销该对象并进行善后工作,这个特殊的成员函数即析构函数(destructor):1.
构函数名与类名相同,但在前面加上字符‘~’,如~CGoods()。2.
析构函数无函数返回类型,与构造函数在这方面是一样的。但析构函数不带任何参数。3.一个类有一个也只有一个析构函数,这与构造函数不同。析构函数可以缺省。4.对象注销时,系统自动调用析构函数。第二十八页,共一百一十页,2022年,8月28日5.
3
引用
在有关函数的学习中,我们知道C++函数中参数的传递方式是传值。在函数域中为参数重新分配内存,而把实参的数值传递到新分配的内存中。它的优点是有效避免函数的副作用,在函数调用中不会无意中修改了实参的值。但如果就是要求改变实参的值,怎么办呢?再者,如果参数是一些简单的数据类型,占据内存不多,重新分配内存问题不大;如果实参是一个复杂的对象,重新分配内存会引起程序执行效率大大下降,怎么办呢?在C++中有一种新的导出型数据类型—引用(reference)可以解决上面的难题。引用又称别名(alias)。第二十九页,共一百一十页,2022年,8月28日5.
3
引用引用是一种非常特殊的数据类型,它不是定义一个新的变量,而是给一个已经定义的变量重新起一个别名,也就是C++系统不为引用类型变量分配内存空间。引用主要用于函数之间的数据传递。引用定义的格式为:类型&引用变量名=已定义过的变量名;例如:doublenumber;double&newnum=number;newnum是新定义的引用类型变量,它是变量number的别名,内存分配见下图。
number称为引用newnum的关联变量。“&”(仍读作ampersand)在这里是引用的说明符。必须注意number和newnum都是double类型。如在程序中修改了newnum也就是修改了number,两位一体。第三十页,共一百一十页,2022年,8月28日5.
3
引用
Xyd1d2temp1.4142.7181.4142.7181.414【例5.2】引用作为函数的形参使用一个函数来交换两个数据。内存分配见图5.5。#include<iostream.h>
voidswap(double&d1,double&d2){doubletemp;temp=d1;d1=d2;d2=temp;}voidmain(void){doublex,y;cout<<"请输入x和y的值"<<'\n';cin>>x>>y;swap(x,y);cout<<"x="<<x<<'\t'<<"y="<<y<<'\n';}图5.5参数d1、d2为引用时内存分配示意
第三十一页,共一百一十页,2022年,8月28日5.3引用
图5.6普通返回
图5.7引用返回【例5.3】采用不同返回方式的求正方形面积函数的比较引用可以作为函数的返回值。一般函数返回值时,要生成一个临时变量作为返回值的拷贝,而用引用作为返回值时,不生成值的拷贝。【例5.3】采用不同返回方式的求正方形面积函数的比较。#include<iostream.h>doubletemp;doublefsqr1(doublea){temp=a*a;returntemp;}double&fsqr2(doublea){temp=a*a;returntemp;}voidmain(){doublex=fsqr1(5.5);//第一种情况
doubley=fsqr2(5.5);//第二种情况
cout<<"x="<<x<<'\t‘<<"y="<<y<<endl;}运行结果为:x=30.25y=30.25运行结果一样,但在内存中的活动却不同。
第三十二页,共一百一十页,2022年,8月28日5.
3
引用*一个声明返回值为引用的函数可以作为左值。可选内容。【例5.4】统计学生成绩,分数在80分以上的为A类,60分以上,80分以下的为B类,60分以下为C类。#include<iostream.h>int&level(intgrade,int&typeA,int&typeB,int&typeC){ if(grade>=80)returntypeA; elseif(grade>=60)returntypeB; elsereturntypeC;}voidmain(){ inttypeA=0,typeB=0,typeC=0; intstudent=9; intarray[9]={90,75,83,66,58,40,80,85,71}; for(inti=0;i<student;i++)
level(array[i],typeA,typeB,typeC)++;//函数调用为左值
cout<<"A类学生数:"<<typeA<<endl; cout<<"B类学生数:"<<typeB<<endl; cout<<"C类学生数:"<<typeC<<endl;}第三十三页,共一百一十页,2022年,8月28日5.
3
引用注意:1.对数组只能引用数组元素,不能引用数组(数组名本身为地址)。2.不能定义引用的引用(引用也是地址),所以当函数的参数为引用时,引用不能作实参。3.const引用:引用在内部存放的是被引用对象的地址,不可寻址的值是不能引用的;当引用作为形参时,实参也不能使用不可寻址的值,更不可能进行类型转换(如:实数转换为整数)。但是const引用不同,它是只读的,为了绝对保证不会发生误改,编译器实现const引用时,生成一个临时对象,引用实际上指向该临时对象,但用户不能访问它。所以const引用可以实现不可寻址的值(包括字面常量)的引用,例如:doubledval=1024;constint&ri=dval;是正确的,编译器将其转换为:doubledval=1024;inttemp=dval;constint&ri=temp;因有临时对象,引用和类型转换都实现了。当const引用作为形参时,实参也能使用不可寻址的值,并能进行类型转换(如例5_7后的说明和例5_7_1:调用构造函数将实数转换为复数)。第三十四页,共一百一十页,2022年,8月28日5.4.2 拷贝构造函数
同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行复制或称拷贝是完全可行的。这个拷贝过程只需要拷贝数据成员,而函数成员是共用的(只有一份拷贝)。在建立对象时可用同一类的另一个对象来初始化该对象,这时所用的构造函数称为拷贝初始化构造函数(CopyConstructor)。对于CGoods类,可以定义拷贝构造函数为:CGoods(CGoods&cgd){Strcpy(Name,cgd.Name);Price=cgd.price;Amount=cgd.Amount;Total_value=cgd.Total_value;}实验六
5.4.4成员对象与构造函数
5.1.4名字空间域与类域第三十五页,共一百一十页,2022年,8月28日5.4.2 拷贝构造函数
这里必须注意拷贝构造函数的参数——同类(class)的对象采用的是引用的方式。如果把一个真实的类对象作为参数传递到拷贝构造函数,会引起无穷递归。所以必须将拷贝构造函数的参数定义为一个类的对象的引用。通常情况下,这种按成员语义支持已经足够。但在某些情况下,它对类与对象的安全性和处理的正确性还不够,这时就要求类的设计者提供特殊的拷贝构造函数和拷贝赋值操作符的定义。
系统会自动提供,称为缺省的按成员语义支持的拷贝构造函数,每个类成员被依次拷贝,亦称为缺省的按成员初始化。按成员作拷贝是通过依次拷贝每个数据成员实现的,而不是对整个类对象按位拷贝。赋值运算符“=”称缺省的按成员拷贝赋值操作符,同类对象之间可以用“=”直接拷贝。
第三十六页,共一百一十页,2022年,8月28日5.4.2 拷贝构造函数
当成员函数的参数为同一类(class)的对象或它的引用,在函数体内使用参数对象的私有数据成员时,可用对象名加成员访问操作符点号进行。从逻辑上讲,每个对象有自己的成员函数,访问同类其他对象的私有数据成员应通过该对象的公有函数,不能直接访问。但在物理上只有一个成员函数拷贝,所以直接访问是合理的。其他实例可见5.5节。对本对象的数据成员不加点号,参见6.2节。注意,仅在成员函数中可以这样做。下面来看一个实例。有一个程序段:CGoodCar1(“夏利2000”,30,98000.00);//调用三个参数的构造函数CGoodCar2=Car1;//调用拷贝构造函数CGoodCar3(Car1);//调用拷贝构造函数,Car1为实参这样三个对象的初始化结果完全一样。在类定义中如果没有显式给出构造函数时,并不是不用构造函数,而是由系统自动调用缺省的构造函数或缺省的拷贝构造函数。如果有程序设计者定义的构造函数(包括拷贝构造函数),则按函数重载的规律,调用合适的构造函数。第三十七页,共一百一十页,2022年,8月28日5.4.2 拷贝构造函数拷贝构造函数并不只是在同类的一个对象去初始化该类的另一个对象时使用,它还在另二个方面使用:1.当函数的形参是类的对象,调用函数时,进行形参与实参结合时使用。这时要在内存新建立一个局部对象,并把实参拷贝到新的对象中。理所当然也调用拷贝构造函数。2.当函数的返回值是类对象,函数执行完成返回调用者时使用。理由也是要建立一个临时对象中,再返回调用者。为什么不直接用要返回的局部对象呢?因为局部对象在离开建立它的函数时就消亡了,不可能在返回调用函数后继续生存,所以在处理这种情况时,编译系统会在调用函数的表达式中创建一个无名临时对象,该临时对象的生存周期只在函数调用处的表达式中。所谓return对象,实际上是调用拷贝构造函数把该对象的值拷入临时对象。如果返回的是变量,处理过程类似,只是不调用构造函数。第三十八页,共一百一十页,2022年,8月28日实验六类与对象的实践——范例1范例:设计一个程序,定义一个矩形类,包括数据成员和函数成员。要求有构造函数、析构函数,完成赋值、修改、显示等功能的接口,并编写main函数测试,要求用一个对象初始化另一对象。[分析]
要确定一个矩形(四边都是水平或垂直方向,不能倾斜),只要确定其左上角和右下角的x和y坐标即可,因此应包括四个数据成员,left,right,top,bottom,即左右上下四个边界值。由构造函数对数据成员赋值,赋值函数完成未初始化的矩形赋值,修改函数可以修改各数据成员,显示函数则给出该矩形参数。classRectangle{ intleft,top,right,bottom;public: Rectangle(intl=0,intt=0,intr=0,intb=0);
//缺省构造函数必须在此指定缺省实参
~Rectangle(){};//析构函数,在此函数体为空
voidAssign(intl,intt,intr,intb); voidSetLeft(intt){left=t;}//以下皆为内联成员函数
voidSetRight(intt){right=t;}voidSetTop(intt){top=t;} voidSetBottom(intt){bottom=t;} voidShow();};//将上述内容保存为rect.h第三十九页,共一百一十页,2022年,8月28日实验六类与对象的实践——范例1#include<iostream.h>#include“rect.h”//构造函数,带缺省参数,缺省值为全0,在声明中指定
Rectangle::Rectangle(intl,intt,intr,intb){left=l;top=t;right=r;bottom=b;}voidRectangle::Assign(intl,intt,intr,intb){left=l;top=t;right=r;bottom=b;}voidRectangle::Show(){cout<<”left-toppointis(”<<left<<”,”<<top<<”)”<<’\n’;cout<<”right-bottompointis(”<<right<<”,”<<bottom<<”)”<<’\n’;}voidRectangle::Draw(CDC*pDC){pDC->Rectangle(left,top,right,bottom);}//将上述内容保存为rect.cpp第四十页,共一百一十页,2022年,8月28日实验六类与对象的实践——范例1#include<iostream.h>#include“rect.h”voidmain(){ Rectanglerect; rect.Show(); rect.Assign(100,200,300,400); rect.Show(); Rectanglerect1(0,0,200,200); rect1.Show(); Rectanglerect2(rect1); rect2.Show();}第四十一页,共一百一十页,2022年,8月28日*
名字空间域和类域
名字空间域内容为阅读,不列入课堂教学!
在C++中支持三种域:局部域、名字空间域和类域。
名字空间域是随标准C++而引入的。它相当于一个更加灵活的文件域(全局域),可以用花括号把文件的一部分括起来,并以关键字namespace开头给它起一个名字,这就是名字空间。例如:namespacens1{floata,b,c;fun1(){……}…}花括号括起来的部分称声明块。声明块中可以包括:类、变量(带有初始化)、函数(带有定义)等。在该域外使用域内的成员时,需加上名字空间名作为前缀,后面加上域操作符“::”,就像在类定义外定义成员函数一样。这里添加了名字空间名称的成员名被称为限定修饰名(qualifiedname)。如:ns1::a,ns1::fun1()等等。最外层的名字空间域称为全局名字空间域(globalnamespacescope),即文件域,对比例4.9在块内访问全局量,只用域操作符“::”。第四十二页,共一百一十页,2022年,8月28日*
名字空间域和类域
与局部域分层次一样,名字空间域也可分层嵌套,也同样有分层屏蔽作用。例如:namespacecplusplus_primer{namespaceMatrixlib{ //名字空间嵌套
classmatrix{……} //名字空间类成员matrix …...}}访问名字空间域中的成员,使用限定修饰名非常不方便,如访问matrix,每次都写:cplusplus_primer::Matrixlib::matrix太麻烦。使用using声明可只写一次限定修饰名。using声明以关键字using开头,后面是被限定修饰的(qualified)名字空间成员名:usingcplusplus_primer::Matrixlib::matrix;//名字空间类成员matrix的using声明以后在程序中使用matrix时,就可以直接使用成员名,而不必使用限定修饰名。 第四十三页,共一百一十页,2022年,8月28日*
名字空间域和类域
名字空间域的引入,主要是为了解决全局名字空间污染(globalnamespacepollution)问题,即防止程序中的全局实体名与C++各种库中声明的全局实体名冲突。C++库是C++程序员编程最有力的工具,对于标准的和通用的部分,程序员不必也不应该自己编写,程序员的注意力应集中在程序的个性部分。使用using指示符可以一次性地使名字空间中所有成员都可以直接被使用,比using声明方便。using指示符以关键字using开头,后面是关键字namespace,然后是名字空间名。标准C++库中的所有组件都是在一个被称为std的名字空间中声明和定义的。在采用标准C++的平台上使用标准C++库中的组件,只要写一个using指示符:usingnamespacestd;就可以直接使用标准C++库中的所有成员。这是很方便的。名字空间可以不连续,分为多段,但它们仍是同一个名字空间。名字空间域不能定义在函数声明、函数定义或类定义的内部。第四十四页,共一百一十页,2022年,8月28日
名字空间域和类域
类体也定义了一个域称为类域。在类域中说明的标识符仅在该类的类域内有效。必须加上“类名::”作限定修饰。类的实体——对象中的公有成员也可以在对象之外访问,但必须使用成员访问操作符“.”,对象名+“.”+成员名。定义类本身的目的就是要实现一个封装性,对外是封闭的,对内是开放的,在程序中并不总是需要用成员访问符之类来引用类成员。多数程序代码本身就在类域中,这些程序可以直接访问类成员。类域作为课堂教学内容,可在此处讲,也可放在节之后节之前讲,第四十五页,共一百一十页,2022年,8月28日
名字空间域和类域在类域中类成员在类体中被声明的顺序同样很重要,后声明的成员不能被先声明的成员引用。但编译器对名字(标识符)的解析分两步,第一步查找在声明中用到的名字,包括数据成员和函数成员声明中用到的参数类型,第二步才是函数成员体内的名字。例如:classstring{ //字符串类
public:typedefintindex_type;//为易读易懂直接用下标型命名
charGetstringElement(index_typeelem){
//内联函数,取串中第几个元素
returnAstring[elem];} //
Astring未说明
private:charAstring[30]; //Astring后说明};表面上看是错的;实际上是对的。因为Astring名字的解析是在第一步,而内联函数使用它是在第二步。第四十六页,共一百一十页,2022年,8月28日
名字空间域和类域
在类域中不仅有数据和函数成员,也可以有类类型说明,可以称为类成员,或成员类,其作用域为类域,出了类说明之外无效。例如嵌套类:classstudent{
public: classstudentID{ //成员类、嵌套类
intvalue; public: ……
};private:
studentIDid;
//嵌套类说明的私有对象
charname[20];}students1,s2;student::studentIDkk;系统并不为嵌套类分配内存空间,即使在student类对象s1和s2中也不为嵌套类studentID分配内存空间,而只为s1.id和s2.id分配内存。在本例中嵌套类studentID在student类外也可以引用,同样要加限定修饰(student::),因为studentID为公有,如为私有则外部不可引用。嵌套类主要功能是防止全局类说明污染,防止类名重复现象。第四十七页,共一百一十页,2022年,8月28日5.4.4 成员对象与构造函数
在定义类的对象时不仅要对对象进行初始化,还要先对成员对象进行初始化。对成员对象初始化,必须调用该成员对象的构造函数来实现。C++中对含对象成员的类对象的构造函数有特殊的格式:类名::构造函数名(参数总表):对象成员1(参数表1),对象成员2(参数表2),……对象成员n(参数表n){……}冒号后用逗号隔开的为要初始化的对象成员,附在后面的参数表1,…,参数表n依次为调用相应对象成员所属的构造函数时的实参表。这些表中的参数通常来自冒号前的参数总表。
【例5.5_1】含有成员对象的类的构造函数:#include<iostream.h>#include<string>classstudent{public:第四十八页,共一百一十页,2022年,8月28日classstudentID{longvalue;public:studentID(longid=0){value=id;cout<<"赋给学生的学号:"<<value<<endl; }~studentID(){cout<<"删除学号:"<<value<<endl;}};//学号类定义,注意分号private:
studentIDid;charname[20];public:student(char*sname=“noname”,longsid=0):id(sid){//sname现暂看作字符串,char*是指向字符的指针类型
cout<<“学生名:”<<sname<<endl;strcpy(name,sname);}第四十九页,共一百一十页,2022年,8月28日这样运行结果为:赋给学生的学号:08002132学生名:朱明删去学号:08002132在student构造函数头部的冒号表示要对对象成员的构造函数进行调用。但在构造函数的声明中,冒号及冒号以后部分必须略去。*以下供学生阅读:不用这种格式将无法把学号传递给学号类对象,如例5_5:student(char*sname,longsid=0){
//sname现暂看作字符串,char*是指向字符的指针类型,见第6章;
cout<<"学生名:"<<sname<<endl;strcpy(name,sname);
studentIDid(sid);}//A目的是将学号转给学号类对象,但未达目的5.4.4 成员对象与构造函数第五十页,共一百一十页,2022年,8月28日5.4.4 成员对象与构造函数这段程序希望通过构造函数中的A行进行初始化,把学生名“朱明”和学号08002132赋给对象SS。运行结果:赋给学生的学号:0//首先调用对象ss的对象成员id的构造函数学生名:朱明//对象ss的数据成员name赋给学生的学号:08002132
//ss构造函数中建立的局部对象id中数据成员value初值删去学号:08002132//局部对象id析构删去学号:0//对象成员id析构这表明构造函数中的A行并没有把学号08002132赋给对象ss中的对象成员id,而是在构造函数中构造了一个名字也为id的studentID局部对象,它只在构造函数中生存,构造函数返回时该局部对象被析构。而对象ss中的对象成员id则因为构造函数中没有指定对它进行初始化的值,所以系统按缺省方式调用了StudentID()建立了对象成员id,所以有第一条输出“赋给学生的学号:0“。A行也可以写为:
studentID::
studentID(sid);结果一样,无法把学号传过去,这时构造函数建立的是一个无名的studentID局部对象。第五十一页,共一百一十页,2022年,8月28日5.4.4 成员对象与构造函数对于不含对象成员的类对象的初始化,也可以套用以上的格式,把部分只需要直接赋初值的变量初始化写在冒号的右边:类名::构造函数名(参数表):变量1(初值1),……,变量n(初值n){……}当然也可以把一部分变量重新放回花括号中的函数体。冒号以后部分实际是函数体的一部分,所以在构造函数的声明中,冒号及冒号以后部分必须略去。第五十二页,共一百一十页,2022年,8月28日5.4 构造函数和析构函数
对于不同作用域的对象类型,构造函数和析构函数的调用如下:
1.对全局定义的对象,当程序进入入口函数main之前对象就已经定义,这时要调用构造函数。整个程序结束时调用析构函数。
2.对于局部定义的对象,每当程序控制流到达该对象定义处时,调用构造函数。当程序控制走出该局部域时,则调用析构函数。
3.对于静态局部定义的对象,在程序控制首次到达该对象定义处时,调用构造函数。当整个程序结束时调用析构函数。第五十三页,共一百一十页,2022年,8月28日5.4 构造函数和析构函数
在正确定义了构造函数和析构函数的前提下,在一个健康的程序中,每个创建的对象必然有一个而且只有一个撤消动作。请读者根据下面程序执行结果,注意每个对象创建和撤消的对应关系:例5.6演示了对象创建和撤消的对应关系。请参看VC++平台上的演示。注意:先建立的对象后撤销。本例可见构造函数和析构函数使用频繁,最好在类说明之外定义构造函数和析构函数,以确保一个函数拷贝,避免代码膨胀。第五十四页,共一百一十页,2022年,8月28日
5.5 运算符的重载
运算符的重载实际是一种特殊的函数重载,必须定义一个函数,并告诉C++编译器,当遇到该重载的运算符时调用此函数。这个函数叫做运算符重载函数,通常为类的成员函数。定义运算符重载函数的一般格式:返回值类型类名::operator重载的运算符(参数表) {……}operator是关键字,它与重载的运算符一起构成函数名。因函数名的特殊性,C++编译器可以将这类函数识别出来。
C++中没有复数类型,我们可以自己来定义一个复数类(class),同样可以用+、-、*、/来进行复数的算术运算。第五十五页,共一百一十页,2022年,8月28日5.5 运算符的重载例5.7定义了复数类,可完成复数基本运算,并应用它进行复数运算。请参看VC++平台上的演示。classComplex{ double Real,Image;public:Complex(doubler=0.0,doublei=0.0):Real(r),Image(i){}//定义构造函数
Complex(Complex&com){Real=com.Real;Image=com.Image;}//定义拷贝构造函数
voidPrint(){cout<<"Real="<<Real<<'\t'<<"Image="<<Image<<'\n';}Complexoperator+(Complex);Complexoperator+(double);voidoperator=(Complex);voidoperator+=(Complex);doubleabs(void);Complexoperator*(Complex);Complexoperator/(Complex);};第五十六页,共一百一十页,2022年,8月28日在本例中重载了运算符“+”、“=”、“+=”和“*”、“/”,以及求模(绝对值)的函数abs(),可以进行复数运算。首先来看“+”的重载。ComplexComplex::operator+(Complexc){
//显式说明局部对象
ComplexTemp(Real+c.Real,Image+c.Image);
//注意:直接写对象c的私有成员,不用调c的公有函数处理
returnTemp;}在做c=c2+c3时,C++编译器把表达式c2+c3解释为:c2.operator+(c3);这样一个函数调用过程,函数c2.operator创建一个局部的Complex对象Temp,把出现在表达式中的两个Complex类对象c2和c3的实部之和及虚部之和暂存其内,然后把这个局部对象返回,赋给Complex类对象c(注意这里调用了拷贝构造函数生成一个无名临时对象过渡)。参见图5.8。5.5 运算符的重载第五十七页,共一百一十页,2022年,8月28日Temp.Real=Real+c2.Real;Temp.Image=Image+c3.Image;c=return(Temp);RealImagec3.Realc3.Image=+局部对象Temp当前对象c2对象c3图5.8显式说明临时对象的“+”运算符执行过程可以用隐式的临时对象替换显式的对象Temp,如例中重载的Operator+(double),它执行复数与实数的加法。ComplexComplex::operator+(doubled){returnComplex(Real+d,Image);}//隐式说明局部对象在return后面跟的是一个表达式,在这里表达式中调用的是类的构造函数,构造函数无返回说明,而不是无返回值,实际上编译器为表达式中的构造函数创立了一个临时对象,临时对象生命期就在该表达式中,返回值就是该临时对象。
第五十八页,共一百一十页,2022年,8月28日
5.5 运算符的重载
使用引用类型变量作为运算符重载函数的参数,可以提高复数类型运算的效率。复数与复数相加的Operator+成员函数的最终形式:Complexcomplex::operator+(constcomplex&c){returncomplex(real+c.real,Image+c.Image);}这里采用complex对象的引用而不是对象本身,调用时不再重新分配内存建立一个复制的对象,函数效率会更高。而在引用形式参数类型说明前加const关键字,表示被引用的实参是不可改变的,如程序员不当心在函数体中重新赋值了被引用的实参,C++编译器会认为出错。
采用引用为参数时,从理论上讲实参必须为左值,不能为表达式,如【例5.7】中c=c+d是正确的,但c=c+0.5是不允许的,0.5不是左值,但在这种情况下VC++允许。
第五十九页,共一百一十页,2022年,8月28日5.5 运算符的重载
在缺省的情况下,C++编译器为每个类生成一个缺省的赋值操作,用于同类的两个对象之间的相互赋值,缺省的语义是类成员逐个相互赋值。对复数类complex如果没有重载赋值运算符=,复数的赋值语义是:Complex&Complex::operator=(Complex&c){real=c.real;image=c.imagereturn*this;}//参见6.2节这种缺省的赋值操作格式对所有类是固定的,这种缺省的格式对复数是合适的,但对其他类缺省的赋值可能产生问题,那时需重载。对所有的类对象,赋值运算符“=”即缺省的按成员拷贝赋值操作符(CopyAssignmentOperator),同类对象之间可以用“=”直接拷贝。因为缺省的赋值操作返回一个复数的引用,所以它可以进行连续赋值如:a=b=c=d;第六十页,共一百一十页,2022年,8月28日5.5 运算符的重载
重载的运算符“+=”标准算法是:Complex&Complex::operator+=(Complex&com){real+=com.real;image+=com.image;return*this;//参见6.2节}小结:
1.运算符重载函数的函数名必须为关键字Operator加一个合法的运算符。在调用该函数时,将右操作数作为函数的实参。第六十一页,共一百一十页,2022年,8月28日5.5 运算符的重载
2.
当用类的成员函数实现运算符的重载时,运算符重载函数的参数(当为双目运算符时)为一个或(当为单目运算符时)没有。运算符的左操作数一定是对象,因为重载的运算符是该对象的成员函数,而右操作数是该函数的参数,其类型并无严格限制。C++不允许重载三目运算符。
3.
单目运算符“++”和“--”存在前置与后置问题。前置“++”格式为:
返回类型类名::operator++(){……}
而后置“++”格式为:
返回类型类名::operator++(int){……}
后置“++”中的参数int仅用作区分,并无实际意义,可以给一个变量名,也可以不给变量名。
4.C++中只有极少数的运算符不允许重载,表5.1中列出了不允许重载的运算符。第六十二页,共一百一十页,2022年,8月28日5.5 运算符的重载运算符运算符名称禁止重载的理由?:三目条件运算符C++中没有定义三目运算符的语法.成员操作符为保证成员操作符对成员访问的安全性::作用域操作符该操作符右操作数不是表达式sizeof类型字长操作符该操作符的操作数为类型名,不是表达式表5.1C++中不允许重载的运算符第六十三页,共一百一十页,2022年,8月28日5.5 运算符的重载
在本小节中学习了有关运算符重载的基础知识,必须指出的是这只是初步的,由于所学知识有限,重载中不少问题还不能解决。如例5.7中:
c=c+d;语句,改为
c=d+c;因为d不是Complex的对象,C++编译器将无法找到合适的重载的“+”运算符对应的函数,最终给出出错信息。
第六十四页,共一百一十页,2022年,8月28日5.6 友元在C++中友元(friend)函数允许在类外访问该类中的任何成员,就象成员函数一样。友元函数用关键字friend说明。下面用友元函数重载运算符“+”,以实现c=d+c。下面用友元函数重载运算符“+”,实现实数与复数的加法。第六十五页,共一百一十页,2022年,8月28日5.6 友元classComplex{……
friendComplexoperator+(double,Complex);
};
//opration+说明为类Complex类的有元函数,
//friend只用于类说明中
……Complexoperator+(doubled,Complexc){
//注意友元不是成员函数,也不加friendreturnComplex(d+c.Real,c.Image);}
//友元函数可以直接访问私有成员voidmain(void){
……c=d+c1;c.print();}这里d+c被C++编译器解释为operator+(d,c),重载了友元函数。由此可知友元函数在特定场合可能是必不可少的。第六十六页,共一百一十页,2022年,8月28日5.6 友元友元函数重载运算符+,有三种形式。另两个的声明为:friendComplexoperator+(Complex,Complex);friendComplexoperator+(Complex,double);则无论是复数与复数相加,还是实数与复数相加(不论实数在前还是在后)都可以用该运算符三个重载函数之一。再进一步,如果使用友元函数friendcomplexoperator+(complexc1,complexc2);无论是复数与复数相加,还是实数与复数相加(不论实数在前还是在后)都可以用该运算符重载函数。因为有例5.7所定义的缺省的构造函数,实数会被强制转换为虚部为零的复数。d+c1被解释为operator+(complex(d),c1)。注意这里的两个参数是传值,在函数内是建立了两个复数对象,而把实参的值传进去,进行运算。参见图5.9。第六十七页,共一百一十页,2022年,8月28日5.6 友元在这里友元函数可以有两个参数,而对应的成员函数只有一个参数,所以友元函数的使用可以更灵活、更方便。使用引用类型变量作为运算符重载函数的参数,以提高复数类型运算的效率和可行性。Operator+友元函数的声明可改进为:friendComplexoperator+(constComplex&c1,constComplex&c2)这里采用Complex对象的引用而不是对象本身,调用时不再重新分配内存建立一个复制的对象,函数效率会更高。加const,实参只读,可防止实参被修改。
图5.9友元函数operator+执行过程内存分配第六十八页,共一百一十页,2022年,8月28日5.6 友元用VC++演示【例5.7_1】用友元函数重载运算符,实现复数的运算。与【例5.7】比较。单目运算符前“++”的成员函数重载方式如下:ComplexComplex::operator++(){return(++Real,++Image);}采用成员函数方式重载与使用都很方便。但采用友元方式则必须使用引用,因为被施加“++”运算的是一个参数。友元函数重载后置“++”如下:friendComplexoperator++(Complex&c,int)//注意友元方式与前者的区别{return(c.Real++,c.Image++);}采用引用类型,后“++”是直接施加于实参。否则施加于拷贝,而实参不变。第六十九页,共一百一十页,2022年,8月28日友元函数注意点:
1.
友元函数不是类的成员函数,在函数体中访问对象的成员,必须用对象名加运算符“.”加对象成员名。这一点和一般函数一样。但友元函数可以访问类中的所有成员(公有的、私有的、保护的),一般函数只能访问类中的共有成员。
2.
友元函数不受类中的访问权限关键字限制,可以把它放在类的公有、私有、保护部分,但结果一样。
3.
某类的友元函数的作用域并非该类作用域。如果该友元函数是另一类的成员函数,则其作用域为另一类的作用域,否则与一般函数相同。友元函数破坏了面向对象程序设计类的封装性,所以友元函数如不是必须使用,则尽可能少用。或者用其他手段保证封装性。友元还有友元类概念:整个类可以是另一个类的友元。友元类的每个成员函数都是另一个类的友元函数,都可访问另一个类中的保护或私有数据成员。定义方法如下:classA{……friendclassB;//声明B为A的友元类
……};第七十页,共一百一十页,20
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 冲剂保健食品批发企业ESG实践与创新战略研究报告
- 果味茶饮料企业ESG实践与创新战略研究报告
- 钢化膜企业数字化转型与智慧升级战略研究报告
- 模块化组合吊灯行业跨境出海战略研究报告
- 财务主管年终总结报告
- 2025年度知识产权转让欠款付款协商协议书
- 二零二五年度建筑施工现场消防安全协议
- 二零二五年度绿色建材认证工程装修材料供应协议
- 二零二五年度能源管理实习劳动合同
- 2025年度智能家居家居家装合同模板
- 2024年南京机电职业技术学院单招职业技能测试题库标准卷
- 县级林长制培训
- 少儿财商教育讲座课件
- 陕西省西安市铁一中2025届高三下学期联合考试英语试题含解析
- 医院医用耗材SPD服务项目投标方案
- 2025东方电气风电限公司招聘63人管理单位笔试遴选500模拟题附带答案详解
- 2025年保密知识试题库附参考答案(精练)
- 2025年事业单位考试(综合管理类A类)综合应用能力试题及解答参考
- 南昌起义模板
- “互联网+”大学生创新创业大赛计划书一等奖
- 敬礼课件教学课件
评论
0/150
提交评论