C++继承,虚函数与多态性专题

阅读: 评论:0

2024年2月8日发(作者:)

C++继承,虚函数与多态性专题

本文作者:黄邦勇帅学习本文首先你应熟悉C++中的构造函数,基本的类的声明及怎样初始化类,关于这些问题,请参看本人所作的《C++构造函数,复制构造函数和析构函数》一文,在这篇文章中作了详细的介绍。本文分两部分即继承和虚函数与多态性,本文第一部分详细讲解了继承时的构造函数和析构函数的问题,父类与子类的同名变量和函数问题,最后介绍了多重继承与虚基类。本文第二部分重点介绍了虚函数与多态性的问题,因此学习虚函数的基础是继承,因此在学习虚函数前应学好继承。本文详细易懂,内容全面,是学习C++的不错的资料。本文内容完全属于个人见解与参考文现的作者无关,其中难免有误解之处,望指出更正。声明:禁止抄袭本文,若需要转载本文请注明转载的网址,或者注明转载自“黄邦勇帅”。主要参考文献:1、C++..第五版.中文版[美]Stephen Prata著孙建春韦强译人民邮电出版社2005年5月2、C++..第四版.中文版Stanley n、Barbara 著李师贤等译人民邮电出版社2006年3月3、C++..第三版.中文版Stanley n等著潘爱民张丽译中国电力出版社2002年5月4、C++入门经典第三版[美]Ivor Horton著李予敏译清华大学出版社2006年1月5、C++参考大全第四版[美]Herbert Schidt著周志荣朱德芳于秀山等译电子工业出版社2003年9月6、21天学通第四版C++ [美]Jesse Liberty著康博创作室译人民邮电出版社2002年3月14 继承(基类,父类,超类),派生类,子类一:继承中的访问权限关系。1.基类,父类,超类是指被继承的类,派生类,子类是指继承于基类的类.2.在C++中使用:冒号表示继承,如class A:public B;表示派生类A从基类B继承而来3.派生类包含基类的所有成员,而且还包括自已特有的成员,派生类和派生类对象访问基类中的成员就像访问自已的成员一样,可以直接使用,不需加任何操作符,但派生类仍然无法访问基类中的私有成员.4.在C++中派生类可以同时从多个基类继承,Java不充许这种多重继承,当继承多个基类时,使用逗号将基类隔开.5.基类访问控制符,class A:public B基类以公有方式被继承,A:private B基类以私有方式被继承,A:protected B基类以受保护方式被继承,如果没有访问控制符则默认为私有继承。6.protected受保护的访问权限:使用protected保护权限表明这个成员是私有的,但在派生类中可以访问基类中的受保护成员。派生类的对象就不能访问受保护的成员了。7.如果基类以public公有方式被继承,则基类的所有公有成员都会成为派生类的公有成员.受保护的基类成员成为派生类的受保护成员7.如果基类以private私有被继承,则基类的所有公有成员都会成为派生类的私有成员.基类的受保护成员成为派生类的私有成员.8.如果基类以protected受保护方式被继承,那么基类的所有公有和受保护成员都会变成派生类的受保护成员.9.不管基类以何种方式被继承,基类的私有成员,仍然保有其私有性,被派生的子类不能访问基类的私有成员.例:继承中的访问权限关系classA{inta;protected:intb;public:intc;A(){a=b=c=1;}};//类B以公有方式从基类A继承classB:publicA{public: intd;B(){//a=2; //错误,不能访问基类中的私有成员b=2; //正确,可以在类中访问基类中的受保护成员,但类的对象不能访问,基类中的受保护成员b在类B中仍然是受保护成员c=d=2;} };//基类中的公有成员c在类B中仍然是公有成员//以受保护和私有方式从基类A继承。classC:protectedAC(){//a=3; //错误,不能访问基类中的私有成员b=c=e=3; }};//这里基类受保护成员b和公有成员c都成为类C中的受保护成员。classD:privateA {public: D(){b=c=4;} };//基类中的公有和受保护成员都成为了类D中的私有成员。//验证受保护和私有方式继承的访问权限。classC1:publicC{public: C1(){b=c=e=4;} };//正确;类A中的成员b和c在类C中是以受保护方式被继承的,b和c都成为了类C中的受保护成员。classD1:publicD{public:D1(){//b=5; //错误,在A中受保护的成员b在类D中是以私有方式继承的,这样b就成为了类D中的私有成员,所以无法访问。{public: inte;

//c=5; //错误,在A中公有的成员c在类D中是以私有方式继承的,这样c就成为了类D中的私有成员,所以无法访问。} };intmain(){A m1; B m2; C m3; D m4;//cout<

ma.f(); //正确,在子类中创建的基类对象,可以直接用对象名调用基类中被子类覆盖或隐藏了的函数,因为这时不会出现二义性。ma.f(1);} //正确,在子类中创建的基类对象,可以直接用对象名调用基类中被子类覆盖或隐藏了的函数,因为这时不会出现二义性。voidg(){cout<<"this g"<<"n"; f(); //正确,但该语句访问的是子类中的不带参数函数f,虽然在类中使用了using语句,但直接调用被子类覆盖了的基类函数时不能使用这种方法A::f(); }};//正确,调用被子类覆盖了的基类中的函数f,注意,虽然使用了using但要访问被子类覆盖了的函数,只能这样访问。intmain(){B m;m.f(); //调用子类中的不带参数的函数,这里不会调用基类中的不带参数的被覆盖的函数f。m.A::f(); //调用基类中被子类覆盖了的函数f,虽然子类使用了using语句,但要访问基类中被覆盖的方法只能像这样使用。

m.f(1); //调用基类重载的f函数,注意这里可以不用::运算符,因为在子类中使用了using,只要子类没有覆盖基类中的方法,都可以这样直接调用。m.A::f(2); } //当然,使用了using后,也可以使用这种方法例:派生类以私有方式被继承时改变基类中的公有成员为公有的方法classA{public: inta,b;voidf(){cout<<"f"<<"n";} voidg(){cout<<"g"<<"n";}};classB:privateA{public: A::f;A::a;//使用::运算符使基类中的成员成为公有的。注意函数名后不能有括号。usingA::g;};//使用using语句使基类中的成员函数g成为类B中的公有成员,注意函数名后不能有括号。intmain(){ B m;//m.b=1; //错误,因为类B是以私有方式继承的,类A中的成员在类B中是私有的,这里不能访问私有成员。m.f(); m.g(); m.a=1;}三:继承时的构造函数和析构函数问题1.在继承中,基类的构造函数构建对象的基类部分,派生类的构造函数构建对象的派生类部分。2.当创建派生类对象时先用派生类的构造函数调用基类的构造函数构建基类,然后再执行派生类构造函数构造派生类。即先构造基类再构造派生类的顺序。执行析构函数的顺序与此相反。3.调用基类带参数的构造函数的方法:在派生类的构造函数中使用初始化列表的形式就可以调用基类带参数的构造函数初始化基类成员,如B():A(int i){},类B是类A的派生类。4.派生类的构造函数调用基类的构造函数的方法为:4.1 如果派生类没有显示用初始化列表调用基类的构造函数时,这时就会用派生类的构造函数调用基类的默认构造函数,构造完基类后,才会执行派生类的构造函数函数体,以保证先执行基类构造函数再执行派生类构造函数的顺序,如果基类没有默认构造函数就会出错。4.2 如果派生类用显示的初始化列表调用基类的构造函数时,这时就会检测派生类的初始化列表,当检测到显示调用基类的构造函数时,就调用基类的构造函数构造基类,然后再构造派生类,以保证先执行基类构造函数再执行派生类构造函数的顺序,如果基类没有定义派生类构造函数初始化列表调用的构造函数版本就会出错。6.如果在基类中没有定义默认构造函数,但定义了其他构造函数版本,这时派生类中定义了几个构造函数的不同版本,这时只要派生类有一个构造函数没有显示调用基类中定义的构造函数版本就会发生错误,因为编译器会首先检查派生类构造函数调用基类构造函数的匹配情况,如果发现不匹配就会出错,即使没有创建任何类的对象都会出错,而不管这个派生类的对象有没有调用派生类的这个构造函数。比如:基类有一个构造函数版本A(int i)而没有定义默认构造函数,派生类B,有这几个版本的构造函数B():A(4){},B(int i):A(5){},再有语句B(int i, int j){}没有显示调用基类定义的构造函数而是调用基类的默认构造函数,如果创建了B m和语句B m(1)时都会提示没有可用的基类默认构造函数可用的错误,虽然这时类B的对象m没有调用派生类B的带有两个形参的构造函数,但同样会出错。7.同样的道理,如果基类中定义了默认构造函数,却没有其他版本的构造函数,而这时派生类却显示调用了基类构造函数的其他版本,这时就会出错,不管你有没有创建类的对象,因为编译器会先在创建对象前就检查构造函数的匹配问题。8.派生类只能初始化他的直接基类。比如类C是类B的子类,而类B又是类A的子类,这时class C:public B{public:

B():A(){} };将会出错,该语句试图显示调用类B的基类类A的构造函数,这时会出现类A不是类C的基类的错误。9.继承中的复制构造函数和构造函数一样,基类的复制构造函数复制基类部分,派生类的复制构造函数复制派生类部分。10.派生类复制构造函数调用基类复制构造函数的方法为:A(const A& m):B(m){}其中B是基类,A是派生类。11.如果在派生类中定义了复制构造函数而没有用初始化列表显示调用基类的复制构造函数,这时不管基类是否定义了复制构造函数,这时出现派生类对象的复制初始化情况时就将调用基类中的默认构造函数初始化基类的成员变量,注意是默认构造函数不是默认复制构造函数,如果基类没有默认构造函数就会出错。也就是说派生类的复制构造函数的默认隐藏形式是B(const B& j):A(){}这里B是A的派生类,也就是说如果不显示用初始化列表形式调用基类的复制构告函数时,默认情况下是用初始化列表的形式调用的是基类的默认构造函数。12.当在派生类中定义了复制构造函数且显示调用了基类的复制构造函数,而基类却没有定义基类的复制构造函数时,这时出现派生类对象的复制初始化情况就将调用基类中的默认复制构造函数初始化基类部分,调用派生类的复制构造函数初始化派生类部分,因为复制构造函数只有一种形式,即A(const A& m){},比如当出现调用时A(const A&

m):B(m){}如果这时基类B没有定义复制构造函数,则该语句将会调用派生类A的默认复制构造函数。12.如果基类定义了复制构造函数,而派生类没有定义时,则会调用基类的复制构造函数初始化基类部分,调用派生类的默认复制构造函数初始化派生类部分。例:基类不定义默认构造函数,派生类不定义复制构造函数而基类定义复制构造函数的情形。//类A不定义默认构造函数classA{public:inta,a1;TUVHIOPQRSS"hahaA"<<"n";}A(inti){a=a1=11;cout<<"goucaoA1"<<"n";}};A(constAEFGHIJKLMFJNKLMOPQRSS"fugouA"<<"n";}//类B不定义复制构造函数classB:publicA{public: intb,b1;//B(){b=b1=2; cout<<"goucaoB"<<"n";} //错误,此语句会调用基类中的默认构造函数,而基类没有默认构造函数。

//B(int i,int_HI`K`NKaMbFFcc错误,同上,此语句没有显示调用基类构造函数,就会调用基类的默认构造函数,而这时基类没有默认构造函数。B(inti):A(2){b=b1=3;cout<<"goucaoB1"<<"n";} jkVHIOPQRSS"hahaB"<<"n";}};//显示调用基类带一个形参的构造函数,注意语法intmain(){ B m(rHMFFOPQRS

//B(int i,intzH{UVL|LHI`K`NKaMbFFcc错误,调用了基类没有定义的带两个参数的构造函数。B(inti):A(2){b=b1=3;cout<<"goucaoB1"<<"n";}B(constB•F€HI`K`NK•MOPQRSS"fugouB"<<"n";} //注意,这里没有显示调用基类的复制构造函数,而是用默认的B(const B„F€H{UVHIb的形式调用的基类中的默认构造函数,注意是默认构造函数而不是默认复制构造函数。‹kVHIOPQRSS"hahaB"<<"n";}};intmain(){ B m(ŒHMFFOPQRSS•ŽJSS•Ž`SS"n";// 输出113。B m1(m); cout<

D的副本,比如class A:public B, public C{}这时类A的对象就只会有一个类D的副本,类A类B类C类D四个类都共享一个类D的副本,比如类D有一个公有成员变量d,则m.d和m.A::d,m.B::d,m.C::d,m.D::d都将访问的是同一个变量。这样类A的对象调用类D中的成员时就不会出现二义性了。5.虚基类的构造函数:比如class B:public virtual D{};class C:virtual public D{}; class A:public B,public C{};这时当创建类A的对象A m时初始化虚基类D将会使用类A的构造函数直接调用虚基类的构造函数初始化虚基类部分,而不会使用类B或者类C的构造函数调用虚基类的构造函数初始化虚基类部分,这样就保证了只有一个虚基类的副本。但是当创建一个类B和类C的对象时仍然会使用类B和类C中的构造函数调用虚基类的构造函数初始化虚基类。例:虚基类及其构造函数classA{public:inta;A(){cout<<"moA"<<"n";}A(inti){a=i;cout<<"A";}};classB:publicvirtualA{public: intb;B(inti):A(4){cout<<"B";} };//以虚基类的方式继承classC:publicvirtualA{public: intc; C():A(5){cout<<"C";}};classD:publicB, publicC{public: intd;D():A(4),C(),B(2){cout<<"D";}};//因为类D是虚基类,所以类D会直接调用虚基类的构告函数构造虚基类部分,而不会使用类B或者类C的构造函数来调用虚基类的构造函数初始化虚基类部分。要调用虚基类中的带参数的构造函数必须在这里显示调用,如果不显示调用就将调用虚基类的默认构造函数。intmain(){D m; //输出ABCD,注意这里没有重复的基类副本A。C m1; //输出AC,虽然D是虚基类,但当创建类C的对象时仍然会使用类C的构造函数调用虚基类的构造函数初始化虚基类部分。cout<

p=ⅆ是可以的,指针p只能访问从基类派生而来的成员,不能访问派生类D特有的成员.因为基类不知道派生类中的这些成员。2.不能使派生类指针指向基类对象.3.如果派生类中覆盖了基类中的成员变量或函数,则当声明一个基类指针指向派生类对象时,这个基类指针只能访问基类中的成员变量或函数。例如:基类B和派生类D都定义了函数f,则B *p; D m; p=&m; m.f()将调用基类中的函数f()而不会调用派生类中的函数f()。4.如果基类指针指向派生类对象,则当对其进行增减运算时,它将指向它所认为的基类的下一个对象,而不会指向派生类的下一个对象,因此,应该认为对这种指针进行的增减操作是无效的.二:虚函数1.为什么要使用虚函数:正如上面第1和3点所讲的,当声明一个基类指针指向派生类对象时,这个基类指针只能访问基类中的成员函数,不能访问派生类中特有的成员变量或函数。如果使用虚函数就能使这个指向派生类对象的基类指针访问派生类中的成员函数,而不是基类中的成员函数,基于这一点派生类中的这个成员函数就必须和基类中的虚函数的形式完全相同,不然基类指针就找不到派生类中的这个成员函数。使用虚函数就实现了一个接口多种方法。2.注意不能把成员变量声明为虚有的,也就是说virtual关见字不能用在成员变量前面。3.正如上面所介绍的,一般应使用基类指针来调用虚函数,如果用点运算符来调用虚函数就失去了它的意义.4.如果基类含有虚函数则当声明了一个基类的指针时,当基类指针指向不同的派生类时,它就会调用相应派生类中定义的虚函数版本.这种调用方法是在运行时决定的,例如在类B中声明了虚函数,C,D,E都从B继承而来且都实现了自已的虚函数版本,那么当定义了一个B类的指针P时,当P指向子类C时就会调用子类C中定义的虚函数,当P指向子类D时就会调用子类D中定义的虚函数,当P指向子类E时就会调用子类E中定义的虚函数.5.虚函数须在基类中用virtual关见字声明也可以在基类中定义虚函数,并在一个或多个子类中重新定义.重定义虚函数时不需再使用virtual关见字,当然也可以继续标明virtual关见字,以便程序更好理解。6.包括虚函数的类被称为多态类.C++使用虚函数支持多态性.7.在子类中重定义虚函数时,虚函数必须有与基类虚函数的声明完全相同的参数类型和数量,这和重载是不同的.如果不相同,则是函数重载,就失去了虚函数的本质.

8.虚函数不能是声明它的类的友元函数,必须是声明它的类的成员函数,不过虚函数可以是另一个类的友元.9.一旦将函数声明为虚函数,则不管它通过多少层继承,它都是虚函数,例如D和B继承,而E又从D继承,那么在B中声明的虚函数,在类E中仍然是虚函数.10.隐藏虚函数:如果基类定义了一个虚函数,但派生类中却定义了一个虚函数的重载板本,则派生类的这个版本就会把基类的虚函数隐藏掉,当使用基类指针调用该函数时只能调用基类的虚函数,而不能调用派生类的重载版本,当用派生类的对象调用基类的虚函数时就会出现错误了,因为基类的虚函数被派生类的重载版本隐藏了。11.带默认形参的虚函数:当基类的虚函数带有默认形参时,则派生类中对基类虚函数的重定义也必须有相同数量的形参,但形参可以有默认值也可以没有,如果派生类中的形参数量和基类中的不一样多,则是对基类的虚函数的重载。对虚函数的重定义也就意味着,当用指向派生类的基类指针调用该虚函数时就会调用基类中的虚函数版本。比如基类定义virtual void f(int i=1, int j=2){}则派生类中必须定义带有两个形参的函数f才是对基类虚函数f的重定义,不然就是函数f的重载版本,比如派生类中定义的void f(),void f(int i),void f(int i=2)都是对函数f的重载,不是对f的重定义。而void f(int i, int j),void f( int i, int j=3),void f(int i=4, int j=5)都是对虚函数f的重定义。12.如果虚函数形参有默认值,那么派生类中的虚数的形参不论有无默认值,当用指针调用派生类中的虚函数时就会被基类的默认值覆盖,即派生类的默认值不起作用。但用派生类的对象调用该函数时,就不会出现这种情况。13.当用指向派生类的基类指针调用虚函数时是以基类中的虚函数的形参为标准的,也就是只要调用的形式符合基类中定义的虚函数的标准就行了。比如基类中定义virtual void f(int i=1,int j=2){}派生类中重定义为void f(int i, int j=3){}这时如果用派生类的对象调用这个派生类中的虚函数f时必须至少要有一个实参,但是用指向派生类的基类指针调用该虚函数时就可以不用任何形参就能调用派生类中的这个函数f,比如语句p->f()就会调用派生类中的虚函数版本。当用指向派生类的基类指针调用虚函数时是以基类中的虚函数的形参为标准的,也就是只要调用的形式符合基类中定义的虚函数的标准就行了。14.析构函数可以是虚函数,但构造函数不能.15.纯虚函数声明形式为virtual 类型函数名(参数列表)=0;注意后面的等于0;16.如果类至少有一个纯虚函数,则这个类就是抽象的。17.如果基类只是声明虚函数而不定义虚函数则此虚函数是纯虚函数.任何派生类都必须实现纯虚函数的自已的版本.如果不实现纯虚函数那么该类也是抽象类。18.抽象类不能有对象,抽象类只能用作其它类的基类,因为抽象类中的一个或多个函数没有定义,所以不能用抽象类声明对象,19.仍然可以用抽象类声明一个指针,这个指针指向派生类对象.20.如果派生类中未定义虚函数,则会使用基类中定义的函数.21.虚函数虚拟特性是以层次结构的方式来继承的,例如C从B派生而且C中重定义了B中的虚函数,而D又从C派生且未重定义B中的虚函数,这时声明一个基类指针P,当P指向类D,并调用D中的虚函数时,由于D中未重定义虚函数他会调用基类中的虚函数版本,这时他会调用类C中的虚函数而不是类B中的虚函数,因为类C比类B更接近于类D.例:虚函数的应用classA{public:inta;virtualvoidf(){cout<<"èéêQë<<"n";}virtualvoidh(inti=1,intìKíHIOPQRSS"îéêQïë<<"n";}ðUVHIOPQRSS"ñéUë<<"n";}//virtual int b; //错误,不能把成员变量声明为虚有的。};classB:publicA{public: intb;voidf(inti){cout<<"paif()"<<"n";}//重载虚函数f。voidf(){cout<<"paiõQë<<"n";}//在派生类中重定义虚函数fvoidh(){intb;b=5; cout<<"paifu"<

m.A::h();}// 调用基类中的虚函数h.13.1.8 虚析构函数1.为什么需要虚析构函数:当使用new运算符动态分配内存时,基类的析构函数就应该定义为虚析构函数,不然就会出问题。比如类B由类A继承而来,则有语句A *p= new A;delete p; 这时没有问题,调用类A的析构函数释放类A的资源。但如果再把类B的内存动态分配给指针p时如p= new B; delete p;如果基类的析构函数不是虚析构函数的话就会只调用基类A中的析构函数释放资源,而不会调用派生类B的析构函数,这时派生类B的资源没有被释放。2.解决这个问题的方法是把基类的析构函数声明为虚析构函数,即在析构函数前加virtual关见字,定义为虚析构函数时当用delete释放派生类的资源时就会根据基类的析构函数自动调用派生类中的析构函数释放派生类的资源。3.只要基类中的析构函数是虚析构函数,则该基类的派生类中的析构函数自动为虚析构函数,虽然派生类中的析构函数前没有virtual关见字,析构函数名字也不一样,但派生类中的析构函数被自动继承为虚析构函数。4.如果要使用new运算符分配内存,最好将析构函数定义为虚析构函数。例:使用new分配内存,但不定义为虚析构函数的情形classA{public:inta; žzBCDEFGHII"ŸKzL<<"n";}};classB:publicA{public: intb; ABCDEFGHII"¡KAL<<"n";}};classC:publicB{public: intc; ¢£BCDEFGHII"¤K£L<<"n";}};intmain(){A ¥NOne¦A;deletep; //输出§KzR//B m; p=¨QRWWWXX此语句没有错,但是将使指针p指向一个静态分配的内存地址,这时不能用delete语句释放指针p的资源。//delete p; //错误,指针p现在指向的内容不是动态分配的内存,而是静态内存,delete只能释放动态分配的内存。p=neÁB; //动态分配派生类B的内存,并把地址赋给指针p。deletep; //输出ÈKzR在这里没有调用派生类的析构函数释放动态分配的派生类的内存资源。BÊNËOWneÌB;deletep1;//输出ÍKAÎJKzÏp1=neÐC;deletep1;//输出ÑKAÎJKz注意,这里没有释放掉子类C的资源。}例:使用new分配内存,且基类定义为虚析构函数的情形classA{public:inta;virtualÖzBCDEFGHII"×KzL<<"n";} };//基类定义为虚析构函数classB:publicA{public: intb;ÚABCDEFGHII"ÛKAL<<"n";}};//派生类B自动继承为虚析构函数classC:publicB{public: intc; à£BCDEFGHII"áK£L<<"n";}};//派生类C也自动继承为虚析构函数intmain(){A ãNOneäA;deletep; //输出åKzp=neæB;deletep; //输出çKAÎJKz因为基类定义的是虚析构函数,所以在这里调用派生类的析构函数释放动态分配的派生类的内存资源,并调用基类的析构函数释放基类的资源B ìN1= neíB;deletep1; //输出îKAÎJKzp1=neïC;deletep1; }//输出ðK£ÎJKAÎJKz这里因为类B的析构函数被自动继承为虚析构函数,所以这里释放了子类C的释源。作者:黄邦勇帅

C++继承,虚函数与多态性专题

本文发布于:2024-02-08 03:45:41,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170733514166428.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:基类   函数   派生类   调用   成员   使用   继承
留言与评论(共有 0 条评论)
   
验证码:
排行榜

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23