【C语言】《动态内存管理全解|malloc/calloc/realloc/free + 经典笔试题剖析》

发布时间:2026/7/27 18:39:58
【C语言】《动态内存管理全解|malloc/calloc/realloc/free + 经典笔试题剖析》 一.动态内存的分配1.概念动态内存分配的核心在于“按需而动”程序不再需要在运行前就划定死内存大小而是在执行过程中根据实时数据量向系统灵活借调内存用多少就申请多少用完即刻释放。2.有动态内存分配的原因我们已经掌握的内存开辟⽅式有int val 20;//在栈空间上开辟四个字节 char arr[10] {0};//在栈空间上开辟10个字节的连续空间但是上述的开辟空间的⽅式有两个特点空间开辟⼤⼩是固定的。数组在申明的时候必须指定数组的⻓度数组空间⼀旦确定了⼤⼩不能调整原因但是对于空间的需求不仅仅是上述的情况。有时候我们需要的空间⼤⼩在程序运⾏的时候才能知道那数组的编译时开辟空间的⽅式就不能满⾜了。C语⾔引⼊了动态内存开辟让程序员⾃⼰可以申请和释放空间就⽐较灵活了。3.动态内存分布图二.动态内存函数介绍1.malloc 函数cplusplus.com/reference/cstdlib/malloc/?kwmallocC语⾔提供了⼀个动态内存开辟的函数void* malloc (size_t size);这个函数向内存申请⼀块连续可⽤的空间并返回指向这块空间的指针。如果开辟成功则返回⼀个指向开辟好空间的指针。如果开辟失败则返回⼀个 NULL 指针因此malloc的返回值⼀定要做检查。返回值的类型是 void* 所以malloc函数并不知道开辟空间的类型具体在使⽤的时候使⽤者⾃⼰来决定。如果参数 size 为0malloc的⾏为是标准是未定义的取决于编译器。实现方式方式一通过指针偏移访问推荐#include stdlib.h int main() { // 申请20个字节 - 存放5个整数 int* p (int*)malloc(20); if (p NULL) { perror(malloc); return 1; } // 使用空间通过指针 偏移量访问 int i 0; for (i 0; i 5; i) { *(p i) i 1; // 等价于 p[i] i 1 } // 释放内存 free(p); // p 仍然是起始地址 p NULL; return 0; }这里的指向始终保持不变始终指向内存块的起始位置释放时直接传递p安全可靠代码清晰符合大多数人的习惯说明*(p i)与p[i]完全等价都是通过起始地址加偏移量来定位元素这种写法不会改变指针变量p本身的值方式二通过指针自增访问错误#include stdlib.h int main() { // 申请20个字节 - 存放5个整数 int* p (int*)malloc(20); if (p NULL) { perror(malloc); return 1; } // 使用空间通过指针自增访问 int i 0; for (i 0; i 5; i) { *p i 1; p; // 每次循环 p 都向后移动 } // 错误此时 p 已经指向内存块末尾之后不是起始地址了 free(p); // 程序崩溃 / 未定义行为 p NULL; return 0; }错误原因循环中执行了p每循环一次p就向后移动一个int的大小循环结束后p指向了内存块最后一个元素之后的位置free(p)接收的已经不是malloc返回的原始起始地址了这会导致程序崩溃或内存释放失败未定义行为2.free函数cplusplus.com/reference/cstdlib/free/C语⾔提供了另外⼀个函数free专⻔是⽤来做动态内存的释放和回收的函数原型如下void free (void* ptr);free函数⽤来释放动态开辟的内存。如果参数 ptr 指向的空间不是动态开辟的那free函数的⾏为是未定义的。如果参数 ptr 是NULL指针则函数什么事都不做。补充注意使用完之后一定要记得使用 free 函数释放所开辟的内存空间。使用指针指向动态开辟的内存使用完并 free 之后一定要记得将其置为空指针。代码示意如下#include stdio.h #include stdlib.h int main() { int arr[10]; // 静态开辟10个整型空间栈上 int* p (int*)malloc(10 * sizeof(int)); // 动态开辟10个int大小的空间堆上 if (p NULL) // 判断p指针是否为空 { perror(main); // 输出错误信息 return 1; // 异常退出 } // 使用空间 for (int i 0; i 10; i) { *(p i) i; // 通过指针偏移赋值 } for (int i 0; i 10; i) { printf(%d , p[i]); // 通过数组下标访问与*(pi)等价 } // 回收空间 free(p); // 释放p所指向的动态内存 p NULL; // 必须手动置为空指针 return 0; }问题解答【问题一】为什么 malloc 前要加上强制类型转换int* p (int*)malloc(10 * sizeof(int));根本原因有两个保持类型一致malloc返回的是void*类型的指针可以装任何类型的数据。而我们定义的p是int*类型的专门用来指向整数数据。把通用类型转换成具体类型让指针和它指向的数据类型相匹配。兼容不同编译器这一点更重要。在C语言中void*可以自动转换成其他指针类型不写强转也能编译通过。但在C中这种自动转换是不允许的编译器会报错。为了让代码在C和C环境下都能顺利编译加上强转是最稳妥的做法。虽然C语言不要求但建议加上。这样写出来的代码可移植性更好也明确告诉读代码的人这块内存我打算当作int数组来用。【问题二】为什么 free 后还要把 p 置成 NULLfree(p); p NULL;用个生活中的例子解释一下free只是把房子拆了但门牌号还在。p这个指针变量里存的地址并没有变它依然指向那块已经不属于你的内存。free(p); // p 现在是个野指针 // 情况一再次使用 *p 10; // 访问已经归还的内存程序随时可能崩溃 // 情况二重复释放 free(p); // 双重释放属于严重的非法操作 // 情况三误判 if (p ! NULL) // p 确实不为空 { *p 10; // 但指向的内存已经无效该不该执行呢 }这就像你把房子退了但手里还攥着旧钥匙。你以为钥匙还在就能开门但实际上那间房已经租给别人了——你硬要开门轻则打不开程序没反应重则被抓现行程序崩溃。把 p 置为 NULL 之后free(p); p NULL; // 门牌号作废 if (p ! NULL) // false不会进入 { // 安全跳过 } free(p); // 释放空指针是安全的C标准规定什么都不做free释放的是内存不是指针变量。指针变量需要你亲手清理。【问题三】为什么 malloc 后要检查 p 是不是 NULLif (p NULL) { perror(main); return 1; }因为系统不是每次都有足够的内存给你用。当内存申请失败时malloc会返回NULL。如果你不检查就直接用int* p malloc(10000000000); // 申请超大内存大概率失败 *p 10; // 对 NULL 解引用 -- 程序直接崩溃段错误3.calloc函数https://cplusplus.com/reference/cstdlib/calloc/C语⾔还提供了⼀个函数叫 calloc calloc 函数也⽤来动态内存分配。原型如下void* calloc (size_t num, size_t size);函数的功能是为 num 个⼤⼩为 size 的元素开辟⼀块空间并且把空间的每个字节初始化为0。与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。示例如下#include stdio.h #include stdlib.h // 提供 calloc 和 free 函数 int main() { // 1. 动态开辟 10 个 int 大小的空间共 40 字节并全部初始化为 0 int* p (int*)calloc(10, sizeof(int)); // 2. 检查内存是否分配成功 if (p NULL) { return 1; // 分配失败程序异常退出 } // 3. 打印这 10 个整数的值 for (int i 0; i 10; i) { printf(%d , *(p i)); // 全部打印 0 } // 4. 释放内存归还给系统 free(p); // 5. 指针置空防止野指针 p NULL; return 0; }所以如果我们对申请的内存空间的内容要求初始化那么可以很⽅便的使⽤calloc函数来完成任务。4.realloc 函数cplusplus.com/reference/cstdlib/realloc/realloc函数的出现让动态内存管理更加灵活。有时会我们发现过去申请的空间太⼩了有时候我们⼜会觉得申请的空间过⼤了那为了合理的使⽤内存我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc 函数就可以做到对动态开辟内存⼤⼩的调整。函数原型如下void* realloc (void* ptr, size_t size);ptr 是要调整的内存地址size 调整之后新⼤⼩返回值为调整之后的内存起始位置。这个函数调整原内存空间⼤⼩的基础上还会将原来内存中的数据移动到 新 的空间。realloc在调整内存空间的是存在两种情况realloc会尽量在原位置帮你扩大空间如果原地不够就找个新地方把数据搬过去并自动释放旧内存。情况一后面够用地址不变原地扩容。情况二后面不够用地址变了数据搬家。情况1当是情况1 的时候要扩展内存就直接原有内存之后直接追加空间原来空间的数据不发⽣变化。情况2当是情况2 的时候原有空间之后没有⾜够多的空间时扩展的⽅法是在堆空间上另找⼀个合适⼤⼩的连续空间来使⽤。这样函数返回的是⼀个新的内存地址。代码示意#include stdio.h #include stdlib.h int main() { // 1. 先申请一块内存 int* ptr (int*)malloc(100); if (ptr NULL) { perror(malloc); return 1; } // 2. 使用内存假设存了一些数据 for (int i 0; i 25; i) // 100字节可以存25个int { ptr[i] i 1; } // 3. 扩容到 1000 字节 int* tmp (int*)realloc(ptr, 1000); if (tmp NULL) { // 扩容失败旧内存还在 perror(realloc); free(ptr); // 释放旧内存避免泄漏 ptr NULL; return 1; } // 4. 扩容成功更新指针 ptr tmp; // 5. 继续使用原来的数据还在 for (int i 25; i 250; i) // 1000字节可以存250个int { ptr[i] i 1; } // 6. 释放 free(ptr); ptr NULL; return 0; }三.常⻅的动态内存的错误1. 对NULL指针的解引⽤操作// 错误代码 void test() { int* p (int*)malloc(INT_MAX / 4); *p 20; // 错误没有检查 p 是否为 NULL free(p); }// 纠正后的代码 void test() { int* p (int*)malloc(INT_MAX / 4); if (p NULL) // 添加判断 { return; // 分配失败就返回 } *p 20; // 安全此时 p 一定不为 NULL free(p); }错因malloc可能分配失败返回 NULL未做检查就直接解引用导致对空指针操作程序崩溃。纠正使用if (p NULL)判断只有指针非空时才进行解引用操作。2.对动态开辟空间的越界访问// 错误代码 void test() { int i 0; int* p (int*)malloc(10 * sizeof(int)); // 申请了10个int共40字节 if (NULL p) { exit(EXIT_FAILURE); } for (i 0; i 10; i) // 错误i 10循环了11次 { *(p i) i; // 当 i 10 时访问了第11个元素越界 } free(p); }// 纠正后的代码 void test() { int i 0; int* p (int*)malloc(10 * sizeof(int)); // 申请了10个int if (NULL p) { exit(EXIT_FAILURE); } for (i 0; i 10; i) // 纠正i 10循环10次 { *(p i) i; // i 的范围是 0~9刚好访问10个元素 } free(p); }错因malloc申请了 10 个int的空间有效索引是 0~9但循环条件i 10导致循环了 11 次当i 10时访问了p[10]超出了申请的内存范围造成越界访问。纠正将循环条件改为i 10确保只访问 0~9 这 10 个有效元素。3.对⾮动态开辟内存使⽤free释放// 错误代码 void test() { int a 10; int *p a; free(p); // 错误p 指向的是栈上的变量 a不是堆上的动态内存 }// 纠正后的代码 void test() { int a 10; int *p a; // free(p); // 删除不能对栈上的变量使用 free // 不需要释放函数结束自动销毁 }错因free只能释放通过malloc、calloc或realloc在堆上动态分配的内存。代码中p指向的是栈上的局部变量a它的内存由系统自动管理函数结束后自动回收不需要手动释放。对非动态内存使用free会导致未定义行为程序可能崩溃。纠正删除free(p)栈上的变量不需要也不能手动释放。4.使⽤free释放⼀块动态开辟内存的⼀部分// 错误代码 void test() { int *p (int *)malloc(100); p; // 错误改变了 p 的指向不再指向起始地址 free(p); // free 的参数不是起始地址 }// 纠正后的代码 void test() { int *p (int *)malloc(100); int *start p; // 保存起始地址 p; // 可以移动 p 去访问内存 free(start); // 释放时传入起始地址 start NULL; p NULL; }错因free必须接收malloc/calloc/realloc返回的原始起始地址。代码中p后p偏移了 4 个字节一个int大小不再指向内存块的起始位置。此时调用free(p)系统无法正确释放内存导致未定义行为程序可能崩溃。纠正在移动指针之前用另一个指针如start保存起始地址释放时传入该备份指针。5.对同⼀块动态内存多次释放// 错误代码 void test() { int *p (int *)malloc(100); free(p); free(p); // 错误对同一块内存重复释放 }// 纠正后的代码 void test() { int *p (int *)malloc(100); free(p); p NULL; // 释放后立即置空 // free(p); // 删除不再重复释放 // 如果再次执行 free(p)因为 p 已经是 NULLfree(NULL) 是安全的 // free(p); // 安全但没必要 }错因第一次free(p)已经将内存归还给系统第二次再次调用free(p)是对同一块已释放内存的重复释放属于未定义行为程序会崩溃。纠正free后立即将指针置为NULL这样即使不小心再次调用free(p)free(NULL)不会做任何操作是安全的。但最好的做法是避免重复释放。6.动态开辟内存忘记释放内存泄漏// 错误代码 void test() { int *p (int *)malloc(100); if (NULL ! p) { *p 20; } // 错误没有 free(p)内存泄漏 } int main() { test(); while(1); // 程序一直运行泄漏的内存一直被占用 }// 纠正后的代码 void test() { int *p (int *)malloc(100); if (NULL ! p) { *p 20; } free(p); // 释放内存 p NULL; } int main() { test(); while(1); }错因函数test中使用malloc在堆上申请了 100 字节但函数结束时没有调用free释放导致这块内存既没有被使用也无法被回收程序持续运行时会一直占用内存造成内存泄漏。纠正在函数结束前加上free(p)释放内存并将指针置NULL。内存泄漏的危害长期运行的程序如服务器如果持续泄漏最终会耗尽系统内存导致程序崩溃或系统卡死。注意忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。切记动态开辟的空间⼀定要释放并且正确释放四. 动态内存经典笔试题分析题⽬1void GetMemory(char* p) { p (char*)malloc(100); } void Test(void) { char* str NULL; GetMemory(str); strcpy(str, hello world); printf(str); } int main() { Test(); return 0; }请问运⾏Test 函数会有什么样的结果GetMemory(str)传参时是把str的值即NULL拷贝了一份给p。在函数内部p malloc(100)确实申请了一块内存但地址是赋给了p外面的str还是NULL。函数结束p销毁申请的内存没人知道地址内存泄漏而外面的str依然是NULL。接着strcpy(str, hello world)相当于往地址0写数据程序崩溃。解法写法一用二级指针void GetMemory(char** p) { *p (char*)malloc(100); } void Test(void) { char* str NULL; GetMemory(str); // 传 str 自己的地址 if (str ! NULL) { strcpy(str, hello world); printf(%s\n, str); free(str); str NULL; } }把str自己的地址传给函数函数通过这个地址直接修改外面的str。void GetMemory(char** p) // 接收的是 str 的地址 { *p (char*)malloc(100); // 通过解引用修改外面的 str } // 调用时传 str GetMemory(str);写法二用返回值char* GetMemory(void) { char* p (char*)malloc(100); return p; } void Test(void) { char* str NULL; str GetMemory(); if (str ! NULL) { strcpy(str, hello world); printf(%s\n, str); free(str); str NULL; } }不传参了函数直接返回申请到的地址外面用变量接着。char* GetMemory(void) { char* p (char*)malloc(100); return p; // 返回地址 } // 调用时接收返回值 str GetMemory();想在函数内部给外面的指针分配内存必须传二级指针或者用返回值。直接用一级指针传进去外面拿不到地址题目2:char *GetMemory(void) { char p[] hello world; return p; } void Test(void) { char *str NULL; str GetMemory(); printf(str); } int main() { Test(); return 0; }请问运⾏Test 函数会有什么样的结果结果程序崩溃问题出在p是函数GetMemory内部的局部数组它是在栈上分配的。函数执行结束后栈帧被回收p占用的内存会被系统重新使用。return p返回的是这块已经失效的内存地址外面的str指向了一块已经被释放的栈空间。再去printf打印它结果是不确定的——可能打印乱码可能打印空也可能直接崩溃。这跟前面malloc分配堆内存不同堆内存需要手动free但栈内存是函数结束自动销毁的不能把栈空间的地址返回给外部使用。解法一改成mallocchar* p (char*)malloc(20); strcpy(p, hello world); return p;char p[]换成malloc申请的内存。堆上的内存不会随函数结束而销毁地址有效。用完要free。解法二加staticstatic char p[] hello world; return p;只加了static。p从栈上搬到静态区生命周期延长到程序结束。不用free。题目3:void GetMemory(char** p, int num) { *p (char*)malloc(num); } void Test(void) { char* str NULL; GetMemory(str, 100); strcpy(str, hello); printf(str); }请问运⾏Test 函数会有什么样的结果有语法错误能正常运行输出hello。但有一个致命隐患没有检查malloc的返回值。如果malloc(100)分配失败返回NULL那么*p NULL外面的str就是NULL。接着strcpy(str, hello)试图往NULL地址拷贝数据程序崩溃。运气好能跑运气不好就崩。错在了少了判空。GetMemory内部申请完内存没有检查是否成功Test里面拿到str直接使用也没有检查。内存分配失败是概率事件尤其内存不足时不检查等于埋了个定时炸弹。纠正void Test(void) { char* str NULL; GetMemory(str, 100); // 加了判空 if (str NULL) { printf(内存分配失败\n); return; } strcpy(str, hello); printf(%s\n, str); // 加了释放 free(str); str NULL; }题⽬4void Test(void) { char *str (char *) malloc(100); strcpy(str, hello); free(str); if(str ! NULL) { strcpy(str, world); printf(str); } }请问运⾏Test 函数会有什么样的结果free(str)释放了内存但str本身的值没有变它仍然指向那块已经归还给系统的地址。if (str ! NULL)这个判断完全无效因为free不会把str变成NULL。判断通过后strcpy(str, world)往一块已经被释放的内存里写数据属于非法操作结果无法预测——可能正常可能乱码可能崩溃。导致野指针悬空指针。五.总结C/C中程序内存区域划分C/C程序内存分配的⼏个区域栈区stack在执⾏函数时函数内局部变量的存储单元都可以在栈上创建函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中效率很⾼但是分配的内存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。堆区heap⼀般由程序员分配释放 若程序员不释放程序结束时可能由OS回收 。分配⽅式类似于链表。数据段静态区static存放全局变量、静态数据。程序结束后由系统释放。代码段存放函数体类成员函数和全局函数的⼆进制代码。普通局部变量在栈区分配出了作用域就销毁。被static修饰的变量存放在静态区程序启动时分配程序结束时才销毁所以生命周期变长了。