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定义如下:

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;
}

执行上段程序,结果显示为
这里写图片描述

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

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

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

可以发现第二个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来执行个虚函数
对象的起始地址即为虚函数表的地址, 我们可以通过对象的地址来取得虚函数表的地址,再依次执行各虚函数,如:

      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:有程序如下

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;
    }
};

int main(void){
    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;

    return 0;
}

运行该程序,得到结果为:
这里写图片描述
即该对象大小为40字节。

即该对象在内存中的结构为
这里写图片描述

可以发现:
- 对象所占内存的大小为8字节的整数倍
- vfptr位于对象的起始位置
- 黑色表示padding,其中padding就占了15字节,因此这种排放方式非常浪费内存。

2、单一继承

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

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

父类和子类关系如下:
这里写图片描述

则其在内存中结构为
这里写图片描述
sizeof(Derived) = (4 + 1 + 3) + (4 + 1 + 3)=16;

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

eg : 父子类图如下所示:
这里写图片描述

则Derived类在内存中的存储结构为:
这里写图片描述

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

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

3、多层继承

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

4、多继承

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

  1. 父类都没有虚函数

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

Derived类在内存中的存储结构如下所示:
这里写图片描述

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

  1. 父类有虚函数

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

则Derived类在内存中的存储结构示意图为
这里写图片描述

sizeof(Derived) = 40字节

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值