从零开始学C++之虚继承和虚函数对C++对象内存模型造成的影响

首先重新回顾一下关于类/对象大小的计算原则:

类大小计算遵循结构体对齐原则

第一个数据成员放在offset为0的位置

其它成员对齐至min(sizeof(member),#pragma pack(n)所指定的值)的整数倍。

整个结构体也要对齐,结构体总大小对齐至各个成员中最大对齐数的整数倍。

win32 可选的有1, 2, 4, 8, 16
linux 32 可选的有1, 2, 4
类的大小与数据成员有关与成员函数无关
类的大小与静态数据成员无关
虚继承对类的大小的影响
虚函数对类的大小的影响


下面通过实例来展示虚继承和虚函数对类大小造成的影响。

测试环境为:Win32 + Vs2008


一、只出现虚继承的情况


 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
 
#include <iostream>
using  namespace std;

class BB
{
public :
       int bb_ ;
};

class B1 :  virtual  public BB
{
public :
       int b1_ ;
};

class B2 :  virtual  public BB
{
public :
       int b2_ ;
};

class DD :  public B1,  public B2
{
public :
       int dd_ ;
};

int main ( void)
{
      cout<< sizeof (BB)<< endl;
      cout<< sizeof (B1)<< endl;
      cout<< sizeof (DD)<< endl;

      B1 b1 ;
       int** p ;

      cout<<&b1 <<endl;
      cout<<&b1 .bb_<< endl;
      cout<<&b1 .b1_<< endl;

      p = ( int **)&b1;
      cout<<p [ 0][ 0]<<endl;
      cout<<p [ 0][ 1]<<endl;

      DD dd ;
      cout<<&dd <<endl;
      cout<<&dd .bb_<< endl;
      cout<<&dd .b1_<< endl;
      cout<<&dd .b2_<< endl;
      cout<<&dd .dd_<< endl;
      p = ( int **)&dd;
      cout<<p [ 0][ 0]<<endl;
      cout<<p [ 0][ 1]<<endl;
      cout<<endl ;
      cout<<p [ 2][ 0]<<endl;
      cout<<p [ 2][ 1]<<endl;

      BB* pp ;

      pp = &dd ;
      dd.bb_ =  10//对象的内存模型在编译时就已经确定了,否则无法定义类的对象,因为要开辟内存
       int base = pp-> bb_;      // 通过间接访问 (其实pp 已经偏移了20 ),这需要运行时的支持
      cout<< "dd.bb_=" <<base<< endl;

       return  0;
}

从零开始学C++之虚继承和虚函数对C++对象内存模型造成的影响


从输出的地址和虚基类表成员数据可以画出对象内存模型图:

virtual base table 

本类地址与虚基类表指针地址的差

虚基类地址与虚基类表指针地址的差

virtual base table pointer(vbptr)


从零开始学C++之虚继承和虚函数对C++对象内存模型造成的影响


从程序可以看出pp是BB* 指针,pp首先指向dd内存,当执行pp->bb_时,先找到首个vbptr,找到虚基类BB地址与虚基类表指针地址的差,也即是20,接着pp偏移20个字节指向了dd对象中的BB部分,然后就访问到了bb_,这是在运行时才做的转换。


二、只出现虚函数的情况


 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
 
#include <iostream>
using  namespace std;

class Base
{
public :
     virtual  void Fun1()
    {
        cout <<  "Base::Fun1 ..." << endl;
    }

     virtual  void Fun2()
    {
        cout <<  "Base::Fun2 ..." << endl;
    }
     int data1_ ;
};

class Derived :  public Base
{
public :
     void Fun2 ()
    {
        cout <<  "Derived::Fun2 ..." << endl;
    }
     virtual  void Fun3()
    {
        cout <<  "Derived::Fun3 ..." << endl;
    }
     int data2_ ;
};

typedef  void (* FUNC)( void );

int main ( void)
{
    cout <<  sizeof (Base) << endl;
    cout <<  sizeof (Derived) << endl;
    Base b ;
     int **p = ( int **)& b;
    FUNC fun = (FUNC) p[ 0][ 0];
    fun();
    fun = (FUNC )p[ 0][ 1];
    fun();
    cout << endl ;

    Derived d ;
    p = ( int **)&d;
    fun = (FUNC )p[ 0][ 0];
    fun();
    fun = (FUNC )p[ 0][ 1];
    fun();
    fun = (FUNC )p[ 0][ 2];
    fun();

     return  0;
}

从输出的函数体可以画出对象内存模型图:


vtbl:虚函数表(存放虚函数的函数指针)

vptr:虚函数表指针

从零开始学C++之虚继承和虚函数对C++对象内存模型造成的影响


从输出可以看出,Derived类继承了Base::Fun1,而覆盖了Fun2,此外还有自己的Fun3。注意,因为Fun3是虚函数,才会出现在虚函数表,如果是一般函数是不会的,因为不用通过vptr间接访问。


三、虚继承与虚函数同时出现的情况:


 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
 
#include <iostream>
using  namespace std;

class BB
{
public :
       virtual  void vfbb()
     {
           cout<< "BB::vfbb" <<endl;
     }
       virtual  void vfbb2()
     {
           cout<< "BB::vfbb2" <<endl;
     }
       int bb_ ;
};

class B1 :  virtual  public BB
{
public :
       virtual  void vfb1()
     {
           cout<< "B1::vfb1" <<endl;
     }
       int b1_ ;
};

class B2 :  virtual  public BB
{
public :
       virtual  void vfb2()
     {
           cout<< "B2::vfb2" <<endl;
     }
       int b2_ ;
};

class DD :  public B1,  public B2
{
public :
       virtual  void vfdd()
     {
           cout<< "DD::vfdd" <<endl;
     }
       int dd_ ;
};

typedef  void (* FUNC)( void);

int main ( void)
{
      cout<< sizeof (BB)<< endl;
      cout<< sizeof (B1)<< endl;
      cout<< sizeof (DD)<< endl;

      BB bb ;
       int** p ;
      p = ( int **)&bb;
      FUNC fun ;
      fun = (FUNC )p[ 0][ 0];
      fun();
      fun = (FUNC )p[ 0][ 1];
      fun();
      cout<<endl ;


      B1 b1 ;
     
      p = ( int **)&b1;
      fun = (FUNC )p[ 0][ 0];
      fun();
      fun = (FUNC )p[ 3][ 0];
      fun();
      fun = (FUNC )p[ 3][ 1];
      fun();

      cout<<p [ 1][ 0]<<endl;
      cout<<p [ 1][ 1]<<endl;
      cout<<endl ;



      DD dd ;
      p = ( int **)&dd;
      fun = (FUNC )p[ 0][ 0];
      fun();
      fun = (FUNC )p[ 0][ 1];  // DD::vfdd 挂在 B1::vfb1的下面
      fun();
      fun = (FUNC )p[ 3][ 0];
      fun();
      fun = (FUNC )p[ 7][ 0];
      fun();
      fun = (FUNC )p[ 7][ 1];
      fun();
     
      cout<<p [ 1][ 0]<<endl;
      cout<<p [ 1][ 1]<<endl;
      cout<<p [ 4][ 0]<<endl;
      cout<<p [ 4][ 1]<<endl;


       return  0;
}

从零开始学C++之虚继承和虚函数对C++对象内存模型造成的影响


从输出的虚基类表成员数据和虚函数体可以画出对象内存模型图:


从零开始学C++之虚继承和虚函数对C++对象内存模型造成的影响


注意:如果没有虚继承,则虚函数表会合并,一个类只会存在一个虚函数表和一个虚函数表指针(同个类的对象共享),当然也不会有 虚基类表和虚基类表指针的存在。



参考:

C++ primer 第四版
Effective C++ 3rd
C++编程规范