C语言的堆内存管理:
什么是堆内存:
是进程的一个内存段(text、data、bss、heap、stack),由程序员手动管理
特点就是足够大,缺点就是使用麻烦,比较危险。
为什么要使用堆内存:
-
随着程序变复杂,数据量开始变多。
-
其它内存段的申请和释放不受控制,堆内存的申请释放受程序员控制。
如何使用堆内存:
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"); } // 指针数组 不规则的二维数组 int* arr[col] = {}; for(int r=0; r<row; r++) { for(int c=0; c<col; c++) { arr[r] = malloc(sizeof(int)*col); } } }
注意:
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配合。
练习:实现自定义的calloc函数,底层调用malloc函数实现。
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]); //printf("%c\n",ptr[1024*33-2]); }
1、当程序首次向malloc申请内存时,此时malloc手里没有堆内存可分配,malloc会向操作系统申请堆内存,操作系统会一次性分配33页内存交给malloc管理(一页内存=4096个字节),之后再向malloc申请内存时,malloc会从这33页内存中分配给调用者。
但这不意味着可以越界访问,因为malloc把使用分配给"其他人",这样会产生脏数据。
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]); }
内存碎片:
什么是内存碎片:
已经释放了使用的权的内存,但无法被malloc再次分配出去,这种内存叫内存碎片。
int* p1 = malloc(4); int* p2 = malloc(4); int* p3 = malloc(4); free(p2); // 此时p2已经被释放,但无法再次分配给p4,p2就是内存碎片,如果后续不再分配较小的内存块,p2可能一直接内存碎片 int* p4 = malloc(16); // 此时p2就有可能被再次分配出来,它就不是内存碎片, int* p5 = malloc(4); // 如果p1或p3被释放,p2就不是内存碎片了 free(p1); free(p3);
内存碎片产生的原因:
内存和释放、分配时间、大小不协调导致的。
一块内存碎片,只是短时间内是碎片,过一段时间它可能就不是碎片了,所以内存碎片不是绝对的。
如何减少内存碎片:
前提:内存碎片只能尽量减少,无法杜绝。
1、尽量使用栈内存(要了解栈内存的特性,要知道栈内存的使用上限,ulimit -s 、ulimit -s <size>)。
2、尽量分配大块内存自己管理。
int* p1 = malloc(16); int* p2 = p1+1; int* p3 = p1+2; int* p4 = p1+3;
3、按照分配的顺序,逆序释放,把堆内存当栈进行管理。
int* p1 = malloc(4); int* p2 = malloc(4); int* p3 = malloc(4); int* p4 = malloc(4); free(p4); free(p3); free(p2); free(p1);
4、内存碎片整理
int* p1 = malloc(4); int* p2 = malloc(4); int* p3 = malloc(4); memcpy(p2,p3,4); swap(p2,p3); free(p2);
内存泄漏:
什么是内存泄漏:
内存已经不再使用,但无法被释放的内存叫内存泄漏。
但这不是最严重的,严重的是反复的内存泄漏,例如:一个函数执行需要分配一块堆内存,等它执行完毕后堆内存没有被释放,等再次调用这个函数时,它又重新分配堆内存,又没有释放,反复这样会导致可用的内存越来越少,系统、程序会变得越来越慢、卡、死机。
注意:程序一旦结束属于它的资源都会被操作系统回收。但不是所有程序都适用该方法来回收资源。
void func(void) { int* p = malloc(40); for(int i=0; i<10; i++) { p[i] = rand() % 100; printf("%d ",p[i]); if(条件) return; } printf("\n"); free(p); } int main() { for(;;) { ... func(); ... } }
产生内存泄漏的原因:
1、只写的内存分配语句,而忘记写内存释放语句,可能是粗心大意,也可能是以为别人会释放。
2、写了内存释放语句,但由于执行流程、执行条件设计有问题,导致释放语句没有执行。
3、与堆内存配合的指针被破坏,改变了指向,导致free语句执行无效。
如何减少内存泄漏:
1、按规则分配、释放内存:
自用:谁申请谁释放,分配语句和释放语句成对出现。
共用:谁知道该释放谁释放,项目组中负责分配和负责释放的人要进行对接。
2、封装malloc和free函数,记录每一次的分配和释放的内存块地址,通过对比记录,就可以发现是否有内存泄漏。
void my_free(void* ptr) { printf("free %p\n",ptr); // 也可以记录到日志中 free(ptr); } void* my_malloc(size_t size) { void* ptr = malloc(size); printf("malloc %p\n",ptr); return ptr; }
3、使用const保护与堆内存配合的指针变量,防止指针被破坏。
int* const p = malloc(4); p = # // 编译会出错 free(p); // 此时释放的肯定是堆内存
如何判断和定位内存泄漏:
1、查看内存的使用情况
windows 任务管理器 、Linux ps -aux命令
2、使用检查内存泄漏的工具:
# 安装该工具的命令 sudo apt install valgrind valgrind是一套Linux下的仿真调试工具集 memcheck是其中一个工具,可以检查程序中的内存问题,如泄漏、越界、非法指针等。可以检测: 使用未初始化的内存 读/写已经被释放的内存 读/写内存越界 读/写不恰当的内存栈空间 内存泄漏 使用malloc和free不匹配等 #使用该工具检测程序: valgrind --tool=memcheck --leak-check=yes ./a.out --tool=<name>指定要使用的工具,默认为memcheck --leak-check=yes|no 是否对内存泄漏给出详细信息
3、根据封装的malloc和free记录到日志的信息进行比对
内存泄漏和内存碎片的危害:
前提:当程序结束时,操作系统会把分配它的所有资源全部回收(包括系统分配给程序的堆内存),所以当程序结束时,内存碎片和内存泄漏就会消失,这也是为什么软件、系统重启能解决很多问题。
注意:服务器端的程序一般需要7*24小时运行,不能随意结束。
客户端的程序可以随意的关闭、重启系统和软件,所以即使发生内存碎片和内存泄漏,也问题不大,但如果是服务端的程序,即使只有少量的内存泄漏和内存碎片,长年累月下来也会导致系统可用的内存越来越少,系统、程序会变得越来越慢、卡、死机。
例子:说明跨函数共享指针变量,需要传递二级指针
#include <stdio.h> #include <stdlib.h> void CreateMem(void* p,size_t n) { //printf("%p\n",&p); *p = malloc(n); if(NULL == *p) { perror("malloc"); } } int main(int argc,const char* argv[]) { int* p = NULL; CreateMem(p,40); //p还是NULL,因为跨函数传递指针必须传递二级指针 printf("%p\n",&p); printf("%p\n",p); for(int i=0; i<10; i++) { p[i] = i; printf("%d\n",p[i]); } free(p); p = NULL; }
练习1:计算出100~10000 之间的素数,结果存储在堆内存中,不要浪费内存。
#include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <math.h> bool is_prime(int num) { for(int i=2; i<=sqrt(num); i++) { if(0 == num % i) return false; } return true; } int main(int argc,const char* argv[]) { int* arr = NULL , cnt = 0; for(int i=100; i<1000; i++) { if(is_prime(i)) { arr = realloc(arr,sizeof(int)*(cnt+1)); arr[cnt++] = i; } } for(int i=0; i<cnt; i++) { printf("%d ",arr[i]); } free(arr); arr =NULL; return 0; }
常用的内存操作函数:
void bzero(void *s, size_t n); 功能:把内存块s的n个字节,赋值为0。 void *memset(void *s, int c, size_t n); 功能:把内存块s的n个字节,赋值为c(0~255) void *memcpy(void *dest, const void *src, size_t n); 功能:从src内存块拷贝n个字节的内容到dest内存块 void *memmove(void *dest, const void *src, size_t n); 功能:与memcpy相同,不同的是当dest与src重叠时,该函数能正常工作,memcpy行为不确定 int memcmp(const void *s1, const void *s2, size_t n); 功能:比较s1和s2内存块的n个字节 s1 > s2 返回1 s1 < s2 返回-1 s1 == s2 返回0