动态内存管理
1.
栈区:在函数被调用时分配,用于存放函数的参数值,局部变量等值。在windows中默认值是1M,在Linux中栈的默认大小是10M
2.
堆区:程序运行时可以在堆区动态地申请一定大小的内存,并在用完之后归还堆区。在Linux系统中堆区的大小接近3G
一般情况下我们需要大块内存,或程序在运行过程中才知道所需要内存大小,我们就从堆区申请
3.
C中的4个动态内存管理函数:malloc,calloc,realloc,free,都需要引用 stdlib.h,malloc.h
4.
malloc向堆区申请一块指定大小的连续内存空间
void* malloc(size_t size)
分配的字节未初始化
若分配成功,则malloc函数返回一个指向该连续空间的无类型指针,该指针内,存放的是连续空间的首地址,也就是其第一个字节的序号
#include#include int main() { int n = 10; int *ip = (int *)malloc(sizeof(int)*n); system("pause"); return 0; }
若分配失败,则返回为NULL
若size_t为0,则malloc的行为是实现定义的。可返回空指针,也可返回非空指针;但不应当解引用这种指针,而且应将它传递给free,以避免内存泄漏
#include#include int main() { int *p = (int*)malloc(0); system("pause"); return 0; }
malloc申请0个字节,p将会指向下越界标记
对其进行解引用存储数据将会改变下越界标记,编译器不会报错,但这种是不被允许的
5.
calloc分配并使用0初始化连续的内存空间
void * calloc(size_t num , size_t size);
为num个元素的数组分配内存,并将所有分配的字节数初始化为0
若分配成功,会返回存储分配空间首地址的无类型指针
若分配失败,则返回NULL
若size为0,则行为是实现定义的,可返回空指针,也可返回不可访问存储空间的指针
通过上述描述那么我们可以自己实现一下这个calloc函数
#include#include void* my_calloc(size_t num, size_t size) { void *ptr = (void*)malloc(num*size); if (ptr != NULL) { memset(ptr, 0, num*size); } return ptr; } int main() { int *ip = (int *)my_calloc(10, sizeof(int)); system("pause"); return 0; }
6. realloc
void* realloc(void *ptr, size_t new_size);
重新分配给定的内存区域。它必须是之前malloc,calloc,realloc所分配过的,并且未被free,未被realloc调用所释放的,否则,结果未定义。
重新分配有三种可能:
第一种:
扩张或收缩ptr所指向的已存在内存,后续未分配空间足够大
第二种:
后续未分配内存空间不够大,但堆空间充足
第三种:
堆空间不足,则不释放旧内存块,并返回空指针
注:
若重新追加到0,其行为是实现定义的,可返回NULL,此情况下可能或可能不释放旧内存,或返回不会用于访问存储的非空指针
7.
free用来释放从malloc,calloc,realloc成功获取到动态内存分配的空间
void free (void * ptr)
若ptr为空指针,则函数不进行操作
若ptr的值不是通过从malloc,calloc,realloc返回的值,则行为未定义
若ptr所指向所指向的内存空间已被释放,则行为未定义
若在free()的返回后继续通过ptr访问内存,则行为未定义 (除非另一个分配函数恰好等于ptr的值)
注:
此函数接受空指针(并不对其处理),以减少特列的数量。不管分配是否成功都可将返回的指针传递给free()
free后,只是做了个标记,告诉系统这块内存不用了,如果被free后,指向此空间的指针没被置空,则仍可操作这块空间
free的过程是将原有的空间的数据用ef,ee,或者其他的乱码进行覆盖,以起到保护数据不被泄漏的作用,并将此空间标记为可申请状态
在进行free时,系统会先判断指针所指向堆区空间的状态,第一次free后,已标记为可申请状态,再次free时,系统判断其不是使用状态,而进行报错
#include#include int main() { int *p = (int *)malloc(sizeof(int)); *p = 12; free(p); *p = 23; system("pause"); return 0; }
8.
malloc对内存的管理分为大内存和小内存
<128的分配方式
一小块,一小块的分配,要释放就将一小块整个释放,不能将申请的空间只释放一部分
9.
当程序结束时,如果malloc申请的空间未被释放,则会随着程序结束一起被释放
10.释放的过程
指向空间首地址的指针的会向上偏移读取头部信息,然后将其申请的空间释放掉,同时为其填充ef,ee,或者dd的乱码
11.对8,9深入代码
#include#include int main() { int *p = (int *)malloc(sizeof(int)* 10); // 40个字节 int *ptr = (int *)malloc(sizeof(int)* 5); // 20个字节 system("pause"); return 0; }
未分配ptr时,p所指向空间的内部布局
分配ptr后,p所指向空间的内部布局
ptr所指向的空间内部布局
对比分析:
12. 堆区和栈区的区别
(1)管理方式:栈由系统自动管理;堆由程序员控制,使用方便,但易产生内存泄漏
(2)生长方向:栈向低地址扩展,是连续的内存区域;堆向高地址扩展,是不连续的内存区域。
这是由于堆区管理系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历
(3)空间大小:栈顶地址和栈的最大容量由系统预先规定(默认1M或10M);堆的大小则受限于计算机系统中有效虚拟内存,32为Linux系统中内存可达2.9G
(4)存储内容:栈在函数调用时,首先压入是函数实参,然后主函数中下条指令的地址压入,最后是被调函数的局部变量。本次调用结束,局部变量先出栈,指令地址出栈,最后栈平衡,程序由该点继续运行下条可执行语句。堆通常在头部用一个字节存放其大小,堆用于生存期与函数调用无关的数据,具体内容由程序员安排
(5)分配方式:栈可静态分配或动态分配。静态分配由编译器完成,如局部变量的分配。动态内存的分配由alloca函数在栈上申请空间,用完后自动释放不需要调用free函数。堆只能动态分配且手动释放
(6)分配效率:栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行,因此效率较高。堆由函数库提供,机制复杂,效率比栈低得多
(7)分配后系统响应:只要栈剩余空间大于申请空间,系统将为程序提供内存,否则报告异常提示栈溢出。操作系统为对维护一个记录空闲地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从链表的空闲链表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存空间,以便于有机会分到足够大小的内存,然后进行返回。大多数系统会在该内存空间首地址处记录本次分配空间的大小,供后续释放函数准确释放本内存空间。由于找到的堆结点大小不一定正好等于申请大小,系统自动将多余的部分重新放入空闲链表中
(8)碎片问题:栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面后进栈的内容已弹出。而频繁的申请与释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率低下。
总结:
堆容易造成内存碎片;由于专门的系统支持,效率很低;由于可能引发用户态和内核态切换,内存申请变得格外昂贵。所以栈在程序中应用最广泛,函数调用也利用栈来完成,调用过程中的参数,返回地址,栈基指针和局部变量等都采用栈的方式存放。建议尽量使用栈,仅在分配大量或大块内存空间时,使用堆。最后使用栈和堆时应避免越界发生,否则可能程序崩溃或破环程序堆,栈结构,产生意想不到的结果



