1. 动态内存管理的意义在C语言中动态内存管理一直是非常重要的普通变量和数组虽然可以开辟内存但它们的大小通常在编译阶段就已经确定无法灵活适应程序运行时的数据规模变化。int val 20; char arr[10] {0}; struct s{ int num; char arr[10] }上述第一行代码会在栈区开辟一块空间用来存放整型变量val第二行代码会在栈区开辟一段连续空间用来存放长度为 10 的字符数组arr。第三行代码开始定义了一个结构体类型 struct s该结构体中包含一个整型成员 num和一个字符数组成员 arr。但这些空间大小往往都是固定的也就是说普通数组在声明时必须提前指定长度程序运行过程中无法灵活调整数组大小。在一些简单场景下这样做没有问题。比如我们明确知道只需要存放 10 个元素那么直接定义数组即可。但是在实际开发中很多时候我们并不能提前确定需要多少空间。例如用户输入多少个数据程序运行前并不知道文件中有多少内容程序读取前并不知道通讯录中要保存多少个联系人程序一开始也无法确定学生成绩管理系统中有多少名学生可能需要运行时才能决定。如果我们仍然使用固定大小的数组就可能出现两个问题第一如果数组开小了数据放不下可能会导致越界访问。int arr[10]; // 如果实际需要存放 100 个数据arr 就不够用了第二如果数组开大了虽然可以避免空间不够的问题但会造成内存浪费。int arr[10000]; // 实际只存放 10 个数据剩下的空间就被浪费了因此固定大小的栈空间并不能很好地应对所有场景。尤其是当程序需要在运行过程中根据实际情况决定申请多少内存时就需要一种更加灵活的内存管理方式。这就是动态内存管理存在的意义C 语言提供了malloc、calloc、realloc和free等函数让程序员可以在程序运行期间主动向堆区申请空间并在使用完毕后主动释放空间。简单来说普通变量和数组通常是在栈区开辟空间栈区空间由系统自动管理使用方便但灵活性较差动态内存是在堆区申请的堆区空间由程序员手动申请和释放使用更加灵活当程序运行时才知道需要多少空间时就应该考虑使用动态内存管理。所以动态内存管理解决的核心问题就是当程序在运行过程中才知道需要多少内存时可以按需申请、按需释放从而更加灵活地使用内存。2. C/C 程序中的内存区域划分1.栈区stack在执⾏函数时函数内局部变量的存储单元都可以在栈上创建函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中效率很⾼但是分配的内存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。2.堆区heap⼀般由程序员分配释放 若程序员不释放程序结束时可能由OS回收 。分配⽅式类似于链表。3.数据段静态区static存放全局变量、静态数据。程序结束后由系统释放。4.代码段存放函数体类成员函数和全局函数的⼆进制代码。3. malloc与free 函数详解3.1 malloc函数malloc是C语⾔提供了⼀个动态内存开辟的函数void* malloc(size_t size);这个函数向内存申请⼀块连续可⽤的空间并返回指向这块空间的指针。1.如果开辟成功则返回⼀个指向开辟好空间的指针。2.如果开辟失败则返回⼀个NULL 指针因此malloc的返回值⼀定要做检查。返回值的类型是 void* 所以malloc函数并不知道开辟空间的类型具体在使⽤的时候使⽤者⾃⼰来决定。3.如果参数 size 为0malloc的⾏为是标准是未定义的取决于编译器。可能分配也可能报错。3.2 free函数C语⾔提供了另外⼀个函数free专⻔是⽤来做动态内存的释放和回收的函数原型如下1 void free (void* ptr);free函数⽤来释放动态开辟的内存。如果参数 ptr 指向的空间不是动态开辟的那free函数的⾏为是未定义的。如果参数 ptr 是NULL指针则函数什么事都不做。malloc和free都声明在stdlib.h 头⽂件中。但函数free后并不一定会将ptr指针后的连续内存置为空故一般在free后要将ptr置为NULL避免空指针。#include stdio.h #include stdlib.h int main() { int arr[3]{0}; int*ptr; ptr(int*)malloc(3*sizeof(arr[0])); for(int i0;i3;i){ *(ptri)i;//为ptr指针所指内存赋值 arr[i]*(ptri);//将ptr内存所指值赋值给arr printf(arr[%d]%-2d,i,arr[i]); printf(ptr[%d]%-2d,i,ptr[i]); printf(\n); } free(ptr); return 0; }此时ptr未置空则ptr所指内存视图如下所示(内存所示地址为小端存储16进制):可以看到ptr所指不为空有野指针风险。#include stdio.h #include stdlib.h int main() { int arr[3]{0}; int*ptr; ptr(int*)malloc(3*sizeof(arr[0])); for(int i0;i3;i){ *(ptri)i;//为ptr指针所指内存赋值 arr[i]*(ptri);//将ptr内存所指值赋值给arr printf(arr[%d]%-2d,i,arr[i]); printf(ptr[%d]%-2d,i,ptr[i]); printf(\n); } free(ptr); ptrNULL; return 0; }置空后ptr所指内存如下(内存所示地址为小端存储16进制Clion2025):可以看到已被置空4. calloc函数与realloc函数4.1 calloc函数C语⾔还提供了⼀个函数叫 calloc calloc 函数也⽤来动态内存分配。原型如下1 void* calloc (size_t num, size_t size);函数的功能是为 num 个⼤⼩为 size 的元素开辟⼀块空间并且把空间的每个字节初始化为0。与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。举个例子#include stdio.h #include stdlib.h int main() { int *p (int*)calloc(10, sizeof(int)); if(NULL ! p) { int i 0; for(i0; i10; i) { printf(%d , *(pi)); } } free(p); p NULL; return 0; }输出结果0 0 0 0 0 0 0 0 0 0此时查看内存视窗图可见p所指内存内容均为0(内存所示地址为小端存储16进制)由图可知p所指地址空间存储字节均为0所以如果我们对申请的内存空间的内容要求初始化那么可以很⽅便的使⽤calloc函数来完成任务。4.2 realloc函数realloc函数的出现让动态内存管理更加灵活。有时会我们发现过去申请的空间太⼩了有时候我们⼜会觉得申请的空间过⼤了那为了合理的使⽤内存我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc 函数就可以做到对动态开辟内存⼤⼩的调整。函数原型如下1 void* realloc (void* ptr,size_t size);ptr是要调整的内存地址size为 调整之后新⼤⼩返回值为调整之后的内存起始位置。这个函数调整原内存空间⼤⼩的基础上还会将原来内存中的数据移动到新的空间。realloc在调整内存空间的是存在两种情况情况1原有空间之后有⾜够⼤的空间情况2原有空间之后没有⾜够⼤的空间情况1当是情况1 的时候要扩展内存就直接原有内存之后直接追加空间原来空间的数据不发⽣变化。int main() { int *ptr (int*)malloc(20); //代码1 - 直接将realloc的返回值放到ptr中 ptr(int*)realloc(ptr,1000); //代码2 - 先将realloc函数的返回值放在temp中不为NULL在放ptr中 int*tempNULL; temp(int*)realloc(ptr,40); if (temp!NULL) { ptrtemp; } free(ptr); return 0; }以下是代码1的内存视图:由此可见ptr所指空间置为空后面放不下情况2当原空间后面没有足够连续空间时realloc(ptr, new_size)大致会做这几步在堆上找一块新的、更大的连续空间把旧空间中的数据拷贝到新空间和malloc区别)对比项mallocrealloc情况2申请新空间是是保留旧数据否是自动复制旧数据否是自动释放旧空间否是用于调整已有内存大小否是释放旧空间(和malloc区别)返回新空间地当是情况2 的时候原有空间之后没有⾜够多的空间时扩展的⽅法是在堆空间上另找⼀个合适⼤⼩的连续空间来使⽤。这样函数返回的是⼀个新的内存地址。以下是代码2运行情况(注意要将代码1给注释掉否则因为ptrNULL,temp也会NULL)5. 对动态内存的常见错误5.1 对NULL指针的解引⽤操作void test(){ int* p(int*)malloc(INT_MAX) *p20; freep(p); }由于INT_MAX过大导致p指针为NULL解引用出错。5.2 对动态开辟空间的越界访问void test(){ int* p(int*)malloc(40); assert(p);//p不为NULL int i; for(i0;i10;i){ p[i]i;}//越界访问 free(p); return 0; }p空间最大只能40个字节此时的i访问越界了。5.3 对⾮动态开辟内存使⽤free释放void test(){ int a10; int*pa; free(p);//非动态申请的堆内存 }p为非动态申请的堆内存5.4 使⽤free释放⼀块动态开辟内存的⼀部分void test() { int *p (int *)malloc(100); p; free(p);//p不再指向动态内存的起始位置 }此时的p不再指向动态内存起始位置5.5 对同⼀块动态内存多次释放void test() { int *p (int *)malloc(100); free(p); free(p);//重复释放 }5.6 动态内存忘记释放void test(){ int *p (int *)malloc(100); if(NULL ! p) { *p 20; } } int main() { test(); while(1); }6. 动态内存经典笔试题分析6.1 题目1void GetMemory(char *p) { p (char *)malloc(100); } void Test(void) { char *str NULL; GetMemory(str); strcpy(str, hello world); printf(str); }请问运⾏Test 函数会有什么样的结果答案NULL。6.2 题目2char *GetMemory(void) { char p[] hello world; return p; } void Test(void) { char *str NULL; str GetMemory(); printf(str); }请问运⾏Test 函数会有什么样的结果答案崩溃6.3 题目3void GetMemory(char **p, int num) { *p (char *)malloc(num); } void Test(void) { char *str NULL; GetMemory(str, 100); strcpy(str, hello); printf(str); }请问运⾏Test 函数会有什么样的结果答案hello6.4 题目4void Test(void) { char *str (char *) malloc(100); strcpy(str, hello); free(str); if(str ! NULL) { strcpy(str, world); printf(str); } }请问运⾏Test 函数会有什么样的结果答案结果是未定义7. 柔性数组也许你从来没有听说过柔性数组flexible array这个概念但是它确实是存在的。C99 中结构中的最后⼀个元素允许是未知⼤⼩的数组这就叫做柔性数组成员。例如struct st_type { int i; int a[0];//柔性数组成员 };或struct st_type { int i; int a[];//柔性数组成员 };7.1 柔性数组特点结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。sizeof 返回的这种结构⼤⼩不包括柔性数组的内存。包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配并且分配的内存应该⼤于结构的⼤⼩以适应柔性数组的预期⼤⼩。例如typedef struct st_type { int i; int a[0];//柔性数组成员 }type_a; int main() { printf(%d\n, sizeof(type_a));//输出的是4 return 0; }7.2 柔性数组的使用//代码1 #include stdio.h #include stdlib.h typedef struct st_type { int i; int a[0];//柔性数组成员 }type_a; int main() { int i 0; type_a *p (type_a*)malloc(sizeof(type_a)100*sizeof(int)); //业务处理 p-i 100; for(i0; i100; i) { p-a[i] i; } free(p); return 0; }这样柔性数组成员a相当于获得了100个整型元素的连续空间。7.3 柔性数组的优劣上述的 type_a 结构也可以设计为下⾯的结构也能完成同样的效果//代码2 #include stdio.h #include stdlib.h typedef struct st_type { int i; int *p_a; }type_a; int main() { type_a *p (type_a *)malloc(sizeof(type_a)); p-i 100; p-p_a (int *)malloc(p-i*sizeof(int)); //业务处理 for(i0; i100; i) { p-p_a[i] i; } //释放空间 free(p-p_a); p-p_a NULL; free(p); p NULL; return 0; }上述 代码1 和 代码2 可以完成同样的功能但是 ⽅法1 的实现有两个好处第⼀个好处是⽅便内存释放如果我们的代码是在⼀个给别⼈⽤的函数中你在⾥⾯做了⼆次内存分配并把整个结构体返回给⽤⼾。⽤户调⽤free可以释放结构体但是⽤⼾并不知道这个结构体内的成员也需要free所以你不能指望⽤⼾来发现这个事。所以如果我们把结构体的内存以及其成员要的内存⼀次性分配好了并返回给⽤⼾⼀个结构体指针⽤户做⼀次free就可以把所有的内存也给释放掉。第⼆个好处是有利于访问速度连续的内存有益于提⾼访问速度也有益于减少内存碎⽚。其实也没多⾼了反正你跑不了要⽤做偏移量的加法来寻址8.总结动态内存管理是 C 语言中非常重要的一部分它解决了普通变量和数组空间大小固定、不够灵活的问题。当程序在运行过程中才知道需要多少内存时就可以通过动态内存管理在堆区按需申请空间。本文主要介绍了malloc、free、calloc 和 realloc这几个常用函数。malloc 用来在堆区申请一块指定大小的连续空间但申请到的空间不会自动初始化calloc 同样用于动态申请空间不过它会将申请到的内存初始化为 0realloc 用来调整已经申请好的动态内存大小当原空间后面有足够连续空间时可能会直接在原地扩容当原空间后面空间不足时则可能会重新申请一块更大的空间并将原数据拷贝过去free 则用于释放动态申请的空间避免造成内存泄漏。在使用动态内存时需要特别注意几个问题申请内存后一定要判断返回值是否为 NULL避免对空指针进行解引用访问动态内存时不能越界不能对非动态申请的空间使用 free不能释放同一块空间多次释放空间时必须传入动态内存的起始地址使用完动态内存后要及时释放避免内存泄漏。此外free 只是释放指针所指向的动态内存并不会自动把指针变量本身置为 NULL。因此释放后建议手动写上free(ptr); ptr NULL;如果你觉得本篇博客对你有帮助不妨点个赞鼓励一下作者。