• 从零开始学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;
    }



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

    virtual base table 

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

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

    virtual base table pointer(vbptr)




    从程序可以看出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:虚函数表指针



    从输出可以看出,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++ primer 第四版
    Effective C++ 3rd
    C++编程规范

  • 相关阅读:
    webpack进阶(二)
    webpack的loader和plugin的区别
    接口和面向接口编程
    设计原则与编程技巧汇总
    W3C的盒子模型和IE的盒子模型
    React的组件
    React的路由react-router
    三种编程命名规则:驼峰命名法,帕斯卡命名法,匈牙利命名法
    gulp 构建 demo
    antd 表单的两种校验方式
  • 原文地址:https://www.cnblogs.com/jiangu66/p/3190354.html
Copyright © 2020-2023  润新知