C++内存布局

scorlw 发布于

C++内存布局

c++

一、字节对齐

一个基本的对象在内存中占用的内存大小主要为:各字段大小+字节对齐

为什么要字节对齐

字节对齐的根本原因在于CPU存取数据的效率问题。为了提高效率,计算机从内存中取数据是按照一个固定长度的。比如在32位机上,CPU每次都是取32bit数据的,也就是4字节。

因此如果一个int型整数的起始地址是0x00000004,则它是字节对齐的,一次性从该地址开始取出32bit的数据即为该int的值。

如果一个int型整数的起始地址是0x00000002,则它需要首先取出0x00000002、0x00000003所在的32bit,然后取出0x00000004、0x00000005所在的32bit数据(蓝色的两块),然后将两个32bit拼接在一起来构成该int整数。

可以发现,这样CPU存取数据的效率会变得非常低、

怎样字节对齐

具体的字节对齐主要有以下三个准则

1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;

2) 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,eg:char型起始地址能被1整除、short型起始地址能被2整除、int型起始地址能被4整除;

3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍。

二、简单结构体的内存布局

结构体test定义如下:

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
typedef struct test{
long long al;
char ac;
short as;
long long al2;
char ac2;
long long al3;
short as2;
int ai;
}test;

int main(void){
printf("sizeof(test) = %d\n",sizeof(test));

test *pt=(test *)malloc(sizeof(test));
cout<<"pt: "<<static_cast<const void *>(pt)<<endl;
cout<<"pt->al : "<<static_cast<const void *>(&(pt->al))<<endl;
cout<<"pt->ac : "<<static_cast<const void *>(&(pt->ac))<<endl;
cout<<"pt->as : "<<static_cast<const void *>(&(pt->as))<<endl;
cout<<"pt->al2: "<<static_cast<const void *>(&(pt->al2))<<endl;
cout<<"pt->ac2: "<<static_cast<const void *>(&(pt->ac2))<<endl;
cout<<"pt->al3: "<<static_cast<const void *>(&(pt->al3))<<endl;
cout<<"pt->as2: "<<static_cast<const void *>(&(pt->as2))<<endl;
cout<<"pt->ai : "<<static_cast<const void *>(&(pt->ai))<<endl;

return 0;
}

输出:

1
2
3
4
5
6
7
8
9
10
sizeof(test) = 48
pt: 0x1e4a70
pt->al : 0x1e4a70
pt->ac : 0x1e4a78
pt->as : 0x1e4a7a
pt->al2: 0x1e4a80
pt->ac2: 0x1e4a88
pt->al3: 0x1e4a90
pt->as2: 0x1e4a98
pt->ai : 0x1e4a9c

题外话:在C语言中打印指针可以用p%,eg:printf(“pa=%p\n”,pa);

该结构体中最宽基本类型为long long = 8 字节,因此sizeof(test)=48是8的整数倍;每个long long型成员的起始地址都为8的整数倍、int型的起始地址都为4的整数倍,依次类推。

经过上述分析,可以得到其内存布局如下图所示:

image-20200801214755968

可以发现第二个char型变量ac2只占用一个字节,但它后面的成员是long long型的,起始地址必须是8的倍数,因此ac2后面的7字节都被浪费掉了,al3从下一个8的倍数开始存放。这样会浪费很多内存,因此我们在设计结构体的时候必须更加谨慎合理,以避免不必要的浪费。

三、C++对象的内存布局

1、没有继承

  • 无虚函数 = 各字段的大小之和+字节对齐
    当C++中一个对象没有继承自其它任何父类,且没有虚函数时,由于类中定义的方法都在方法区,并不在类所在内存中,因此该类型的大小为:各字段的大小之和+字节对齐,与C语言中的结构体的内存占用情况完全相同。
  • 有虚函数 = sizeof(vfptr)+各字段大小之和+内存对齐
    但是当该类型中含有虚函数时,则还要考虑虚函数表指针vfptr的大小;当一个类中定义了虚函数时,根据C++对象模型可知,该类型的对象就会产生一个虚函数表vtbl,所有定义的虚函数都会依次排放在该虚函数表中,同时在对象的起始位置分配一个虚函数指针vfptr指向vtbl。在32位机器上,指针为4字节,因此当一个类函数虚函数时,它对应的对象所占内存大小为:sizeof(vfptr)+各字段大小之和+内存对齐

首先在进行后续的测试前,先了解如何通过vfptr来执行个虚函数

对象的起始地址即为虚函数表的地址, 我们可以通过对象的地址来取得虚函数表的地址,再依次执行各虚函数,如:

1
2
3
4
5
6
7
8
9
10
typedef void(*Fun)(void);
Test t;
Fun pFun = NULL;

cout << "虚函数表地址:" << (int*)(&t) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&t) << endl;

int** pVfptr = (int**)&t; //pVtab为指向虚函数表的指针
pFun = (Fun)pVfptr[i]; //将虚函数表视为一个数组,则pVfptr[i]为第i个虚函数
pFun(); //然后就可以调用该虚函数了

有了上述的基础之后,就可以开始后面的测试了

eg:有程序如下

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
#include <iostream>
#include <memory> //auto_ptr的头文件
using namespace std;
class Test
{
public:
long long al;
char ac;
long long al2;
int ai;
virtual void ff()
{
cout << "Test::ff()" << endl;
}
virtual void gg()
{
cout << "Test::gg()" << endl;
}
};
typedef void (*Fun)(void);
Test t;
Fun pFun = NULL;
int main(void)
{
cout << "虚函数表地址:" << (int *)(&t) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int *)*(int *)(&t) << endl;

Test *pt = new Test();
printf("sizeof(test) = %d\n", sizeof(Test));

int **pVtab = (int **)pt;
Fun pFun;
cout << "[0] Test::_vptr->" << endl;
for (int i = 0; (Fun)pVtab[0][i] != NULL; i++)
{
pFun = (Fun)pVtab[0][i];
cout << " [" << i << "] ";
pFun();
}
cout << "pt->al : " << static_cast<const void *>(&(pt->al)) << endl;
cout << "pt->ac : " << static_cast<const void *>(&(pt->ac)) << endl;
cout << "pt->al2 : " << static_cast<const void *>(&(pt->al2)) << endl;
cout << "pt->ai : " << static_cast<const void *>(&(pt->ai)) << endl;
system("pause");
return 0;
}

输出:

1
2
3
4
5
6
7
8
9
虚函数表地址:0x40c040
虚函数表 — 第一个函数地址:0x409940
sizeof(test) = 40
[0] Test::_vptr->
[0] Test::ff()
pt->al : 0xd84a78
pt->ac : 0xd84a80
pt->al2 : 0xd84a88
pt->ai : 0xd84a90

即该对象大小为40字节。

即该对象在内存中的结构为

image-20200801214731453

可以发现:

- 对象所占内存的大小为8字节的整数倍

- vfptr位于对象的起始位置

- 黑色表示padding,其中padding就占了15字节,因此这种排放方式非常浪费内存。

2、单一继承

在单一继承中,子类只继承自一个父类,这又可以分为简单直接继承、父类或子类中有虚函数、虚继承等几种情况。

1)简单直接继承(父类、子类都没有虚函数)
这种情况比较简单,父类、子类都没有虚函数,则都没有虚函数表,子类在内存中各成员变量根据其继承和声明顺序依次放在后面,先父类后子类。则子类大小为:父类各字段大小之和+子类各字段大小之和+字节对齐

父类和子类关系如下:

image-20200801215041736

则其在内存中结构为

image-20200801215105010

sizeof(Derived) = (4 + 1 + 3) + (4 + 1 + 3)=16;

2)有虚函数(父类或子类中有虚函数)
当父类有虚函数表时,则父类中会有一个vfptr指针指向父类虚函数对应的虚表。当一个子类继承自含有虚函数的父类时,就会继承父类的虚函数,因此子类中也会有vfptr指针指向虚函数表。当子类重写了虚函数时,虚表中对应的虚函数就会被子类重写的函数覆盖。此时子类大小就为:sizeof(vfptr) + 父类各字段大小之和 + 子类各字段大小之和+字节对齐

eg : 父子类图如下所示:

image-20200801215143103

则Derived类在内存中的存储结构为:

image-20200801215206631

则sizeof(Derived) = 4 + (4 + 1 + 3) + (4 + 1 + 3)=20,且子类的虚函数表覆盖了父类的ff()方法。

1)vfptr在内存的起始位置。
2)成员变量根据其继承和声明顺序依次放在后面,先父类后子类。
3)在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新,子类新定义的虚函数会增加到虚函数表中。

3、多层继承

多层继承的分析与上述各例完全相同,一层一层的向下分析即可。

4、多继承

Java只允许单继承,但是在C++中是可以多继承的,即一个子类同时继承自多个父类。这种情况也可以分为父类都没有虚函数和父类有虚函数两种情况。

  1. 父类都没有虚函数

当所有的父类都没有虚函数时,这种情况比较简单,子类所占内存的大小为:所有父类所有字段之和+子类所有字段之和+字节对齐
eg:

image-20200801215235571

Derived类在内存中的存储结构如下所示:

image-20200801215253035

则sizeof(Derived) = 8+8+8 = 24 字节
成员变量按照继承和声明的顺序排列,依次为ba1、bc1、ba2、bc2、da、dc。

  1. 父类有虚函数

    当一个父类有虚函数时,表明那个父类存在虚函数表,因此在那个父类的结构中会包含一个虚函数指针vfptr。而当多个父类中定义了虚函数时,则那些父类中都会包含一个vfptr,并且有虚函数的父类在没有虚函数父类的前面。当子类重写了那些虚函数时,就会在第一个定义了虚函数的父类的虚函数表中覆盖父类定义的虚函数,当子类增加了新的虚函数时,也会将新增的虚函数增加至那个虚函数表中。

    image-20200801215313360

    则Derived类在内存中的存储结构示意图为

    image-20200801215332989

    sizeof(Derived) = 40字节

    可以发现:
    1)Base2、Base3 中定义了虚函数,因此出现在Base1的前面
    2)子类Derived重写了父类Base2的ff()方法,因此Base2的虚函数表被覆盖了
    3)子类新增的虚函数hh()增加到了第一个虚函数表,也就是Base2的虚函数表中

    虚继承

    虚继承解决了从不同途径继承的类具有共同基类的问题,使得共同基类只有一份拷贝。解决了二义性的问题,也节省了内存。

    虚继承的一般类图如下所示:

    img

    sizeof(Derived) = 40 字节。经过分析,可以得到Derived类在内存中的存储结构示意图为:

    这里写图片描述

    总结:

    1. 各部分在内存中的存放顺序为先父类、后子类、最后公共基类,即先存放Base1、Base2,然后是Derived,最后才是Base类
    2. 对于虚拟继承,子类中会增加一个vbptr指针,它指向的值要么是0,要么是-4,表示公共基类相对于子类的偏移,也即Base类相对于Derived类的偏移。当Base类中有虚函数时,为-4;否则为0。
    3. 在VS编译器中,子类和公共基类之间会通过一个NULL指针分隔开。在其它编译器中可能没有这个字段

    对于每个类的虚函数表可以依次分析:

    • 首先是Base类,Base类的ff()函数首先被Base1覆盖,然后又被Derived类覆盖,Base类的gg()函数并未被覆盖,因此Base类的虚函数表中指向的虚函数分别为:Derived::ff()、Base::gg()。
    • 然后是Base1类,Base1类原本定义了两个虚函数,但是ff()函数是覆盖了父类Base中的函数,因此不在Base1的虚函数表中,只有bf1()。同时子类Derived中新定义了一个hh()虚函数,要插入到内存中的第一个虚函数表,因此会插入到Base1类的虚函数表中,因而Base1类中会有Base1::bf1()、Derived::hh()两个函数
    • 最后是Base2类,只有Base2::bf2()一个虚函数。

    简单的验证代码如下所示:

    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
    typedef void (*Fun) (void );

    class Base{
    public:
    Base():ba(333){}
    int ba;
    virtual void ff(){
    cout<<"Base::ff()"<<endl;
    }
    virtual void gg(){
    cout<<"Base::gg()"<<endl;
    }
    };

    class Base1:virtual public Base{
    public:
    Base1():ba(11){}
    int ba;
    virtual void ff(){
    cout<<"Base::ff()"<<endl;
    }
    virtual void Bf1(){
    cout<<"Base::Bf1()"<<endl;
    }
    };

    class Base2:virtual public Base{
    public:
    Base2():ba(22){}
    int ba;
    virtual void Bf2(){
    cout<<"Base2::Bf2()"<<endl;
    }
    };

    class Derived:public Base1,public Base2{
    public:
    Derived():da(55){Base1();Base2();}
    int da;
    void ff(){
    cout<<"Derived::ff()"<<endl;
    }
    virtual void hh(){
    cout<<"Derived::hh()"<<endl;
    }
    };

    int main(void){
    Derived *pt=new Derived();
    printf("sizeof(Derived) = %d\n",sizeof(Derived));
    int len=sizeof(Derived)/4;

    int** pVtab = (int**)pt;
    Fun pFun;

    cout<<"pt[0] :"<<endl;
    for (int i=0; (Fun)pVtab[0][i]!=NULL; i++){
    pFun = (Fun)pVtab[0][i];
    pFun();
    }
    cout<<"pt[1] :"<<endl;
    cout << " ["<<0<<"] ";
    cout<<(int)*pVtab[1]<<endl;

    cout<<"pt[2] :"<<(int)pVtab[2]<<endl;

    cout<<"pt[3] :"<<endl;
    for (int i=0; (Fun)pVtab[3][i]!=NULL; i++){
    pFun = (Fun)pVtab[3][i];
    pFun();
    }
    cout<<"pt[4] :"<<endl;
    cout << " ["<<0<<"] ";
    cout<<(int)*pVtab[4]<<endl;

    cout<<"pt[5] :"<<(int)pVtab[5]<<endl;
    cout<<"pt[6] :"<<(int)pVtab[6]<<endl;
    cout<<"pt[7] :"<<(int)pVtab[7]<<endl;

    cout<<"pt[8] :"<<endl;
    pFun = (Fun)pVtab[8][0];
    for(int i=0;(pFun = (Fun)pVtab[8][i])!=NULL;++i){
    pFun();
    }

    cout<<"pt[9] :"<<(int)pVtab[9]<<endl;

    system("pause");
    return 0;
    }

原文地址:https://blog.csdn.net/u012658346/article/details/50775742