C++程序设计自考4737第5章_第1页
C++程序设计自考4737第5章_第2页
C++程序设计自考4737第5章_第3页
C++程序设计自考4737第5章_第4页
C++程序设计自考4737第5章_第5页
已阅读5页,还剩79页未读 继续免费阅读

下载本文档

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

文档简介

第5章特殊函数和成员构造函数和析构函数都是构造型成员函数(特殊成员函数)。本节将介绍一些更特殊的函数,所以涉及面更广,也比较繁琐。静态成员相当于类中的“全局变量”,为该类的所有对象共享。友元不属于类,但允许它存取类对象的私有成员。这保证了程序的效率,并为扩充类的接口提供了一定的灵活性,但也破坏了类的封装性,使用时应慎重。本章也简要介绍转换函数,但不涉及转换函数的继承和虚转换函数。,主要内容,5.1对象成员的初始化5.2静态成员5.3友元函数5.4const对象5.5数组和类5.6指向类成员的指针5.7求解一元二次方程,5.1对象成员的初始化可以在一个类中说明具有某个类的类型的数据成员,这些成员称为对象成员。在类A中说明对象成员的一般形式为:classA类名1成员名1;类名2成员名2;.类名n成员名n;说明对象成员是在类名之后给出对象成员的名字。为初始化对象成员,A类的构造函数要调用这些对象成员所在类的构造函数,A类的构造函数的定义形式如下:A:A(参数表0):成员1(参数表1),成员2(参数表2),成员n(参数表n)/其他操作,冒号“:”后由逗号隔开的项组成成员初始化列表,其中的参数表给出为调用相应成员所在类的构造函数时应提供的参数。这些参数一般来自“参数表0”,可以使用任意复杂的表达式,其中可以有函数调用。如果某项的参数表为空,则表中相应的项可以省略。对象成员构造函数的调用顺序取决于这些对象成员在类中说明的顺序,与它们在成员初始化列表中给出的顺序无关。当建立A类的对象时,先调用对象成员的构造函数,初始化对象成员,然后才执行A类的构造函数,初始化A类中的其他成员。析构函数的调用顺序与构造函数正好相反。【例5.1】分析下面程序中析构函数与构造函数的调用顺序。#includeusingnamespacestd;,classobjectprivate:intval;public:object():val(0)coutDefaultconstructorforobject“endl;object(inti):val(i)coutConstructorforobject“valendl;object()coutDestructorforobjectvalendl;,classcontainerprivate:objectone;/初始化顺序与对象成员/产生的顺序有关objecttwo;/排在前面的先初始化intdata;public:container():data(0)coutDefaultconstructorforcontainerendl;container(inti,intj,intk);container()coutDestructorforcontainerdataendl;,container:container(inti,intj,intk):two(i),one(j)/与初始化列表顺序无关data=k;coutConstructorforcontainerdataendl;voidmain()containerobj,anObj(5,6,10);程序运行结果如下:Defaultconstructorforobject/为对象成员one/调用默认构造函数Defaultconstructorforobject/为对象成员two调用/默认构造函数Defaultconstructorforcontainer/为对象obj调用/container类的默认构造函数,Constructorforobject6/为对象成员one调用/构造函数Constructorforobject5/为对象成员two调用/构造函数Constructorforcontainer10/为对象anObj调用/container类的有参构造函数Destructorforcontainer10/调用container类的/析构函数析构对象anObjDestructorforobject5/析构数据成员为5的对象twoDestructorforobject6/析构数据成员为6的对象oneDestructorforcontainer0/调用container类的/析构函数析构对象objDestructorforobject0/析构对象twoDestructorforobject0/析构对象one,在这个程序中,container类包含有两个object类的对象成员one和two。对基本数据类型的成员的初始化也可以在成员初始化列表中进行,例如:container:container(inti,intj,intk):two(i),one(j),data(k)当初始化const成员和引用成员时,必须通过成员初始化列表进行。例如:classexampleprivate:constintnum;intpublic:example(intn,intf):num(n),ret(f);,5.2静态成员简单成员函数是指声明中不含const、volatile、static关键字的函数。如果类的数据成员或成员函数使用关键字static进行修饰,这样的成员称为静态数据成员或静态成员函数,统称为静态成员。【例5.2】分析下面程序的输出结果。classTeststaticintx;/静态数据成员intn;public:Test()Test(inta,intb)x=a;n=b;staticintfunc()returnx;/静态成员函数,staticvoidsfunc(Test/设置对象b的数据成员n,coutb.Getn();cout“”b.func();/x属于所有/对象,输出25cout“”c.func();/x属于所有/对象,输出25Testa(24,56);/将类的x值改为24couta.func()b.func()b.func()endl;,静态数据成员只能说明一次,如果在类中仅对静态数据成员进行声明,则必须在文件作用域的某个地方进行定义。在进行初始化时,必须进行成员名限定。例如:intTest:x=25;在构造函数中,也可以使用类成员限定,例如:Test(inta,intb)Test:x=a;n=b;虽然还没有建立对象,但静态成员已经存在。除静态数据成员的初始化之外,静态成员遵循类的其他成员所遵循的访问限制。由于数据隐藏的需要,静态数据成员通常被说明为私有的,而通过定义公有的静态成员函数来访问静态数据成员。,注意:由于static不是函数类型中的一部分,所以在类声明之外定义静态成员函数时,不使用static。在类中定义的静态成员函数是内联的。一般来说,通过成员名限定访问静态成员,比使用对象名访问静态成员要好,因为静态成员不是对象的成员。静态成员可以被继承,这时,基类对象和派生类的对象共享该静态成员,除此之外,在类等级中对静态成员的其他特性(例如,静态成员在派生类中的访问权限、在派生类中重载成员函数等)的分析与一般成员类似。类中的任何成员函数都可以访问静态成员,但静态成员函数只能通过对象名(或指向对象的指针)访问该对象的非静态成员,因为静态成员函数没有this指针,例如sfunc()的定义。构造对象a之后,改变类的x值,也即所有对象的x值都变成此值。输出结果如下:,25582525242424由此可见,静态成员与一般成员有下列不同之处。可以不指向某个具体的对象,只与类名连用。在没有建立对象之前,静态成员就已经存在。静态成员是类的成员,不是对象的成员。静态成员为该类的所有对象共享,它们被存储于一个公用内存中。没有this指针,所以除非显式地把指针传给它们,否则不能存取类的数据成员。静态成员函数不能被说明为虚函数。静态成员函数不能直接访问非静态函数。,【例5.3】使用静态类对象的例子。不要混淆类的静态成员与静态类对象。静态类对象是使用关键字static声明的类的对象,所以静态类对象实质上就是静态类变量,但要注意它的构造函数与析构函数的调用特点。本例说明了静态类对象的特殊性。#includeusingnamespacestd;classtestprivate:intn;public:test(inti)n=i;coutconstructor:iendl;test()coutdestructor:nendl;intgetn()returnn;voidinc()+n;,voidmain()coutloopstart:endl;for(inti=0;i3;i+)statictesta(3);testb(3);a.inc();b.inc();couta.n=a.getn()endl;coutb.n=b.getn()endl;coutloopend.endl;coutExitmain()endl;,程序输出结果如下:loopstart:constructor:3constructor:3a.n=4b.n=4destructor:4constructor:3a.n=5b.n=4destructor:4constructor:3,a.n=6b.n=4destructor:4loopend.Exitmain()destructor:6程序中创建两个类对象:静态类对象a和普通类对象b。由程序输出可见,对静态类对象a而言,第一次执行它的定义时,调用构造函数使得a.n=3,而a.inc()使得a.n=4,输出其值4之后,for循环进入下一轮循环。在以后的循环中,再没有调用构造函数;直到所有程序执行结束,它才在退出时调用析构函数。由此可见,它具有如下性质:,构造函数在代码执行过程中,第一次遇到它的变量定义时被调用,但直到整个程序结束之前仅调用一次。析构函数在整个程序退出之前被调用,同样也只调用一次。对普通类对象b而言,因为它是for循环语句中的局部类对象,所以生命期只能与本次循环共存,每当循环体的本次循环结束时,它都要调用一次析构函数。通过比较它们的输出结果,很容易看出它们之间的区别。,5.3友元函数有时两个概念上相近的类要求其中一个可以无限制地存取另一个的成员。例如,一个链表的实现需要一个类代表单个结点,另一个处理链表本身。链表成员由表管理者存取,但管理者也需要能完全存取结点类的成员。友元函数(简称友元)解决了这类难题。友元可以存取私有成员、公有成员和保护成员。其实,友元可以是一个类或函数,尚未定义的类也可以作为友元引用。图5.1是其示意图。,图5.1友元函数图解示意图,由此可见,友元函数是类的外部函数,它是声明该函数的类的成员函数,不是friend所在类的成员函数,但可访问friend所在类中的所有对象的私有成员、公有成员和保护成员。,1.类本身的友元函数图5.1的特殊情况是为本类声明一个友元函数。这时,虽然在类中说明它,但它并不是类的成员函数,所以在类外面像普通函数那样定义这个函数。【例5.4】给出使用友元函数计算两点距离的例子。,【例5.4】使用友元函数计算两点距离的例子。#include#include/这类头文件需要.h形式usingnamespacestd;classPointprivate:doubleX,Y;public:Point(doublexi,doubleyi)X=xi,Y=yi;doubleGetX()returnX;doubleGetY()returnY;frienddoubledistances(Point,/像普通函数一样定义友元函数doubledistances(Point,dinstances函数求得距离为1.41421。在这个程序中,函数distances被说明为类Point的友元。由于友元不是Point类的成员,所以没有this指针,在访问该类的对象的成员时,必须使用对象名,而不能直接使用Point类的成员名。由上面可见,友元函数其实就是一个一般的函数,仅有的不同点是:它在类中说明,可以访问该类所有对象的私有成员。虽然友元是在类中说明的,但其名字的作用域在类外,友元作用域从声明友元处开始,与声明它所在的类同时结束。因此,友元说明可以代替该函数的函数说明。,友元说明可以出现于类的私有或公有部分,这没有什么差别。程序员应将友元看做类的接口的一部分,因为友元说明也必须出现于类中。使用友元函数的主要目的是为了程序的效率。友元函数由于可以直接访问对象的私有成员,因而省去了调用类的成员函数的开销。它的另一个优点是:类的设计者不必在考虑好该类的各种可能使用情况之后才能设计这个类,类的使用者可以根据需要,通过使用友元增加类的接口。使用友元的主要问题是:它允许友元函数访问对象的私有成员,这破坏了封装和数据隐藏,导致程序的可维护性变差,因此在使用友元时必须权衡得失。,2.将成员函数用做友元一个类的成员函数(包括构造函数和析构函数)可以通过使用friend说明为另一个类的友元,这是图5.1的标准情况。将类One的成员函数func()说明为类Two的友元,因为func()是属于类One的,所以要使用限定符说明它的出处,即One:func(Two,classTwo;/先声明类Two以便类One引用Two,classTwoprivate:inty;public:Two(intb)y=b;intGety()returny;friendvoidOne:func(Two,TwoObj2(8);coutObj1.Getx()Obj2.Gety()endl;Obj1.func(Obj2);coutObj1.Getx()Obj2.Gety()endl;类Two通过friend将类One的成员函数func声明为友元函数,所以One的对象可以通过func访问Two对象的私有成员,具体使用方式由函数func的定义决定。显然,友元函数func不是类Two的成员函数,它由类One定义。它不用对象名即可自由存取它所在类One的成员,使用对象名可以存取Two类的成员(r.y=x说明这一点)。输出结果为:5855,3.将一个类说明为另一个类的友元可以将一个类说明为另一个类的友元。这时,整个类的成员函数均具有友元函数的性能,声明友元关系简化为“friendclass类名;”,图5.1中简化为“friendclassone;”。这样,类One的所有成员函数都是类Two的友元。【例5.6】类One的对象可以通过任意一个成员函数访问类Two的对象的私有数据。#includeusingnamespacestd;classTwointy;public:friendclassOne;/声明One为Two的友元;,classOne/类One的成员函数均是类Two的友元intx;public:One(inta,Two/使用自己的及友元/类对象的私有数据,voidmain()TwoObj2;OneObj1(23,Obj2,55);Obj1.Display(Obj2);/输出2355本例演示了类One的成员函数可以访问类Two对象的私有成员,还演示了使用构造函数同时产生两个类的对象并初始化对象的例子。Display函数则显示两个类对象的数据。需要注意的是,友元关系是不传递的,即当说明类A是类B的友元,类B又是类C的友元时,类A却不是类C的友元。这种友元关系也不具有交换性,即当说明类A是类B的友元时,类B不一定是类A的友元。当一个类要和另一个类协同工作时,使一个类成为另一个类的友元是很有用的。,4.友元和派生类友元声明与访问控制无关。友元声明在私有区域进行或在公有区域进行是没有多大区别的。虽然友元函数的声明可以置于任何部分,但通常置于能强调其功能的地方以使其直观,对友元函数声明的惟一限制是该函数必须出现在类声明内的某一部分。友元关系是无等级的。友元可以访问任何一个类成员函数可以访问的对象,这比一个派生类可以访问的对象还多。友元关系是不能继承的。一个派生类的友元只能访问该派生类的直接基类的公有和保护成员,不能访问私有成员。当友元访问直接基类的静态保护成员时,只能使用对象名而不能使用成员名限定。,5.4const对象可以在类中使用const和volatile关键字定义数据成员和成员函数,也可以使用const和volatile关键字来修饰一个对象,这时对象的状态就不能使用一般的成员函数来访问。一个const对象只能访问const成员函数,一个volatile对象只能访问volatile成员函数,否则将产生编译错误。这两个关键字的用法很特别,下面举例说明它们的用法。1.常量成员常量成员包括常量数据成员、静态常数据成员和常引用。静态常数据成员仍保留静态成员特征,需要在类外初始化。常数据成员和常引用只能通过初始化列表来获得初值。,【例5.7】常数据成员初始化和常引用作为函数参数。#includeusingnamespacestd;classBaseprivate:intx;constinta;/常数据成员只能通过初/始化列表来获得初值staticconstintb;/静态常数据成员constint,voidShow()coutx,a,b,rendl;voidDisplay(constdouble/常引用作为函数参数,建立Base类的对象a1和a2,并以(104,118)和(119,140)作为初值,分别调用构造函数,通过构造函数的初始化列表给对象的常数据成员赋初值。程序输出结果如下:104,118,125,104119,140,125,1193.141592.常引用作为函数参数使用引用作为函数参数,传送的是地址。但有时仅希望将参数的值提供给函数使用,并不允许函数改变对象的值,这时可以使用常引用作为参数。例如Display函数的声明voidDisplay(constdouble则可以将一个double对象作为参数,但Display不能改变r所引用的对象,即不会破坏实参。,3.常对象在对象名前使用const,声明常对象,但声明时必须同时进行初始化,而且不能被更新。定义的语法如下:类名const对象名(参数表);/必须初始化例如定义类Base的一个常对象a:Baseconsta(25,68);4.常成员函数如果声明一个成员函数为const函数,则等于告诉编译器可以为一个const对象调用这个函数。编译器认为没有声明为const的成员函数可以修改对象中的数据成员,所以不允许为一个const对象调用非const成员函数。const放在函数声明之前意味着返回值是常量,但这不符合语法。必需将关键字const放在函数参数表之后,才能说明该函数是一个const成员函数。,声明函数是常成员函数的格式如下:类型标识符函数名(参数列表)const;关键字const放在函数参数表之后,函数体之前。为了保证不仅声明const成员函数,而且确实也定义为const函数。编译器强迫程序员在定义函数时必须重申const声明。也就是说,const已经成为这种成员函数识别符的一部分,编译器和连接程序都要检查const。定义格式如下:类型标识符类名:函数名(参数列表)const/函数体也可以在类中使用内联函数定义const函数,const位于函数参数表之后,函数体之前:类型标识符函数名(参数列表)const/函数体以这样严格的方式声明函数对基类是很有用的,基类中的成员函数可以被派生类中的函数覆盖,声明成员函数时,在函数体之前加上const可以防止覆盖函数改变数据成员的值。,【例5.8】const对象调用const成员函数。在Base类中说明了两个同名函数Show(),其中一个是常函数。在主函数中说明了两个对象a和b,通过对象a调用的是没有使用const修饰的普通成员函数。因为对象b是常对象,所以通过对象b调用的应该是使用const修饰的常成员函数。程序中还定义了一个常数据成员p,p只能通过初始化列表来获得初值。#includeusingnamespacestd;classBaseprivate:doublex,y;constdoublep;,public:/通过初始化列表来获得初值Base(doublem,doublen,doubled):p(d)x=m;y=n;voidShow();voidShow()const;voidBase:Show()coutx,yp=pendl;voidBase:Show()const/定义时必须也使用constcoutx,yconstp=pendl;,voidmain()Basea(8.9,2.5,3.1416);constBaseb(2.5,8.9,3.14);b.Show();/调用voidShow()consta.Show();/调用voidShow()程序输出结果如下:8.9,2.5p=3.14162.5,8.9constp=3.14,在常成员函数里,不能更新对象的数据成员,也不能调用该类中没有用const修饰的成员函数。如果将一个对象说明为常对象,则通过该对象只能调用它的const成员函数,不能调用其他成员函数。本程序定义一个const对象b,所以b.Show()只能调用voidShow()const函数。由此可见,可以用这种方法实现类似函数重载的功能。【例5.9】演示了这种使用方法。声明常对象时,const也可如本程序那样,放在前面。下面两种声明是等效的:constBaseb(2.5,8.9,3.14);Baseconstb(2.5,8.9,3.14);【例5.9】const函数返回的常量对象与其他常量对象一起使用。#includeusingnamespacestd;,classConstFunpublic:intf5()constreturn5;/返回常量对象intObj()return45;voidmain()ConstFuns;/一般对象constinti=s.f5();/对象s使用f5()初始化常整数constintj=s.Obj();/对象s使用Obj()初始化常整数intx=s.Obj();/对象s使用Obj()作为整数inty=s.f5();/对象s使用f5()作为整数,coutij/输出5和45xy;/输出45和5constConstFund;/const对象intk=d.f5();/常量对象d只能调用返回常量的函数coutk=kendl;/输出5普通对象可使用全部成员函数,但常量对象只能使用返回常量的成员函数。程序运行后的输出为:545455k=5,5.5数组和类,ANSIC可以声明的数组类型都适用于C+,但类的引入产生了一系列新的数组类型。常见的有如下几种:类对象数组;类对象指针数组;类成员指针数组;静态数据成员指针数组。下面举例说明类对象数组和类对象指针数组的使用方法。,【例5.10】使用类对象数组和指针的例子。classTestintnum;doublef1;public:Test(intn)num=n;/一个参数的构造函数Test(intn,doublef)num=n;f1=f;/两个参数的/构造函数intGetNum()returnnum;doubleGetF()returnf1;#includeusingnamespacestd;,voidmain()Testone2=2,4,*p;Testtwo2=Test(1,3.2),Test(5,9.5);for(inti=0;iGetNum()GetF()endl;,输出如下:one0=2one1=4two0=(1,3.2)two1=(5,9.5)编译器调用适当的构造函数建立数组的每一个分量。如果找不到合适的构造函数,则产生错误信息。该例中定义两个构造函数。数组one调用一个参数的构造函数Test(int)来初始化数组,即使用Test:Test(intn);以产生数组的每一个分量。对于一个参数的数组,还可以像普通数组那样初始化。数组two在定义时使用带两个参数的构造函数Test(int,float),并使用无名对像的方式进行初始化。例如Test(1,3.2)调用构造函数Test:Test(intn,floatf);,来产生无名对象,初始化数组的第1个分量。注意无名对象与临时对象(第4.4节)的区别。数组名代表数组首地址。下面是使用类对象指针数组的主程序,它们的输出结果相同。【例5.11】仍然使用类Test,但改用类对象指针数组的演示主程序。#includeusingnamespacestd;voidmain()Test*one2=newTest(2),newTest(4);Test*two2=newTest(1,3.2),newTest(5,9.5);for(inti=0;iGetNum()endl;,for(i=0;iGetNum()GetF()endl;类对象指针数组比类对象数组稍为复杂一些,但声明的语法是相似的。数组one和two都是直接用动态分配的对象初始化的,编译器自动调用构造函数Test:Test(int)来产生one的每一个分量,自动调用构造函数Test:Test(int,float)来产生two的每一个分量。main函数中显示了用指针访问成员函数的方法,该表示方法与结构指针数组类似。由本节的例题可见,直接调用构造函数可以产生无名对象。无名对象可以作为实参传递给函数、构造一个新的对象或用来初始化引用。,当无名对象用来作为对象或引用的初始化时,只调用构造函数,不调用析构函数,因为此时的无名对象已和被创建的新对象或新引用联系在一起,只有当对象或引用的生存期结束时才调用析构函数来释放对象或引用。其它情况下生成的无名对象,使用完后就被立即调用析构函数释放。例如无名对象作为函数参数或者作为赋值语句的右值时,就会被立即释放。可以在上面的主程序中使用如下语句验证。Testa=Test(35,9.8);couta.num=a.GetNum(),a.f1=“a.GetF()*”有相似的作用。它的左值必须是一个指向类对象的指针,它的右值是该类的一个特定数据成员。在语句“x.*p=1;”中,x是类A的对象,p是指向类A的数据成员b,代表将1赋给b。语句“px-*p=2;”的px是指向A类的对象x的指针,这时的p已经改为指向类A的数据成员c,本语句的作用是将2赋给A的数据成员c。指针p指示的是当前数据成员,很容易分析出程序的输出为:812。由此可见,在使用指向类数据成员的指针访问对象的某个数据成员时,必须指定一个对象。如果该对象由对象名或引用标识,则使用运算符“.*”;如果是使用指向对象的指针来标识的,则使用运算符“-*”。对数据成员的限制是它们必须是公有的。,指向类的静态数据成员的指针的定义和使用,与一般指针的定义和使用方法一样。【例5.13】使用指向类的静态数据成员指针。classApublic:A()staticintnum;intA:num;,voidmain()int*p=/输出56由于静态数据成员不属于任何对象,所以访问指向静态数据成员的指针时不需要对象。由于静态数据成员是在类中说明的,所以取它的地址时需要进行成员名限定。,2.指向类成员函数的指针指向类成员函数的指针与指向类数据成员的指针工作原理相似,用途也相似。主要的区别在于语法更复杂。假设类A的成员函数为“voidfa(void);”,如要建立一个指针pafn,它可以指向任何无参数和无返回值的类A的成员函数:void(A:*pafn)(void);pafn是一个指向类A的成员函数的指针,此成员函数既无参数,也无返回值。下面的例子说明了pafn如何被赋值并用以调用函数fa:pafn=A:fa;/指向类A的成员函数fa的指针pafn

温馨提示

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

评论

0/150

提交评论