十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言动态内存管理详解:从malloc到柔性数组

C语言动态内存管理详解:从malloc到柔性数组 1. 引言在C语言的学习过程中内存管理始终是一个绕不开的核心话题。很多初学者在编写程序时往往只关注逻辑是否正确却忽略了内存的分配与释放。实际上动态内存管理是C语言中极为重要的一环它直接关系到程序的稳定性、性能和资源利用率。本文将基于经典教材内容系统梳理C语言动态内存管理的核心知识点包括malloc、free、calloc、realloc四大函数的使用方法常见的内存错误经典笔试题分析以及柔性数组这一进阶概念。希望通过本文能够帮助读者建立起完整的内存管理知识体系。2. 为什么要有动态内存分配在接触动态内存之前我们已经掌握的内存开辟方式主要有以下两种int val 20; //在栈空间上开辟四个字节 char arr[10] {0}; //在栈空间上开辟10个字节的连续空间但是上述的开辟空间的方式有两个特点空间开辟大小是固定的一旦确定无法在程序运行过程中改变。数组在声明的时候必须指定数组的长度数组空间一旦确定了大小就不能调整。然而对于空间的需求不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道那数组的编译时开辟空间的方式就不能满足了。例如程序需要根据用户输入的数量来存储数据这个数量在编译时是无法预知的。正是为了解决这类问题C语言引入了动态内存开辟让程序员自己可以申请和释放空间这样就比较灵活了。动态内存分配使得程序能够在运行时根据实际需求灵活地调整内存使用大大提高了程序的适应性和资源利用效率。3. malloc和free3.1 malloc函数C语言提供了一个动态内存开辟的函数void* malloc (size_t size);功能向内存的堆区申请一块连续可用的空间并返回指向这块空间的起始地址。使用malloc函数时需要注意以下几点如果开辟成功则返回这块空间的起始地址。如果开辟失败则返回一个NULL指针因此malloc的返回值一定要做检查。返回值的类型是void*所以malloc函数并不知道开辟空间的类型具体在使用的时候使用者自己来决定。如果参数size为0malloc的行为是标准未定义的取决于编译器。3.2 free函数C语言提供了另外一个函数free专门是用来做动态内存的释放和回收的函数原型如下void free (void* ptr);free函数用来释放动态开辟的内存。使用free时需要注意如果参数ptr指向的空间不是动态开辟的那free函数的行为是未定义的。如果参数ptr是NULL指针则函数什么事都不做。malloc和free都声明在stdlib.h头文件中。下面通过一个完整的示例来演示malloc和free的配合使用#include stdio.h #include stdlib.h int main() { int num 0; scanf(%d, num); int* ptr NULL; ptr (int*)malloc(num*sizeof(int)); if(NULL ! ptr) //判断ptr指针是否为空 { int i 0; for(i0; inum; i) { *(ptri) 0; } } free(ptr); //释放ptr所指向的动态内存 ptr NULL; //是否有必要 return 0; }关于free之后是否需要将指针置为NULL这是一个值得思考的问题。free只是释放了指针所指向的内存空间但指针本身仍然保存着原来的地址此时指针变成了野指针。为了避免后续误用这个指针建议在free之后将指针置为NULL。4. calloc和realloc4.1 calloc函数C语言还提供了一个函数叫calloccalloc函数也用来动态内存分配。原型如下void* calloc (size_t num, size_t size);calloc函数的特点函数的功能是为num个大小为size的元素开辟一块空间并且把空间的每个字节初始化为0。与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。下面通过一个示例来验证calloc的初始化行为#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所以如果我们对申请的内存空间的内容要求初始化那么可以很方便的使用calloc函数来完成任务。4.2 realloc函数realloc函数的出现让动态内存管理更加灵活。有时我们会发现过去申请的空间太小了有时候我们又会觉得申请的空间过大了那为了合理的使用内存我们一定会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存大小的调整。函数原型如下void* realloc (void* ptr, size_t size);参数说明ptr是要调整的内存地址。size是调整之后新大小单位是字节。返回值为调整之后的内存起始位置。这个函数在调整原内存空间大小的基础上还会将原来内存中的数据移动到新的空间。realloc在调整内存空间时存在两种情况情况1原有空间之后有足够大的空间。此时要扩展内存就直接在原有内存之后直接追加空间原来空间的数据不发生变化。情况2原有空间之后没有足够大的空间。此时扩展的方法是在堆空间上另找一个合适大小的连续空间来使用这样函数返回的是一个新的内存地址。由于上述的两种情况realloc函数的使用就要注意一些。下面通过代码对比来展示正确的使用方式#include stdio.h #include stdlib.h int main() { int *ptr (int*)malloc(100); if(ptr ! NULL) { //业务处理 } else { return 1; } //扩展容量 //代码1 - 直接将realloc的返回值放到ptr中 ptr (int*)realloc(ptr, 1000); //这样可以吗(如果申请失败会如何) //代码2 - 先将realloc函数的返回值放在p中不为NULL再放ptr中 int *p NULL; p realloc(ptr, 1000); if(p ! NULL) { ptr p; } //业务处理 free(ptr); return 0; }代码1存在一个隐患如果realloc申请失败会返回NULL此时直接将返回值赋给ptr会导致原来的内存地址丢失造成内存泄漏。而代码2先将返回值暂存在p中判断不为NULL后再赋给ptr这样即使申请失败原来的内存仍然可以通过ptr访问和释放。4.3 malloc、calloc、realloc对比为了更直观地理解三个函数之间的区别下面通过表格进行对比对比项malloccallocrealloc函数原型void* malloc(size_t size)void* calloc(size_t num, size_t size)void* realloc(void* ptr, size_t size)参数含义size申请空间的字节数num元素个数size每个元素的大小ptr原内存地址size调整后的新大小初始化不初始化内存中为随机值自动将每个字节初始化为0不初始化保留原内存中的数据功能申请一块连续的内存空间申请一块连续的内存空间并清零调整已申请内存的大小并搬运原数据返回值成功返回起始地址失败返回NULL成功返回起始地址失败返回NULL成功返回新地址失败返回NULL原内存仍有效使用场景需要一块未初始化的内存需要一块已清零的内存需要扩容或缩容已申请的内存小结malloc负责申请内存calloc在申请的同时完成清零初始化realloc则用于调整已有内存的大小。三者配合free使用才能构建健壮的动态内存管理代码。5. 常见动态内存错误动态内存使用过程中常见的错误有以下几类每一类都可能导致程序崩溃或产生难以排查的bug。5.1 对NULL指针的解引用操作void test() { int *p (int *)malloc(INT_MAX/4); *p 20; //如果p的值是NULL就会有问题 free(p); }这段代码的问题在于malloc可能返回NULL内存不足时此时对NULL指针解引用会导致程序崩溃。正确的做法是在使用前判断指针是否为NULL。5.2 对动态开辟空间的越界访问void test() { int i 0; int *p (int *)malloc(10*sizeof(int)); if(NULL p) { exit(EXIT_FAILURE); } for(i0; i10; i) { *(pi) i; //当i是10的时候越界访问 } free(p); }这里申请了10个int大小的空间但循环访问了11个元素i从0到10当i等于10时发生了越界访问。越界访问会破坏堆内存的元数据可能导致程序崩溃或产生不可预知的行为。5.3 对非动态开辟内存使用free释放void test() { int a 10; int *p a; free(p); //ok? }free只能释放动态开辟的内存。这里p指向的是栈上的变量a对栈上的内存调用free行为是未定义的可能导致程序崩溃。5.4 使用free释放一块动态开辟内存的一部分void test() { int *p (int *)malloc(100); p; free(p); //p不再指向动态内存的起始位置 }free要求传入的指针必须指向动态内存的起始位置。这里p自增后不再指向起始位置此时调用free会导致未定义行为。5.5 对同一块动态内存多次释放void test() { int *p (int *)malloc(100); free(p); free(p); //重复释放 }同一块内存被释放两次称为双重释放double free这会导致堆管理器的元数据被破坏程序可能崩溃或产生安全问题。5.6 动态开辟内存忘记释放内存泄漏void test() { int *p (int *)malloc(100); if(NULL ! p) { *p 20; } } int main() { test(); while(1); return 0; }忘记释放不再使用的动态开辟的空间会造成内存泄漏。随着程序运行时间的增长内存泄漏会逐渐耗尽系统内存最终导致程序崩溃。切记动态开辟的空间一定要释放并且正确释放。6. 动态内存经典笔试题分析在面试和笔试中动态内存管理是高频考点。下面通过四道经典题目来深入理解动态内存的使用陷阱。6.1 题目1值传递无法修改指针#include stdio.h #include stdlib.h void GetMemory(char* p) { p (char*)malloc(100); } void Test(void) { char* str NULL; GetMemory(str); /*这里传递的是str的数值, 在GetMemory函数中,接收到的是str这个变量的数值 新建了一个变量p 导致没有达到修改str的目的 */ strcpy(str, hello world); printf(str); } int main() { Test(); return 0; }运行结果程序崩溃。分析GetMemory函数的参数是值传递函数内部对p的修改不会影响到外部的str。因此str仍然是NULL对NULL指针调用strcpy会导致程序崩溃。正确的做法是传入二级指针或者通过返回值返回分配的内存地址。正确修改:int GetMemory(char** p) // 改为返回 int0表示成功-1表示失败 { *p (char*)malloc(100); if (*p NULL) { perror(use malloc); return -1; // 通知调用者失败了 } return 0; } void Test(void) { char* str NULL; // 检查分配是否成功 if (GetMemory(str) ! 0) { return; // 分配失败直接退出防止后续崩溃 } strcpy(str, hello world); printf(%s\n, str); // 安全的打印方式 free(str); str NULL; // 防止悬空指针 } int main() { Test(); return 0; }6.2 题目2返回栈空间地址#include stdio.h #include stdlib.h char* GetMemory(void) { char p[] hello world; //这里是在栈区开放的空间,函数结束后,p的空间就被释放 return p; } void Test(void) { char* str NULL; str GetMemory(); //这个时候str指向的就是已释放的栈空间,未定义行为 //如果p释放的空间没有被占用,可能会正常打印 printf(str); } int main() { Test(); return 0; }运行结果输出乱码或程序崩溃。分析p是函数内部的局部数组存储在栈上。函数返回后栈空间被回收p指向的内存内容已经失效。此时str指向的是已释放的栈空间访问它属于未定义行为。正确的做法是使用静态数组或动态内存。正确修改:#include stdio.h #include stdlib.h char* GetMemory(void) { static char p[] hello world; //1.static //2.动态开辟(记得检测和释放) return p; } void Test(void) { char* str NULL; str GetMemory(); printf(str); } int main() { Test(); return 0; }6.3 题目3二级指针的正确使用#include stdio.h #include stdlib.h void GetMemory(char **p, int num) { *p (char *)malloc(num); } void Test(void) { char *str NULL; GetMemory(str, 100); strcpy(str, hello); printf(str); } int main() { Test(); return 0; }运行结果输出hello。分析这里通过二级指针正确修改了外部的str指针使其指向动态分配的内存。但这段代码存在内存泄漏问题因为分配的内存没有被释放。正确的做法是在使用完毕后调用free释放内存。正确修改:#include stdio.h #include stdlib.h #include string.h // 将返回值改为 int0表示成功-1表示失败方便调用者判断 int GetMemory(char** p, int num) { if (p NULL || num 0) { return -1; // 防御性检查 } *p (char*)malloc(num); if (*p NULL) { return -1; // 分配失败 } return 0; // 分配成功 } void Test(void) { char* str NULL; // 接收返回值并判断 if (GetMemory(str, 100) ! 0) { printf(内存分配失败!\n); return; // 分配失败提前退出防止后续崩溃 } strcpy(str, hello); printf(%s\n, str); // 安全的打印方式 free(str); // 释放内存防止泄露 str NULL; // 避免悬空指针好习惯 } int main() { Test(); return 0; }6.4 题目4free后未置空指针#include stdio.h #include stdlib.h void Test(void) { char* str (char*)malloc(100); strcpy(str, hello); free(str); //这个时候str指向的空间被释放,str变成了野指针 //野指针不为空,下面对野指针进行访问,导致程序崩溃 if (str ! NULL) { strcpy(str, world); printf(str); } } int main() { Test(); return 0; }运行结果输出world或程序崩溃行为未定义。分析free(str)后str变成了野指针但str的值并不为NULL。此时if(str ! NULL)判断为真继续对已释放的内存进行写操作属于未定义行为。正确的做法是在free后立即将str置为NULL。#include stdio.h #include stdlib.h void Test(void) { char* str (char*)malloc(100); strcpy(str, hello); free(str); str NULL; if (str ! NULL) { strcpy(str, world); printf(str); } } int main() { Test(); return 0; }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; }输出结果为4说明sizeof计算结构大小时不包含柔性数组的内存。7.2 柔性数组的使用#include stdio.h #include stdlib.h // 定义结构体包含柔性数组 struct S { int n; int arr[]; // C99标准柔性数组写法原为 int arr[0] }; int main() { // 1. 动态分配内存结构体大小 5个int的大小 (4 5*4 24字节) struct S* ps (struct S*)malloc(sizeof(struct S) 5 * sizeof(int)); if (ps NULL) { perror(use malloc); return 1; } // 2. 初始化结构体成员和数组 ps-n 100; int i 0; for (i 0; i 5; i) { ps-arr[i] i 1; // 填充前5个元素1, 2, 3, 4, 5 } // 3. 扩容将数组大小从5个int扩展到10个int // 注意这里使用临时指针 ptr 接收 realloc 结果避免 realloc 失败导致原指针 ps 丢失 struct S* ptr realloc(ps, sizeof(struct S) 10 * sizeof(int)); if (ptr NULL) { perror(realloc); // 严谨的错误处理扩容失败释放原内存并退出防止内存泄漏 free(ps); ps NULL; return 1; } else { // 扩容成功将新地址赋给 ps并置空临时指针 ptr ps ptr; ptr NULL; // 填充新扩展的5个元素 for (i 5; i 10; i) { ps-arr[i] i 1; // 填充元素6, 7, 8, 9, 10 } } // 4. 释放动态分配的内存 free(ps); ps NULL; // 防止野指针 return 0; }7.3 柔性数组的优势
返回列表