
虚基类与虚函数.ppt
51页5.2.2 虚基类 1.虚基类的概念 在C++语言中,一个类不能被多次说明为一个派生类的直接基类,但可以不止一次地成为间接基类这就导致了一些问题为了方便 说明,先介绍多继承的“类格”表示法 派生类及其基类可用一有向无环图(DAG)表示,其中的箭头表示“由派生而来”类的DAG常称为一个“类格”复杂类格画出来通常更容易理解例如:2021/5/231 例 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; } };C类自己数据成员B对象数据成员L对象数据成员A对象数据成员L对象数据成员 C的对象LALBC 这时,next有两个赋值语句next=0; 具有二义性,它是将A::next置为零,还是将B::next置为零,或者将两者都置为0,需要在函数f()中被显式的说明.2021/5/232如果希望间接基类L与其派生类的关系是如下图C++语言提供了这种描述手段。
它将L说明为A和B的虚基类 LABC2021/5/233 当在多条继承路径上有一个公共的基类,在这些路径中的某几条路经汇合处,这个公共基类就会产生多个实例 如果只想保存这个基类的一个实例,可以将这个公共基类说明为虚拟基类或称虚基类 它仅是简单地将关键字virtual加到基类的描述上,例如改写上述例子为例5-202021/5/234 例 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 2021/5/235 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的顺序变了2021/5/236 一个派生类的对象的地址可以直接赋给虚基类的指针,例如: C obj; L * ptr=&obj; 这时不需要强制类型转换,并且,一个虚基类的引用可以引用一个派生类的对象,例如: C obj2; L &ref=obj2; 反之则不行,无论在强制类型转换中指定什么路径,一个虚基类的指针或引用不能转换为派生类的指针或引用例如: C * P=(C*)(A*)ptr; 将产生编译错误2021/5/237 2. 虚基类对象的初始化 虚基类的初始化与多继承的初始化在语法上是一样的,但隐含的构造函数的调用次序有点差别 虚基类构造函数的调用次序是这样规定的: 1. 虚基类的构造函数在非虚基类之前调用 2. 若同一层次中包含多个虚基类,虚基类构造函数按它们说明的次序调用 3. 若虚基类由非虚基类派生,则遵守先调用基类构造函数,再调用派生类构造函数的规则。
2021/5/238 例如 : class X : public Y, virtual public Z { } X one; 将产生如下调用次序: Z() Y() X() 这里Z是X的虚基类,故先调用Z的构造函数,再调用Y的构造函数,最后才调用派生类X自己的构造函数 例 5-222021/5/239 # include "iostream.h" class base{ public: base(){cout<<"Base"< 2021/5/2312 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 虚基类和非虚基类的不同 AVABCD在D中仅仅一个v错误,具有二义性,在D中有两个a调用次序:B( ): V( ) A( ) B( )C( ): A( ) C( )D( ): D( )2021/5/2313 5.3 虚函数与多态性对于普通成员函数的重载,可表达为下面的方式: 1) 在同一个类中重载 2) 在不同类中重载 3) 基类的成员函数在派生类中重载 因此,重载函数的访问是在编译时区分的,有以下三种方法:2021/5/2314 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的对象2021/5/2315例 5-26 #include 例如: 利用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=&B_ob; p=&D_ob;派生类对象派生类成员基类成员2021/5/2317#include 但是相反却不正确,即不能用指向派生类的指针指向一个基类的对象 2.希望用基类指针访问其公有派生类的特定成员,必须将基类指针用显式类型转换为派生类指针例如: ((D_class*)p)->show_phone( );2021/5/2319 5.3.2 虚函数 例 5-28 #include 其本质的原因在于普通成员函数的调用是在编译时静态区分2021/5/2321 如果随着p所指向的对象的不同p—>who()能调用不同类中who()版本,这样就可以用一个界面p->who()访问多个实现版本:Base中的who(), First_d 中的 who(),以及Second_d中的who(),这在编程时非常有用 实际上,这表达了一种动态的性质,函数调用p->who()依赖于运行时p所指向的对象虚函数提供的就是这种解释机制如果在base中将成员函数who()说明为虚函数,则修改上述程序为例5-29 虚函数是在基类中被冠以virtual的成员函数,它提供了一种接口界面虚函数可以在一个或多个派生类中被重新定义,但要求在派生类中 重新定义时,虚函数的函数原型,包括返回类型,函数名,参数个数,参数类型的顺序,必须完全相同2021/5/2322class First_d : public Base { public: First_d(int a ):Base(a) { } void who(){“First derivation “< 这种在运行时刻将函数界面与函数的不同实现版本进行匹配的过程,称为晚期匹配,也称为运行时的多态性p->who()2021/5/2324 基类函数f具有虚特性的条件是: 1) 在基类中,将该函数说明为virtual函数 2)定义基类的公有派生类 3) 在基类的公有派生类中原型一致地重载该虚函数 4) 定义指向基类的指针变量,它指向基类的公有派生类的对象 例 5-302021/5/2325 void main( ) { derived d; base * bp=&d; bp->vf1( ); bp->vf2( ); bp->f( ); } 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( ); };错误,仅返回类型不同具有虚特性一般函数重载,参数不同,虚特性丢失一般的函数重载,非虚函数的重载2021/5/2326例 5-31 #include 此时,需要根据成员函数中this指针和它所指向的对象来判断调 用的是哪个函数例 5-332021/5/2330#include fun2,fun3,fun4,fun5是虚函数,故基类的指针变量,指向派生类时访问的是派生类中定义的成员2021/5/2332例 5-35 #include 2021/5/2333 4 . 在构造函数和析构函数中调用虚函数 在构造函数和析构函数中调用虚函数时,采用静态联编即它们所调用的虚函数是自己的类或者它的基类中的虚函数,但不是任何在派生类中定义的虚函数 例 5-362021/5/2334#include 2021/5/2335 5 . 析构函数可以定义为虚函数 构造函数不能为虚函数,而析构函数可以定义为虚函数 若析构函数为虚函数,那么当使用delete释放基类指针指向的派生类对象时,先调用派生类的析构函数,再调用基类的析构函数2021/5/2336class Deriver: public Base{ int d; public: Deriver(int num1,int num2):Base(num1) {d=num2;cout<<“Deriver create\n”;} ~Deriver() {cout<<“Deriver destory\n”;} }; 例 5-37#include 有人可能会想,A1::f( )离A4更近,因为A1是A4的直接基类, 而A2不是pa4->f()应该调用A1:f( ),而不是调用A2::f( )情况并非如此,由DAG图可见,根据继承路径pa4->f( )应有两种调用选择: A2::f( ); 和 A1::f( ) 2021/5/23415.3.3 纯虚函数及抽象类 基类表示抽象的概念,如figure是一个基类表示有型的东西,可以派生出封闭图形和非封闭图形两类Shape体现了一个抽象的概念,在figure中定义一个求面积的函数显然是无意义的,但可以将其声明为一个虚函数,提供一个派生的公共界面,并由各派生类提供求面积的各自版本因此基类的有些虚函数没有定义是很正常的,但是要求派生类必须重新定义这些虚函数 为此 C++引入了纯虚函数的概念2021/5/2342 纯虚函数是一个在基类中说明的虚函数它在基类中没有定义,要求任何派生类必须定义自己的版本 纯虚函数具有以下的形式: virtual type func_name(参数表)=0; 在构造函数和析构函数中调用虚函数使用静态编联,因此在这两个函数中不能调用纯虚函数。 但其它函数可以调用纯虚函数2021/5/2343 如果一个类至少有一个纯虚函数,就称这个类为抽象类 抽象类可以定义一种接口,由派生类提供各种实现 Ø 抽象类只能用作其它类的基类.Ø 可以用作声明抽象类的指针和引用 Ø 不能创建对象Ø 不能用作参数Ø 不能用作函数返回类型或显式转换的类型2021/5/2344例 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 & h(shape &); 错误,抽象类不能建立对象可以声明抽象类的指针错误,抽象类不能作为返回类型错误,抽象类不能作为参数类型可以声明抽象类的引用不能用作显式类型转换2021/5/2345 如果派生类没有原型一致地重载该纯虚函数。 从基类继承来的纯虚函数,在派生类中仍是纯虚函数 例如: class ab_circle : public shape { int radius ; public : void rotate(int){…}; }; 由于 shape :: draw( )是一个纯虚函数,缺省的ab_cricle :: draw也是一个纯虚函数,这时ab_circle仍为抽象类2021/5/2346要使ab_circle类为非抽象的,必须如下说明: class ab_circle : public shape { int radius ; public : void rotate(int){…}; void draw( ){…} };2021/5/23471. 下列选项中正确的是下列选项中正确的是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) static2021/5/23486. 类型转换函数的作用是———————— 7.赋值重载函数与赋值构造函数应用中的区别是———————8 C++中局部变量和全局变量重名时,欲访问全局变量,应该——————————9 下列程序运行时会出现严重错误,为什么?#include 4.类的派生有主要有三种方式,分别用描述符public, protected 和 表示2021/5/2350部分资料从网络收集整理而来,供大家参考,感谢您的关注!。
