CPP对象内存布局 (上)

发布于 2021-09-14  45 次阅读


基本规则

1、一般情况下,编译器会按顺序把成员变量放到内存里。

2、类的非虚成员函数不占用空间。某个类成员函数其实是所有实例共用的,可以看作是在调用的时候隐式传入了一个this指针。(这也说明了,如果在成员函数里头声明了一个静态局部变量,所有实例调用的时候操作的是同一个静态局部变量。

3、 类的静态成员变量不占用空间

4、空的类占用一个字节,这是由于cpp规定的不能有两个对象共用一个地址空间。这一个字节用于占位,这样在实例化的时候就会因为这一个字节而分配内存地址。

此外,类在计算大小的时候,除了以上规则,还需要考虑和继承相关的规则、以及内存对齐机制。

内存对齐机制

一般情况下,编译器会按顺序处理类的成员变量,#pragma pack(n)作为一个预编译指令用来设置多少个字节对齐的。值得注意的是,n的缺省数值是按照编译器自身设置,一般为8,合法的数值分别是1、2、4、8、16。

为了描述方便,把按n字节内存对齐的内存分配单元称作【pack(n)】

这里直接看一个例子,假设按4字节对齐,即 pack(4):

class test {
private :
    char c='1';//1byte 
    int i;//4byte
    short s=2;//2byte
};

首先对单个成员变量分配内存:

  • 对于c,它是一个char,占1字节。先分配1字节。那么对于一个pack(4)来说,还剩下3字节的空间。
  • 之后是i,int类型占4字节,而当前pack只剩下3字节,不够了。所以跳过这3字节的空间,把i放到一个新的pack里。
  • 再之后是s,short类型占2字节,前一个pack已经满了,所以把s放到新的pack里,新pack剩下2字节。
  • 所以目前一共是分配了1(char)+3(对齐)+4(int)+2(short) = 10字节

之后还需要对整体的类进行对齐:

  • 目前的10字节不是4的整数倍,即不是整数倍的pack(4),所以需要补齐到12字节。

到这里,编译器为这个类分配了3个pack(4)。最后的内存结构如图:

这里再看另外一个例子,这里仅仅是调换了变量的声明顺序:

class test2 {
private:
    int i;//4byte
    char c = '1';//1byte 
    short s = 2;//2byte
};

其内存分布如图:

可以看到只占用了两个pack(4),与上一个例子不同的是,分配完char之后,当前的pack剩余3字节,而short占用2字节,可以放入当前的pack,所以可以直接分配。

出现继承的情况 --无虚函数

一般继承规则 --成员变量

一般性的继承中,假设基类不存在虚函数,派生类会单纯地按照继承顺序复制基类的成员变量到本身,之后跟上自己的成员变量。同样的,基类的非虚成员函数不会放到类内,一样是通过隐式的this指针调用。

看一个例子:

可以看到Class D中,按顺序,先把C的成员放进来,再把B的成员放进来;而B继承了A,所以A的成员排在B自己的成员之前,D在继承时也保持了这么一个顺序。

一般继承规则 --非虚成员函数

非虚成员函数,不管是不是静态的,实际上都是存在于类的外部。通过指针类型来隐式传入this指针进行调用。编译器在处理的时候,实际上是对函数进行了重命名。

例如Class B有一个函数叫做fun(),编译过后可能就叫做: __ClassB-Fun(); 调用成员函数的时候实际上就是在调用 __ClassB-Fun() 。在调用的时候,编译器会根据一定规则确定下来这里调用的函数是哪个版本的,将成员函数调用替换成编译后的函数名,这些操作是在编译期完成的,也叫做静态联编。

现在,ClassB被ClassD继承了,那么一个D类型的实例调用fun的时候,就会先在 ClassD 中寻找叫fun的函数,如果没有,那么就会去父类中寻找。

这种情况下,如果D类中有一个同名函数,注意,是名字相同。那么根据之前的逻辑,先在 ClassD 中寻找叫fun的函数 ,直接就找到了。就不会继续找父类的函数了,也就是说,父类的函数被隐藏了。

所以,当子类有一个同名函数,并且参数与父类的相同,这种时候调用的一定是子类的函数。但如果参数不同呢?由于优先寻找到的是子类的函数,所以如果你想按照父类的函数传参数,编译器只会提示你参数不匹配。

有关函数重载

这里稍微提一下函数重载,函数重载和类成员函数的处理方式差不多,都是通过编译器重命名完成的。但函数重载的条件是,声明的同名函数处于一个作用域内。而父子类中的同名函数不是同一个作用域,不涉及重载,而是以上解释的搜索逻辑。

出现继承的情况 --存在虚函数

这是CPP面向对象的精髓,即动态多态的实现方式,相对于函数的覆盖和重载在编译期就能确定。这里的函数调用是在运行期确定的,又叫做动态联编。

具体分析见下一章:CPP对象内存布局 (下)


当其他人都认为你要鸽的时候,你鸽了,亦是一种不鸽