本文试图回答这样3个问题:
- c语言结构体内存对齐的规则?
- class、enum、union的作用和区别?
- VC等主流编译器究竟是如何管理一个类的分配空间的?
这是一个c语言类型内存分配最基本的问题了,我以前看到的一些说法经过自己的测试并不完全正确,学会了这个思路之后,以后再被问到这个问题就再也不怕了!
首先看一段代码:
class A{
char a;//1B
double b;//8B
int c;//4B
};
int main(){
cout<
你说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个规则在心中,再也不怕被问到字节对齐的问题了!
这个话题其实很经典,任何c/c++程序员都应该说出它们的基本用法和区别。。但我翻看了一下我曾经的所有笔记,竟然没有任何一篇涉猎到这个话题!今天借此给补上!
1. class与structc语言并没有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的改进在于:
-
enum可以当作一个封装手段了!(当然你也可以选择不封装)
-
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。
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)
。接下来我通过侯捷老师的课程简单分析一下这些情况:
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条)