测试环境
平台:32位
编译环境:VS2008
虚继承相关背景
如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的属性和方法(假设这些属性和方法是公有的且都是公有继承)。从设计角度讲,这个实现是错误的,它容易产生二义性且浪费内存空间。
虚继承可以解决这个问题,虚继承可以为最远的派生类提供唯一的基类成员,而不重复产生多次拷贝。
问题:从一个例子开始
假设有虚继承结构如下,虚基类A中有int成员a_;B1虚继承自A,有int成员b1_;B2虚继承自A,有int成员b2_;D继承自B1、B2,有int成员d_。
在32位架构下,类A、B1、B2、D的大小?哎!我比较笨!只好写个程序看看结果先。
1 #include <iostream> 2 using namespace std; 3 4 class A 5 { 6 public : 7 int a_ ; 8 }; 9 10 class B1 : virtual public A 11 { 12 public : 13 int b1_ ; 14 }; 15 16 class B2 : virtual public A 17 { 18 public : 19 int b2_ ; 20 }; 21 22 class D : public B1, public B2 23 { 24 public : 25 int d_ ; 26 }; 27 28 int main (void) 29 { 30 cout << "sizeof(A) = " << sizeof(A) << endl; 31 cout << "sizeof(B1) = " << sizeof(B1)<< endl; 32 cout << "sizeof(D) = " << sizeof(D) << endl; 33 34 return 0; 35 }
运行结果如下:
sizeof(A)的结果符合我们的预期,但是sizeof(B1)和sizeof(D)的结果就有点不能理解了。
分析:从查看B1类对象成员属性的地址开始
我们不妨先构造一个B1类对象,然后打印该对象中的各个属性地址来看看究竟。
1 #include <iostream> 2 using namespace std; 3 4 class A 5 { 6 public : 7 int a_ ; 8 }; 9 10 class B1 : virtual public A 11 { 12 public : 13 int b1_ ; 14 }; 15 16 class B2 : virtual public A 17 { 18 public : 19 int b2_ ; 20 }; 21 22 class D : public B1, public B2 23 { 24 public : 25 int d_ ; 26 }; 27 28 int main (void) 29 { 30 cout << "sizeof(A) = " << sizeof(A) << endl; 31 cout << "sizeof(B1) = " << sizeof(B1)<< endl; 32 cout << "sizeof(D) = " << sizeof(D) << endl << endl; 33 34 B1 ob1; 35 cout << "&ob1 = 0x" << &ob1 << endl; 36 cout << "&ob1.a_ = 0x" << &ob1.a_ << endl; 37 cout << "&ob1.b1_ = 0x" << &ob1.b1_ << endl; 38 39 return 0; 40 }
运行结果如下:
我们发现ob1的首地址既不等于成员a_的地址也等于成员b1_的地址。那从首地址开始的4个字节存放的是什么内容呢?
分析:B1类对象内存模型
下面来分析原理,根据刚才获取的B1类大小以及该类对象中各个属性的地址分布,绘制B1类内存模型图如下:
从内存模型图中可以看出:该对象的内存分为B1部分和虚基类部分。从B1部分的首地址开始的4个字节存放的是一个vbptr(B1部分的虚基类表指针),该指针指向了一个虚基类表(virtual base table of B1),这个虚基类表中存放了:
- B1部分首地址与vbptr(B1)所在地址之差,即0x0013F8AC - 0x0013F8AC = 0。
- 虚基类部分首地址与vbptr(B1)所在地址之差,即0x0013F8B4 - 0x0013F8AC = 8。
好了,我们来验证下该内存模型是否正确。
1 #include <iostream> 2 using namespace std; 3 4 class A 5 { 6 public : 7 int a_ ; 8 }; 9 10 class B1 : virtual public A 11 { 12 public : 13 int b1_ ; 14 }; 15 16 class B2 : virtual public A 17 { 18 public : 19 int b2_ ; 20 }; 21 22 class D : public B1, public B2 23 { 24 public : 25 int d_ ; 26 }; 27 28 int main (void) 29 { 30 cout << "sizeof(A) = " << sizeof(A) << endl; 31 cout << "sizeof(B1) = " << sizeof(B1)<< endl; 32 cout << "sizeof(D) = " << sizeof(D) << endl << endl; 33 34 B1 ob1; 35 cout << "&ob1 = 0x" << &ob1 << endl; 36 cout << "&ob1.a_ = 0x" << &ob1.a_ << endl; 37 cout << "&ob1.b1_ = 0x" << &ob1.b1_ << endl << endl; 38 39 long ** pVbptr = (long **)&ob1; 40 cout << "virtual base table of B1 : " << endl; 41 cout << " [0] : " << pVbptr[0][0] << endl; 42 cout << " [1] : " << pVbptr[0][1] << endl; 43 44 return 0; 45 }
运行结果如下:
分析:D类对象内存模型
有了前面的分析,下面的分析就简单多了。我们还是构造一个D类对象,然后打印下该对象中各个属性的地址。
1 #include <iostream> 2 using namespace std; 3 4 class A 5 { 6 public : 7 int a_ ; 8 }; 9 10 class B1 : virtual public A 11 { 12 public : 13 int b1_ ; 14 }; 15 16 class B2 : virtual public A 17 { 18 public : 19 int b2_ ; 20 }; 21 22 class D : public B1, public B2 23 { 24 public : 25 int d_ ; 26 }; 27 28 int main (void) 29 { 30 cout << "sizeof(A) = " << sizeof(A) << endl; 31 cout << "sizeof(B1) = " << sizeof(B1)<< endl; 32 cout << "sizeof(D) = " << sizeof(D) << endl << endl; 33 34 D od; 35 cout << "&od = 0x" << &od << endl; 36 cout << "&od.a_ = 0x" << &od.a_ << endl; 37 cout << "&od.b1_ = 0x" << &od.b1_ << endl; 38 cout << "&od.b2 = 0x" << &od.b2_ << endl; 39 cout << "&od.d_ = 0x" << &od.d_ << endl; 40 41 return 0; 42 }
运行结果如下:
根据运行结果绘制D类对象内存模型图如下:
至于原理:在前面也已经说明了,这里就不再赘述了。
扩展:访问D类对象的成员变量是如何实现的?
当有一个虚基类指针指向了派生类对象,那么这次指向只是简单的赋值吗?
1 #include <iostream> 2 using namespace std; 3 4 class A 5 { 6 public : 7 int a_ ; 8 }; 9 10 class B1 : virtual public A 11 { 12 public : 13 int b1_ ; 14 }; 15 16 class B2 : virtual public A 17 { 18 public : 19 int b2_ ; 20 }; 21 22 class D : public B1, public B2 23 { 24 public : 25 int d_ ; 26 }; 27 28 int main (void) 29 { 30 D od; 31 A* pa; 32 pa = &od; 33 34 return 0; 35 }
我们在32行设置下断点,单步运行后发现:
pa真正的值不等于&od,更有意思的是它们的偏移量为0x001DFEBC - 0x001DFEA8,也就是20(10进制)。
因此可以确定的是:在pa=&od执行时,程序内部会先通过该对象中的vbptr,然后通过vbptr(虚基类表指针)找到vbtbl(虚基类表),最终在vbtbl中找到虚基类表中虚基类部分与vbptr的偏移值,然后将指针指向加上偏移值的地址。
总结
本文针对虚继承情况,分析了在32位架构+VS2008编译环境下类对象内存模型,虽然有与分析内容相符合的运行结果,但这并不代表所有编译条件下(如g++)构造的内存模型都如上述情况(不同编译器:虚基类部分在整个对象模型中的位置会有些差异)。不过话又说回来,它们生成对象模型的原理其实都是类似的。