一起探索C++类内存分布

C++ 类中内存分布具体是怎么样,尤其是C++中含有继承、虚函数、虚拟继承以及菱形继承等等情况下。

由于在linux下没有windows下显示直观,我们采用vs2015进行调试。


  • 部署环境

    我们在 属性->C/C++ ->命令行 -> /d1 reportSingleClassLayoutXXX ,XXX表示类名;

图片


  • 单个基础类

    1
    2
    3
    4
    5
    6
    7
    8
    class Base
    {
    private:
    int a;
    int b;
    public:
    void test();
    };

    内存分布:

    1
    2
    3
    4
    5
    class Base size(8):
    +-- -
    0 | a
    4 | b
    +-- -

    总结:我们发现普通类的内存分布是根据声明的顺序进行的,成员函数不占用内存。


  • 基础类+继承类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Base
    {
    int a;
    int b;
    public:
    void test();
    };

    class Divide :public Base
    {
    public:
    void run();
    private:
    int c;
    int d;
    };

    内存分布:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Divide size(16) :
    +-- -
    0 | +-- - (base class Base)
    0 | | a
    4 | | b
    | +-- -
    8 | c
    12 | d
    +-- -

    总结:根据内存分布,我们发现普通继承类,内存分布也是按照声明的顺序进行的,成员函数不占用内存;类的顺序是先基类,后子类。


  • 含有虚函数的基类

    1
    2
    3
    4
    5
    6
    7
    8
    class Base
    {
    int a;
    int b;
    public:
    void test();
    virtual void run();
    };

    内存分布:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Base size(12) :
    +-- -
    0 | {vfptr}
    4 | a
    8 | b
    +-- -

    Base::$vftable@:
    | &Base_meta
    | 0
    0 | &Base::run

    总结:带有虚函数的内存分布分为两部分,一部分是内存分布,一部分是虚表;我们从最上面发现,vfptr是放在了内存开始处,然后才是成员变量;虚函数run前面表示这个虚函数的序号为0


  • 含有虚函数的基类+继承子类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Base
    {
    int a;
    int b;
    public:
    void test();
    virtual void run();
    };

    class Divide :public Base
    {
    public:
    void DivideFun();
    virtual void run();
    private:
    int c;
    int d;
    };

    内存分布:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Divide size(20) :
    +-- -
    0 | +-- - (base class Base)
    0 | | {vfptr}
    4 | | a
    8 | | b
    | +-- -
    12 | c
    16 | d
    +-- -

    Divide::$vftable@:
    | &Divide_meta
    | 0
    0 | &Divide::run

    总结:我们发现继承类,虚表只有一个,还是在内存开始处,内存排布顺序与普通继承类是一致的;


  • 含有虚函数的基类+继承子类(多增加一个虚函数)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Base
    {
    int a;
    int b;
    public:
    void test();
    virtual void run();
    };

    class Divide :public Base
    {
    public:
    void DivideFun();
    virtual void run();
    virtual void DivideRun();
    private:
    int c;
    int d;
    };

    内存分布:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Divide size(20) :
    +-- -
    0 | +-- - (base class Base)
    0 | | {vfptr}
    4 | | a
    8 | | b
    | +-- -
    12 | c
    16 | d
    +-- -

    Divide::$vftable@:
    | &Divide_meta
    | 0
    0 | &Divide::run
    1 | &Divide::DivideRun

    总结:虚表还是继承于基类,在虚表部分多了DivideRun序号为1的虚函数;


  • 多重继承

    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
    class Base
    {
    int a;
    int b;
    public:
    virtual void run();
    };

    class Divide1 :public Base
    {
    public:
    virtual void run();
    private:
    int c;
    };

    class Divide2 :public Base
    {
    public:
    virtual void run();
    private:
    int d;
    };

    class Divide :public Divide1, Divide2
    {
    public:
    virtual void run();
    private:
    int d;
    };

    内存分布:

    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
    class Divide1 size(16) :
    +-- -
    0 | +-- - (base class Base)
    0 | | {vfptr}
    4 | | a
    8 | | b
    | +-- -
    12 | c
    +-- -

    Divide1::$vftable@:
    | &Divide1_meta
    | 0
    0 | &Divide1::run

    Divide1::run this adjustor: 0

    class Divide2 size(16) :
    +-- -
    0 | +-- - (base class Base)
    0 | | {vfptr}
    4 | | a
    8 | | b
    | +-- -
    12 | d
    +-- -

    Divide2::$vftable@:
    | &Divide2_meta
    | 0
    0 | &Divide2::run

    Divide2::run this adjustor: 0

    class Divide size(36) :
    +-- -
    0 | +-- - (base class Divide1)
    0 | | +-- - (base class Base)
    0 | | | {vfptr}
    4 | | | a
    8 | | | b
    | | +-- -
    12 | | c
    | +-- -
    | +-- - (base class Divide2)
    | | +-- - (base class Base)
    | | | {vfptr}
    | | | a
    | | | b
    | | +-- -
    | | d
    | +-- -
    | d
    +-- -

    Divide::$vftable@Divide1@:
    | &Divide_meta
    | 0
    0 | &Divide::run

    Divide::$vftable@Divide2@:
    | -16
    0 | &thunk: this -= 16; goto Divide::run

    Divide::run this adjustor: 0

    总结:主要看最后一个Divide类,内存排列顺序先是Divide1,后是Divide2,在Divide1和Divide2中各有一份虚表;


  • 虚拟继承(菱形继承)

    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
    class Base
    {
    int a;
    int b;
    public:
    virtual void run();
    };

    class Divide1 :virtual public Base
    {
    public:
    virtual void run();
    private:
    int c;
    };

    class Divide2 :virtual public Base
    {
    public:
    virtual void run();
    private:
    int d;
    };

    class Divide :public Divide1, Divide2
    {
    public:
    virtual void run();
    private:
    int d;
    };

    内存分布:

    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
    class Divide1 size(20) :
    +-- -
    0 | {vbptr}
    4 | c
    +-- -
    +-- - (virtual base Base)
    8 | {vfptr}
    12 | a
    16 | b
    +-- -

    Divide1::$vbtable@:
    0 | 0
    1 | 8 (Divide1d(Divide1 + 0)Base)

    Divide1::$vftable@:
    | -8
    0 | &Divide1::run

    Divide1::run this adjustor: 8
    vbi: class offset o.vbptr o.vbte fVtorDisp
    Base 8 0 4 0

    class Divide2 size(20) :
    +-- -
    0 | {vbptr}
    4 | d
    +-- -
    +-- - (virtual base Base)
    8 | {vfptr}
    12 | a
    16 | b
    +-- -

    Divide2::$vbtable@:
    0 | 0
    1 | 8 (Divide2d(Divide2 + 0)Base)

    Divide2::$vftable@:
    | -8
    0 | &Divide2::run

    Divide2::run this adjustor: 8
    vbi: class offset o.vbptr o.vbte fVtorDisp
    Base 8 0 4 0

    class Divide size(32) :
    +-- -
    0 | +-- - (base class Divide1)
    0 | | {vbptr}
    4 | | c
    | +-- -
    8 | +-- - (base class Divide2)
    8 | | {vbptr}
    12 | | d
    | +-- -
    16 | d
    +-- -
    +-- - (virtual base Base)
    20 | {vfptr}
    24 | a
    28 | b
    +-- -

    Divide::$vbtable@Divide1@:
    0 | 0
    1 | 20 (Divided(Divide1 + 0)Base)

    Divide::$vbtable@Divide2@:
    0 | 0
    1 | 12 (Divided(Divide2 + 0)Base)

    Divide::$vftable@:
    | -20
    0 | &Divide::run

    总结:通过内存分布可知,Divide1Divide2都是两个虚表,Divide中却是成了3个虚表,只有一份base;所以说:虚继承的作用是减少了对基类的重复,代价是增加了虚表指针的负担(增加了更多的需指针)