c++类型内存分配规则
Posted zkccpro
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 Aa,b,c aa;//可以 enum Aa,b,c;//可以 enum A:inta,b,c;//不行
-
c++11中的enum:
c++11对enum的改进在于:
-
enum可以当作一个封装手段了!(当然你也可以选择不封装)
-
enum 支持自定义存储类型了,指定了不同的存储类型意味着枚举占用大小即为该类型大小。
//test with g++ -std=c++11 enum A:chara,b,c;//可以,a对外可见 //sizeof (A) => 1 enum class Aa,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模式我理解还是会分配额外调试头尾的。
以上是关于c++类型内存分配规则的主要内容,如果未能解决你的问题,请参考以下文章