c++类型内存分配规则

c++类型内存分配规则

本文试图回答这样3个问题:

  • c语言结构体内存对齐的规则?
  • class、enum、union的作用和区别?
  • VC等主流编译器究竟是如何管理一个类的分配空间的?

一、c语言结构体内存对齐规则

这是一个c语言类型内存分配最基本的问题了,我以前看到的一些说法经过自己的测试并不完全正确,学会了这个思路之后,以后再被问到这个问题就再也不怕了!

首先看一段代码:

class A{
    char a;//1B
    double b;//8B
    int c;//4B
};
int main(){
    cout<<sizeof(A)<<endl;
}

你说A类型的大小是多少?照我以前的认知,会觉得A类在32位编译后占16B,64位编译后20B。这种想法是 错误 的!今天用VC验证了一下,其实编译器处理结构体字节对齐的规则与编译位数无关!

上段代码的A类占了24B空间!(很遗憾,照我之前的想法就要掉入大坑了。。。)24B这个答案在GNU和VC下均得到了验证。接下来我们来看看为什么是这个答案。

规则1:类内成员存放的起始相对地址必须是其对齐大小的整数倍

这里碰到一个新概念:对齐大小。这在第三条规则细说,现在先认为对齐大小就是其自身类型大小即可。

先用规则1分析一下A类的大小:
请添加图片描述
​ 图1 按照规则1分析A类的大小

容易想清楚,a和b之间并不是紧密无间的,而是被填充了7B,**这样是确保b的起始地址是8的整数倍。**不过,仅按照规则1分析,并不能得出正确答案24B,还差4B。。。这就需要规则2了。

规则2:类的占用大小必须是其最大成员变量对齐大小的整数倍;如不是,则需上调

用这条规则很容易分析出正确答案,24B:

请添加图片描述
图2 按规则2分析A类的大小

A类的最大成员对齐大小为8B。而按规则1计算后A类占有20B,不是8的倍数,所以需上调至24B。

规则3:c++类类型的对齐大小是其最大成员对齐大小,内置类型的对齐大小则是其类型大小

有了规则1和2,我们还并不能推导出所有情况:当类成员中含有类类型时,对齐大小就不是其类型大小了!

比如A类,其对齐大小应是8B,而不是其类型大小24B。这一点我在VC和GNU上都已测试过,代码就不展示了。

  • 最后,有了以上3个规则在心中,再也不怕被问到字节对齐的问题了!

二、 class、enum、union的作用和区别

这个话题其实很经典,任何c/c++程序员都应该说出它们的基本用法和区别。。但我翻看了一下我曾经的所有笔记,竟然没有任何一篇涉猎到这个话题!今天借此给补上!

1. class与struct

c语言并没有class这个关键字,在c++看来class和struct大部分情况都可以通用。毕竟c++被创造出来的初衷实际是作为“better c”,“c with classes”。所以在c语言中只对数据的封装,与c++中对数据、方法的封装实际上应该是一回事。

细枝末节的区别在于两点:class和struct的默认成员访问级别不同;class和struct的默认继承级别不同。在实际c++工程开发中,我们大部分情况可以忽略二者的区别,当你觉得自己需要一种c风格的数据封装、并且方法只有数据的get,set,那就用struct。其他情况都用class。这就不会出什么大问题。。

2. enum的“前世今生”

众所周知,c/c++的发展历程粗略可以分为3个阶段:c语言 -> c++98 -> c++11

而在这3个阶段中,enum的用法均有一些改变,下面简单说说这个话题:

  • c语言中的enum和c++98中的enum:

    支持c结构体风格的声明和定义形式,不支持自定义存储类型:

    //test with gcc
    //test with g++ -std=c++98
    enum A{a,b,c} aa;//可以
    enum A{a,b,c};//可以
    enum A:int{a,b,c};//不行
    
  • c++11中的enum:

    c++11对enum的改进在于:

    1. enum可以当作一个封装手段了!(当然你也可以选择不封装)

    2. enum 支持自定义存储类型了,指定了不同的存储类型意味着枚举占用大小即为该类型大小。

    //test with g++ -std=c++11
    enum A:char{a,b,c};//可以,a对外可见
    //sizeof (A) => 1
    enum class A{a,b,c};//可以,a对外不可见
    //sizeof (A) => 4 (默认存储类型为int)
    
  • 最后,关于enum在c++工程开发中的惯用法:

    其实enum关键字在c语言中被创造出来的初衷是:宏太多了,把一些相关的宏写在一起方便管理,所以直到c++11之前并没有对enum作封装的用法。

    但在面向对象的开发范式中,如果enum没有封装功能的话,很容易形成作用域污染,引起名称冲突!

    所以,一个c++2.0中好的编程习惯是:避免单独使用enum,而要尽可能使用enum class。

3. 不咋常用的union

union在现代c++中是一个比较冷门的关键字了。虽然用侯捷提到的一种编程技巧可以稍微节省类分配空间,但这却带来类似c风格的“恼人”写法!因此在c++中很少出现union。尽管如此,我们还是要了解一下union的机制,因为工作中或许你就会“很不幸地”遇到了一些c代码接口,而你不得不去通读它以明白其工作机制,而一般在c代码库中还是会用到很多的union的。。

union A{
    int a;
    double b;
};

我们需要知道的是,**union的类型大小是其最大成员的类型大小。**关于一些c语言用union在类内节省空间的“奇淫技巧”,不了解也罢。(你用我推荐,我用我不用。。)

三、 VC等主流编译器究竟是如何管理一个类的分配空间

看标题这句话可能有点看不懂啊!啥意思呢:前面我们已经搞懂了c++编译器确保在运行时给一个类分配多大空间的问题,比如:

sizeof(A);

上面的sizeof关键字可以给出A类的类型大小,但这个类型大小也只是语言层面抽象给我们的。而在语言底层的某些场景中,为了保证语言的抽象能正确运作,为一个类分配的真正大小可能并不是sizeof(A)。接下来我通过侯捷老师的课程简单分析一下这些情况:

1. 堆区分配和栈区分配的不同

c/c++编译器更“愿意”处理栈区的“值语义”变量,因为堆区的对象语义需要付出的代价也更高昂。这体现在:栈可以依靠汇编级别的栈指针运动机制(后面的文章会从汇编的层面介绍发生调用时栈指针的动作)严格保证所有自动变量的分配与释放。操作也相对简单,几行汇编代码即可实现;而堆区对象的分配和释放则远远没有那么简单!它需要malloc的一套复杂的机制确保其运行正确,不出现底层内存失去管理的情况。堆区内存管理最首要的问题就是:如何确定某片内存是被分配过的?

比如,一个经典的问题是:为什么malloc函数需要指定分配空间的字节数,而free却只需指定对象就行了?这说明free肯定通过某种方法提前获知了该对象分配时占用的内存。这就需要分配内存时,额外给对象的内存外层套上一对"cookie片“,用于标记该对象分配的大小:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fqJA09pA-1651063806887)(D:\资料\CSDN博文\新增博客\c++专栏新增\c++类型内存规则-3.jpg)]
请添加图片描述

​ 图3 在对象(堆)内存的外层套上cookie以确保free可以找到正确的释放位置

2. 堆区数组与堆区变量分配的不同

想想我们在c++中是怎么释放动态数组的内存?是不是用delete[]?那delete[]delete有啥区别?这要追溯到动态数组(堆区数组)和堆区变量分配上的不同。首先,来确定一下动态数组(堆区数组)和堆区变量指的是啥?

int* arr=new int[3];//动态数组(堆区数组)
int* a=new int;//堆区变量

请添加图片描述
​ 图4 和堆区变量比,堆区数组多了一个”数组大小标志:3“

可以看到,多了4字节的数组大小标志,来记录堆区数组的长度,以便多次调用delete,这是为了可以调用到数组中每个对象的析构函数,防止类中含有指针,造成泄露!

另外,图4比图3还多了一个pad填充,这和结构体字节对齐不要搞混!堆区内存分配时的pad才是为了向8字节对齐而进行的填充,而结构体字节对齐从来没有”向8字节对齐“的这种说法!

3. VC中debug模式和release模式分配的不同

这个从表面上看很好理解,debuger需要为每个变量记录一些额外信息,所以在内存分配的时候自然要增加一些空间占用,具体用来干啥的这个有点深奥了,暂时还研究不透啊哈哈哈。。。
请添加图片描述
​ 图5 和release模式相比,debug模式在内存分配上的增加

可以作为扩展知识了解一下,VC下dubug模式的做法是在对象内存前加上32B的调试头,后面加上4B的调试尾。最后别忘了向8B对齐做填充。

  • 最后,对于 VC等主流编译器究竟是如何管理一个类的分配空间 这部分知识的逻辑一定要清晰,栈上值语义变量的分配根本不会有“cookie”和“数组大小标志”这种额外分配机制,这些额外分配机制是为了确保堆区内存释放正确而专门定制的!而对于栈上的debug模式我理解还是会分配额外调试头尾的。
  • 0
    点赞
  • 4
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术工厂 设计师:CSDN官方博客 返回首页
评论

打赏作者

zkccpro

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值