C/C++程序设计
1
多继承 (multiple inheritance)指在一个继承树层次的剖面上派生类可以拥有不止一个直接基类。多重继承是 C++语言后期引进的代码重用的模式,单一继承对应派生类上逆时恒只有一个直接基类。多重继承打破了单一继承这种单枝独上的简单性。
C风格的类型转换对于系统级的操作是必不可少的。
C++鉴于 C风格的类型转换未提供足够明显的安全保障机制,特引进新形式的类型变换规则。
C/C++程序设计
2
一、多个直接基类二、虚拟基类三、多继承的构造函数四、名称的二义性
C/C++程序设计
3
一、多个直接基类基类是简单的类,派生类是略微复杂的类。一个派生类同时继承若干直接基类,意味着派生类的功能是基类功能的综合。
多继承的语法格式是通过若干继承方式和直接基类名引入的,继承方式由关键字 private,public,protected给出,
该继承方式的含义等同于单继承情形。关键字 virtual出现于多继承的语法格式且贡献额外的 4字节内存到派生类的对象,但与多态类的动态绑定无关,仅是为了解决内存共享名称歧义而卷入进来的。
C/C++程序设计
4
多继承的派生格式为:
class 派生类名:
继承方式 1 基类名 1,继承方式 2 基类名 2,...,
virtual public 基类名 n
{ 派生类的成员声明语句 ; };
直接基类是唯一的即冒号之后的基类名 CBasen等不能重复出现,基类构造函数的调用次序根据冒号之后基类表的排列顺序进行,虚拟继承优先。析构函数的调用一般遵循与对应构造函数相反的次序进行。基类的说明次序影响派生类对象的内存分布,但语言未规定各基类成员在内存的先后顺序,这取决于编译器的具体实现。
C/C++程序设计
5
下面是一个简单的示例程序。
[例 ] 多个直接基类
# 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;
};
C/C++程序设计
6
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;
};
C/C++程序设计
7
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(); }
C/C++程序设计
8
程序无 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
C/C++程序设计
9
程序有 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
C/C++程序设计
10
无关键字 virtual的内存布局 有关键字 virtual的内存布局
CDerived d?this=0065FDE8 CDerived d?this=0065FDE4
CBase1()this=0065FDF0 CBase1()this=0065FDF4
CBase1 CBase2
CDerived
longl; 0065FDE8
intm_b2;0065FDEC
intm_b1;0065FDF0
intm_d; 0065FDF4
longl; 0065FDE4
intm_b2; 0065FDE8
vbp 0065FDEC
intm_d; 0065FDF0
intm_b1; 0065FDF4
图 多个直接基类的类层次和内存布局
C/C++程序设计
11
如上的派生类 CDerived拥有两个基类 CBase1,CBase2,
左边的输出结果表明基类的构造函数按照声明的次序调用,
右边的结果说明关键字 virtual修饰的基类优先调用。
派生类对象的数据成员是基类相应数据成员之和再加上本身的新添数据成员,但 this指针由于多个基类的存在而有多值。
virtual修饰的基类称为虚拟基类,其确切含义在后面介绍。
C/C++程序设计
12
二、虚拟基类关键字 virtual可以在多继承的时候修饰直接基类中的一个或多个,被 virtual限定的基类称为虚拟基类,对应的继承模式可称为虚拟继承。虚拟继承不同于虚拟函数,虽然两者同用一个关键字 virtual。 相应于虚拟继承编译器增添 4字节的数据以鉴别非虚拟继承。这四个字节的数据项可称为指向虚基类表 (virtual base class table)的指示符 vbp。
虚拟基类的数据成员只有一个内存映像为派生类所共享并且可适当减少基类的歧义。
C/C++程序设计
13
[例 ] 关键字 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);
}
};
C/C++程序设计
14
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;
};
C/C++程序设计
15
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;
};
C/C++程序设计
16
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(); }
C/C++程序设计
17
CBase1 CBase2
ClassD
CTop CTopintmt; 0065FDE0
int b1; 0065FDE4
int mt; 0065FDE8
long l; 0065FDEC
int b2; 0065FDF0
int d; 0065FDF4
CBase1 CBase2
CTop
ClassD
vbp1; 0065FDDC
int b1;0065FDE0
vbp2; 0065FDE4
long l;0065FDE8
int b2;0065FDEC
int d; 0065FDF0
int mt;0065FDF4
无关键字 virtual的内存布局 virtual继承的内存布局
C/C++程序设计
18
不带关键字 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
C/C++程序设计
19
虚拟继承的输出结果为:
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
C/C++程序设计
20
从中可以看出,虚拟继承和非虚继承内存分布是不一样的,这亦导致作用于其上的目标代码相应的发生变动,因为成员函数通过 this指针发挥作用。单继承情形对于特定派生类构成单枝独上的继承层次,因此只有一个 this指针管理单继承的集合数据。多继承状况从最晚派生类向上攀沿时,上逆的路径由于基类不止一个因而编译器存在多路选择,多继承的基类的声明列表提供路径的分叉指引。其中一个分叉为主干分叉,最晚派生类的对象的 this指针归属这一分叉,其余的基类各分配一个 this值引领另外的分叉。
非虚拟继承各分叉之间不粘连,枝枝独立向上,虚拟继承使得选定的虚基类合并在一起,虚基类对应的基部对象
(subobject)只有唯一的数据状态为派生类所共享。
C/C++程序设计
21
三、多继承的构造函数生成派生类的对象时编译器按照如下的次序调用构造函数:
1,同一类层次中嵌入对象的构造函数,嵌入对象构造函数按照在类中的声明次序依次调用,与成员初始化语法的排放次序无关。嵌入对象如果存在才调用。
先调用嵌入对象的构造函数,然后调用组合类或包容类的构造函数。
2,基类构造函数。多继承情形先声明的先调用即从左到右的次序调用基类构造函数。优先执行虚拟基类的构造函数,接着执行非虚拟基类的构造函数。
C/C++程序设计
22
3,派生类的构造函数。
最晚派生类的构造函数最晚调用。
总的原则是从左到右从上到下地启动继承树层次上的构造函数一次。根据先遍历左子树的算法实现这一途径。非虚拟继承的基类的构造函数视重复继承的次数可被多次调用。
C/C++程序设计
23
析构函数调用调用次序与构造函数相反。设派生类拥有多个直接基类其名称为 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,..)
{ 当前类的成员赋值语句系列 ; }
C/C++程序设计
24
上面的 CDerived类的构造函数包括多个形式参数,形式参数的类型为 ti,形参为 vi。
后跟冒号引出的直接基类初始化构造函数列表,当前类嵌入对象构造函数初始化列表,这与单继承的情况相仿,派生类显式调用直接基类的构造函数和嵌入对象调用自身类的构造函数,初始化列表的先后排列次序可以变动,与构造函数的调用次序无关。
另外增加了一个重要的虚拟基类的构造函数调用序列,
表示最新派生类的构造函数专门初始化非直接的虚拟基类的子集合数据。
C/C++程序设计
25
所有虚拟基类都由 (most derived)最新或最晚派生类的构造函数负责显式初始化,不管最新派生类离虚拟基类相隔多远,这确保虚基类的构造函数仅被调用一次。
如果出现虚拟基类,则最新派生类的构造函数冒号语法中成员初始化列表中必须明显地调用虚基类的构造函数,除非虚拟基类存在一个缺省构造函数供编译器隐含调用。
如果基类存在带参的构造函数,则派生类也应提交带参构造函数,以便参数能够向上传递给基类构造函数。每一个类存在一个缺省构造函数是方便的,这样可以在对象生成时被隐含调用,此时对象用缺省构造函数的缺省值进行初始化。前面例题中就是这样调用的。
C/C++程序设计
26
[例 ] 非虚拟继承两个基类同时包含两个嵌入对象的派生类
#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); }
C/C++程序设计
27
[例 ] 多继承间接基类的初始化
#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
CDerived
CBase2CBase1
C/C++程序设计
28
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;
};
CBase2
CDerived
CBase1
CTop CTop
C/C++程序设计
29
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);
}
C/C++程序设计
30
以上程序输出,
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
C/C++程序设计
31
从上面的输出结果可以看出,虚拟继承时虚拟基类的数据状态由最新派生类的构造函数负责初始化,虚拟基类的构造函数调用一次;此时虚拟基类的直接派生类的构造函数冒号之后涉及到虚拟基类的构造函数调用被忽略,以保证虚拟继承时虚基类的基部对象仅构造一次。
非虚拟继承时顶层基类的构造函数是通过直接派生类转递参数的,顶层基类被继承多少次相应地通过其构造函数就调用多少次,由此形成的继承树是分开的。
C/C++程序设计
32
四、名称的二义性多继承由于一个类可拥有不止一个基类,在多继承的树层次的交汇点向上搜寻时,由于名称的不唯一引发的冲突称为二义性。名称的唯一性彻底根除名称的歧义性。
名称的二义性来源分为两种情况:一种情况是基类的名称隐含索引造成的,名称的隐含索引即是省略掉类域分辨符方式的索引,另一种情况是间接基类的非虚拟继承。
如果采用虚拟继承,因为只有虚拟基类的唯一实例,所以对虚拟基类中名称的访问不会引起二义性,虚拟基类引入的目的之一就是剔除其本身的二义性。
C/C++程序设计
33
[例 ] 不唯一的名称 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; }
};
C/C++程序设计
34
考虑派生类 D的对象 objd,D objd; 则对派生类本身存在的函数 Set的调用,objd.Set(2); 不引起二义性。
C
D
B
A
C/C++程序设计
35
根据名称总是优先采用派生类的支配原则,完整对象所在派生类具有的名称索引是直接的,因此 objd.Set(2)调用等价于 objd,D::Set(2)不引起二义性。
同理对函数 f的调用 objd.f ()也不存在二义性,沿着各分支向上搜寻时 f 的名称仅出现在一个分叉中,在同一分叉的名称相当于单继承的情形,编译器优先采用派生类的成员名称。
C/C++程序设计
36
对于多继承的基类 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;
C/C++程序设计
37
以全限定名的方式清晰地索引成员名称或在类的设计之初,精心地设置唯一的命名,将类 B中的 m_n命名为 m_nb,
将类 C中的 m_n命名为 m_nc等,这种方法好。
非虚拟基类重复继承构成的情形,由于间接基类 CTop
出现在继承树的两个分叉中,因此完整对象对于非虚拟基类
CTop成员的访问导致二义性。
C/C++程序设计
38
下面的例子说明二义性的产生和解决途径。
[例 ] 清除名称二义性的途径
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;
};
CBase2
CDerived
CBase1
CTop CTop
C/C++程序设计
39
对于派生类 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;
以明确表示派生类对象的地址往哪个分叉映射。
C/C++程序设计
40
解决二义性的方法是采用直接基类的类域分辨符构成全限定名,通知编译器采用这个分叉基类的成员名称,而不是可引起二义性的间接基类的类域分辨符。
C/C++程序设计
41