虚基类与虚函数.ppt
2023/10/22,1,5.2.2 虚基类,1.虚基类的概念 在C+语言中,一个类不能被多次说明为一个派生类的直接基类,但可以不止一次地成为间接基类。这就导致了一些问题。为了方便 说明,先介绍多继承的“类格”表示法。派生类及其基类可用一有向无环图(DAG)表示,其中的箭头表示“由派生而来”。类的DAG常称为一个“类格”。复杂类格画出来通常更容易理解。例如:,例 5-19 class L public:int next;class A:public L;class B:public L;class C:public A,public B public:void f()next=0;,这时,next有两个赋值语句next=0;具有二义性,它是将A:next置为零,还是将B:next置为零,或者将两者都置为0,需要在函数f()中被显式的说明.,2023/10/22,3,如果希望间接基类L与其派生类的关系是如下图,C+语言提供了这种描述手段。它将L说明为A和B的虚基类。,2023/10/22,4,当在多条继承路径上有一个公共的基类,在这些路径中的某几条路经汇合处,这个公共基类就会产生多个实例。如果只想保存这个基类的一个实例,可以将这个公共基类说明为虚拟基类或称虚基类。它仅是简单地将关键字virtual加到基类的描述上,例如改写上述例子为例5-20,例 5-20 class L public:int next;class A:virtual public L;class B:virtual public L;class C:public A,public B public:void f()next=0;,这时C类对象中只有L的一个复制,因而函数C:f()中的语句next=0;没有二义性。对于类C而言,L类是B类的虚基类,而是类A的真基类;但对于类B而言,L类还是B类的真基类。例 5-21,或class A:public virtual L,或class A:public virtual,class L public:int next;class A:virtual public L;class B:virtual public L;class C:public B,public A public:void f()next=0;,此例中,对于类C而言,L类是A类的虚基类,而是类B的真基类。,派生时,A,B的顺序变了,2023/10/22,7,一个派生类的对象的地址可以直接赋给虚基类的指针,例如:C obj;L*ptr=将产生编译错误。,2023/10/22,8,2.虚基类对象的初始化 虚基类的初始化与多继承的初始化在语法上是一样的,但隐含的构造函数的调用次序有点差别。虚基类构造函数的调用次序是这样规定的:1.虚基类的构造函数在非虚基类之前调用。2.若同一层次中包含多个虚基类,虚基类构造函数按它们说明的次序调用。3.若虚基类由非虚基类派生,则遵守先调用基类构造函数,再调用派生类构造函数的规则。,2023/10/22,9,例如:class X:public Y,virtual public Z X one;将产生如下调用次序:Z()Y()X()这里Z是X的虚基类,故先调用Z的构造函数,再调用Y的构造函数,最后才调用派生类X自己的构造函数。例 5-22,#include iostream.h class base public:base()coutBaseendl;class base2 public:base2()coutBase2endl;class level1:public base2,virtual public base public:level1()coutlevel1endl;class level2:public base2,virtual public base public:level2()coutlevel2endl;class toplevel:public level1,virtual public level2 public:toplevel()couttoplevelendl;,toplevel view;void main(),当建立对象view时,将产生如下调用次序:level2()level1()toplevel()而level2()要求:base()base2()level2()level1()要求 base2()level1()toplevel()要求 toplevel(),所以,构造函数的调用顺序为:base()base2()level2()base2()level1()toplevel(),例5-23 class base;class base2;class level1:public base2,virtual public base;class level2:public base2,virtual public base;class toplevel:virtual public level1,public level2;toplevel view;,level1():base()base2()level1()level2():base2()level2()toplevel():toplevel(),当建立对象view时,将产生如下调用次序:,此例中,对于toplevel的而言,base是level2的虚基类,2023/10/22,12,例 5-24 class B;class X:virtual public B;class Y:virtual public B;class Z:public B;class AA:public X,public Y,public Z;,这里AA具有两个B类的子对象:Z的B和x与Y共享的虚拟的B。,class V public:int V;class A public:int a;class B:public A,virtual public V;class C:public A,Virtual public V;class D:public B,public C public:void f();,void D:f()v+;a+;,例 5-25 虚基类和非虚基类的不同。,在D中仅仅一个v,错误,具有二义性,在D中有两个a,调用次序:B():V()A()B()C():A()C()D():D(),2023/10/22,14,5.3 虚函数与多态性,对于普通成员函数的重载,可表达为下面的方式:1)在同一个类中重载 2)在不同类中重载 3)基类的成员函数在派生类中重载 因此,重载函数的访问是在编译时区分的,有以下三种方法:,2023/10/22,15,1.根据参数的特征加以区分,例如:Show(int,char)与Show(char*,float)不是同一函数,编译能区分。2.使用“:”加以区分,例如:Circle:Show有别于Point:Show 3.根据类对象加以区分。ACircle.Show()调用Circle:Show()APoint.Show()调用Point:Show()这里ACircle和APoint分别是Circle和Point的对象。,例 5-26#include class A public:void fun()cout“In A”endl;class B:public A public:void fun()cout“In B”endl;class C:public B public:void fun()cout“In C”endl;,void main()C Cobj;Cobj.fun();Cobj.B:fun();Cobj.A:fun();A,5.3.1 基类对象的指针指向派生类对象,指向基类和派生类的指针变量是相关的,假设B_class是基类,D_class是从B_class公有派生出来的派生类,任何被说明为指向B_class的指针也可以指向D_class。例如:,利用p,可以访问从基类B_class继承的成员,但D_class自己定义的成员,p不能访问。例如:例5-27,指向类型B_class的对象的指针,类型B_class的对象,类型D_class的对象,p指向类型D_class的对象,它是 B_class 的派生类,p指向类型B_class的对象,B_class*p;B_class B_ob;D_class D_ob;p=,#include#include class B_classchar name80;public:void put_name(char*s)strcpy(name,s);void show_name()coutnamen;,main()B_class*p;B_class B_ob;D_class*dp;D_class D_ob;p=,错误,错误,该程序输出:Thomas Edison Albert Einstein 555555_1234 555555_1234,class D_class:public B_class char phone_num80;public:void put_phone(char*num)strcpy(phone_num,num);void show_phone()coutphone_numn;,基类指针指向派生类类对象,只能访问基类成员,派生类指针指向派生类类对象,访问所有成员,基类指针,派生类指针,1.可以用一个指向基类的指针指向其公有派生类的对象。但是相反却不正确,即不能用指向派生类的指针指向一个基类的对象。2.希望用基类指针访问其公有派生类的特定成员,必须将基类指针用显式类型转换为派生类指针。例如:(D_class*)p)-show_phone();,5.3.2 虚函数,例 5-28#include class Base protected:int x;public:Base(int a)x=a;void who()cout“base”x“n”;,class Second_d:public Base public:Second_d(int a):Base(a)void who()cout“Second derivation”x“n”;,class First_d:public Base public:First_d(int a):Base(a)void who()cout“First derivation”x“n”;,1.虚函数的概念,void main()Base*p;Base base_obj(1);First_d first_obj(2);Second_d second_obj(3);p=,该程序输出:base 1base 2base 3First derivation 2Second derivation 3,p-who(),指向基类的指针p,不管是指向基类的对象base_obj还是指向派生的对象first_obj和second_obj,p-who()调用的都是基类定义的 who()的版本.必须显式地用 first_obj.who();和 second_obj.who();才能调用类first_d和类second_d中定义的who()的版本。其本质的原因在于普通成员函数的调用是在编译时静态区分。,2023/10/22,22,如果随着p所指向的对象的不同pwho()能调用不同类中who()版本,这样就可以用一个界面p-who()访问多个实现版本:Base中的who(),First_d 中的 who(),以及Second_d中的who(),这在编程时非常有用。实际上,这表达了一种动态的性质,函数调用p-who()依赖于运行时p所指向的对象。虚函数提供的就是这种解释机制。如果在base中将成员函数who()说明为虚函数,则修改上述程序为例5-29,虚函数是在基类中被冠以virtual的成员函数,它提供了一种接口界面。虚函数可以在一个或多个派生类中被重新定义,但要求在派生类中 重新定义时,虚函数的函数原型,包括返回类型,函数名,参数个数,参数类型的顺序,必须完全相同。,class First_d:public Base public:First_d(int a):Base(a)void who()“First derivation“x“n”;,#include class Base protected:int x;public:Base(int a)x=a;virtual void who()cout“base”x“n”;,class Second_d:public Base public:Second_d(int a):Base(a)void who()“Second derivation“x“n”;,void main()Base*p;Base base_obj(1);First_d first_obj(2);Second_d second_obj(3);p=,程序输出:base 1 First derivation 2 Second derivation 3 First derivation 2 Second derivation 3,基类的虚函数who()定义了一种接口,在派生类中此接口定义了不同的实现版本,由于虚函数的解释机制,实现了“单界面,多实现版本”的思想。这种在运行时刻将函数界面与函数的不同实现版本进行匹配的过程,称为晚期匹配,也称为运行时的多态性。,p-who(),基类函数f具有虚特性的条件是:1)在基类中,将该函数说明为virtual函数。2)定义基类的公有派生类。3)在基类的公有派生类中原型一致地重载该虚函数。4)定义指向基类的指针变量,它指向基类的公有派生类的对象。例 5-30,void main()derived d;base*bp=,class base public:virtual void vf1();virtual void vf2();virtual void vf3();void f();class derived:public base public:void vf1();void vf2(int);char vf3();void f();;,错误,仅返回类型不同,具有虚特性,一般函数重载,参数不同,虚特性丢失,一般的函数重载,非虚函数的重载,例 5-31#include class figure protected:double x,y;public:void set_dim(double i,double j=0)x=i;y=j;virtual void show_area()cout“No area computation defined”;cout“for this class.n”;,class triangle:public figure public:void show_area()cout“Triangle with high”;coutx“and base”y;cout“has an area of”;coutx*0.5*y“n”;,class square:public figurepublic:void show_area()cout“Square with dimension”;coutx“*”y;cout“has an area of”;coutx*y“n”;;,class circle:public figure public:void show_area()cout“Circle with radius”;coutx;cout“has an aera of”;cout3.14*x*x;,void main()figure*p;triangle t;square s;circle c;p=p-show_area();,程序输出:Triangle with high 10 and base 5 has an area of 25.0Square with dimension 10*5 has an area of 50.0Circle with radius 9 has an area of 254.34,2.可以使用成员名限定可以强制使用静态联编,例 5-32#include class A public:virtual void fun()cout“In A”end1;,class B:public A public:void fun()cout“In B”end1;,class C:public B public:void fun()cout“In C”end1;,void main()C Cobj;Cobj.fun();Cobj.B:fun();Cobj.A:fun();A*Aref=,调用B:fun()不是C:fun()使用成员名限定可以强制使用静态联编,2023/10/22,30,3.在成员函数中调用虚函数,在一个基类或派生类的成员函数中,可以直接调用等级中的虚函数。此时,需要根据成员函数中this指针和它所指向的对象来判断调 用的是哪个函数。,例 5-33,#include class A public:virtual void fun1()cout“A1-2”endl;fun2();virtual void fun2()cout“A2-3”endl;fun3();virtual void fun3()cout“A3-4”endl;fun4();virtual void fun4()cout“A4-5”endl;fun5();virtual void fun5()cout“A end”endl;,class B:public A public:void fun1()cout“B 1-2”;fun2();void fun2()cout“B 2-3”;fun3();void fun3()cout“B 3-4”;fun4();void fun4()cout“B 4-5”;fun5();void fun5()cout“B end”endl;,void main()A*Apointer1=new A;A*Apointer2=new B;Apointer1-fun1();Apointer2-fun1();delete Apointer1;delete Apointer1;,程序输出:A 1-2 A 2-3 A 3-4 A 4-5 A end B 1-2 B 2-3 B 3-4 B 4-5 B end,fun2()相当于fun2(const A*this)this即为Apointer2,因此仍然调用所指向对象中的函数,例 5-34#include class A public:void fun1()cout“A1-2”endl;fun2();virtual void fun2()cout“A2-3”endl;fun3();virtual void fun3()cout“A3-4”endl;fun4();virtual void fun4()cout“A4-5”endl;fun5();virtual void fun5()cout“A end”endl;,class B:public A public:void fun1()cout“B 1-2”endl;fun2();void fun2()cout“B 2-3”endl;fun3();void fun3()cout“B 3-4”endl;fun4();void fun4()cout“B 4-5”endl;fun5();void fun5()cout“B end”endl;,void main()A*Apointer=new B;Apointer-fun1();delete Apointer;,程序输出:A 1-2B 2-3B 3-4B 4-5B end,fun1()不是虚函数,故基类的指针变量,指向派生类时只能访问基类中定义的成员。,fun2,fun3,fun4,fun5是虚函数,故基类的指针变量,指向派生类时访问的是派生类中定义的成员。,例 5-35#include class A public:virtual void fun1()cout“A1-2”endl;fun2();virtual void fun2()cout“A2-3”endl;fun3();virtual void fun3()cout“A3-4”endl;fun4();virtual void fun4()cout“A4-5”endl;fun5();virtual void fun5()cout“A end”endl;,class B:public A public:void fun3()cout“B 3-4”endl;fun4();void fun4()cout“B 4-5”endl;fun5();,void main()A*Apointer=new B;Apointer-fun1();delete Apointer;,程序输出:A 1-2 A 2-3 B 3-4 B 4-5 A end,基类虽然将fun1,fun2定义为虚函数,但在派生类中并没有原型一致的重载它们,所以要调用基类中的函数。,2023/10/22,34,4.在构造函数和析构函数中调用虚函数,在构造函数和析构函数中调用虚函数时,采用静态联编。即它们所调用的虚函数是自己的类或者它的基类中的虚函数,但不是任何在派生类中定义的虚函数。例 5-36,#include class A public:A()cout“A is Creating”endl;virtual void fun1()cout“A fun1”endl;virtual void fun2()cout“A fun2”endl;A()cout“A is Destroy”endl;,class B:public A public:B()cout“B is Creating”endl;fun1();void fun()fun1();B()cout“B is destroy”end1;fun2();,class C:public B public:C()cout“C is Creating”endl;void fun1()coutC fun1”endl;virtual void fun2()coutC fun2”endl;C()cout“C is Destroy”endl;fun2();,void main()C Cobj;Cobj.fun();,程序输出:A is CreatingB is CreatingA fun1C is CreatingC fun1C is destroyC fun2 B is destroyA fun2A is destroy,在构造函数和析构函数中调用虚函数时,是自己的类或者它的基类中的虚函数,但不是任何在派生类中定义的虚函数。,2023/10/22,36,5.析构函数可以定义为虚函数,构造函数不能为虚函数,而析构函数可以定义为虚函数。若析构函数为虚函数,那么当使用delete释放基类指针指向的派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。,class Deriver:public Base int d;public:Deriver(int num1,int num2):Base(num1)d=num2;cout“Deriver createn”;Deriver()cout“Deriver destoryn”;,例 5-37#include class Base int b;public:Base(int num)b=num;cout“Base createn”;Base()cout“Base destroyn”;,void main()Base*pb1,*pb2;pb1=new Base(1);pb2=new Deriver(2,3);delete pb1;delete pb2;cout“*n”;Base Bobj(4);Deriver Dobj(5,6);,程序输出:Base create Base create Deriver create Base destroy Base destory*Base create Base create Deriver create Deriver destroy Base destroy Base destory,基类对象指向派生类对象时,释放时不调用派生类的析构函数,例 5-38#include class Base int b;public:Base(int num)b=num;cout“Base createn”;virtual Base()cout“Base destroyn”;class Deriver:public Base int d;public:Deriver(int num1,int num2):Base(num1)d=num2;cout“Deriver createn”;Deriver()cout“Deriver destoryn”;,void main()Base*pb1,*pb2;pb1=new Base(1);pb2=new Deriver(2,3);delete pb1;delete pb2;,Base create Base create Deriver create Base destroy Deriver destroy Base destroy,需要先调用派生类的析构函数,再调用基类的析构函数,2023/10/22,39,6.多继承和虚基类,在多继承中,由于能从多条路径继承,如何确定函数调用时应该激活哪一个函数版本呢?下述例子说明了一个问题 如:例 5-39,struct A1 int f();struct A2:virtual A1/缺省为公有派生方式 int f();struct A3:A2;struct A4:A3,virtual A1;void f(A2*pa2,A4*pa4)pa2-f();/调用 A2:f()pa4-f();/调用 A2:f()继承路径如图:,A1():A1()A3():A2()A3()A4():A4(),创建A4的对象时,构造函数的调用次序为:,由于A2是A1的派生类,A2中重新定义的函数f覆盖了类A中定义的函数f,而且A4和A2都将A1说明为虚基类,因此,pa4-f()调用的是 A2:f()虚基类也能应用这个规则。,有人可能会想,A1:f()离A4更近,因为A1是A4的直接基类,而A2不是。pa4-f()应该调用A1:f(),而不是调用A2:f()。情况并非如此,由DAG图可见,根据继承路径pa4-f()应有两种调用选择:A2:f();和 A1:f(),2023/10/22,42,5.3.3 纯虚函数及抽象类,基类表示抽象的概念,如figure是一个基类表示有型的东西,可以派生出封闭图形和非封闭图形两类。Shape体现了一个抽象的概念,在figure中定义一个求面积的函数显然是无意义的,但可以将其声明为一个虚函数,提供一个派生的公共界面,并由各派生类提供求面积的各自版本。因此基类的有些虚函数没有定义是很正常的,但是要求派生类必须重新定义这些虚函数。为此 C+引入了纯虚函数的概念。,2023/10/22,43,纯虚函数是一个在基类中说明的虚函数它在基类中没有定义,要求任何派生类必须定义自己的版本。纯虚函数具有以下的形式:virtual type func_name(参数表)0;在构造函数和析构函数中调用虚函数使用静态编联,因此在这两个函数中不能调用纯虚函数。但其它函数可以调用纯虚函数。,2023/10/22,44,如果一个类至少有一个纯虚函数,就称这个类为抽象类。抽象类可以定义一种接口,由派生类提供各种实现。抽象类只能用作其它类的基类.可以用作声明抽象类的指针和引用。不能创建对象。不能用作参数 不能用作函数返回类型或显式转换的类型。,例 5-40class point;class shape point center;public:point where()return center;void move(point p)center=p;draw();virtual void rotate(int)=0;virtual void draw()=0;,shape x;shape*p;shape fun();void g(shape);x=shape(23);shape,错误,抽象类不能建立对象,可以声明抽象类的指针,错误,抽象类不能作为返回类型,错误,抽象类不能作为参数类型,可以声明抽象类的引用,不能用作显式类型转换,2023/10/22,46,如果派生类没有原型一致地重载该纯虚函数。从基类继承来的纯虚函数,在派生类中仍是纯虚函数。例如:class ab_circle:public shape int radius;public:void rotate(int);由于 shape:draw()是一个纯虚函数,缺省的ab_cricle:draw也是一个纯虚函数,这时ab_circle仍为抽象类。,2023/10/22,47,要使ab_circle类为非抽象的,必须如下说明:class ab_circle:public shape int radius;public:void rotate(int);void draw();,1.下列选项中正确的是A)构造函数可以重载,析构函数不能重载 B)构造函数不能重载,析构函数可以重载C)构造函数可以重载,析构函数也可以重载 D)构造函数不能重载,析构函数也不能重载2.类的析构函数的作用是 A)一般成员函数 B)类的初始化 C)对象的初始化 D)删除对象3.对友元函数的正确描述是A)友元函数的实现必须在类的内部定义 B)友元函数是类的成员函数C)友元函数拨坏了破坏了类的封装性和隐藏性 D)友元函数不能访问类的私有成员4.在C+中,数据封装要解决的问题是A)数据的规范化 B)便于数据转换 C)避免数据丢失 D)防止不同模块之间数据的访问5 对结构体中定义的成员,默认的访问权限为A)public B)protected C)private D)static,6.类型转换函数的作用是 7赋值重载函数与赋值构造函数应用中的区别是8 C+中局部变量和全局变量重名时,欲访问全局变量,应该9 下列程序运行时会出现严重错误,为什么?#include void main()char*p,*q;p=new char10;q=new char10;strcpy(p,ABCD);q=p;delete p;delete q;10类中的保护段数据与共有段数据的主要区别是什么?,10.C+建立类族的实现是通过 A)类的嵌套 B)虚函数 C)类的继承 D)抽象类11.不能被派生类继承的有 构造函数 B)虚函数 C)静态成员函数 D)赋值操作函数1.纯虚函数是 A)virtual int vf(int);B)void vf(int)=0;C)virtual void vf()=0;D)virtual void vf(int)2.设置虚基类的目的是 A)简化程序 B)消除二义性C)提高运行效率 D)减少目标代码3设置虚函数的目的是实现动态编联,用关键字 标识虚函数。4类的派生有主要有三种方式,分别用描述符public,protected 和 表示。,