C++虚继承初识

struct Employee { ... };
struct Manager : Employee { ... };
struct Worker : Employee { ... };
struct MiddleManager : Manager, Worker { ... };
如果经理类和工人类都继承自雇员类,很自然地,它们每个类都会从雇员类获得一份数据拷贝。如果不作特殊处理,一线经理类的实例将含有两个雇员类实例,它们分别来自两个雇员基类。如果雇员类成员变量不多,问题不严重;如果成员变量众多,则那份多余的拷贝将造成实例生成时的严重开销。更糟的是,这两份不同的雇员实例可能分别被修改,造成数据的不一致。因此,我们需要让经理类和工人类进行特殊的声明,说明它们愿意共享一份雇员基类实例数据。

很不幸,在C++中,这种共享继承被称为虚继承,把问题搞得似乎很抽象。虚继承的语法很简单,在指定基类时加上virtual关键字即可。
struct Employee { ... };
struct Manager : virtual Employee { ... };
struct Worker : virtual Employee { ... };
struct MiddleManager : Manager, Worker { ... };
使用虚继承,比起单继承和多重继承有更大的实现开销、调用开销。回忆一下,在单继承和多重继承的情况下,内嵌的基类实例地址比起派生类实例地址来,要么地址相同(单继承,以及多重继承的最靠左基类),要么地址相差一个固定偏移量(多重继承的非最靠左基类)。然而,当虚继承时,一般说来,派生类地址和其虚基类地址之间的偏移量是不固定的,因为如果这个派生类又被进一步继承的话,最终派生类会把共享的虚基类实例数据放到一个与上一层派生类不同的偏移量处。请看下例:

http://images.cnblogs.com/cnblogs_com/dylanwind/For%20Paste/CPPMem07.jpg

struct G : virtual C {
   int g1;
   void gf();
};
译者注:GdGvbptrGIn G, the displacement of G’s virtual base pointer to G)意思是:在G中,G对象的指针与G的虚基类表指针之间的偏移量,在此可见为0,因为G对象内存布局第一项就是虚基类表指针; GdGvbptrCIn G, the displacement of G’s virtual base pointer to C)意思是:在G中,C对象的指针与G的虚基类表指针之间的偏移量,在此可见为8

http://images.cnblogs.com/cnblogs_com/dylanwind/For%20Paste/CPPMem08.JPG

struct H : virtual C {
   int h1;
   void hf();
};
http://images.cnblogs.com/cnblogs_com/dylanwind/For%20Paste/CPPMem09.JPG
struct I : G, H {
   int i1;
   void _if();
};
暂时不追究vbptr成员变量从何而来。从上面这些图可以直观地看到,在G对象中,内嵌的C基类对象的数据紧跟在G的数据之后,在H对象中,内嵌的C基类对象的数据也紧跟在H的数据之后。但是,在I对象中,内存布局就并非如此了。VC++实现的内存布局中,G对象实例中G对象和C对象之间的偏移,不同于I对象实例中G对象和C对象之间的偏移。当使用指针访问虚基类成员变量时,由于指针可以是指向派生类实例的基类指针,所以,编译器不能根据声明的指针类型计算偏移,而必须找到另一种间接的方法,从派生类指针计算虚基类的位置。

VC++中,对每个继承自虚基类的类实例,将增加一个隐藏的虚基类表指针vbptr)成员变量,从而达到间接计算虚基类位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类而言,虚基类表指针与虚基类之间的偏移量。

其它的实现方式中,有一种是在派生类中使用指针成员变量。这些指针成员变量指向派生类的虚基类,每个虚基类一个指针。这种方式的优点是:获取虚基类地址时,所用代码比较少。然而,编译器优化代码时通常都可以采取措施避免重复计算虚基类地址。况且,这种实现方式还有一个大弊端:从多个虚基类派生时,类实例将占用更多的内存空间;获取虚基类的虚基类的地址时,需要多次使用指针,从而效率较低等等。

VC++中,G拥有一个隐藏的虚基类表指针成员,指向一个虚基类表,该表的第二项是GdGvbptrC。(在G中,虚基类对象C的地址与G虚基类表指针之间的偏移量(当对于所有的派生类来说偏移量不变时,省略“d”前的前缀))。比如,在32位平台上,GdGvptrC8个字节。同样,在I实例中的G对象实例也有虚基类表指针,不过该指针指向一个适用于“G处于I之中的虚基类表,表中一项为IdGvbptrC,值为20

观察前面的GHI我们可以得到如下关于VC++虚继承下内存布局的结论:
· 首先排列非虚继承的基类实例;
· 有虚基类时,为每个基类增加一个隐藏的vbptr,除非已经从非虚继承的类那里继承了一个vbptr
· 排列派生类的新数据成员;
· 在实例最后,排列每个虚基类的一个实例。

该布局安排使得虚基类的位置随着派生类的不同而浮动不定,但是,非虚基类因此也就凑在一起,彼此的偏移量固定不变。


原文地址:https://www.cnblogs.com/enumhack/p/464f609d65d2054a38dbb332b1dba4a2.html