当前位置: 首页 > news >正文

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

一.动态内存的分配

1.概念:

动态内存分配的核心在于“按需而动”:程序不再需要在运行前就划定死内存大小,而是在执行过程中,根据实时数据量向系统灵活借调内存,用多少就申请多少,用完即刻释放

2.有动态内存分配的原因

我们已经掌握的内存开辟⽅式有:

int val = 20;//在栈空间上开辟四个字节 char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的⽅式有两个特点:

  • 空间开辟⼤⼩是固定的。
  • 数组在申明的时候,必须指定数组的⻓度,数组空间⼀旦确定了⼤⼩不能调整

原因:但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间⼤⼩在程序运⾏的时候才能知道,那数组的编译时开辟空间的⽅式就不能满⾜了。C语⾔引⼊了动态内存开辟,让程序员⾃⼰可以申请和释放空间,就⽐较灵活了。

3.动态内存分布图


二.动态内存函数介绍

1.malloc 函数

cplusplus.com/reference/cstdlib/malloc/?kw=malloc

C语⾔提供了⼀个动态内存开辟的函数:

void* malloc (size_t size);

这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。

  • 如果开辟成功,则返回⼀个指向开辟好空间的指针。
  • 如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定。
  • 如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。

实现方式:

方式一:通过指针偏移访问(推荐)

#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指针,则函数什么事都不做。

补充注意

  1. 使用完之后一定要记得使用 free 函数释放所开辟的内存空间。
  2. 使用指针指向动态开辟的内存,使用完并 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]); // 通过数组下标访问,与*(p+i)等价 } // 回收空间 free(p); // 释放p所指向的动态内存 p = NULL; // 必须手动置为空指针 return 0; }

问题解答

【问题一】为什么 malloc 前要加上强制类型转换?

int* p = (int*)malloc(10 * sizeof(int));

根本原因有两个:

保持类型一致

malloc返回的是void*类型的指针,可以装任何类型的数据。而我们定义的pint*类型的,专门用来指向整数数据。把通用类型转换成具体类型,让指针和它指向的数据类型相匹配。

兼容不同编译器

这一点更重要。在C语言中,void*可以自动转换成其他指针类型,不写强转也能编译通过。但在C++中,这种自动转换是不允许的,编译器会报错。为了让代码在C和C++环境下都能顺利编译,加上强转是最稳妥的做法。虽然C语言不要求,但建议加上。这样写出来的代码可移植性更好,也明确告诉读代码的人:这块内存我打算当作int数组来用。


【问题二】为什么 free 后还要把 p 置成 NULL?

free(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 是不是 NULL?

if (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只能释放通过malloccallocrealloc在堆上动态分配的内存。代码中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 已经是 NULL,free(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

内存泄漏的危害:长期运行的程序(如服务器)如果持续泄漏,最终会耗尽系统内存,导致程序崩溃或系统卡死。

注意:

忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。切记:动态开辟的空间⼀定要释放,并且正确释放


四. 动态内存经典笔试题分析

题⽬1:

void 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,但栈内存是函数结束自动销毁的,不能把栈空间的地址返回给外部使用

解法一:改成malloc

char* p = (char*)malloc(20); strcpy(p, "hello world"); return p;

char p[]换成malloc申请的内存。堆上的内存不会随函数结束而销毁,地址有效。用完要free


解法二:加static

static char p[] = "hello world"; return p;

只加了staticp从栈上搬到静态区,生命周期延长到程序结束。不用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; }

题⽬4:

void 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++程序内存分配的⼏个区域:

  1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配⽅式类似于链表。
  3. 数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。

普通局部变量在栈区分配,出了作用域就销毁。static修饰的变量存放在静态区,程序启动时分配,程序结束时才销毁,所以生命周期变长了。

http://www.jsqmd.com/news/1275769/

相关文章:

  • 广州做小程序商城的公司有哪些?从“广深商圈竞争”角度看选型
  • 深入解析ADC寄存器:从FIFO状态到采样序列的嵌入式实战
  • LLM在教育游戏中的应用:从个性化互动到技术实现
  • 2026成都品牌金饰变现指南!周大福、老凤祥黄金回收核心规则与计价标准详解 - 奢侈品回收评测
  • rrtools完全指南:如何用R构建可复现研究项目的终极方案
  • Stereo-RCNN源码精读:从ROI Align到3D框细化的关键代码解析
  • EasyApplyJobsBot与其他求职工具对比:为什么它是最佳选择
  • ✨海口美兰黄金回收避坑终极清单!持证门店光谱无损检测,卖黄金不被压价 - 全城热点
  • alexa-smarthome消息结构深度剖析:request与response设计模式
  • 拯救Mac磁盘空间:为什么Mole终端清理工具是开发者的必备神器
  • 数以轻舟Agent V4发布:底层重构 + 语义自定义
  • 3个步骤打造专属互动猫咪:BongoCat模型自定义完全指南
  • 深入解析DS90UB921-Q1 EVM:FPD-Link III串行器硬件设计与调试实战
  • AI C4D风格一致性失控?用这4个神经辐射场校准参数锁定镜头语言,让100帧动画风格偏差≤0.8%(附MATLAB验证脚本)
  • PytorchNetHub部署指南:将你的深度学习模型快速部署到生产环境
  • 一种可信数据空间中不同厂商连接器赋能隐私计算的方法
  • LMK05028时钟芯片EEPROM与参考时钟检测配置实战指南
  • BeautifulDiscord:让你的Discord界面焕然一新的终极CSS自定义工具
  • DDrawCompat:让经典DirectX游戏在现代Windows上完美运行的终极兼容方案
  • 魔兽争霸3现代优化神器:WarcraftHelper让你的经典游戏焕发新生![特殊字符]
  • 如何利用TLS Poison通过图片标签实现浏览器CSRF攻击
  • Cortex-M3系统控制与异常处理:从寄存器到可靠系统的构建
  • SQL Server 数据库查看各表的记录数
  • Graph Engineering,重构AI智能体协作的底层逻辑,让复杂任务高效落地
  • 别被通用低代码模板困住!行业专属方案才是数字化落地关键
  • MVVM Dialogs常见问题解答:从安装到高级应用
  • Qlib量化投资平台实战指南:3步构建你的AI投资策略
  • 2026年主流海关数据工具对比:外贸海关数据服务商全景盘点
  • 如何用AI短视频引擎告别内容创作焦虑?Pixelle-Video帮你实现高效创作
  • 如何打造纯净高效的百度贴吧个性化体验?