版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第5章对象和类5.1类及其实例化5.2类和对象的性质5.3结构和联合5.4构造函数5.5析构函数5.6综合例题5.7重载对象的赋值运算符5.8对象成员的初始化5.9类模板与标准模板库5.10面向对象编程的文件规范实验习题本章重点介绍在C++中定义类、建立和使用对象的方法。虽然同类对象在其数据成员的取值方面是不相同的,但可以共用相同的代码。类是对同类对象的描述,它不但描述了对象之间的公有接口,同时也给出了对象的内部实现(数据成员和成员函数)。像构造枚举和结构一样,类也是一种用户自己构造的数据类型并遵循C++的规定。例如,类也要先声明后使用;不管声明的内容是否相同,声明同一个名字的两个类是错误的,类是具有惟一标识符的实体;在类中声明的任何成员不能使用extern、auto和register关键字进行修饰;类中声明的变量属于该类,在某些情况下,变量可以被该类的不同实例所共享。5.1类及其实例化
5.1.1定义类类和其他数据类型不同的是,组成这种类型的不仅可以有数据,而且可以有对数据进行操作的函数,它们分别叫做类的数据成员和类的成员函数,而且不能在类声明中对数据成员使用表达式进行初始化。1.声明类类是对一组性质相同对象的程序描述。在C++中声明类的一般形式为
class类名{private:私有数据和函数public:公有数据和函数protected:保护数据和函数};类声明以关键字class开始,其后跟类名。类所声明的内容用花括号括起来,右花括号后的分号作为类声明语句的结束标志。这一对花括号“{}”之间的内容称为类体。类中定义的数据和函数称为这个类的成员(数据成员和成员函数)。类成员均具有一个属性,叫做访问权限,通过它前面的关键字来定义。顾名思义,关键字private、public和protected以后的成员的访问权限分别是私有、公有和保护的,把这些成员分别叫做私有成员、公有成员和保护成员。访问权限用于控制对象的某个成员在程序中的可访问性,如果没有使用关键字,则所有成员默认声明为private权限。这些关键字的使用顺序和次数也都是任意的。【例5.1】描述点的Point类。classPoint{//类名Point private://声明为私有访问权限 intx,y;//私有数据成员 public://声明为公有访问权限 voidSetxy(inta,intb);//无返回值的公有成员函数 voidMove(inta,intb);//无返回值的公有成员函数 voidDisplay();//无返回值的公有成员函数 intGetx();//返回值为int的公有成员函数 intGety();//返回值为int的公有成员函数};//类声明以分号结束x和y是私有成员,Setxy、Display、Move、Getx和Gety是公有成员。因为只是声明函数,所以可只给出函数原型。【例5.2】是其等效的声明方式。【例5.2】使用默认关键字及改变关键字顺序和次数的Point类。classPoint{//类名Point intx;//默认私有数据成员public://声明为公有访问权限 //无返回值的公有成员函数Setxy的函数原型 voidSetxy(int,int);//无返回值的公有成员函数Move的函数原型 voidMove(int,int); voidDisplay();//无返回值的公有成员函数的函数原型 intGetx();//返回值为int的公有成员函数的函数原型 intGety();//返回值为int的公有成员函数的函数原型private://声明为私有访问权限 inty;//私有数据成员};//类定义以分号结束由此可见,成员函数声明的规则与第4章所述的函数声明规则相同。2.定义成员函数类中说明的成员函数用来对数据成员进行操作。例如,Point类的Setxy函数用来为该类的对象设置初始值,而当调用成员函数Getx时,则返回一个对象的数据成员x的值。在类中只对这些成员函数进行了函数声明,还必须在程序中实现这些成员函数。定义成员函数的一般形式为
返回类型类名∷成员函数名(参数列表){成员函数的函数体//内部实现}其中“∷”是作用域运算符,“类名”是成员函数所属类的名字,“∷”用于表明其后的成员函数是属于这个特定的类。换言之,“类名∷成员函数名”的意思就是对属于“类名”的成员函数进行定义,而“返回类型”则是这个成员函数返回值的类型。余下的工作就是定义成员函数的函数体。例如Setxy是类Point的成员函数,它没有返回值,则定义如下:
void
Point∷
Setxy(inta,intb){x=a;y=b;}将“voidPoint∷Setxy(inta,intb)”理解为定义Point的函数成员Setxy(inta,intb),该成员带有两个整型参数,函数没有返回值(void)。按此方法,可写出其他几个成员函数的定义:
voidPoint∷Move(inta,intb){x=x+a;y=y+b;}voidPoint∷Display(){cout<<x<<″,″<<y<<endl;}intPoint∷Getx(){returnx;}intPoint∷Gety(){returny;}也可以使用关键字inline将成员函数定义为内联函数,例如:
inlineintPoint∷Getx(){returnx;}如果在声明类的同时,在类体内给出成员函数的定义,则默认为内联函数。例如在类中将声明Getx的语句“intGetx();”改为“intGetx(){returnx;}”,则Getx为内联函数。一般直接在类体内给出简单成员函数的定义。有些成员函数的实现方式不止一种,例如voidPoint∷Display(){cout<<Getx()<<″,″<<Gety()<<endl;}是调用成员函数Getx()和Gety()实现的,它们使用了cout流,应在定义之前包含如下语句:
#include<iostream>usingnamespacestd;3.数据成员的赋值不能在类体内给数据成员赋值,即下面的方法是错误的:ClassPoint{ intx=25,y=56; ……};当然,在类体外面就更不允许了。数据成员的具体值是用来描述对象的属性的。只有产生了一个具体的对象,这些数据值才有意义,所以又称对象的初值或对象初始化。这跟简单数据类型的道理一样,int是整数类型,但需要声明整数对象之后才有意义,“intx=5;”使得整数对象x的值为5,只是类具有成员函数而已。假设已经有了一个对象A,则可使用“.”运算符调用成员函数Setxy赋初值。例如:A.Setxy(25,56);将对象A的数据成员x和y分别赋给25和56,即A.x=25,A.y=56。其实,真正的初始化是使用与Point同名的构造函数Point(int,int)实现的。在1.10.1节介绍使用string的对象str时,使用“stringstr(″realis″);”语句。按此推理,可以写出产生Point的对象的语句:PointA(25,56);这就是使用构造函数产生类的实例。不过,现在还没有定义构造函数Point(int,int),所以还不能使用这种方法。但可以看到数据封装的迹象了!Point类是用户定义的一种类型,Point类所说明的数据成员描述了对象的内部数据结构,对数据成员的访问通过类的成员函数来进行。使用Point在程序中声明变量,具有Point类的类型的变量被称为Point的对象。只有产生类的对象,才能使用这些数据和成员函数。类Point不仅可以声明对象,还可以声明对象的引用和对象的指针,语法与基本数据类型一样。PointA,B;//定义Point类型的对象A和BPoint*p=&A;//定义指向对象A的Point类型的指针Point&R=B;//定义R为Point类型对象B的引用5.1.2使用类的对象对象和引用都使用运算符“.”访问对象的成员,指针则使用“->”运算符。例如:A.Setxy(25,88);//为对象A设置初值R.Display();//显示对象B的数据成员//B.x和B.y之值p->Display();//显示指针p所指对象A的数据//成员A.x和A.y之值【例5.3】根据上面对Point类的定义,演示使用Point类的对象。voidprint(Pointa)//使用Point的对象a作为函数参数{a.Display();}//显示对象a的数据成员的值voidmain(){ PointA,B;//声明对象 A.Setxy(25,55);//为对象A赋初值 B=A;//B的数据成员取A的数据成员之值 A.Display();//显示A的数据成员 A.Move(-10,20);//移动A print(A);//等价于A.Display(); print(B);//等价于B.Display()cout<<A.Getx()<<endl;//只能使用A.Getx(),不能使用A.x}本例中的print函数使用Point的对象作为参数。C++推荐使用下面的引用的形式:voidprint(Point&a)//使用对象的引用作为函数参数{a.Display();}//显示引用对象a的数据成员之值对象A移动之后,对象B仍为原来的值,所以输出如下:
25,55//原来的A和B15,75//新的A25,55//原来的B15//A.Getx()返回对象A的数据成员x的值如果在print函数或主函数里使用如下语句,则产生错误。cout<<A.x<<″,″<<A.y<<endl;//错误!暂不涉及还没有介绍的保护成员,可以归纳出如下规律:①类的成员函数可以直接使用自己类的私有成员(数据成员和成员函数);②类外面的函数不能直接访问类的私有成员(数据成员和成员函数);③类外面的函数只能通过类的对象使用该类的公有成员函数,例如print和main函数;④对象A和B的成员函数的代码一样,不同对象的区别是属性的取值。在程序运行时,通过为对象分配内存来创建对象。在创建对象时,使用类作为样板,故称对象为类的实例。从表5.1中可以看出,A和B两个对象占据内存中的不同区域。A的数据是A.x和A.y,而B的数据是B.x和B.y,它们各有表现自己的属性数据,但用来操作数据的代码均是一样的,例如A.Getx()和B.Getx()的代码一样。为节省内存,在建立对象时,只分配用于保存数据的内存,代码为每个对象共享。类中定义的代码放在计算机内存的一个公用区中,供该类的所有对象共享。这只是C++编译器实现对象的一种方法,作为程序员仍要将对象理解为是由数据和代码组成的。正是由于类拥有这两类成员,才使得数据封装等思想得以实现。表5.1类Point的两个实例A和B对象A对象B对象成员对象成员分配内存内容对象成员对象成员分配内存内容A.xA.x的值B.xB.x的值A.yA.y的值B.yB.y的值A.Setxy(int,int)Setxy函数代码B.Setxy(int,int);Setxy函数代码A.Move(int,int)Move函数代码B.Move(int,int);Move函数代码A.Display()Display函数代码B.Display()Display函数代码A.Getx()Getx函数代码B.Getx()Getx函数代码A.Gety()Gety函数代码B.Gety()Gety函数代码【例5.4】演示使用内联函数定义Point类及使用Point类指针和引用的完整例子。#include<iostream>//包含头文件usingnamespacestd;//声明命名空间classPoint{//使用内联函数定义类Pointprivate://声明为私有访问权限 intx,y;//私有数据成员public://声明为公有访问权限voidSetxy(inta,intb)//无返回值的内联公有成员函数{x=a;y=b;}voidMove(inta,intb)//无返回值的内联公有成员函数{x=x+a;y=y+b;}voidDisplay()//无返回值的内联公有成员函数{cout<<x<<″,″<<y<<endl;}
intGetx(){returnx;}//返回值为int的内联公有成员函数intGety(){returny;}//返回值为int的内联公有成员函数};//类定义以分号结束voidprint(Point*a)//类指针作为print函数的参数定义重载函数{a->Display();}voidprint(Point&a)//类引用作为print函数的参数定义重载函数{a.Display();}voidmain()//主函数{PointA,B,*p;//声明对象和指针Point&RA=A;//声明对象RA引用对象AA.Setxy(25,55);//使用成员函数为对象A赋值B=A;//使用赋值运算符为对象B赋值p=&B;//类Point的指针指向对象Bp->Setxy(112,115);//使用指针调用函数Setxy重新设置B的值print(p);//传递指针显示对象B的属性p->Display();//再次显示对象B的属性RA.Move(-80,23);//引用对象RA调用Move函数print(A);//验证RA和A同步变化print(&A);//直接传递A的地址作为指针参数}程序运行结果如下:112,115112,115-55,78-55,78面向对象的程序设计,是通过为数据和代码建立分块的内存区域,以便提供对程序进行模块化的一种程序设计方法,这些模块可以被用做样板,在需要时再建立其副本。根据这个定义,对象是计算机内存中的一块区域,通过将内存分块,每个模块(即对象)在功能上保持相对独立。另外,定义也表明如下问题。5.1.3数据封装①这些内存块中不但存储数据,而且也存储代码,这对保证对象受保护很重要。只有对象中的代码才可以访问存储于这个对象中的数据,这清楚地限定了对象所具有的功能(即一个对象在一个软件系统中所能起到的作用),并使对象保护它自己不受未知的外部事件的影响,从而使自己的数据和功能不会因此遭到破坏。②这些内存块的结构可被用做样板产生对象的更多副本。例如,一旦定义了一个窗口对象,只要内存允许,就可以建立许多这样的对象。在面向对象的程序中,对象的行为只有向对象发送消息才能引用,例如通过Display发出显示消息,所以说面向对象是消息处理机制。对象之间只能通过成员函数调用实现相互通信。这样,对象之间的相互作用方式是仔细控制的,一个对象外部的代码就没有机会通过直接修改对象的内存区域妨碍对象发挥其功能。当对象的一个函数被调用时,对象执行其内部的代码来响应这个调用,这就使对象呈现出一定的行为,即表现出该对象的功能。对象被视为能做出动作的实体,动作在对象相互作用时被激励,换句话说,对象就像在宿主计算机上拥有数据和代码,并能相互通信的具有特定功能的一台较小的计算机。对象的这一特点导致了模拟现实世界的一种新型方法:面向对象就是将世界看成是由一组彼此相关并能相互通信的实体即对象组成的。程序中的对象映射现实世界中的对象。C++对其对象的数据成员和成员函数的访问是通过访问控制权限来限制的。可以试验一下,将【例5.1】中对数据成员x和y的声明改为public,就可以在主函数中直接使用语句“cout<<A.x;”输出A的数据成员x之值。这就取消了它的封装性。由此可见,private是限制类之外的函数的访问权。只要将数据成员或成员函数使用private限定,就设定了一道防线。不过,如果都在防线之内,这个类也就没有用处了。所以它还必须留下与外面打交道的接口,这通过具有public权限的成员函数实现(目前暂不涉及protected)。由此可见,在C++中,数据封装是通过类来实现的。在类中指定了各成员的访问权限。一般情况下将数据成员说明为私有的,以便隐藏数据;而将部分成员函数说明为公有的,用于提供外界和这个类的对象相互作用的接口(界面),从而使得其他函数(例如main函数)也可以访问和处理该类的对象。对于那些仅是为支持公有函数的实现而不作为对象界面的成员函数,也将它们说明为私有的。公有的成员函数是外界所能观察到(访问到)的对象界面,它们所表达的功能构成对象的功能,使同一个对象的功能能够在不同的软件系统中保持不变。这样,当数据结构发生变化时,只需要修改少量的代码(类的成员函数的实现代码)就可以保证对象的功能不变。只要对象的功能保持不变,则公有的成员函数所形成的接口就不会发生变化。这样,对象内部实现所做的修改就不会影响使用该对象的软件系统。这就是面向对象程序设计使用数据封装为程序员的程序开发活动所带来的益处。在传统的C程序设计风格中,数据保存在数据结构中,然后生成函数来操作这些数据。最后将此结构和函数放进源文件,单独编译,作为模块。这个方法的缺点是,即使该结构和函数是放在一起使用的,仍然可以不通过使用函数就能直接存取数据。C++使用封装的方法较好地解决了这个问题。可以像第4章那样,对成员函数重载或使用默认参数。下面的例子演示了使用私有成员函数封装函数,并使用成员函数重载和默认参数。【例5.5】构造一个求4个正整数中最大者的类Max,并用主程序验证它的功能。classMax{//声明类 private://封装数据成员和成员函数 inta,b,c,d;//数据成员 intMaxi(int,int);//只允许类内部的成员函数调用
5.1.4成员函数重载及默认参数public://对外界的接口 voidSet(int,int,int,int);//设置对象初值 intMaxi();//求最大值}A[3];//声明类的对象数组,定义结束//类中成员函数的实现intMax∷Maxi(intx,inty)//求两个数的最大值{return(x>y)?x:y;}voidMax∷Set(intx1,intx2,intx3=0,intx4=0)//使用两个默认参数{a=x1;b=x2;c=x3;d=x4;}intMax∷Maxi()//求自己类中4个数的最大值{intx=Maxi(a,b);//x和y为Maxi()函数的局部整数对象 inty=Maxi(c,d);returnMaxi(x,y);}//主程序#include<iostream>usingnamespacestd;voidmain(){ A[0].Set(12,45,76,89);//为数组对象A[0]置初值 A[1].Set(12,45,76);//为数组对象A[1]置初值 A[2].Set(12,45);//为数组对象A[2]置初值 for(inti=0;i<3;i++)//输出对象求值结果 cout<<A[i].Maxi()<<″″;}程序演示了可在声明类的同时也声明类的对象,这里是声明对象数组A,作用与在主程序里使用“MaxA[3];”语句相同。为了提高可读性,一般不在声明类时声明对象,这里只是为了演示它的性质。程序输出结果为:897645类中重载了函数Maxi,一个原型为Maxi(int,int),用来求两数中的大者。因为它只被自己的成员函数使用,所以定义为private。另一个原型为Maxi(),它调用两次Maxi(int,int),然后再用这两次的结果作为Maxi(int,int)的参数,求出4个数中的最大值。赋值成员函数Set使用2个默认参数是为了书写方便。在定义Point类的两个对象A和B之后,当执行语句“A.Setxy(25,55);”时,A.x和A.y就被赋值。但是,函数Setxy(int,int)作为代码,在计算机里是和具体的对象分开存储的,它是怎样知道,是要对A进行操作而不是对B进行操作呢?当执行A.Setxy(25,55)时,成员函数Setxy(int,int)有一个隐藏参数,名为this指针。也就是说,源程序被编译器编译后,Setxy(inta,intb)实际上是如下形式:5.1.5this指针voidPoint∷Setxy(inta,intb,(Point*)this){this->x=a;this->y=b;}这时,成员函数的this指针指向对象A。成员中对x和y的引用表示是引用对象A的成员x和y。对于任何访问该成员函数的类的对象,C++编译器都认为是访问this指针所指向的对象中的成员。由于不同的对象调用成员函数Setxy(int,int)时,this指针指向不同的对象,因此成员函数Setxy(int,int)可以为不同对象的x和y置初值。使用this指针,保证了每个对象可以拥有自己的数据成员,但处理这些数据成员的代码可以被所有的对象共享。由此可见,C++规定,当一个成员函数被调用时,系统自动向它传递一个隐含的参数,该参数是一个指向接受该函数调用的对象的指针,从而使成员函数知道该对哪个对象进行操作。在程序中,可以使用关键字this来引用该指针。this指针是C++实现封装的一种机制,它将对象和该对象调用的成员函数连接在一起,在外部来,每一个对象都拥有自己的成员函数。如果在定义Setxy函数时使用this指针,也不要给出隐含参数,而写成如下形式:voidPoint∷Setxy(inta,intb){this->x=a;this->y=b;}除非特殊需要,一般情况下都省略符号“this->”,而让系统进行默认设置。因为类本身就是一种新的数据类型,所以一个类可以作为另一个类的成员。假设有A和B两个类,可以通过在B类里定义A的对象作为B的数据成员,或者定义一个返回类型为A的函数作为B的成员函数。假设定义了坐标点的类Point,矩形类Rectangle的属性需要一个坐标点及长和宽。一个Point的对象恰好可以作为矩形的顶点坐标,即Rectangle可以使用Point的一个对象作为数据成员。在Rectangle类中,用Point类定义一个返回Point类型指针的函数作为Rectangle的成员函数。5.1.6一个类的对象作为另一个类的成员【例5.6】使用对象成员的例子。#include<iostream>usingnamespacestd;classPoint{//定义点类 intx,y;public: voidSet(inta,intb){x=a;y=b;} intGetx(){returnx;} intGety(){returny;}};classRectangle{//在矩形类里使用Point类的成员PointLoc;//定义一个Point类的对象作为顶点intH,W;//H为高,W为宽public:voidSet(intx,inty,inth,intw);Point*GetLoc();//用Point类定义返回指针的函数intGetHeight(){returnH;}intGetWidth(){returnW;}};voidRectangle∷Set(intx,inty,inth,intw){Loc.Set(x,y);//初始化坐标顶点H=h;W=w;}Point*Rectangle∷GetLoc()//返回类型Point*,作//为Rectangle的成员函数{return&Loc;}//返回对象Loc的地址voidmain(){ Rectanglerect; rect.Set(10,2,25,20); cout<<rect.GetHeight()<<″,″<<rect.GetWidth()<<″,″; Point*p=rect.GetLoc();//定义Point类的指针//对象p并初始化 cout<<p->Getx()<<″,″<<p->Gety()<<endl;}新类Rectangle具有一个顶点,所以也具有Point类的属性。这就是第1章提到的类的结构关系(聚合关系,有时又称包含)。构成的新类不能直接操作另一个类的数据,必须通过原构成类的对象使用它们的成员函数来实现,本例使用Loc.Set(x,y)方式实现。程序输出结果为:25,20,10,2结合前面的例子,可以归纳对象的一些基本特性如下:①对象之间可以相互赋值。例如,如下语句使A和B的数据成员有相同的值:PointA,B;A.Setxy(25,55);B=A;②可使用对象数组。例如,“MaxA[3];”定义数组A可以存储3个Point类的对象。5.2类和对象的性质
5.2.1类对象的性质③可使用指向对象的指针,使用取地址运算符&将一个对象的地址置于该指针中,如:Point*p=&A;p->Display();注意:不能取私有数据成员的地址,也不能取成员函数的地址。有关指向成员的指针在后面的章节中讨论。指向对象指针的算术运算规则与第4章介绍的一样,不再赘述。④对象可以用做函数参数,这时参数传递采用传值方式,即在被调用函数中对形参所作的改变不影响调用函数中作为实参的对象。如果要传址,可以采用对象的引用或指针作为参数。但是,如果参数对象被修改,相应的实参对象也将被修改。C++推荐使用引用作为参数传递。为了避免被调用函数修改原来对象的数据成员,可以使用第4章介绍的const修饰符。⑤对象作为函数参数时,可以使用对象值、对象引用和对象地址(指针)。以介绍过的print函数为例,它们的参数传递形式为:voidprint(Pointa){a.Display;}voidprint(Point&a){a.Display;}voidprint(Point*p){p->Display;}它们的原型分别为print(Point),print(Point&)和print(Point*)。对于对象A而言,print(&A)调用的是原型为print(Point*)的函数形式。注意:参数为对象和引用时,编译系统无法区别,重载print只能选择其中的一种。⑥一个对象可以用做另一个对象的成员。例如在【例5.6】中,定义Point类的对象Loc。1.使用类的权限假设声明了Point类,为了简单具体,讨论数据成员为私有,成员函数为公有的情况。这时会有函数(主函数和一般函数)、对象和类本身的成员函数需要使用Point类的成员。①类本身的成员函数可以使用类的所有成员(私有和公有成员)。②类的对象只能访问公有成员函数,例如输出x只能使用A.Getx(),不能使用A.x。5.2.2类的性质③其他函数不能使用类的私有成员,也不能使用公有成员函数,它们只能通过类的对象使用公有成员函数。④一个类可以使用另外一个类的对象,这个类也只能通过那个类的对象使用该类的成员函数,通过成员函数使用数据成员,例如Loc.Set(x,y)。2.不完全的类声明类不是内存中的物理实体,只有当使用类产生对象时,才进行内存分配,这种对象建立的过程称为实例化。实例化一词在面向对象程序设计范围内使用广泛,用以表示产生类的实例或物理实体的动作。在某些语言中,对象就被称作实例。虽然在C++中并不是这样,但实例化仍然被广泛使用。应当注意的是:类必须在其成员被使用之前进行声明。有时也需要将类作为一个整体来使用,而不存取其成员。声明指针就是这种情况,例如:classMembersOnly;//不完全的类声明MembersOnly*club;//定义一个全局变量类指针voidmain(){...//函数体}//主函数classMembersOnly{...//类体};//完全定义该类第一条语句称为不完全类声明,它用于在类没有完全定义之前就引用该类的情况,例如引用另一文件中定义的类。由于类标识符MembersOnly通过不完全类声明进入了作用域,所以就可以声明全局变量指针club。编译器执行到该指针的声明处时,只了解指针所指类型是一个叫MembersOnly的类,而不了解其他任何情况。不完全声明的类不能实例化,企图实例化会产生编译出错信息;不完全声明仅用于类和结构,企图存取没有完全声明的类成员,也会引起编译出错信息。另外,将数据成员的声明放在最后,有利于先理解类的界面。如果类体内有内联函数,这就像是在声明数据成员之前已经用到它们,确实是“使用在前,说明在后”。这在类中是允许的,因为C++编译器先扫描类的成员说明,最后才处理内联成员函数的代码体。3.空类尽管类的目的是封装代码和数据,它也可以不包括任何声明。例如:
classEmpty{};这种类没有任何行为,但可以产生空类对象。voidmain(){Emptyobject;
}为什么要产生空类呢?在开发大的项目时,需要在一些类还没有完全定义或实现时进行先期测试。这常称为“插头”,用来保证代码能正确地被编译,从而允许测试其中的一部分。不过这是早期的做法,对强类型检查的编译器,会给出一个警告。可给空类增加一个无参数构造函数(见构造函数一节)以消除警告。例如:classEmpty{public:Empty(){}};4.类作用域声明类时所使用的一对花括号形成所谓的类作用域。在类作用域中声明的标识符只在类中可见。例如:classexample{ intnum;};inti=num;//错,num在此不可见intnum;//正确,num与类中说明的数据//成员num具有不同的作用域即使该成员函数的实现是在类定义之外给出的,类作用域也包含了类中成员函数的作用域。因此,当成员函数内使用一个标识符时,编译器先在包含类作用域中寻找,例如下面的例子:classMyClass{ intnumber;public: voidset(int);};intnumber;//这个number不属于类MyClassvoidMyClass∷set(inti){number=i;//使用类MyClass中的//标识符number}类中的一个成员名可以使用类名和作用域运算符来显式指定,这称为成员名限定。例如:voidMyClass∷set(inti){MyClass∷number=i;//显式指定访问MyClass//类中的标识符number}在程序中,分析对象的生存期与过去分析变量的生存期的方法一样,由对象说明来决定。类中各数据成员的生存期由对象的生存期决定,当对象存在时,它们存在;对象消失时,它们也消失。成员函数具有外部连接属性。在程序中使用成员选择运算符(“.”或“->”)访问一个对象的成员时,其后的名字是引用该对象所在类中声明的成员名。例如:PointA;//声明类对象,假设类内有成员函数Getx()intGetx(){return5;};//类外面声明的函数intx=Getx();//访问函数Getx(),返回5inty=A.Getx();//访问类Point中声明的//成员函数Getx()在类中说明的枚举成员也使用成员选择运算符存取,枚举成员名隐藏在类作用域中。这些枚举成员不属于任何对象,为该类所有的对象共享。因此,对这些枚举成员使用成员名限定方法进行存取比较恰当。但是,在一个友元函数访问类中的枚举成员时,有时必须使用成员选择运算符。友元函数在后面的章节讨论。除非编译器在处理类声明时遇到了标识其结束的右花括号,否则这个声明仍然是引用性声明。引用性声明所声明的类名不能用来建立对象,只能用来声明指针或引用,或用在函数声明中。例如:classMyClass{ MyClassmember;//错 MyClass*p;//正确};classYourClass{private: MyClassd;//正确};在上例中,当在类MyClass中声明成员member时,类名MyClass仅作了引用性声明,因而这个语句是错误的。由此可见,C++为类中声明的标识符引入了新的类作用域概念。这些标识符只有与类对象连用时才进入作用域。在可能出现二义性的情况下,必须使用作用域限定符“∷”。作用域限定符“∷”的特点将在后继章节中讨论。在C++中,封装也可以由struct和union等关键字提供,它们也能将数据和函数组合成一个类,所以也可以使用结构和联合来定义类。结构是类的一种特例,其中成员在默认情况下是公有的。尽管C++允许在结构中定义成员函数,但它本身更适合处理数据,例如像职工记录和日期这样的数据结构。联合以关键字union来声明,成员默认为公有并且在某个给定时间,只出现一个成员。联合使若干数据成员使用同一地址。可以直接在类中使用无名联合,例如:5.3结构与联合ClassCU{union{ intivalue; floatfvalue;} ……//}在关键字union后面没有给出联合名,这是一个无名联合声明,它声明ivalue和fvalue共享同一块内存,无名联合中声明的数据项名字可以被直接存取。无名联合不仅可以用于声明类中的数据成员,而且可用于程序中需要共享数据项的其他地方,例如在函数作用域中,或在文件作用域中。无名联合不能有成员函数,因为无名联合中成员的作用域在联合之外。目前介绍的类都是与其他无关的单一的类。如果能通过这些已有的类来建立新类,则新建立的类叫做“派生类”,而原来的类称为“基类”。这些将在第6章详细介绍,目前只是强调联合既不能用做任何类的基类,也不能从任何类中派生出联合,因为联合在特定的时刻只有一个数据成员处于“激活”状态。此外,在联合中也不能说明虚函数。建立一个对象时,对象的状态(数据成员的取值)是不确定的。为了使对象的状态确定,必须对其进行正确的初始化。C++有称为构造函数的特殊成员函数,它可自动进行对象的初始化。初始化和赋值是不同的操作,当C++语言预先定义的初始化和赋值操作不能满足程序的要求时,程序员可以定义自己的初始化和赋值操作。5.4构造函数由于初始化的重要性,调用构造函数并不是由用户来完成的;是否调用这个函数也不是可选的,而是由编译器来调用,所以编译器必须总能知道调用哪个函数。最容易也最符合逻辑的方法是指定这函数的名称与类名一样。这个函数没有返回值则是基于下面的考虑:如果它有返回值,编译器就必须知道如何处理返回值,这样就会大大增加编译器的工作,也降低了效率。但如果采取只能由用户自己来显式调用构造函数方式的话,就又容易破坏安全性。5.4.1定义构造函数因此,让构造函数的名字和类名同名,在定义构造函数时不能指定返回类型,即使是void类型也不可以。【例5.7】的程序说明构造函数的定义和执行过程。【例5.7】构造函数的定义和执行过程实例程序。classTest{private: intnum;public: Test();//声明不带参数的构造函数 Test(int);//声明带1个参数的构造函数};#include<iostream>usingnamespacestd;Test∷Test():num(0)//定义不带参数的构造函数{cout<<″Initializingdefault″<<endl;}Test∷Test(intn):num(n)//定义带1个参数的构造函数{cout<<″Initializing″<<″″<<n<<endl;}voidmain(){Testx;//使用不带参数的构造函数产生对象xTesty(15);//使用带参数的构造函数产生对象yTestarray[2]={5,7};//使用带参数的构造函数//产生对象数组array}程序中和Test类同名的两个成员函数是构造函数,一个不带参数,一个带有1个参数。“:num(n)”完成“num=n”的功能,这里称初始化列表。它与下面的方法等价:Test∷Test(intn){cout<<″Initializing″<<″″<<n<<endl;num=n;}由此可见,类可以有多个构造函数,在类体里的声明形式如下:类名(形参1,形参2,…形参n);//可以没有形参类的构造函数可以在类体内(内联函数)声明时定义,也可以在类体外定义。可以使用初始化列表或者在构造函数的函数体内定义。假设数据成员为x1,x2,…,xn,则有如下两种形式:
类名∷类名(形参1,形参2,…,形参n):x1(形参1),x2(形参2),…,xn(形参n){}
类名∷类名(形参1,形参2,…,形参n){x1=形参1;x2=形参2;……xn=形参n;}构造函数的参数是无顺序排列,只要保证相互的对应顺序即可。可以使用默认参数或重载。在程序中说明一个对象时,程序自动调用构造函数来初始化该对象。当执行到语句
Testx;时,程序为对象x分配内存,然后调用不带参数的构造函数来初始化这段内存,将x的数据成员num初始化为零,输出“Initailizingdefault”。当程序执行到语句Testy(15);时,程序为对象y分配内存,然后调用带有参数的构造函数来初始化这段内存,将y的数据成员num初始化为15,输出“Initializing15”。当程序执行到语句Testarray[2]={5,7}时,程序为对象数组array[2]分配内存,然后调用带参数的构造函数来初始化这段内存。它必须为每一个数组元素调用一次构造函数,先将array[0]的数据成员num初始化为5,输出“Initializing5”;再将array[1]的数据成员num初始化为7,输出“Initializing7”。当声明一个外部对象时,外部对象只是引用在其他地方声明的对象,程序并不为外部对象说明调用构造函数。如果是全局对象或静态对象,在main函数执行之前要调用它们的构造函数。使用下面的主程序演示全局对象的情况://使用前面定义的类Testglobal(5);voidmain(){cout<<″Enteringmainandexitingmain″<<endl;}程序在进入main函数之前先构造全局对象,输出Initializing5进入主函数之后输出为:Enteringmainandexitingmain。运算符new用于建立生存期可控的对象,new返回这个对象的指针。由于类名被视为一个类型名,因此,使用new建立动态对象的语法和建立动态变量的情况类似,其不同点是new和构造函数一同起作用。下面程序使用前面的类定义,说明如何使用new运算符及其工作过程。
5.4.2构造函数和运算符new//使用前面的类定义voidmain(){Test*ptr1=newTest;Test*ptr2=newTest(5);deleteptr1;deleteptr2;}运行这个程序,输出结果是:InitializingdefaultInitializing5当使用new建立一个动态对象时,new首先分配足以保存Test类的一个对象所需要的内存,然后自动调用构造函数来初始化这块内存,再返回这个动态对象的地址。和说明对象时的情况一样,对于表达式newTest;new调用不带参数的构造函数,输出
Initializingdefault而对于表达式newTest(5);new调用了带参数的构造函数,输出
Initializing5使用new建立的动态对象在不用时必须用delete删除,以便释放所占空间。不带参数的构造函数又称做默认构造函数,原因是当没有为一个类定义任何构造函数的情况下,C++编译器总要自动建立一个不带参数的构造函数。例如,如果在上面的例子中没有为Test类定义任何构造函数,则C++编译器要为它产生一个默认构造函数,这个默认构造函数的形式如下:
Test∷Test(){}即它的函数体是空的。因此,当建立Test类的一个对象时,对象的状态是不确定的,即没有被初始化。5.4.3默认构造函数和默认参数一旦程序定义了自己的构造函数,系统就不再提供这个默认构造函数。如果程序员没有再定义一个无参数的构造函数,但又声明了一个没有初始化的对象(如“TestA;”),则因系统已经不再提供默认构造函数,编译就会出错。同理,如果只声明对象数组(即不提供初值),因为每个元素对象均需要调用一次默认构造函数来为自己初始化,所以必须要求有一个默认构造函数。如果程序中定义了有参数的构造函数,千万别忘记为它定义一个无参数的构造函数。需要说明的是“默认”也称为“缺省”,本书统一使用“默认”。解决的另一种方法是将相应的构造函数全部使用默认参数设计。【例5.8】设计构造函数的默认参数。#include<iostream>usingnamespacestd;classTest{private: intx,y;public: Test(int=0,int=0);//声明两个参数均为默认参数};Test∷Test(inta,intb):x(a),y(b)//定义构造函数{cout<<″Initializing″<<x<<″,″<<y<<endl;}voidmain(){ TestA;TestB(15,55);Testarray[2];}声明构造函数原型时不需要给出参数名,即使用“int=0,int=0”。程序输出如下:Initializing0,0Initializing15,55Initializing0,0Initializing0,0在说明全局对象数组和静态对象数组这两种情况下,程序都在main函数之前为这些对象数组的每个元素调用默认构造函数。若没有为一个类定义默认参数的构造函数时,则在说明对象数组时必须提供初始值。第8章将进一步介绍对象数组。引用在类中一个很重要的用途是用于复制构造函数。这是一类特殊而且重要的函数,通常用于使用已有的对象来建立一个新对象。首先来看看有关浅拷贝的知识。所谓浅拷贝就是普通的数值复制。在通常情况下,编译器建立一个默认复制构造函数,这个复制构造函数采用浅拷贝来使用已有的对象来建立新对象。对类X而言,原型为:
X∷X(X&)5.4.4复制构造函数从这个函数原型来看,首先它是一个构造函数,因为这毕竟是在创造一个新对象。其次,它的参数有些特别,是它自己对象的引用,即用现有的对象来创建。使用引用是从程序执行效率的角度考虑的。为了不改变原有对象,更普通的形式是像下面这样使用const限定:X∷X(constX&)这在很多情况下是行得通的,但是也存在意外。最明显的例子是,如果存在指针时,是简单地将两个指针指向同一地址还是指向不同的存储区域?简单地处理往往导致错误的发生,因为简单的赋值可能使两个指针指向了同一块存储区域(见下图)。当指针P1销毁后,另外一个指针p2指向了无效区域,访问这个指针可能会引起严重的错误。为了避免这一点,需要小心地处理有关数值拷贝的情况。例如,开辟一块新的存储区域,再将对象的指针指向它(详见后面的5.7节)。由此可见,浅拷贝策略常常会有麻烦。为了克服这样的缺点,需要自定义复制构造函数。像调用构造函数一样,如果自定义了复制构造函数,编译器只调用程序员为它设计的复制构造函数。对【例5.8】的Test类而言,复制构造函数的原型为:Test(Test&);//复制构造函数它的定义如下:Test∷Test(Test&t){x=t.x;y=t.y;}构造函数Test(Test&)是复制构造函数。注意:在这个函数的实现中,它访问了对象的私有成员,这是允许的。C++中,在一个类中定义的成员函数可以访问该类任何对象的私有成员。这个类由复制构造函数构成的成员函数具有特殊的作用:在使用该类的一个对象初始化该类的另一个对象时,调用这个函数(有时又称为复制初始化构造函数)。例如:Testobj1(56,85);Testobj2(obj1);//调用复制构造函数使用obj1//的数据成员初始化obj2最后这条语句调用构造函数Test∷Test(Test&),使用obj1来初始化obj2。这个构造函数必须使用引用参数。另外,为了安全起见,推荐使用的原型为:
Test(constTest&);
在对象消失时,使用析构函数释放由构造函数分配的内存。构造函数、复制构造函数和析构函数是构造型成员函数的基本成员,应深刻理解它们的作用并熟练掌握设计的方法。5.5析构函数因为调用析构函数也是由编译器来完成的,所以编译器必须总能知道应调用哪个函数。最容易、也最符合逻辑的方法是指定这个函数的名称与类名相同。为了与构造函数区分,再在析构函数的前面加上一个“~”号。在定义析构函数时,不能指定任何返回类型,即使指定void返回类型也不行。析构函数也不能指定参数,但是可以显式地说明参数为void,即形如X∷~X(void)。从函数重载角度分析,一个类也只能定义一个析构函数且不能指明参数。5.5.1定义析构函数仍以Test类为例来说明析构函数的行为。在类的声明中增加析构函数的声明如下:
~Test();然后在函数体内输出一条信息,例如:Test∷~Test(){cout<<″Destructorisactive″<<endl;}使用下面的测试程序说明析构函数的作用:voidmain(){Testx(5,68);cout<<″Exitingmain″<<endl;}执行此程序,其输出是:
Initializing5,68//调用构造函数Exitingmain//主程序输出Destructorisactive//自动调用析构函数析构函数在对象的生存期结束时被自动调用。在上面这个程序中,对象x的生存期在遇到右括号时结束,因此,析构函数的调用发生在上面这个程序的最后一条输出语句之后。当对象的生存期结束时,程序为这个对象调用析构函数,然后回收这个对象占用的内存。全局对象和静态对象的析构函数在程序运行结束之前调用。数组的每个元素调用一次析构函数。全局对象数组和静态对象数组的析构函数在程序结束之前被调用,请读者自行分析。运算符delete与析构函数一起工作。当使用运算符delete删除一个动态对象时,它首先为这个动态对象调用析构函数,然后再释放这个动态对象占用的内存,这和使用new建立动态对象的过程正好相反。下面的程序用于说明建立和释放一个动态对象数组的情况。//使用【例5.8】的Test类voidmain(){ Test*ptr=newTest[2];delete[]ptr;}5.5.2析构函数和运算符delete程序的输出如下:Initializing0,0//调用构造函数Initializing0,0//调用构造函数Destructorisactive//调用析构函数Destructorisactive//调用析构函数表达式newTest[2]首先分配2个Test类的对象所需的内存,然后分别为这2个对象调用一次构造函数。当使用delete释放动态对象数组时,必须告诉这个动态对象数组有几个元素对象,C++使用“[]”来实现。即语句delete[]ptr;//注意不要错为ptr[]使运算符delete知道ptr指向的是动态对象数组,delete将为动态数组的每个对象调用一次析构函数,然后释放ptr所指向的内存。当程序先后创建几个对象时,系统按后建先析构的原则析构对象。当使用delete调用析构函数时,则按delete的顺序析构。注意动态数组和指针数组的区别。voidmain(){Test*ptr[2]={newTest(2,4),newTest(7,8)}; for(inti=0;i<2;i++) deleteptr[i];}必须为每个数组元素调用delete析构相应的对象数组元素所占的空间。输出结果如下:Initializing2,4Initializing7,8DestructorisactiveDestructorisactive数据成员使用指针时,需要为它们申请空间,要注意区分动态对象数组和指针数组的区别,以及析构时的异同。注意分析下面的例子。#include<iostream>usingnamespacestd;classTest{private:char*str;public:Test(char*str="Here!"); ~Test();};Test::Test(char*s)//定义构造函数{ str=newchar[strlen(s)+1]; strcpy(str,s);cout<<"Initializing"<<str<<endl;}Test::~Test(){cout<<"Delete"<<str<<endl;}voidmain(){Test*ptr1=newTest("Wearehere!");Test*ptr2=newTest[2]; Test*ptr3[2]={newTest("We"),newTest("You")};deleteptr1; delete[]ptr2; for(inti=0;i<2;i++) deleteptr3[i];}第一个创建的是一个Test对象的指针ptr1,初始化的值为:Wearehere!析构需要使用一个单独的语句“deleteptr1;”。第二个创建的是一个Test对象的动态数组,这个数组有两个元素,初始化的值在内中顺序排列为:
Here!\0Here!\0指针ptr2指向首地址,需要使用语句delete[]ptr2;来析构这分配给这两个对象的存储空间。第三个创建的是一个Test对象的动态数组,这个数组有两个元素,初始化的值分别为:
对象
ptr3[0]内容为We;ptr3[1]内容为You。所以要使用循环语句逐个析构。输出结果如下:InitializingWearehere!InitializingHere!InitializingHere!InitializingWeInitializingYouDeleteWearehere!DeleteHere!DeleteHere!DeleteWeDeleteYou如果在定义类时没有定义析构函数,C++编译器要为这个类产生一个默认的析构函数。正如没有给类定义构造函数,C++编译系统会产生一个默认构造函数一样,编译器也为它产生一个函数体为空的默认析构函数:
Test∷~Test(){}5.5.3默认析构函数【例5.9】演示调用构造函数及析构函数的综合例题。下面这个程序综合演示了调用构造函数、调用复制构造函数及调用析构函数的顺序,并演示了调用复制构造函数的3种不同情况。另外,本程序还比较了函数使用对象和对象的引用为参数的情况。5.6综合例题#include<iostream>usingnamespacestd;classPoint{private: intX,Y;public: Point(inta=0,intb=0)//构造函数 {X=a;Y=b;cout<<″Initializing″<<endl;} Point(constPoint&p);//复制构造函数 intGetX(){returnX;} intGetY(){returnY;} voidShow(){cout<<″X=″<<X<<″,Y=″<<Y<<endl;} ~Point(){cout<<″delete...″<<X<<″,″<<Y<<endl;}};Point∷Point(constPoint&p)//定义复制构造函数{
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 小学五年级德育与法治教学设计:尊重差异 共享美好包容
- 2026年巴陵石化人员招聘参考题库及详细答案解析
- 2026年浙江省交通投资集团有限公司人员招聘笔试参考题库及详细答案解析
- 2026年中国电信新疆分公司人员招聘笔试参考题库及详细答案解析
- 2026年海南炼化公司人员招聘笔试参考试题及详细答案解析
- 2026年宁夏建设投资集团公司人员招聘考试题库及详细答案解析
- 2026年江铃汽车集团公司人员招聘笔试参考题库及详细答案解析
- 2026年华远国际陆港集团有限公司人员招聘考试备考题库及详细答案解析
- 2026年中国铁路经济规划研究院有限公司人员招聘笔试参考试题及详细答案解析
- 2026年中国铁路工程集团有限公司人员招聘考试参考试题及详细答案解析
- 2025年及未来5年市场数据中国硫氰酸铵市场运行态势及行业发展前景预测报告
- 泌尿外科肿瘤术后康复指南
- 建筑装饰工程竣工验收与移交方案
- 医院反恐防暴安全培训课件
- 医疗护理员考核试题(有答案)
- 2025年法务专业知识试题及答案
- 乳品评鉴师技能竞赛理论考试题库500题(含答案)
- 《预防校园暴力共建和谐校园》主题班会
- CJJT8-2011 城市测量规范
- 02章 电催化过程
- 凝血障碍的健康宣教
评论
0/150
提交评论