zl程序教程

您现在的位置是:首页 >  其他

当前栏目

C++|内存管理|数组内存分配机制

2023-03-15 22:02:29 时间

本文参考Effective C++与编译器源码

引言:你是否想过数组和指针为什么sizeof不同,你是否想过为什么new[]需要指定长度,而delete[]不需要,你是否质疑过为什么传数组一定要顺带传大小,你是否还以为堆上一定分配着数组大小? 以下为您深(浅)入探索C++中的内存模型。


本文内容为自己的读书笔记+实验,如无泛用性,杠精退散。

数组-》指针,退化之路

int a[5][5];
int **b = new int* [5];

这两者有什么不同呢?显然,对a进行sizeof,大小应该是100,而b则是8,也就是说,数组显然存在着某种额外的信息,告诉着你数组的大小。

很多无水平的教科书会对数组和指针进行混淆,事实上,由数组在传参中转化为指针的过程是一种退化,丢失了大小信息。

然而,这种退化并不是万能的

int a[5][5];
void fun(int ** para);

你会发现,如果数组和指针可以任意转化的话,应该是能匹配的,然而,事实上却完全不可。

因为在a[1]这样的过程中,计算a的偏移量是依赖于元素的大小的,int**对象+1的偏移量会是int*的大小,而不是int[5]的大小. 而对于int*和 int[5]而言,他们的元素是一样的int。

总而言之,退化只能退化顶层的数组。


如何存储数组的大小

  1. 对于栈中的自动对象,int a[5]等,直接由编译器提供大小,作为一种立即数直接参与汇编码中,这也是为什么栈数组必须使用常数的缘故,因为作为代码的一部分这必须是编译期间已知的。
  2. 对于堆上的内置类型或POD结构体(int,char等等),不存储大小,因为编译器根本无需析构,也没有必要知道数组具体的大小。内存的释放由malloc/free存储的字节大小处理即可。
  3. 对于堆上有构造或者析构函数的对象,存储大小有两种典型方式。一种是在分配的对象前一段内存处分配size_t的大小存储大小,另一种则是用关联数组,对将地址和对应的大小进行关联。前者实现简便,后者则避免了内存修改导致大小被污染的风险。

事实上,很多人都有这样的误解,即所有数组前面都存放着大小,然而看了这一段,你会发现编译器很聪明,不会把内存浪费在无意义的地方。


new[]的流程解析

new的操作看似简单,实际上却由编译器进行重排,内联展开后插入很多隐藏的代码

1.判断数据类型
2.计算内存大小(依据1中是否需要存储大小给予额外的空间)
3.new_array函数直接调用new_scalar(事实上你的[]并没有实际作用,
仅仅是一种提示,真正的改变是由编译器的额外代码完成的)
4.new_scalar调用系统的malloc函数
5.malloc函数查找到空余内存,开辟一段chunk,将chunk标记为已使用,然后记录chunk大小。(依赖于系统)
6.返回chunk的首指针
7.如果1中判断需要进行析构或者构造,则首先存储大小,再让指针加上一段偏移量,
对于最终的指针,根据对象的大小和数量对于分配后每段内存进行对应的构造。
8.返回(偏移后)的指针。

可以看出,事实上malloc的大小会根据编译器对于数据类型的识别而改变,所以不能轻易地把所有的数组都当做存储大小混为一谈。在new[]操作符中,一部分内存用于存储数组大小;而在malloc操作符中,一部分内存用于存储字节大小。关于malloc的实现。