一、 生存期
从变量值的存在时间(生存期)来观察。有的变量在程序运行的整个过程都是存在的,而有的变量则是在调用其所在函数才临时分配存储单元,而在函数调用结束后该存储单元就马上释放了,变量就不存在了。
1.1 静态存储方式
是指在程序运行期间由系统分配固定的存储空间的方式。
1.2 动态存储方式
是指在程序运行期间根据需要进行动态的分配存储空间的方式。下面我们讨论一下free因何崩溃。
二、 free崩溃
void __cdecl free(
_Pre_maybenull_ _Post_invalid_ void* _Block
);
本声明来自VS2017, 从声明我们可以看到,free函数的参数只有1个——无类型的指针。这就带来一个问题:释放申请的空间,不借助其他参数,操作系统如何知晓用完待释放的空间的大小呢?既然函数传参时不能体现,肯定就在别处解决了。需要留心边界标志法——大概含义就是申请下来的空间还要有头部、尾部:头部用于记录大小,尾部用于碎片整理过程(小的空闲碎片合并成大的空闲碎片)。free因何越界原因就很清楚了,极有可能是因为破坏了边界信息:
2.1 上越界
#include<malloc.h>
#include<stdio.h>
#include<assert.h>
int main()
{
int *p = (int*)malloc(10); //申请10byte的空间
assert(NULL != p);
p++;
free(p); //p向上偏移进行解引用取出块的大小时发生错误 段错误
return 0;
}
2.2 下越界
#include<malloc.h>
#include<stdio.h>
#include<assert.h>
int main()
{
int *p = (int*)malloc(10); //申请10byte的空间
assert(NULL != p);
for (int i = 0; i < 10 + 1; ++i) //故意设置成数组访问越界
{
p[i] = 1;
}
free(p); //下越界
return 0;
}
2.3 重复释放同一块空间
#include<malloc.h>
#include<stdio.h>
#include<assert.h>
int main()
{
int *p = (int*)malloc(10); //申请10byte的空间
assert(NULL != p);
free(p);
free(p); //第二次,属于重复free
return 0;
}
三、 realloc
对于动态内存开辟这个行为:我们不会去讨论生存期或者地址高低,前者不必要是因为动态开辟的内存会一直存在 until 主动free或者程序结束;后者是因虚拟地址空间和段页机制的存在,先申请的内存不一定位于低地址,后申请的内存不一定位于高地址。
3.1 情况1
#include<malloc.h>
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
int main()
{
int *p = (int*)malloc(10); //申请10byte的空间
assert(NULL != p);
printf("%x\n", p);
int *q = (int*)malloc(10); //防止在p的尾部追加空间
p = (int*)realloc(p, 200); //原有空间释放,申请200byte的空间
printf("%x\n", p);
free(p);
return 0;
}
3.2 情况2
#include<malloc.h>
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
int main()
{
int *p = (int*)malloc(10); //申请10byte的空间
assert(NULL != p);
printf("%x\n", p);
int *q = (int*)malloc(10); //防止在p尾部添加空间
p = (int*)realloc(p, 6); //原有空间释放,申请6byte的空间
p[8] = 100;
printf("%x\n", p);
free(p); //如果是新申请的空间,越界时将崩溃,但是最后验证了并没有崩溃
return 0;
}
3.3 小结
void* __cdecl realloc(
_Pre_maybenull_ _Post_invalid_ void* _Block,
_In_ _CRT_GUARDOVERFLOW size_t _Size
);
声明来自VS2017,_Size指的是新空间的大小。策略是的:1 如果再分配的空间 ≤ 原有的空间,realloc实际上是一个空壳函数,什么也不做,申请下来的空间还是原来的空间,但是这一过程对用户透明。这一点在3.2节得到了验证——并没有执行内存紧缩,以空间换区时间,回收内存是一件很麻烦的事情;2 如果再分配的空间>原有空间,可能会在原有空间后面追加一块空间;也可能是先释放原有空间,然后新申请一块空间。后者是更为常见的,因为无法确保原有空间后是否还有足够的空间。