[C++] 虚继承对C++对象内存模型的影响

测试环境

     平台: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++)构造的内存模型都如上述情况(不同编译器:虚基类部分在整个对象模型中的位置会有些差异)。不过话又说回来,它们生成对象模型的原理其实都是类似的。

 

(完)

posted @ 2014-03-09 00:18  helloamigo  阅读(798)  评论(0编辑  收藏  举报