版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
面向对象程序设计
游戏准备:猜先、让子等黑子先走绘制棋局画面判断输赢白棋下子绘制棋盘画面判断输赢返回步骤2输出棋局结果7.1基于过程与面向对象的设计思想玩家对象:黑白双方,这两方的行为是一模一样的棋盘对象:负责绘制棋局画面裁判对象:规则系统,负责判定诸如犯规、输赢等。玩家对象接受用户输入,通告棋盘对象,棋盘对象更新棋局画面,同时由裁判对象负责棋局判定。
基于过程的程序设计
面向对象的程序设计7.2继承在面向对象程序设计中,程序由类构成,继承关系是类之间最主要的关系继承表达“是一种(“isa”)”关系类Car、Boat、Submarine分别继承Vehicle类,通过继承获得Vehicle类的属性(数据成员)和方法(成员函数)派生类各自实现Vehicle类界定的派生类必须支持的行为派生类还可以添加特有的其他属性和方法继承背后的主要目的是扩展和代码重用classVehicle{
//…};//“:”表示Car继承自Vehicle,称Vehicle为基类,Car为派生类classCar:publicVehicle{
//…};classBoat:publicVehicle{
//…};classSubmarine:publicVehicle{
//…};7.2继承在C++中,称被继承的类为基类(baseclass),称继承得到的新类为派生类(derivedclass)
7.2.1派生类的定义派生类的定义:class<派生类类名>:<public|protected|private><基类类名>{ //派生类的成员声明};关键字public代表公有继承,如果换成protected,就是保护继承,换成private就是私有继承,C++支持public、protected和private三种类间继承机制,关键字public、private和protected描述基类成员在派生类中的可见性。7.2.1派生类的定义在类继承体系中,基类定义了所有派生类共有的对外公开接口(publicinterface),派生类继承基类的成员(成员函数和数据成员),派生类还可以改写(override)继承来的成员函数,或者增加新的成员,以实现其独特的行为。注意:类继承中的关键字public、private和protected描述的是基类成员在派生类中的可见性,而类定义中的public、private和protected是对类成员可见性的约束,不能混淆。7.2.2继承的3种方式1、public继承public继承保留基类成员函数和数据成员的可见性不变:基类的public成员,在派生类中还是public的基类的protected成员,在派生类中还是protected的基类的private成员对派生类不可见,派生类中不可以调用基类的private成员在类的public、private和protected三种继承中,派生类都不能访问基类的private成员,从而给了基类一定的私有空间,在基类的private部分改变时,不影响派生类。
classPeople{ //基类public:
walk(); eat(); private:
//…};classStudent:publicPeople{//派生类public: study();};public继承需要满足派生类“isa”基类,满足面向对象理论中Liskov替换原则:LSPpublic继承public继承表达的关系是:People类是Student类的基类,Student类是People类的一个派生类,Student类“是一种(“isa”)”People类,如果不能推导出“isa”关系,就不应该使用public继承。在这里,“isa”
是形象的说法,并不严格,public继承要求满足的是面向对象理论中的Liskov替换原则(LiskovSubstitutionPrinciple,LSP),意思是“从行为上而言,派生类应该可以完全替换基类完成基类的行为”。public继承下面是一个误用public继承的例子:classRectangle{public:
//…
virtualvoidsetHeight(intnewHeight);
virtualvoidsetWidth(intnewWidth);
virtualintheight()const; //返回height
virtualintwidth()const; //返回width
//…};classSquare:publicRectangle{
//… setLength(intnewLength);};public继承Squares;s.setWidth(100);s.setLength(200);现在s的长和宽分别是200和100,s不再是正方形,问题出在虽然Square是一种Rectangle,但是Square类不能完全替换Rectangle类完成基类的行为。public继承classCircle:publicEllipse{ //…};classPenguin:publicBird{ //…};classTurkey:publicBird{ //…};这些问题是同质的,无论具体public继承的是什么,不良的继承设计总是由于派生类无法实现基类一个或多个成员函数给出的接口造成的。解决的办法是要么使基类功能弱一些,派生类功能强一些,要么消除继承关系。绝大多数不良的继承设计都可以归结为“正方形不是矩形”或者“圆不是一种椭圆”的例子。public继承7.2.2继承的3种方式2、private继承基类的public成员成为派生类的private成员基类的protected成员成为派生类的private成员基类的private成员对派生类不可见,派生类中不可以调用基类的private成员基类public或protected成员,由于成了派生类private成员,无法再往下继承,即:派生类只能调用其直接基类的public或protected成员在派生类的派生类不可以调用其间接基类的任何成员classPeople{ //基类public:
walk(); eat(); private:
//…};classEt:privatePeople{ //派生类public: study();};private继承是一种实现继承,这里继承的目的是重用People类中walk()、eat()方法的实现代码。private
继承private
继承从语义角度上来说,private继承是一种实现继承,private继承和public继承最大的区别就在于,private继承不满足“isa”的关系,private继承表达的是“用…来实现”。在实现中使用private继承的目的纯粹为了代码重用,派生类Dprivate继承自基类B,是指D对象用B对象来实现,private继承的目的是利用基类B中的某些代码,而不是基类B和派生类D之间有什么概念上的联系,private继承只是一种实现技术。private继承不符合Liskov替换原则,派生类不能自动转换成为基类,即使使用类型转换(static_cast或dynamic_cast)也只能得到一个空指针,在private继承下,不可以通过基类指针操作派生类对象:People*p=newET(); //错误,ET不是People7.2.2继承的3种方式3、protected继承基类的public成员成为派生类的protected成员基类的protected成员也成为派生类的protected成员基类的private成员对派生类不可见,派生类中不可以调用基类的private成员与private继承不同,在protected继承下,由于派生类的派生类的成员可以调用基类的protected成员,基类成员可以继续往下继承protected继承也是实现继承,protected继承和private继承都不表明派生类“是一种(“isa”)”基类。区别是protected继承允许派生类的派生类使用继承关系,如果希望基类成员在派生类的后续派生中延续,就使用
protected继承。
继承访问控制规则继承访问控制基类成员访问控制在派生类中的访问控制publicpublicpublicprotectedprotectedprivate不可访问protectedpublicprotectedprotectedprotectedprivate不可访问privatepublicprivateprotectedprivateprivate不可访问今天编写的代码可以被将来编写的代码调用class
B{ //基类public:
voidfoo(); //基类接口
//…};class
D:publicB{ //派生类
//…};class
E
:publicB{ //派生类
//…};……Bfoo()Dfoo()Efoo()Xfoo()7.3
虚函数今天编写的代码可以调用未来编写的代码7.3
虚函数通过继承机制,将一组相关的类组织起来,构成类继承体系;通过关键字virtual
将基类的成员函数声明为虚函数;派生类改写(override)从基类继承的虚函数;支持将派生类对象当作基类对象,传递给基类指针或者初始化基类引用;支持通过基类指针或引用,动态调用具体派生类改写的虚函数。C++的做法是:首先定义一个接口,如果类B继承类A,实现类A接口的方法,那么就能在程序运行的时候调用到类B,为实现这个目标,C++提供的支持有:7.3
虚函数这样,调用相同的基类接口,面对不同的派生类对象,调用的是不同的实现,即多态:相同的操作,不同的行为。有了多态,就可以实现利用今天编写的代码调用未来编写的派生类的代码。C++中基类的接口,就是虚函数,在程序运行时,对虚函数的调用,透明地转换为对不同派生类的函数的调用,这在C++中称为动态绑定。7.3.1虚函数的定义例如:classAnimal{public: //用关键字virtual定义虚函数
virtualvoideat(){/*函数体*/}};虚函数是一种非静态的成员函数,定义一个函数为虚函数的方法是在其函数定义前加上关键字virtual,一般形式为:virtual<类型说明符><函数名>(<参数表>){ <函数体>}7.3.1虚函数的定义classB{
//基类Bpublic:virtualvoidfoo();
//虚函数
//…};classD:publicB{
//从B继承public:virtualvoidfoo();
//可以附加virtual关键字,virtual不是必须的
//…};classE:publicD{
//从D继承public:voidfoo();//基类的虚函数在其任意层次的派生类中也还是虚函数
//…};voidD::foo(){
//在类外实现虚函数时,不能再加virtual关键字//…}7.3.1虚函数的定义虚函数只能是类成员函数。基类声明的虚函数,在派生类及其任意派生层次的派生类中都是虚函数。但为了提高代码的清晰性,可在每一层类的定义中再显式地附加virtual关键字。注意:在类外提供虚函数的实现时,不能再加virtual关键字,否则会编译报错,这点和类中const成员函数不一样。如果在类外实现const成员函数,那么定义和实现处都要有const关键字。而对于虚函数,在基类定义的地方要加virtual关键字,在派生类的定义处可加可不加virtual关键字,在类外虚函数的实现处不能加virtual关键字。7.3.2虚函数的应用那么,在使用时可以:B*b=newD();//虽然b是指向类B的指针,但执行的函数却是D::foo()!b->foo();
假设有下面的类层次:classB{public:
virtualvoidfoo(){cout<<"B::foo()iscalled"<<endl;}};classD:publicB{public:
virtualvoidfoo(){cout<<"D::foo()iscalled"<<endl;}};1、通过基类指针调用虚函数7.3.2虚函数的应用说明:把派生类当作基类使用,派生类转换到基类,有两种方式:如果是采用引用或指针方式,则编译器只是把派生类的引用或指针传递过去;如果是采用对象实例方式,则编译器会把派生类对象中基类部分复制成一个基类对象再使用,这时发生的就是5.4.3节函数的引用调用的第3部分提到的:派生类的变量被“切割”的问题。另外,基类不能转换到派生类,即不能把基类当作派生类使用,因为基类中没有派生类的特有部分。7.3.2虚函数的应用虚函数只能借助于指针或者引用来实现多态,如果使用具体的派生类对象,即通过派生类对象名和成员选择运算符(.)调用虚函数,则所调用的虚函数是在编译时确定(称为静态绑定),调用的是派生类改写的虚函数,如派生类没有改写则是其继承来的基类虚函数。2、虚函数的静态调用7.3.2虚函数的应用classB{public:
virtualvoidfoo();};classD:publicB{
virtualvoidfoo();};Bb=newD();b.foo();
//D::foo()被调用3、通过基类的非虚函数调用虚函数注意:this->area();语句通过基类的this指针调用虚函数Shape::area(
),实现的结果也是多态。//shape.hclassShape{public: voidprint();
virtualfloatarea();};#include"shape.h"voidShape::print()const{ floata=this->area();
//area()是虚函数
//…}【例7.1】一个简单的多态示例程序。#include<iostream>usingnamespacestd;classAnimal{
//基类public:
virtualvoideat(){//用关键字virtual定义eat函数为虚函数
cout<<"IeatlikeagenericAnimal.\n"; }};classWolf:publicAnimal{
//派生类Wolf继承基类Animalpublic: voideat(){
//改写(override)基类的虚函数
cout<<"Ieatlikeawolf!\n"; }};classFish:publicAnimal{ //派生类Fish继承基类Animalpublic: voideat(){ //改写基类的虚函数
cout<<"Ieatlikeafish!\n"; }};//继承,但未改写基类的虚函数classOtherAnimal:publicAnimal{};intmain(){
//数组声明,元素类型是指向基类Animal的指针Animal*anAnimal[4];
anAnimal[0]=newAnimal(); //指向基类对象的指针
anAnimal[1]=newWolf(); //指向派生类Wolf对象的指针
anAnimal[2]=newFish(); //指向派生类Fish对象的指针
//类OtherAnimal对象的指针anAnimal[3]=newOtherAnimal();for(inti=0;i<4;i++)//通过基类指针调用基类虚函数,动态绑定 anAnimal[i]->eat();
return0;}运行结果:IeatlikeagenericAnimal.Ieatlikeawolf!Ieatlikeafish!IeatlikeagenericAnimal.7.3.3overload和override重载(overload)是指相同作用域内函数之间同名但参数不同的情形。C++不支持跨域重载,重载只发生在相同的作用域内。在同一个类中,多个参数不同的同名函数构成重载。在基类和派生类间,若成员函数同名而且参数不同,那么派生类成员函数将遮蔽基类同名成员函数,而不是重载。改写(override)指派生类改写继承的基类虚函数,以实现或者改变基类函数的行为。派生类继承了基类的成员函数,虚函数通常在派生类中被改写。只有派生类改写基类虚函数,在用指向派生类对象的基类指针调用基类虚函数时,才是多态。参数不同包括三层含义:一是参数类型不同,二是参数顺序不同,三是参数个数不同。注意,仅仅参数名字不同,不算参数不同。classBase{public:
virtualvoidf();
virtualvoidg(int);};classDerived:publicBase{public: voidf(); //override,改写基类虚函数Base::f() voidg(); //不是override,遮蔽了基类的同名函数
Base::g()};7.3.3overload和override表7-1派生类成员和基类public/protected成员名字的关系派生类成员与基类的public/protected成员同名不同名成员函数数据成员虚函数非虚函数参数与返回类型相同不同改写遮蔽遮蔽,但派生类成员不该遮蔽基类成员遮蔽新增成员,不影响继承成员7.3.4纯虚函数1、纯虚函数概念为了表述不能实例化的基类,即不允许创建该基类的对象,C++引入抽象类(abstractclass)的概念,抽象类概念通过纯虚函数(purevirtualfunction)表达。7.3.4纯虚函数2、纯虚函数声明classBase{public: voidmf1();
//非虚函数
virtualvoidmf2();
//是虚函数,但不是纯虚函数
virtualvoidmf3()=0;
//纯虚函数};纯虚函数的声明是在类的虚函数声明式之后加上“=0”。将类的成员函数声明为纯虚函数表达的设计意图是:该函数目前尚无定义(实现),在派生类中必须被改写(override)。7.3.4纯虚函数若类的一个或多个成员函数被声明为纯虚函数,则由于纯虚函数没有具体实现,导致该类不能被实例化。称包含纯虚函数的类为抽象类,抽象类只能被继承,而不能直接创建实例,试图实例化一个抽象类将引发编译错误。
Baseb; //错误:不可实例化一个抽象类7.3.4纯虚函数3、基类纯虚函数有待于在派生类被改写classDerived:publicBase{
//没有定义mf1:正确,不该再定义mf1, //继承Base::mf1()的接口与强制实现
//没有定义mf2:没关系,继承Base::mf2() voidmf3();};voidDerived::mf3{
//…}Derivedd;
//正确:Derived::mf3改写了Base::mf37.3.4纯虚函数classD2:publicBase{//没有定义mf1:不改写,继承Base::mf1()的接口与强制实现
//没有定义mf2:没关系,继承了Base::mf2() //没有定义mf3:没关系,但D2因此也是抽象类};//错误:尚未改写纯虚函数Base::mf3(),D2也还是抽象类D2d;4、抽象类不支持实例化纯虚函数的意思是:“我是一个抽象类!不要将我实例化!”,纯虚函数用于规范派生类的行为,它宣告:“我的派生类都会有这个函数”,如果派生类没有改写基类的纯虚函数,则该派生类仍然是抽象类。7.3.4纯虚函数基类的虚函数(纯虚函数)目的是提供函数接口,虚函数和纯虚函数都可以在派生类中被改写,以多态的形式被调用。派生类改写虚函数必须确保和基类虚函数具有相同的返回类型、参数个数和参数类型。含有纯虚函数的类被称为抽象类,抽象类不支持实例化。只含虚函数而无纯虚函数的类不是抽象类。纯虚函数在基类中声明,必须在派生类中被改写,即提供函数的实现后才可以使用。5、public继承下的public虚函数和纯虚函数小结7.3.4纯虚函数非虚函数意味着派生类继承函数接口,以及一个强制实现。普通虚函数(非纯虚函数)意味着派生类继承函数接口,以及一个缺省实现。纯虚函数意味着派生类仅仅继承函数接口。【例7.2】纯虚函数使用示例。classConcreteCircle{public:voidradius(doubleradius){_radius=radius;}doubleradius(){
return_radius;}voidrender(){cout<<"画一个半径为“ <<_radius <<"的实心圆"<<endl;}private:double_radius;};classHollowCircle{public:voidradius(doubleradius){_radius=radius;}doubleradius(){
return_radius;}voidrender(){cout<<"画一个半径为“ <<_radius <<"的空心圆"<<endl;}private:double_radius;};//AbstractCircle.h#ifndefABSTRACTCIRCLE#defineABSTRACTCIRCLEclassAbstractCircle{public: voidradius(doubleradius){ _radius=radius; }doubleradius(){
return_radius;}
virtualvoidrender()=0;
//声明纯虚函数protected: double_radius;};#endif//HollowCircle.h#include<iostream>#include"AbstractCircle.h"usingnamespacestd;classHollowCircle:publicAbstractCircle{public:voidrender(){ cout<<"画一个半径为" <<_radius <<"的空心圆"<<endl; }};//ConcreteCircle.h#include<iostream>#include"AbstractCircle.h"usingnamespacestd;classConcreteCircle:publicAbstractCircle{public: voidrender(){ cout<<"画一个半径为" <<_radius <<"的实心圆"<<endl; }};//main.cpp#include<iostream>#include"AbstractCircle.h"#include"ConcreteCircle.h"#include"HollowCircle.h"usingnamespacestd;voidrender(AbstractCircle&circle){
circle.render();}intmain(){ ConcreteCircleconcrete; concrete.radius(10.0); render(concrete);
HollowCirclehollow; hollow.radius(20.0); render(hollow);
return0;}运行结果:画一个半径10的实心圆画一个半径20的空心圆7.4
面向对象程序设计在C++面向对象程序设计中,设计独立的类是相对容易的,难点在于正确选择继承与组合,合理设计基类和派生类的类层次结构,确定成员函数是public还是protected,是虚函数还是非虚函数,下面讨论这些设计问题。7.4.1继承与组合在面向对象程序设计中,对事物的抽象有两种角度:部分和种类,对应到程序实现中就是组合与继承。继承可以将一群相关的类组织起来,共享其间共同的数据和操作。不过,继承并不是程序设计时表达类间关系的唯一选择,而且有时继承会让程序更复杂。在程序设计时,应优先考虑类的组合(composition),当组合不能解决问题时,才考虑继承。7.4.1继承与组合类的组合是指类的数据成员是另一个类的对象。将类构建在其它类之上,组合的含义是“有一个(“hasa”)”或“用…来实现”,组合表示类的整体/部分关系。classAddress{…};classPhoneNumber{…};classPerson{public:
//…private: stringname; //string对象
Addressaddress; //Address对象
PhoneNumbervoiceNumber; PhoneNumberfaxNumber;};classHead{public: voidthink();};classFoot{public: voidwalk(); voidjump();};classMan{public: voidthink(){m_head.think();} voidwalk(){m_foot.walk();} voidjump(){m_foot.jump();}
//…private: Headm_head; Footm_foot;
//…};如果允许Man从Head、Foot派生,那么Man将自动具有think()、walk()、jump()这些功能,代码十分简短并且运行正确,但这种设计方法却是不正确的。7.4.1继承与组合classMan:privateHead,privateFoot{//功能正确的错误设计};7.4.1继承与组合其次,在如下两种情况下,使用private继承或者protected继承:需要改写基类B中的private/protected虚函数;不希望基类B被其它用户直接使用。要使得基类B不被其它用户直接使用,有两种方法:一是将B设计为抽象类,即让B包含纯虚函数;二是把类B的构造函数或析构函数设为private或者protected,这样系统就不能调用构造/析构函数,当然就不能创建对象了。7.4.1继承与组合classB{protected: B();
virtual~B();};classD:privateB{ //…};Bb; //错误:构造函数、析构函数为protected,不能生成对象Dd; //正确:派生类D可以调用基类B的protected构造函数B()因此,通常将析构函数声明为protected,如下:7.4.1继承与组合private继承和protected继承的选择原则是:在不能应用组合的情况下优先考虑private继承;如果派生类还需要能被继续继承下去,选择protected继承;其它情况,尽可能使用类的组合。private(protected)继承意味着“用…来实现”。如果派生类Dprivate(protected)继承基类B,则D的对象只不过是用类B的对象来实现而已,类B和类D的对象之间不存在概念上的关系。组合意味着“有一个”或“用…来实现”。如果类A包含一个类B的数据成员,类A的对象要么具有一个类型为B的成员,要么在实现中使用了类B的对象。7.4.2设计基类与派生类1、基类与派生类的关系派生类作为基类的具体化基类是对派生类的抽象。基类抽取派生类的公共特征,派生类是基类的具体化,具体派生类在基类的基础上添加数据成员和成员函数。派生类是基类定义的延续基类作为抽象基类。其中有些操作并未实现,派生类实现抽象基类中定义的操作,这时,派生类是基类的实现,是基类定义的延续。派生类是基类的组合在多继承时,派生类有多于一个的基类,这时派生类将是所有基类行为的组合。7.4.2设计基类与派生类2、设计基类找出所有派生类共同的操作,设计基类;区分哪些操作必须根据派生类的不同而有不同的实现方式,这些操作应该成为整个继承体系中的虚函数;确定每个操作的存取层级;如果基类实例化没有意义,则设计基类为抽象基类。找出基类中无实质意义的虚函数,令其为纯虚函数;凡基类定义有一个或者多个虚函数,则应该将其析构函数声明为虚函数,并提供实现。7.4.2设计基类与派生类一般地,如果希望派生类只继承基类成员函数的接口,则将基类成员函数设计为纯虚函数;如果希望派生类继承基类成员函数的接口和实现,同时还能改写继承而来的实现,则将基类成员函数设计为虚函数;如果希望派生类继承基类成员函数的接口和实现,但是不允许改写其实现,这时,应将基类成员函数设计为非虚函数。7.4.2设计基类与派生类3、设计派生类classBase{…};classD1:PublicBase{…};classD2:PublicD1{…};派生类包含两个部分:从基类继承的成员和自己定义的成员。派生类区别于基类的主要途径是添加数据成员和成员函数,以及改写基类的虚函数。在设计派生类时,需要考虑,究竟是要将基类中的虚函数改写,还是原封不动地加以继承。如果原封不动地继承纯虚函数,则这个派生类也是抽象类,同样不能实例化。在派生类使用中,不需要刻意区分“继承而来的成员”和“自身定义的成员”,两者的使用完全透明。7.4.2设计基类与派生类4、使用虚函数实现多态在public继承下,共同的基类意味着共同的特性,若派生类D1和D2都继承自基类B,则D1和D2均拥有从基类B继承的成员函数和数据成员。classB{};classD1:publicB{};classD2:publicB{};在基类设计中,如果希望一个函数需要在不同的派生类里有不同的表现,那么就应该将其设计为虚函数,基类中的虚函数定义接口,在派生类中改写虚函数,提供接口的不同实现。#include<iostream>usingnamespacestd;classB{public:
virtualvoidfoo(){ cout<<"调用了:B::foo()"<<endl;};virtual~B(){};//基类通常需要有一个public虚析构函数};classD2:publicB{public:voidfoo(){//改写基类虚函数
cout<<"调用了:D2::foo()"<<endl; }};【例7.3】改写虚函数实现多态示例。classD1:publicB{public:voidfoo(){//改写基类虚函数
cout<<"调用了:D1::foo()"<<endl;}};classE1:publicD1{public:voidfoo(){//改写基类虚函数
cout<<"调用了:E1::foo()"<<endl; }};#include<vector>intmain(){ B*p0=newB(); B*p1=newD1(); B*p2=newD2(); B*p3=newE1();
std::vector<B*>vecB; vecB.push_back(p0);vecB.push_back(p1); vecB.push_back(p2);vecB.push_back(p3); for(std::vector<B*>::iteratorit=vecB.begin();it!=vecB.end();++it){ (*it)->foo();
delete*it; //通过基类指针(B*)释放派生类对象
} return0;}运行结果:调用了:B::foo()调用了:D1::foo()调用了:D2::foo()调用了:E1::foo()7.4.2设计基类与派生类5、避免遮蔽继承得到的成员intx; //全局变量voidFunc(){ doublex; //局部变量
std::cin>>x; //读取一个新值,赋值给局部变量x}语句std::cin>>x;
操作的是局部变量x,内层作用域内的x“遮蔽”了外层作用域的同名全局变量x。图7-1全局作用域与局部作用域classBase{private: intx;public:
virtualvoidmf1()=0;
virtualvoidmf2(); voidmf3();
//…};7.4.2设计基类与派生类classDerived:publicBase{public:
virtualvoidmf1(); voidmf4();
//…};7.4.2设计基类与派生类在继承体系中,虽然派生类继承了基类的成员,包括成员函数和数据成员,但是C++并不将派生类和基类的成员合并到同一个作用域中,这时,派生类的作用域嵌套在基类作用域之中,如图7-2所示:图7-2派生类作用域嵌套在基类作用域之中7.4.2设计基类与派生类Derivedd;intx;//…d.mf1(); //正确:调用Derived::mf1()d.mf1(x); //错误:Derived::mf1()遮蔽了Base::mf1(int)和Base::mf1()d.mf2();
//正确:调用Base::mf2()d.mf3(); //正确:调用Derived::mf3()d.mf3(x);
//错误:Derived::mf3()遮蔽了Base::mf3()和Base::mf3(double)图7-3派生类成员函数遮蔽基类同名的成员函数7.4.2设计基类与派生类遮蔽之外,C++支持解除遮蔽的手段。如果Derived类确实需要调用从基类继承来的同名函数,怎么办?办法是把那些函数通过using声明导入到同一个域里,因为要求用户显式使用using声明,避免了意外调用的可能:classBase{private: intx;public:
virtualvoidmf1()=0;
virtualvoidmf1(int);
virtualvoidmf2(); voidmf3(); voidmf3(double);
//…};classDerived:publicBase{public://让基类Base内名为mf1和mf3的所有成员在Derived作用域内都可见
usingBase::mf1;
usingBase::mf3;
virtualvoidmf1(); voidmf3(); voidmf4();
//…};
使用using声明后类Base和类Derived的作用域如图7-4所示。现在重载发生了,这是因为类Derived中的语句usingBase::mf1;明确告诉编译器,要把类Base域中的mf1引入当前域,请编译器“一视同仁”。图7-4使用using声明后基类成员函数在派生类作用域中可见7.4.2设计基类与派生类7.4.2设计基类与派生类Derivedd;intx;//…d.mf1(); //正确:仍然调用Derived::mf1()d.mf1(x); //现在正确:调用Base::mf1()d.mf2(); //正确:仍然调用Base::mf2()d.mf3(); //正确:调用Derived::mf3()d.mf3(x); //现在正确:调用Base::mf3()这表示如果从一个带有重载函数的基类继承,而且只想重定义或改写基类的一部分重载函数,那么需要为每一个未改写的函数使用using声明;否则,未改写的函数将被遮蔽。7.4.2设计基类与派生类classB{public: voidmf();//非虚函数
//…};classD:publicB{public: voidmf();
//遮蔽了B::mf()
//…};Dd; //d是类型D的一个对象B*pB=&d;D*pD=&d;pB->mf(); //调用B::mf()
pD->mf(); //调用D::mf()6、不要重定义基类的非虚函数7.4.2设计基类与派生类7、虚函数与默认实参类的虚函数也可以有默认实参,不过函数的默认实参,包括虚函数的默认实参都是在编译时确定,而虚函数是在运行时动态绑定,即:通过基类的引用或指针调用虚函数时,默认实参是在基类虚函数中声明的值。通过派生类引用或指针调用虚函数时,默认实参是在派生类虚函数中声明的值。如果通过基类的引用或指针调用虚函数,但实际指向的是派生类的对象,那么传递给这个虚函数的默认实参是基类虚函数中声明的值,而不是派生类中声明的!所以,在派生类中一般不要重新定义继承而来的基类虚函数的默认参数值。7.4.2设计基类与派生类8、private虚函数classB{public: voidfoo(){bar();} //调用基类声明的虚函数bar()private:
virtualvoidbar(){…} //private虚函数};classD:publicB{private:
virtualvoidbar(){…} //private虚函数};7.4.2设计基类与派生类private虚函数的语义是:基类B告诉派生类D,最好改写bar()函数,但是不要管它如何使用,也不要调用这个函数,这个private虚函数由基类负责调用。基类的一个普通private成员函数,表示这是一个不可被更改的实现细节;基类的一个private虚成员函数,表示这是一个需要派生类修改的实现细节。7.4.3基类与派生类的构造派生类对象中其实含有多个子对象,包括基类构造函数初始化的“基类子对象”,以及由派生类构造函数初始化的“派生类子对象”,当构造、复制、赋值和撤销派生类的对象时,也会相应地构造、复制、赋值和撤销其中的基类子对象。下面分别讨论基类和派生类的构造、析构和复制控制。7.4.3基类与派生类的构造1、构造函数与析构函数的调用顺序派生类构造函数不仅必须初始化派生类的数据成员,还必须初始化基类的数据成员。C++的规则是:创建一个派生类的对象时,先调用基类的构造函数,然后才调用派生类的构造函数;派生类对象被撤销时,析构函数的调用顺序刚好相反,首先调用的是派生类的析构函数,然后才调用基类的析构函数。7.4.3基类与派生类的构造2、基类构造函数继承对基类的构造函数的影响是:在设计构造函数时,必须考虑派生类的访问权限,基类成员函数可以是public、protected或private,不过,因为构造派生类需要调用基类构造函数,基类构造函数不能定义为private。那么,基类构造函数该选择public还是protected?答案是:如果基类是包含纯虚函数的抽象类,则其构造函数应定义为protected。基类若不是抽象类,则其构造函数一般定义为public。//头文件:base.h#ifndef_BASE //防止头文件被重复引用#define_BASEclassBase{ //抽象类,包含一个纯虚函数public:
virtualfoo()=0;//纯虚函数
virtual~Base(); //基类析构函数一般为:public的虚析构函数protected: Base(); //抽象类的构造函数设为protected的非虚函数};#endif【例7.4】基类和派生类的设计以及构造函数、析构函数的调用顺序。//头文件:derived.h#ifndef_DERIVED//防止头文件被重复引用#define_DERIVED#include"base.h"classDerived:publicBase{public: virtualfoo(); Derived(); ~Derived();};#endif7.4.3基类与派生类的构造//实现文件:base.cpp#include"base.h"#include<iostream>usingnamespacestd;Base::Base(){ cout<<"基类构造函数:Base::Base()被调用"<<endl;}Base::~Base(){ cout<<"基类析构函数:Base::~Base()被调用"<<endl;}//实现文件:derived.cpp#include"derived.h"#include<iostream>usingnamespacestd;Derived::foo(){ //实现纯虚函数
cout<<"派生类foo的实现:Derived::foo()被调用"<<endl;}Derived::Derived(){ cout<<"派生类构造函数:Derived::Derived()被调用"<<endl;}Derived::~Derived(){ cout<<"派生类析构函数:Derived::~Derived()被调用"<<endl;}//主程序:main.cpp#include"base.h"#include"derived.h"#include<iostream>usingnamespacestd;intmain(){ Base*d=newDerived; d->foo();
deleted;
return0;}运行结果:基类构造函数:Base::Base()被调用派生类构造函数:Derived::Derived()被调用派生类foo的实现:Derived::foo()被调用派生类析构函数:Derived::~Derived()被调用基类析构函数:Base::~Base()被调用7.4.3基类与派生类的构造3、派生类构造函数构造函数与析构函数不能被派生类继承,每个类都应该定义自己的构造函数和析构函数,如果没有定义,编译器会自行生成默认构造函数和默认析构函数。默认构造函数都不带任何形参,函数体都为空,派生类默认构造函数除了初始化派生类的数据成员,还调用基类默认构造函数初始化基类数据成员。当类成员包含指针成员时,默认构造函数实现的按位拷贝往往是不够的,需要程序员提供构造函数,以保证类中指针数据成员被正确地初始化。7.4.3基类与派生类的构造为保证基类数据成员的初始化先于派生类数据成员,只能在派生类的构造函数的初始化列表中调用其直接基类的构造函数。注意,派生类构造函数的初始化列表只能初始化派生类成员,不能直接初始化从基类继承的成员。派生类的构造函数使用初始化列表调用基类构造函数的一般形式为:<派生类名>(<参数表>):<基类名>(<基类构造函数参数表>){
//派生类构造函数体};7.4.3基类与派生类的构造4、构造函数不能是虚函数在类继承体系中,首先构造函数不能是虚函数,没有虚构造函数,其次虚机制在构造函数和析构函数中不起作用,就是说在基类构造函数和基类析构函数中对虚函数的调用不构成“多态”。【例7.5】构造函数和析构函数中,对虚函数的调用不构成多态。//头文件:b.h#ifndef_B_#define_B_classB{public: B(); ~B();
virtualvoidvirt();
virtualvoidrelease();};#endif//头文件:d.h#ifndef_D_//防止头文件被重复引用#define_D_#include"b.h"classD:publicB{public: D(); ~D(); voidvirt(); //改写基类B的虚函数
voidrelease();//改写基类B的虚函数};#endif//实现文件:b.cpp#include"b.h"#include<iostream>usingnamespacestd;B::B(){ //构造函数中虚机制不起作用,无论如何都是B::virt()被调用
cout<<"在B::B()中,"; virt();}B::~B(){//
析构函数中虚机制不起作用,无论如何都是B::release()被调用
cout<<"在B::~B()中,"; release();}voidB::virt(){ cout<<"B::virt()被调用"<<endl;}voidB::release(){ cout<<"B::release()被调用"<<endl;}//实现文件:d.cpp#include"d.h"#include<iostream>usingnamespacestd;D::D(){ cout<<"在D::D()中,"; virt();}D::~D(){ cout<<"在D::~D()中,"; release();}voidD::virt(){ cout<<"D::virt()被调用"<<endl;}voidD::release(){ cout<<"D::release()被调用"<<endl;}//主程序:main.cpp#include"b.h"#include"d.h"#include<iostream>usingnamespacestd;intmain(){
//构造函数中和析构函数中,对虚函数的调用不构成多态
B*b=newD; //基类B的指针b,调用B::virt() deleteb; //通过基类指针释放派生类对象
cout<<endl; D*d=newD; //派生类D的指针d,调用D::virt() deleted; //通过派生类指针释放派生类对象
return0;}7.4.3基类与派生类的构造现在的问题是,通常派生类中也申请有资源,怎么才能确保释放派生类的资源呢?似乎可以在基类的析构函数中调用虚函数,即在B::~B()中调用B::release(),但上例表明,在析构函数中,对虚函数的调用不构成多态,在B::~B()中调用release(),调用的总是B::release(),调用不到派生类改写的D::release(),那么如何才能释放派生类的资源?答案是虚析构函数。7.4.4基类和派生类的析构1、虚析构函数在多态程序设计中,常常通过基类指针释放派生类对象。这时如果基类的析构函数非虚,则仅执行基类的析构函数,不执行派生类的析构函数。而通常派生类中申请的资源由派生类析构函数负责释放,不执行派生类的析构函数意味着:用基类指针释放派生类对象将导致资源泄漏。classB{public: B(){ptrB=newchar[10];} //基类申请了内存
~B(){delete[]ptrB;} //释放内存,注意:基类析构函数非虚
virtualvoidfoo();
//…private: char*ptrB;};7.4.4基类和派生类的析构classD:publicB{public: D(){ptrD=newchar[20];} //派生类申请了内存
~D(){delete[]ptrD;} //派生类析构函数释放派生类申请的内存
virtualvoidfoo(); //改写基类虚函数B::foo()
//…private: char*ptrD;};voidfunc(){ B*b=newD;
//… //通过基类指针b操纵派生类对象
deleteb;//因B::~B()非虚函数,只执行了B::~B(),D::~D()未被调用}7.4.4基类和派生类的析构最后通过基类B类型的指针,释放派生类D的对象,因为基类B的析构函数非虚,程序在执行deleteb;时,只执行B::~B(),派生类D的析构函数D::~D()未被调用到,派生类申请的内存资源没有机会释放,导致内存泄漏。解决的方法是在基类析构函数的返回类型前加上关键字virtual,将基类的析构函数声明为虚析构函数,格式:virtual~<类名>(){}7.4.4基类和派生类的析构将基类的析构函数声明为虚析构函数,根据虚函数的派生规则,在其所有派生层次中,派生类的析构函数都是虚函数,通过基类指针或引用,就可以调用派生类的析构函数,完成派生类对象的析构,虚析构函数的调用顺序是:从最深的派生类开始,依次调用其析构函数。classB{public: B(){ptrB=newchar[10];} //基类申请了内存
//虚析构函数,释放基类申请的内存
virtual~B(){delete[]ptrB;} private: char*ptrB;};7.4.4基类和派生类的析构在C++中,当一个类包含虚函数,并设计为通过基类指针操纵派生类对象,则其析构函数必须是虚析构函数。如果其不是被设计为基类,或者虽然设计成基类,但并不打算通过基类指针来操纵派生类,就是说,不按照多态的方式来使用,则无需将其析构函数声明为虚函数。7.4.4基类和派生类的析构在例7.5中,只要将基类B的析构函数定义为虚析构函数,就可以保证通过基类指针释放派生类对象时,派生类可以正确析构,修改后的b.h如下://头文件:b.h#ifndef_B_ //防止头文件被重复引用#define_B_classB{public: B();
virtual~B();
virtualvoidvirt();
virtualvoidrelease();};#endif修改后程序的运行结果:在B::B()中,B::virt()被调用在D::D()中,D::virt()被调用在D::~D()中,D::release()被调用在B::~B()中,B::release()被调用在B::B()中,B::virt()被调用在D::D()中,D::virt()被调用在D::~D()中,D::release()被调用在B::~B()中,B::release()被调用7.4.4基类和派生类的析构2、纯虚析构函数在面向对象程序设计中,有时需要限制一个类,使其成为不支持实例化的抽象类,但是又想为所有的虚函数都提供缺省实现,不适合设为纯虚函数。由于抽象类的目的是用作多态基类,而多态基类的析构函数必须是虚析构函数,所以此时可以把析构函数设为纯虚函数,称为纯虚析构函数(purevirtualdestructor)。classB{ //抽象类public:
//...
virtual~B()=0; //纯虚析构函数};7.4.4基类和派生类的析构注意:一定要为纯虚析构函数提供一份实现。不像其它纯虚函数,即使在派生类中,纯虚析构函数被改写,基类的纯虚析构函数也还是要被调用到。程序中没有提供纯虚析构函数的实现时,编译器会报错,通常纯虚析构函数的一个空白实现即可满足要求,如:B::~B(){} //纯虚析构函数的实现通常为空7.4.5基类和派生类的复制控制对一个类而言,在编译时,对于构造函数、拷贝构造函数、赋值运算符或者取地址运算符,如果程序中需要:而程序员没有提供定义,则编译器会自动生成相应的缺省实现,什么情况下程序中会需要这些函数?classBase{}; //定义一个空的类constBaseb1; //需要构造函数Baseb2(b1); //需要拷贝构造函数b2=b1; //需要赋值运算符Base*pb1=&b1; //需要取址运算符constBase*pb2=&b2; //需要const取址运算符7.4.5基类和派生类的复制控制calssBase{};编译以后相当于:classBase{public: Base(); //默认构造函数
Base(constBase&rhs); //拷贝构造函数
~Base(); //默认析构函数
Base&operator=(constBase&rhs); //赋值运算符
Base*operator&(); //取址运算符
constBase*operator&()const; //const取址运算符};7.4.5基类和派生类的复制控制inlineBase::Base(){} //默认构造函数空白实现inlineBase::~Base(){} //默认析构函数空白实现inlineBase*Base::operator&(){returnthis;} //取址运算符//const取址运算符inline
constBase*Base::operator&(){returnthis;}在类继承体系中,派生类需要显式调用基类的拷贝构造函数和赋值运算符,实现基类数据成员的复制或者赋值。编译器生成的缺省实现如下所示,其中:默认构造函数、默认析构函数不做任何操作,拷贝构造函数和赋值运算符按“位拷贝”语义复制对象的非静态数据成员,取址运算符返回对象地址。7.4.5基类和派生类的复制控制1、派生类的拷贝构造函数如果派生类定义拷贝构造函数,则需要显式调用基类拷贝构造函数初始化对象的基类部分:classBase{/*…*/};classDerived:publicBase{public: Derived(constDerived&d):Base(d);//显式调用 Base::Base(constBase&)
//… //初始化派生类的其他成员};7.4.5基类和派生类的复制控制2、派生类的赋值运算符赋值运算符(operator=)通常与拷贝构造函数类似,如果派生类定义赋值运算符,则也需要显式调用基类的赋值运算符复制对象的基类部分,如下:Derived&Derived::operator=(constDerived&rhs){ if(this!=&rhs){ //防止自身复制
Base::operator=(rhs); //调用基类的赋值运算符Base::operator=
…
//复制派生类的其它成员
}
r
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 商洛地区柞水县2025-2026学年第二学期四年级语文期末考试卷(部编版含答案)
- 宝鸡市金台区2025-2026学年第二学期四年级语文第八单元测试卷(部编版含答案)
- 水下钻井设备操作工岗前技能掌握考核试卷含答案
- 诊断试剂生产工安全管理模拟考核试卷含答案
- 斫琴师安全实践测试考核试卷含答案
- 2026年能效提升项目验收标准:节能量核定方法
- 六安市舒城县2025-2026学年第二学期四年级语文第七单元测试卷(部编版含答案)
- 宜宾市兴文县2025-2026学年第二学期五年级语文第八单元测试卷(部编版含答案)
- 巴彦淖尔盟杭锦后旗2025-2026学年第二学期三年级语文期末考试卷(部编版含答案)
- 许昌市鄢陵县2025-2026学年第二学期五年级语文期末考试卷(部编版含答案)
- 2025年4月自考00015英语(二)试题
- 《医学免疫学》习题集题库+答案
- 2025年土壤环境科学与治理考试题及答案
- 认识水课件-科学一年级下册冀人版
- 口腔材料学 第六章 树脂基复合材料学习课件
- 江苏省南京市(2024年-2025年小学六年级语文)部编版质量测试(下学期)试卷及答案
- DB45T 2329-2021 溶洞旅游接待服务规范
- (高清版)WST 418-2024 受委托医学实验室选择指南
- 清廉学校建设工作清单表格
- 幼儿园幼儿园小班社会《兔奶奶生病了》
- (新版)老年人能力评估师理论考试复习题库(含答案)
评论
0/150
提交评论