C语言的堆内存管理:
C语言中没有管理堆内存的语句,C标准库中提供一套管理堆内存的函数,这些函数底层封装了各操作系统的堆内存管理接口,所以可以跨平台使用,这些函数声明在 stdlib.h 头文件中。
malloc函数:
void *malloc(size_t size); 功能:向malloc申请位size字节的堆内存块 size: 要申请的内存块字节数 如果申请数组形式的内存块,size=sizeof(数组元素类型)*数组长度 返回值: 如果申请成功,则返回内存块首地址 失败申请失败,则返回NULL,现在有堆内存无法满足size个字节的需求 // 如果size的值过大,就会申请失败 int main(int argc,const char* argv[]) { int* p = malloc(0xffffffff); printf("%p\n",p); // (nil) } // 向malloc申请n字节内存 int main(int argc,const char* argv[]) { int* p = malloc(4); printf("请输入一个整数:"); scanf("%d",p); printf("%d\n",*p); } // 向malloc申请一块 一维数组内存 int main(int argc,const char* argv[]) { int len; printf("请输入数组的长度:"); scanf("%d",&len); int* arr = malloc(sizeof(int)*len); for(int i=0; i<len; i++) { printf("%d ",arr[i]); } } // 向malloc申请一块 二维数组内存 int main(int argc,const char* argv[]) { int row,col; printf("请输入二维数组的行数和列数:"); scanf("%d%d",&row,&col); int (*arr)[col] = malloc(sizeof(int)*row*col); for(int r=0; r<row; r++) { for(int c=0; c<col; c++) { printf("%d ",arr[r][c]); } printf("\n"); } }
注意:
1、使用malloc申请到的内存块,里面的内容是不确定的,malloc不会帮我们初始化,可以使用bzero,memset函数进行初始化。
2、如果size等于0,返回NULL或唯一个的地址,并且该地址可以通过free释放而不出错,但不能使用它指向的内存。
calloc函数:
void *calloc(size_t nmemb, size_t size); 功能:申请nmemb个size个字节的内存块,专门用于申请数组型的内存块。 nmemb:数组的长度 size:数组元素的字节数 返回值:与malloc相同 注意:使用calloc申请的内存块,所有字节会被初始化0。 int main(int argc,const char* argv[]) { int len; printf("请输入数组的长度:"); scanf("%d",&len); int* arr = calloc(len,sizeof(int)); for(int i=0; i<len; i++) { // 输出结果肯定是0 printf("%d ",arr[i]); } }
注意:
1、calloc所申请也是一块连续的内存块,所以nmemb和size的参数位置可以调换,就相当于calloc内部调用了malloc函数,只是比malloc多了初始化步骤,而且比malloc的可读性更高。
2、malloc比calloc申请内存的速度快,或者使用malloc+bzero配合。
free函数:
void free(void *ptr); 功能:释放堆内存 ptr:要释放的内存块的首地址,它必须是malloc、calloc函数的返回值 注意: int main(int argc,const char* argv[]) { int len = 20; int* arr = malloc(sizeof(int)*len); for(int i=0; i<len; i++) { arr[i] = 0x01020304; printf("%d ",arr[i]); } printf("\n"); // 释放掉使用权,并破坏一部分内容 free(arr); // 非法使用 for(int i=0; i<len; i++) { printf("%d ",arr[i]); } printf("\n"); }
注意:
1、free释放的是使用权,只破坏内存块的一部分内存,大部分数据还在,这样设计的原因是释放速度比较快,就像在硬盘上删除文件一样,只是把存储文件那片区域的使用释放旧,数据还存储在磁盘上,这也是我们能进行数据恢复的原因。
2、free的参数可以是空指针,不会出现错误,也不会执行任何操作,这也是空指针比野指针安全的原因。
3、如果内存被重复释放则会出现"double free or corruption (fasttop)",程序会异常停止,所以在第一次释放内存后,要把与堆内存配合的指针及时的赋值为空,防止重复释放产生的错误。
realloc函数:
void *realloc(void *ptr, size_t size); 功能1:把已有的堆内存块调小 ptr是malloc、calloc、realloc的返回值,也就已有堆内存块首地址 size < oldsize 此时不需要关心realloc的返回值 int main(int argc,const char* argv[]) { int* p1 = malloc(1024); p1 = realloc(p1,10); int* p2 = malloc(1024); printf("%p %p\n",p1,p2); } 功能2:把已有的堆内存块调大 ptr是malloc、calloc、realloc的返回值,也就已有堆内存块首地址 情况1:如果ptr后续的内存没有被占用,realloc会在ptr的基础上进行扩大 情况2:如果ptr后续的内存已经被占用,realloc会重新分配一块符合要求的内存块,并把ptr上的内容拷贝到新的内存块,然后释放ptr,再返回新内存块的首地址 使用此功能时,我们必须重新接收realloc函数的返回值,我们无法预料realloc执行的是情况1还是情况2。 // 情况1 int main(int argc,const char* argv[]) { int* p1 = malloc(40); int* p2 = realloc(p1,80); // 此时p1 == p2,是在p1的基础上直接扩大的 printf("%p %p\n",p1,p2); } // 情况2 int main(int argc,const char* argv[]) { int* p1 = malloc(40); for(int i=0; i<10; i++) p1[i] = i+1; // 占了p1r的后续位置,realloc就不能在p1的基础上直接扩展了 int* tmp = malloc(4); int* p2 = realloc(p1,80); // 此时p1 != p2,重新分配了一块内存,先把p1的内容拷贝新内存块,并把p1释放掉了,然后返回新内存块的地址 printf("%p %p\n",p1,p2); for(int i=0; i<10; i++) { printf("%d %d\n",p1[i],p2[i]); } } 功能3:释放内存 ptr是malloc、calloc、realloc的返回值 0==size,此时realloc的功能就相当于free 功能4:申请内存 NULL==ptr,0<size 此时的功能就相当于malloc
注意:
虽然realloc具有释放和申请堆内存的功能,但我们一般不使用,而是直接使用malloc和free,主要使用的是realloc的调整内存块大小的功能。
总结:
使用堆内存只需要掌握malloc和free函数即可,对于calloc和realloc函数了解即可。
关于堆内存的相关问题(补充):
疑问:堆内存越界时为什么超过135160才会出现段错误?
#include <stdio.h> #include <stdlib.h> int main(int argc,const char* argv[]) { char* ptr = malloc(1); // 只要越界的不超过135160,就不会出现段错误 printf("%c\n",ptr[135160]); } 1、当程序首次向malloc申请内存时,此时malloc手里没有堆内存可分配,malloc会向操作系统申请堆内存,操作系统会一次性分配33页内存交给malloc管理(一页内存=4096个字节),之后再向malloc申请内存时,malloc会从这33页内存中分配给调用者。
2、使用malloc申请的每个内存块前面会有4~12个字节的空隙,malloc会根据所申请的内存块的大小自动调整空隙的大小。
3、内存块前面的空隙有两部分:
空隙的前0~8字节:用于内存对齐
空隙的末尾4字节:也就是内存块前面的4字节,存储首malloc的管理信息,这块信息被破坏会影响后续malloc、free、printf、scanf函数的使用。
int main(int argc,const char* argv[]) { int* p1 = malloc(8); p1[-1] = 0; // 管理信息会影响p1的释放 p1[0] = 1234; // 申请的4字节内存块 p1[1] = 5678; // 空闲字节 p1[2] = 6666; // 空闲字节 p1[3] = 0; // 管理信息,一旦被破坏会影响后续malloc函数的使用 int* p2 = malloc(8); int* p3 = malloc(12); int* p4 = malloc(4); printf("%p \n",p1); printf("%p \n",p2); printf("%p \n",p3); printf("%p \n",p4); }
为什么是135160会出现段错误:
操作系统交给malloc33页内存(135168个字节),可访问的范围是0~135167,malloc会预留8个字节的空隙,返回给程序的是33页内存的第9个字节的地址(33页内存还剩135160个字节),所以可访问的范围是0~135159,只要在这个范围就不会出现段错误。
这种分配机制的优点:
1、避免了频繁打扰操作系统,而影响操作系统的速度。
2、段错误产生的原因是被操作系统发现非法使用内存,所以我们使用malloc分配的内存越界时,只要不超过33页范围就不会产生段错误。
使用堆内存越界的后果:
1、越界使用的是空隙的空闲字节,一切正常,可以安全访问。
int main() { int* p = malloc(4); p[0] = 123; // 申请到的内存块 p[1] = 456; // 空闲 p[2] = 789; // 空闲 }2、越界破坏了malloc的管理信息,会影响后续malloc、free、scanf、printf函数的使用。
int main() { int* p = malloc(4); p[3] = 0; // 存储着malloc的管理信息,后续无法继续申请堆内存 p[-1] = 0; // 存储着malloc的管理信息,p内存块无法释放 }3、越界使用malloc还未分配出去的内存,虽然不会产生段错误,但后续malloc把它分配出去后,可能会产生脏数据。
int main(int argc,const char* argv[]) { int* p1 = malloc(4); p1[4] = 6666; printf("%d\n",p1[4]); // 6666 printf("%d\n",p1[4]); // 随机值 } int main(int argc,const char* argv[]) { int* p1 = malloc(4); p1[4] = 6666; int* p2 = malloc(4); // malloc把p1[4]位置内存分配给了p2 *p2 = 123456789; printf("%d\n",p1[4]); printf("%d\n",*p2); }4、超出33页范围,就产生段错误。
int main(int argc,const char* argv[]) { char* ptr = malloc(1); // 只要越界的不超过135160,就不会出现段错误 printf("%c\n",ptr[135160]); } int main(int argc,const char* argv[]) { int* p1 = malloc(4); printf("%d\n",p1[33790]); }