410 likes | 542 Views
第 22 章 多重继承和类型变换. 多继承 (multiple inheritance) 指在一个继承树层次的剖 面上派生类可以拥有不止一个直接基类。多重继承是 C++ 语 言后期引进的代码重用的模式,单一继承对应派生类上逆时 恒只有一个直接基类。多重继承打破了单一继承这种单枝独 上的简单性。 C 风格的类型转换对于系统级的操作是必不可少的。 C++ 鉴于 C 风格的类型转换未提供足够明显的安全保障 机制,特引进新形式的类型变换规则。. 一、多个直接基类 二、虚拟基类 三、多继承的构造函数 四、名称的二义性. 一、多个直接基类
E N D
第22章 多重继承和类型变换 • 多继承(multiple inheritance)指在一个继承树层次的剖 • 面上派生类可以拥有不止一个直接基类。多重继承是C++语 • 言后期引进的代码重用的模式,单一继承对应派生类上逆时 • 恒只有一个直接基类。多重继承打破了单一继承这种单枝独 • 上的简单性。 • C风格的类型转换对于系统级的操作是必不可少的。 • C++鉴于C风格的类型转换未提供足够明显的安全保障 • 机制,特引进新形式的类型变换规则。
一、多个直接基类 • 二、虚拟基类 • 三、多继承的构造函数 • 四、名称的二义性
一、多个直接基类 • 基类是简单的类,派生类是略微复杂的类。一个派生类 • 同时继承若干直接基类,意味着派生类的功能是基类功能的 • 综合。 • 多继承的语法格式是通过若干继承方式和直接基类名引 • 入的,继承方式由关键字private,public,protected给出, • 该继承方式的含义等同于单继承情形。关键字virtual出现于 • 多继承的语法格式且贡献额外的4字节内存到派生类的对 • 象,但与多态类的动态绑定无关,仅是为了解决内存共享名 • 称歧义而卷入进来的。
多继承的派生格式为: • class 派生类名: • 继承方式1 基类名1, 继承方式2 基类名2,..., • virtual public 基类名n • { 派生类的成员声明语句; }; • 直接基类是唯一的即冒号之后的基类名CBasen等不能 • 重复出现,基类构造函数的调用次序根据冒号之后基类表的 • 排列顺序进行,虚拟继承优先。析构函数的调用一般遵循与 • 对应构造函数相反的次序进行。基类的说明次序影响派生类 • 对象的内存分布,但语言未规定各基类成员在内存的先后顺 • 序,这取决于编译器的具体实现。
下面是一个简单的示例程序。 • [例]多个直接基类 • # include<stdio.h> • struct CBase1 • { CBase1(int n=1) { m_b1=n; • printf ("CBase1::CBase1()this=%p,%p\n",this, • &m_b1);} • void Show() • { printf ("%d.CBase1::Show() sizeof(CBase1)=%d\n", • m_b1,sizeof (CBase1)); } • int m_b1; • };
class CBase2 • { public: • CBase2 (int n=2) • { m_b2=n; • printf ("CBase2::CBase2()this=%p,%p\n",this,&m_b2); • } • void Show() • { printf ("%d.CBase2::Show() • sizeof (CBase2) =%d\n",m_b2,sizeof (CBase2)); } • long l; • protected: int m_b2; • };
class CDerived:public CBase2, virtual public CBase1 • { public:CDerived (int n=3) • { m_d=n;printf ("CDerive::CDeriv • this=%p,%p\n",this,&m_d); } • int m_d; • int Show(); • }; • int CDerived::Show() • { CBase1::Show(); CBase2::Show(); • printf ("%d.CDerived::Show() • sizeof (CDerived) =%d\n",m_d, sizeof (CDerived)); • return 0; } • void main() { CDerived d; d.Show(); }
程序无virtual的运行输出结果: • CBase2::CBase2()this=0065FDE8,0065FDEC • CBase1::CBase1()this=0065FDF0,0065FDF0 • CDerive::CDerive this=0065FDE8,0065FDF4 • 1.CBase1::Show() sizeof(CBase1)=4 • 2.CBase2::Show() sizeof(CBase2)=8 • 3.CDerived::Show() sizeof(CDerived)=16
程序有virtual的运行输出结果如下: • CBase1::CBase1()this=0065FDF4,0065FDF4 • CBase2::CBase2()this=0065FDE4,0065FDE8 • CDerive::CDerive this=0065FDE4,0065FDF0 • 1.CBase1::Show() sizeof(CBase1)=4 • 2.CBase2::Show() sizeof(CBase2)=8 • 3.CDerived::Show() sizeof(CDerived)=20
CBase1 CBase2 long l; 0065FDE4 int m_b2; 0065FDE8 vbp 0065FDEC int m_d; 0065FDF0 int m_b1; 0065FDF4 long l; 0065FDE8 int m_b2; 0065FDEC int m_b1; 0065FDF0 int m_d; 0065FDF4 CDerived • 无关键字virtual的内存布局 有关键字virtual的内存布局 • CDerived dthis=0065FDE8 CDerived dthis=0065FDE4 • CBase1()this=0065FDF0 CBase1()this=0065FDF4 图 多个直接基类的类层次和内存布局
如上的派生类CDerived拥有两个基类CBase1,CBase2, • 左边的输出结果表明基类的构造函数按照声明的次序调用, • 右边的结果说明关键字virtual修饰的基类优先调用。 • 派生类对象的数据成员是基类相应数据成员之和再加上 • 本身的新添数据成员,但this指针由于多个基类的存在而有 • 多值。 • virtual修饰的基类称为虚拟基类,其确切含义在后面介 • 绍。
二、虚拟基类 • 关键字virtual可以在多继承的时候修饰直接基类中的一 • 个或多个,被virtual限定的基类称为虚拟基类,对应的继承 • 模式可称为虚拟继承。虚拟继承不同于虚拟函数,虽然两者 • 同用一个关键字virtual。 相应于虚拟继承编译器增添4字节 • 的数据以鉴别非虚拟继承。这四个字节的数据项可称为指向 • 虚基类表(virtual base class table)的指示符vbp。 • 虚拟基类的数据成员只有一个内存映像为派生类所共享 • 并且可适当减少基类的歧义。
[例]关键字virtual对多重继承的影响。从输出结果注意构造 • 函数调用的次序。 • # include<stdio.h> • class CTop • { public: • int mt; • CTop (int n=0) • { mt=n; • printf ("%d.CTop()this=%p,%p\n",mt,this,&mt); • } • };
struct CBase1:virtual public CTop • { CBase1 (int n=1) • { b1=n; • printf ("CBase1()this=%p,%p\n",this,&b1); • } • void Show() • { printf("%d.CBase1::Show()sizeof(CBase1)=%d\n", • b1,sizeof(CBase1)); • } • int b1; • };
class CBase2: public virtual CTop • { public: • CBase2 (int n=2) • { b2=n;printf("CBase2()this=%p,%p\n",this,&b2); } • void Show() • { printf("%d.CBase2::Show()sizeof(CBase2)=%d\n", • b2,sizeof(CBase2)); • } • long l; • protected: int b2; • };
class ClassD: public CBase1,public CBase2 • { public: int d; • ClassD (int n=3) • { d=n; printf ("ClassD()this=%p,%p\n",this,&d); } • int Show(); • }; • int ClassD::Show() • { CBase1::Show(); CBase2::Show(); • printf ("%d.ClassD::Show()sizeof(ClassD)=%d\n", • d,sizeof (ClassD)); return 0; • } • void main() { ClassD d; d.Show(); }
intmt; 0065FD E0 int b1; 0065FDE4 int mt; 0065FDE8 long l; 0065FDEC int b2; 0065FD F0 int d; 0065FDF4 vbp1; 0065FDDC int b1;0065FDE0 vbp2; 0065FDE4 long l;0065FDE8 int b2;0065FDEC int d; 0065FD F0 int mt;0065FDF4 CTop CTop CTop ClassD CBase1 CBase2 CBase1 CBase2 ClassD virtual继承的内存布局 无关键字virtual的内存布局
不带关键字virtual的输出结果如下: • 0. CTop()this=0065FDE0,0065FDE0 • CBase1()this=0065FDE0,0065FDE4 • 0.CTop()this=0065FDE8,0065FDE8 • CBase2()this=0065FDE8,0065FDF0 • ClassD()this=0065FDE0,0065FDF4 • 1.CBase1::Show()sizeof(CBase1)=8 • 2.CBase2::Show()sizeof(CBase2)=12 • 3.ClassD::Show() sizeof(ClassD)=24
虚拟继承的输出结果为: • 0.CTop()this=0065FDF4,0065FDF4 • CBase1()this=0065FDDC,0065FDE0 • CBase2()this=0065FDE4,0065FDEC • ClassD()this=0065FDDC,0065FDF0 • 1.CBase1::Show()sizeof(CBase1)=12 • 2.CBase2::Show()sizeof(CBase2)=16 • 3.ClassD::Show() sizeof(ClassD)=28
从中可以看出,虚拟继承和非虚继承内存分布是不一样从中可以看出,虚拟继承和非虚继承内存分布是不一样 • 的,这亦导致作用于其上的目标代码相应的发生变动,因为 • 成员函数通过this指针发挥作用。单继承情形对于特定派生 • 类构成单枝独上的继承层次,因此只有一个this指针管理单 • 继承的集合数据。多继承状况从最晚派生类向上攀沿时,上 • 逆的路径由于基类不止一个因而编译器存在多路选择,多继 • 承的基类的声明列表提供路径的分叉指引。其中一个分叉为 • 主干分叉,最晚派生类的对象的this指针归属这一分叉,其 • 余的基类各分配一个this值引领另外的分叉。 • 非虚拟继承各分叉之间不粘连,枝枝独立向上,虚拟继 • 承使得选定的虚基类合并在一起,虚基类对应的基部对象 • (subobject)只有唯一的数据状态为派生类所共享。
三、多继承的构造函数 • 生成派生类的对象时编译器按照如下的次序调用构造函 • 数: • 1. 同一类层次中嵌入对象的构造函数,嵌入对象构造函 • 数按照在类中的声明次序依次调用,与成员初始化语法的排 • 放次序无关。嵌入对象如果存在才调用。 • 先调用嵌入对象的构造函数,然后调用组合类或包容类 • 的构造函数。 • 2. 基类构造函数。多继承情形先声明的先调用即从左到 • 右的次序调用基类构造函数。优先执行虚拟基类的构造函 • 数,接着执行非虚拟基类的构造函数。
3. 派生类的构造函数。 • 最晚派生类的构造函数最晚调用。 • 总的原则是从左到右从上到下地启动继承树层次上的构 • 造函数一次。根据先遍历左子树的算法实现这一途径。非虚 • 拟继承的基类的构造函数视重复继承的次数可被多次调用。
析构函数调用调用次序与构造函数相反。设派生类拥有析构函数调用调用次序与构造函数相反。设派生类拥有 • 多个直接基类其名称为CBase1, CBase2,.., CBasen和多个 • 虚拟基类其名称为CTop1, ...,CTopk且同时具备多个嵌入对 • 象objEmbed1,objEmbed2,...,objEmbedm, 则带参构造函 • 数的成员初始化语法为: • CDerived::CDerived (t1 v1,t2 v2,...,tn vn ) • : • CBase1(v1,v2),...,CBasen (vi,vn,...), • CBase2 (v2,v3),objEmbed1(v1,v3,...), • objEmbed2 (v2,v3,...vn),...,objEmbedm (v1,...,vn), • CTop1(v2,v3),...,CTopk (vi,vn,..) • { 当前类的成员赋值语句系列; }
上面的CDerived类的构造函数包括多个形式参数,形式上面的CDerived类的构造函数包括多个形式参数,形式 • 参数的类型为ti, 形参为vi。 • 后跟冒号引出的直接基类初始化构造函数列表,当前类 • 嵌入对象构造函数初始化列表,这与单继承的情况相仿,派 • 生类显式调用直接基类的构造函数和嵌入对象调用自身类的 • 构造函数,初始化列表的先后排列次序可以变动,与构造函 • 数的调用次序无关。 • 另外增加了一个重要的虚拟基类的构造函数调用序列, • 表示最新派生类的构造函数专门初始化非直接的虚拟基类的 • 子集合数据。
所有虚拟基类都由(most derived)最新或最晚派生类的 • 构造函数负责显式初始化,不管最新派生类离虚拟基类相隔 • 多远,这确保虚基类的构造函数仅被调用一次。 • 如果出现虚拟基类,则最新派生类的构造函数冒号语法 • 中成员初始化列表中必须明显地调用虚基类的构造函数,除 • 非虚拟基类存在一个缺省构造函数供编译器隐含调用。 • 如果基类存在带参的构造函数,则派生类也应提交带参 • 构造函数,以便参数能够向上传递给基类构造函数。每一个 • 类存在一个缺省构造函数是方便的,这样可以在对象生成时 • 被隐含调用,此时对象用缺省构造函数的缺省值进行初始 • 化。前面例题中就是这样调用的。
[例]非虚拟继承两个基类同时包含两个嵌入对象的派生类[例]非虚拟继承两个基类同时包含两个嵌入对象的派生类 • #include <stdio.h> • struct A { A (int n=1) { printf ("A=%d,",n); } }; • struct B { B (int n=2) { printf ("B=%d,",n); } }; • struct D:public A, public B • { D (int n,int m): A(n), b(m) • { printf ("D=%d\n",n+m); } • B b; A a; • }; • void main() { D d (100,200); }
CTop CBase2 CBase1 CDerived • [例]多继承间接基类的初始化 • #include<stdio.h> • class CTop • { public: int m_t; • CTop(int n=0) • { m_t=n; • printf ("%d.CTop=%d," , • m_t,sizeof (CTop)); • } • };
CTop CTop CBase2 CBase1 CDerived • struct CBase1:virtual public CTop • { int m_b1; • CBase1(int n=1):CTop(n) • { m_b1=n;printf("%d.CBase1=%d,", • m_b1,sizeof (CBase1)); } • }; • class CBase2: public virtual CTop • { public: CBase2 (int n=2):CTop(n) • { m_b2=n; • printf("%d.CBase2=%d,", • m_b2,sizeof(CBase2)); } • protected: int m_b2; • };
class CDerived:public CBase1,public CBase2 • { public: • CDerived (int n):CBase2(n-3),CBase1(n-2),CTop(n-1) • { m_d=n; • printf ("%d.CDerived=%d\n",m_d,sizeof(CDerived)); • } • int m_d; • }; • void main() • { • CDerived d(4); • }
以上程序输出: • 3.CTop=4, 2.CBase1=12, • 1.CBase2=12, 4.CDerived=24 • 去掉上面程序的virtual关键字,同时将派生类的构造函 • 数通过去掉CTop(n-1)改为: • CDerived(int n):CBase2(n-3),CBase1(n-2) • { m_d=n; • printf("%d.CDerived=%d\n", • m_d,sizeof(CDerived)); } • 此时输出: • 2.CTop=4, 2.CBase1=8, 1.CTop=4, • 1.CBase2=8, 4.CDerived=20
从上面的输出结果可以看出,虚拟继承时虚拟基类的数从上面的输出结果可以看出,虚拟继承时虚拟基类的数 • 据状态由最新派生类的构造函数负责初始化,虚拟基类的构 • 造函数调用一次;此时虚拟基类的直接派生类的构造函数冒 • 号之后涉及到虚拟基类的构造函数调用被忽略,以保证虚拟 • 继承时虚基类的基部对象仅构造一次。 • 非虚拟继承时顶层基类的构造函数是通过直接派生类转 • 递参数的,顶层基类被继承多少次相应地通过其构造函数就 • 调用多少次,由此形成的继承树是分开的。
四、名称的二义性 • 多继承由于一个类可拥有不止一个基类,在多继承的树 • 层次的交汇点向上搜寻时,由于名称的不唯一引发的冲突称 • 为二义性。名称的唯一性彻底根除名称的歧义性。 • 名称的二义性来源分为两种情况:一种情况是基类的名 • 称隐含索引造成的,名称的隐含索引即是省略掉类域分辨符 • 方式的索引,另一种情况是间接基类的非虚拟继承。 • 如果采用虚拟继承,因为只有虚拟基类的唯一实例,所 • 以对虚拟基类中名称的访问不会引起二义性,虚拟基类引入 • 的目的之一就是剔除其本身的二义性。
[例]不唯一的名称m_n,Set,f()可产生二义性。 • #include<stdio.h> • class A { public: void f(){} }; • class B: public A • { public : void f(){} • public : int m_n; • void Set (int n) { m_n=n; } • }; • struct C { int m_n; void Set (int n) { m_n=n; } }; • class D:public C,public B • { public: int m_d; • void Set (int n) { m_d=n; } • };
A C B D • 考虑派生类D的对象objd: D objd; 则对派生类本身存 • 在的函数Set的调用: objd.Set(2); 不引起二义性。
根据名称总是优先采用派生类的支配原则,完整对象所根据名称总是优先采用派生类的支配原则,完整对象所 • 在派生类具有的名称索引是直接的, 因此objd.Set(2)调用等 • 价于objd .D::Set(2)不引起二义性。 • 同理对函数f的调用objd.f ()也不存在二义性,沿着各分 • 支向上搜寻时 f 的名称仅出现在一个分叉中,在同一分叉的 • 名称相当于单继承的情形,编译器优先采用派生类的成员名 • 称。
对于多继承的基类B和C中同时出现名称m_n,对m_n对于多继承的基类B和C中同时出现名称m_n,对m_n • 隐含的索引方式: • objd.m_n; • 系统会提示error: • 'D::m_n' is ambiguous, 'm_n' could be the in base • ‘C’ of class ‘D’ or the ‘m_n’ in base ‘B’ of class ‘D’。 • 编译器提醒名称m_n在C类或B类中两处出现, 但不知道 • 到底启用B::m_n或是C::m_n。二义性发生在较晚派生类不 • 拥有该名称,而多继承不同的分叉基类中同时出现相同的名 • 称。解决歧义的方法是加上直接基类的类域分辨符: • objd.B::m_n; 或者 objd.C::m_n;
以全限定名的方式清晰地索引成员名称或在类的设计之以全限定名的方式清晰地索引成员名称或在类的设计之 • 初,精心地设置唯一的命名,将类B中的m_n命名为m_nb, • 将类C中的m_n命名为m_nc等,这种方法好。 • 非虚拟基类重复继承构成的情形,由于间接基类 CTop • 出现在继承树的两个分叉中,因此完整对象对于非虚拟基类 • CTop成员的访问导致二义性。
CTop CTop CBase1 CDerived CBase2 • 下面的例子说明二义性的产生和解决途径。 • [例]清除名称二义性的途径 • class CTop • { public: int m_t; }; • struct CBase1: public CTop • { int m_b1; }; • class CBase2: public CTop • { protected: int m_b2; }; • class CDerived: public CBase1,public CBase2 • { • public: int m_d; • };
对于派生类CDerived的对象d的定义: • CDerived d; • 以下两个访问: • d.m_t; 或 d.CTop::m_t; • 都是有歧义的,此时应写成: • d.CBase2::m_t; 或 d.CBase1::m_t; • 表示采用直接基类CBase2或CBase1分叉上的数据状态。 • 同样指针强制类型转换时应写为: • CTop* pobjt=(CBase2*)&d; • 或: • pobjt=(CBase1*)&d; • 以明确表示派生类对象的地址往哪个分叉映射。
解决二义性的方法是采用直接基类的类域分辨符构成全解决二义性的方法是采用直接基类的类域分辨符构成全 • 限定名,通知编译器采用这个分叉基类的成员名称,而不是 • 可引起二义性的间接基类的类域分辨符。