C语言的堆内存管理:
什么是堆内存:
是进程的一个内存段(text、data、bss、heap、stack),由程序员手动管理
特点就是足够大,缺点就是使用麻烦,比较危险。
为什么要使用堆内存:
-
随着程序变复杂,数据量开始变多。
-
其它内存段的申请和释放不受控制,堆内存的申请释放受程序员控制。
如何使用堆内存:
C语言中没有管理堆内存的语句,C标准库中提供一套管理堆内存的函数,这些函数底层封装了各操作系统的堆内存管理接口,所以可以跨平台使用,这些函数声明在 stdlib.h 头文件中。
malloc函数:
void *malloc(size_t size); 功能:向malloc申请连续的size字节的堆内存块 size: 要申请的内存块字节数 如果申请数组形式的内存块,size=sizeof(数组元素类型)*数组长度 返回值: 如果申请成功,则返回内存块首地址,绝大多数情况是成功 失败申请失败,则返回NULL,例如现在有堆内存无法满足size个字节的需求
#include <stdio.h> #include <stdlib.h> int main(int argc,const char* argv[]) { int* p = malloc(0xffffffff); if(NULL == p) { printf("堆内存申请失败\n"); return -1; } printf("%p\n",p); *p = 100; printf("%d\n",*p); // 申请一维数组堆内存 int* arr = malloc(4*200); for(int i=0; i<200; i++) { printf("%d ",arr[i]); arr[i] = i; } // 申请二维数组堆内存 int row,col; printf("请输入二维数组的行、列:"); scanf("%d %d",&row,&col); // 数组指针 int (*arrp)[col] = malloc(sizeof(int)*row*col); for(int i=0; i<row; i++) { for(int j=0; j<col; j++) { printf("%d ",*(*(arrp+i)+j)); //printf("%d ",arrp[i][j]); } printf("\n"); } // 申请不规则二维数组堆内存 int row,col; printf("请输入二维数组的行、列:"); scanf("%d %d",&row,&col); // 指针数组 int* arr[row] = {}; for(int i=0; i<row; i++) { arr[i] = malloc(sizeof(int)*(col+i)); } }
注意:
1、使用malloc申请到的内存块,里面的内容是不确定的,malloc不会帮我们初始化,可以使用bzero,memset函数进行初始化。
2、如果size等于0,返回NULL或唯一个的地址,并且该地址可以通过free释放而不出错,但不能使用它指向的内存。
3、通过一次malloc申请出来的内存,如果成功时该内存段必定连续,如果分多次malloc申请多段内存段,每段内存段之间不一定连续
calloc函数:
void *calloc(size_t nmemb, size_t size); 功能:申请nmemb个size个字节的内存块,专门用于申请数组型的内存块。 nmemb:数组的长度 size:数组元素的字节数 返回值:与malloc相同 注意:使用calloc申请的内存块,所有字节会被初始化0。
注意:
1、calloc所申请也是一块连续的内存块,所以nmemb和size的参数位置可以调换,就相当于calloc内部调用了malloc函数,只是比malloc多了初始化步骤,而且比malloc的可读性更高。
2、malloc比calloc申请内存的速度快,或者使用malloc+bzero配合。
练习:实现自定义的calloc函数,底层调用malloc函数实现。
void* my_calloc(size_t nmemb,size_t size) { if(0 == nmemb || 0 == size) return NULL; char* ptr = malloc(nmemb*size); bzero(ptr,nmemb*size); return ptr; }
free函数:
void free(void *ptr); 功能:释放堆内存 ptr:要释放的内存块的首地址,它必须是malloc、calloc函数的返回值
注意:
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的返回值 功能2:把已有的堆内存块调大 ptr是malloc、calloc、realloc的返回值,也就已有堆内存块首地址 情况1:如果ptr后续的内存没有被占用,realloc会在ptr的基础上进行扩大 情况2:如果ptr后续的内存已经被占用,realloc会重新分配一块符合要求的内存块,并把ptr上的内容拷贝到新的内存块,然后释放ptr,再返回新内存块的首地址 使用此功能时,我们必须重新接收realloc函数的返回值,我们无法预料realloc执行的是情况1还是情况2。 #include <stdio.h> #include <stdlib.h> int main(int argc,const char* argv[]) { int *p = malloc(4); printf("%p\n",p); *p = 100; p = realloc(p,8); printf("%p %d\n",p,*p); } 功能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[4096*33-9]) }
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函数的使用。
注意:堆内存越界的后果?
为什么是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把它分配出去后,可能会产生脏数据。
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* p = malloc(16); int* p1 = p+1; int* p2 = p+2; int* p3 = p+3; free(p); // 使用较为麻烦
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); // p2指向p3 p3指向p2 free(p2); // 碎片 *p3; // 变量名p3正常使用
内存泄漏:
什么是内存泄漏:
内存已经不再使用,但无法被释放的内存叫内存泄漏。
void func(int num) { int* p = malloc(num); if(条件) return; free(p); } int main(void) { for(;;) { func(4); } free(p) }
但这不是最严重的,严重的是反复的内存泄漏,例如:一个函数执行需要分配一块堆内存,等它执行完毕后堆内存没有被释放,等再次调用这个函数时,它又重新分配堆内存,又没有释放,反复这样会导致可用的内存越来越少,系统、程序会变得越来越慢、卡、死机。
注意:程序一旦结束属于它的资源都会被操作系统回收。但不是所有程序都适用该方法来回收资源。
产生内存泄漏的原因:
1、只写的内存分配语句,而忘记写内存释放语句,可能是粗心大意,也可能是以为别人会释放。
2、写了内存释放语句,但由于执行流程、执行条件设计有问题,导致释放语句没有执行。
3、与堆内存配合的指针被破坏,改变了指向,导致free语句执行无效。
int* p = malloc(4); *p = 100; p = NULL; free(p);
如何减少内存泄漏:
1、按规则分配、释放内存:
自用:谁申请谁释放,分配语句和释放语句成对出现。
共用:谁知道该释放谁释放,项目组中负责分配和负责释放的人要进行对接。
2、封装malloc和free函数,记录每一次的分配和释放的内存块地址,通过对比记录,就可以发现是否有内存泄漏。
void* my_malloc(size_t size) { void* ptr = malloc(size); printf("debug: my_malloc:%p\n",ptr); return ptr; } void my_free(void* ptr) { printf("debug: my_free:%p\n",ptr); // 可以记录到日志 free(ptr); }
3、使用const保护与堆内存配合的指针变量,防止指针被破坏。
int* const p = malloc(4); p = NULL; // 无法修改
如何判断和定位内存泄漏:面试题*
1、查看内存的使用情况
windows 任务管理器 、Linux ps -aux命令
大致确定是哪个进程发生了内存泄漏
2、使用检查内存泄漏的工具:
#更新软件源 sudo apt-get update # 安装该工具的命令 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小时运行,不能随意结束。
客户端的程序可以随意的关闭、重启系统和软件,所以即使发生内存碎片和内存泄漏,也问题不大,但如果是服务端的程序,即使只有少量的内存泄漏和内存碎片,长年累月下来也会导致系统可用的内存越来越少,系统、程序会变得越来越慢、卡、死机。
说明:想要共享指针变量,必须传递二级指针
void create_mem(void** p,size_t n) { *p = malloc(n); printf("create_mem:%p\n",*p); if(NULL == *p){ printf("malloc error\n"); } } int main(int argc,const char* argv[]) { int* p = NULL; create_mem(&p,40); printf("--------%p\n",p); for(int i=0; i<10; i++) { p[i] = i; //printf("--------\n"); printf("%d ",p[i]); } free(p); p = NULL; }
练习1:计算出100~10000 之间的素数,结果存储在堆内存中,尽量不要浪费内存。
#include <stdio.h> #include <stdbool.h> #include <stdlib.h> bool is_prime(int num) { for(int i=2; i<=num/2; i++) { if(0 == num%i) return false; } return true; } int main(int argc,const char* argv[]) { int* p = NULL; int cnt = 0; for(int i=100; i<10000; i++) { if(is_prime(i)) { cnt++; // 扩容 p = realloc(p,cnt*4); p[cnt-1] = i;u } } for(int i=0; i<cnt; i++) { printf("%d ",p[i]); } free(p); p = NULL; }
常用的内存操作函数:
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