在 C 语言编程中,内存管理是绕不开的核心知识点。我们一开始学的变量、数组,都是在栈上开辟内存,比如int val = 20或者char arr[10] = {0}。但这种方式有个明显的局限:空间大小固定,数组声明时必须指定长度,运行中还不能调整。可实际开发里,很多时候我们要到程序跑起来才知道需要多少内存 ------ 比如用户输入数据的数量、读取文件的大小。这时候,动态内存分配就该登场了,它能让我们灵活申请和释放内存,完美解决固定内存的痛点。今天就从基础到实战,把动态内存管理讲明白!
一、动态内存的 "核心工具":4 个关键函数
动态内存操作主要靠malloc、free、calloc、realloc这四个函数,它们都声明在stdlib.h头文件里,缺一不可
1. malloc:最基础的 "内存申请器"
函数原型:void* malloc (size_t size);
- 功能:向内存(堆区)申请一块连续可用的空间,返回指向这块空间的指针。
- 三个关键注意点:
- 申请成功:返回有效指针,需要自己强转成对应类型(比如
int*、char*)。 - 申请失败:返回**
NULL指针,所以一定要检查返回值**,不然会踩坑! - 参数为 0:行为不确定,取决于编译器,尽量别这么用。
- 申请成功:返回有效指针,需要自己强转成对应类型(比如
cpp
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(20);
if (p == NULL)
{
perror("malllc");
return 1;
}
int i = 0;
for (i; i < 5; i++)
{
(*(p + i)) = i + 1;
}
for (i = 0; i < 5; i++)
{
printf("%d ", *(p + i));
}
return 0;
}

注意:开辟空间后,未赋值的空间是随机值
2. free:内存的 "回收工"
函数原型:void free (void* ptr);
- 功能:专门释放动态开辟的内存,把空间还给系统。
- 两个关键注意点:
- 只能释放堆区 内存:如果给它传栈区变量的地址(比如
int a=10; free(&a);),行为未定义,程序可能崩溃。 - 传
NULL:函数啥也不做,所以释放后把指针设为NULL很安全。 - 传入要释放内存的起始位置
- 只能释放堆区 内存:如果给它传栈区变量的地址(比如
这里要强调:动态内存申请后一定要释放,不然会造成内存泄漏,程序运行越久占内存越多,最后可能卡死。
上面代码后面加上:
cpp
free(p);
p = NULL;
避免p成为野指针
3. calloc:带 "初始化" 的内存申请
函数原型:void* calloc (size_t num, size_t size);
- 功能:和
malloc类似,也是申请动态内存,但多了个 "初始化" 功能 ------ 会把申请的每个字节都设为 0。 - 参数含义:
num是元素个数,size是每个元素的大小。比如calloc(10, sizeof(int))就是申请 10 个 int 的空间,每个都初始化为 0。
cpp
#include <stdio.h>
#include <stdlib.h>
int main() {
int* p = (int*)calloc(10, sizeof(int));
if (NULL != p) {
for (int i = 0; i < 10; i++) {
printf("%d ", *(p + i));
}
}
free(p);
p = NULL;
return 0;
}

4. realloc:内存的 "伸缩器"
函数原型:void* realloc (void* ptr, size_t size);
- 功能:调整已动态开辟内存的大小,比如之前申请的空间不够用了,或者太大了想缩小。
- 两个关键注意点。
- 两种调整情况:
- 情况 1:原内存后面有足够空间,直接在后面追加,返回原地址。
- 情况 2:原内存后面空间不够,会在堆区找一**块新的合适空间,返回新地址,**会把原来的数据复制到新空间,释放旧的内存空间。
- 情况3:调整失败,返回空指针
- 两种调整情况:
cpp
#include <stdio.h>
#include <stdlib.h>
int main() {
int* p = (int*)calloc(5, sizeof(int));
if (p == NULL) {
perror("mallloc");
return 1;
}
for (int i = 0; i < 5; i++)
{
*(p + i) = i + 1;
}
//空间不够,realloc申请内存40个字节
int *ptr=realloc(p, 40);
if (ptr != NULL)
{
p = ptr;
}
else
{
//失败使用原来的内存
perror("realloc");
}
//可以使用40个字节的内存
for (int i = 5; i < 10; i++)
{
*(p + i)= i+1;
}
//打印内存中的内容
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}

调试:



二、动态内存的 "避坑指南":6 个常见错误
动态内存用不好容易出问题,下面这 6 个错误一定要避开!
1. 对 NULL 指针解引用
malloc、calloc、realloc都可能返回 NULL,如果直接用*p = 20,程序会崩溃。
cpp
// 错误示范
void test() {
int* p = (int*)malloc(INT_MAX / 4); // 申请超大空间,大概率失败
*p = 20; // 如果p是NULL,这里直接报错
free(p);
}
// 正确做法:先检查p是否为NULL
2. 越界访问动态内存
和数组越界一样,动态内存也不能超范围访问,会触发未定义行为。
cpp
void test() {
int* p = (int*)malloc(10 * sizeof(int)); // 10个int,索引0-9
if (NULL == p) exit(EXIT_FAILURE);
for (int i = 0; i <= 10; i++) {
*(p + i) = i; // i=10时越界,错误!
}
free(p);
}
3. 用 free 释放非动态内存
free只能释放堆区内存,栈区的局部变量不能用 free 释放。
cpp
void test() {
int a = 10;
int* p = &a;
free(p); // 错误!a在栈上,不是动态内存
}
5. 多次释放同一块内存
一块动态内存只能 free 一次,多次 free 会导致程序崩溃。
cpp
void test() {
int* p = (int*)malloc(100);
free(p);
free(p); // 错误!重复释放
}
6. 忘记释放内存(内存泄漏)
这是最常见的错误!申请的动态内存不用了不释放,程序运行期间内存会一直被占用,直到程序结束才会被系统回收(但长期运行的程序比如服务器,会越跑越卡)。
cpp
void test() {
int* p = (int*)malloc(100);
if (p != NULL) *p = 20;
// 没有free(p),内存泄漏!
}
7.不再内存的起始位置释放内存(易错)
cpp
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(20);
if (p == NULL)
{
perror("malllc");
return 1;
}
int i = 0;
for (i; i < 5; i++)
{
*p = i+ 1;
p++;
}
for (i = 0; i < 5; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}

p指针不指向内存的起始位置,释放会发生错误。
三、动态内存经典笔试题分析(高频考点)
笔试题是检验动态内存掌握程度的核心,下面这几道经典题几乎是面试 / 考试必出,我们逐题拆解。
笔试题 1:函数传参错误导致内存泄漏 + 野指针
cpp
// 题目:想通过函数给指针p分配内存,运行后会出什么问题?
void GetMemory(char* p) {
p = (char*)malloc(100); // 申请100字节内存
}
void Test(void) {
char* str = NULL;
GetMemory(str); // 调用函数
strcpy(str, "hello world"); // 拷贝字符串
printf(str);
}
问题分析:
- 传参错误 :
GetMemory的参数是char* p(值传递),函数内的p是str的临时拷贝,修改p的指向不会影响外部的str。 - 野指针访问 :函数执行后,
str依然是NULL,strcpy(str, ...)对 NULL 指针解引用,程序崩溃。 - 内存泄漏 :
GetMemory里malloc的 100 字节没有free,且指针丢失,内存永远无法释放。
正确写法(两种方案):
cpp
// 方案1:传二级指针(推荐)
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(str);
free(str); // 释放内存
str = NULL; // 避免野指针
}
}
// 方案2:函数返回指针
char* GetMemory() {
char* p = (char*)malloc(100);
return p;
}
void Test(void) {
char* str = NULL;
str = GetMemory();
if (str != NULL) {
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
}
笔试题 2:返回栈区指针导致野指针
cpp
#include <stdio.h>
#include <stdlib.h>
// 题目:这个函数返回的指针能用吗?
char* GetString(void) {
char p[] = "hello world"; // 栈区局部数组
return p; // 返回数组首地址
}
void Test(void) {
char* str = NULL;
str = GetString(); // 接收返回值
printf(str); // 打印结果?
}
int main()
{
Test();
return 0;
}

问题分析:
char p[] = "hello world"是栈区局部变量,函数执行结束后,栈区空间会被系统回收。GetString返回的是p的地址,但该地址对应的空间已失效,str成为野指针。- 调用
printf(str)时,访问的是已释放的栈区空间,结果是随机的(未定义行为)
修改:
cpp
#include <stdio.h>
#include <stdlib.h>
// 方案1:用静态变量(数据段,程序结束才释放)
char* GetString(void) {
static char p[] = "hello world"; // static修饰,存放在数据段
return p;
}
// 方案2:用动态内存(堆区,手动释放)
//char* GetString(void) {
// char* p = (char*)malloc(12); // 12字节:hello world + 结束符'\0'
// strcpy(p, "hello world");
// return p;
//}
void Test(void) {
char* str = NULL;
str = GetString(); // 接收返回值
printf(str); // 打印结果?
}
int main()
{
Test();
return 0;
}
笔试题 3:重复释放 + 内存泄漏
cpp
// 题目:这个函数有几个问题?
void Test(void) {
char* str = (char*)malloc(100); // 申请内存
strcpy(str, "hello"); // 拷贝字符串
free(str); // 释放内存
if (str != NULL) { // 检查指针是否为空
strcpy(str, "world"); // 再次拷贝
printf(str);
free(str); // 释放内存
}
}
int main()
{
Test();
return 0;
}
问题分析:
- 野指针访问 :
free(str)后,str的指向的内存已释放,但str本身没有置 NULL,依然指向原地址(野指针)。 - 非法内存操作 :
strcpy(str, "world")访问已释放的堆区内存,程序可能崩溃。 - (进阶)如果在
strcpy后再次free(str),会导致重复释放,同样触发未定义行为。
修改:
cpp
#include <stdio.h>
#include <stdlib.h>
void Test(void) {
char* str = (char*)malloc(100);
if (str != NULL) { // 先检查再使用
strcpy(str, "hello");
free(str);
str = NULL; // 释放后立即置NULL,关键!
}
if (str != NULL) { // 此时str是NULL,不会执行内部代码
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
笔试题 4:realloc 使用不当导致内存泄漏
cpp
// 题目:想扩容内存,哪里错了?
void Test(void) {
char* p = (char*)malloc(100);
strcpy(p, "hello");
p = (char*)realloc(p, 200); // 扩容到200字节
strcpy(p + 5, "world");
printf(p);
free(p);
}
问题分析:
realloc(p, 200)如果扩容失败,会返回NULL,此时p被赋值为NULL,原来malloc的 100 字节指针丢失,导致内存泄漏。- 即使扩容成功,代码本身能运行,但存在隐藏风险(失败时崩溃 + 泄漏)。
修改:
cpp
#include <stdio.h>
#include <stdlib.h>
void Test(void) {
char* p = (char*)malloc(100);
if (p != NULL) {
strcpy(p, "hello");
// 用临时指针接收realloc返回值
char* temp = (char*)realloc(p, 200);
if (temp != NULL) { // 检查扩容是否成功
p = temp;
strcpy(p + 5, "world");
printf(p);
}
free(p); // 无论扩容是否成功,都要释放原内存
p = NULL;
}
}
int main()
{
Test();
return 0;
}
四、柔性数组:动态内存的 "进阶用法"
你可能没听过柔性数组,但它确实是 C99 标准里的特性,用好了很方便。
1. 什么是柔性数组?
结构体的最后一个元素可以是未知大小的数组,这就是柔性数组。
cpp
// 两种写法(有些编译器不支持int a[0],可以用int a[])
struct st_type {
int i;
int a[0]; // 柔性数组成员
};
2. 柔性数组的特点
- 前面必须有至少一个其他成员(比如上面的 int i)。
sizeof计算结构体大小时,不包含柔性数组的内存(比如sizeof(struct st_type)是 4,只算 int i 的大小)。- 如:

- 必须用
malloc动态分配内存,且分配的空间要大于结构体本身大小,给柔性数组留空间
3. 怎么用柔性数组?
cpp
#include <stdio.h>
#include <stdlib.h>
struct s {
char c;
int i;
int a[0]; // 柔性数组(C99 也可写成 int a[])
};
int main()
{
// 初始分配:结构体固定部分 + 5个int的柔性数组空间
struct s* ps = (struct s*)malloc(sizeof(struct s) + 5 * sizeof(int));
if (ps == NULL)
{
perror("malloc");
return 1;
}
// 初始化结构体成员和柔性数组
ps->c = 'A'; // 补充初始化字符成员
ps->i = 100;
for (int i = 0; i < 5; i++)
{
ps->a[i] = i; // 柔性数组赋值:0 1 2 3 4
}
// 打印初始柔性数组内容
printf("初始柔性数组:");
for (int i = 0; i < 5; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
// 关键修正:realloc 传递内存块起始地址 ps,且扩大空间到 10 个int
struct s* ptr = (struct s*)realloc(ps, sizeof(struct s) + 10 * sizeof(int));
if (ptr != NULL)
{
ps = ptr; // 调整成功,更新指针
printf("内存调整成功!\n");
// 给新增的柔性数组元素赋值
for (int i = 5; i < 10; i++)
{
ps->a[i] = i; // 新增元素:5 6 7 8 9
}
// 打印调整后的完整柔性数组
printf("调整后柔性数组:");
for (int i = 0; i < 10; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
else
{
perror("realloc"); // 调整失败,保留原内存
}
// 释放内存(避免内存泄漏)
free(ps);
ps = NULL; // 野指针置空
return 0;
}

方法二:
cpp
#include <stdio.h>
#include <stdlib.h>
struct s
{
int n; // 表示数组的元素个数
int* arr; // 指向动态分配的数组
};
int main()
{
// 1. 为结构体本身分配内存
struct s* p = (struct s*)malloc(sizeof(struct s));
if (p == NULL) // 检查内存分配是否成功
{
perror("malloc for struct");
return 1;
}
// 2. 初始化结构体成员,并为arr指针分配数组内存
p->n = 5; // 初始数组有5个int元素
p->arr = (int*)malloc(p->n * sizeof(int));
if (p->arr == NULL) // 检查数组内存分配是否成功
{
perror("malloc for arr");
free(p); // 分配arr失败时,先释放已分配的结构体内存
p = NULL;
return 1;
}
// 3. 使用数组:给arr赋值并打印初始内容
for (int i = 0; i < p->n; i++)
{
p->arr[i] = i + 1; // 赋值:1,2,3,4,5
}
printf("初始数组内容:");
for (int i = 0; i < p->n; i++)
{
printf("%d ", p->arr[i]);
}
printf("\n");
// 4. 调整数组大小:用realloc把arr的空间扩大到10个int
int* ptr = (int*)realloc(p->arr, 10 * sizeof(int));
if (ptr != NULL) // 检查realloc是否成功
{
p->arr = ptr; // 调整成功,更新arr指针
p->n = 10; // 同步更新数组元素个数为10
ptr = NULL; // 临时指针置空,避免野指针
// 给新增的5个元素赋值(索引5~9)
for (int i = 5; i < p->n; i++)
{
p->arr[i] = i + 1; // 赋值:6,7,8,9,10
}
// 打印调整后的数组内容
printf("调整后数组内容:");
for (int i = 0; i < p->n; i++)
{
printf("%d ", p->arr[i]);
}
printf("\n");
}
else
{
perror("realloc for arr"); // 调整失败,保留原数组
// 失败时p->arr仍有效,无需额外处理,后续正常释放即可
}
// 5. 释放内存(关键:先释放子指针arr,再释放结构体p)
free(p->arr); // 释放数组内存
p->arr = NULL; // 野指针置空
free(p); // 释放结构体内存
p = NULL;
return 0;
}
核心差异对比表:
| 特性 | 结构体 + 柔性数组(int a [0]) | 结构体 + 普通指针成员(int* arr) |
|---|---|---|
| 内存布局 | 结构体固定部分 + 柔性数组成员在同一块连续内存中 | 结构体固定部分(含指针)和数组内存是两块独立的内存(指针指向另一块) |
| 内存分配次数 | 只需 1 次 malloc(整体分配) | 需要 2 次 malloc(先分配结构体,再分配数组) |
| realloc 使用 | 对结构体起始地址 realloc(整体调整) | 对指针指向的数组地址 realloc(仅调整数组) |
| 内存释放 | 只需 1 次 free(释放结构体地址即可) | 需要 2 次 free(先释放数组,再释放结构体) |
| 内存碎片 | 碎片少(连续内存) | 碎片多(两块独立内存,释放顺序错易泄漏) |
| 可移植性 | C99 标准支持(部分编译器需用 int a []) | 完全兼容所有 C 编译器,无兼容性问题 |
| 使用场景 | 适合数组和结构体生命周期一致的场景 | 适合数组需要独立管理(如单独替换、释放)的场景 |
4. 柔性数组的优势
对比用指针实现的类似功能,柔性数组有两个明显好处:
- 方便释放内存:一次 free 就能释放结构体和数组的所有内存,不用手动释放成员指针。
- 访问速度更快:内存是连续的,减少内存碎片,CPU 缓存效率更高。
五、总结:C/C++ 程序的内存区域划分
最后我们梳理一下程序的内存布局,帮你更好理解动态内存的位置:
- 栈区(stack):存放局部变量、函数参数、返回值等,自动分配释放,效率高但空间小(向下增长)。
- 堆区(heap):动态内存分配的区域,由程序员申请释放,空间较大(向上增长)。
- 数据段(静态区):存放全局变量、静态变量(static 修饰),程序结束后系统释放。
- 代码段:存放函数二进制代码、只读常量(比如字符串常量),不可修改。
- 内核空间:用户代码不能读写,操作系统使用。
六、关键点回顾
- 动态内存核心函数:
malloc(申请)、free(释放)、calloc(申请 + 初始化)、realloc(扩容 / 缩容),使用前必须检查返回值是否为 NULL。 - 常见坑点:NULL 解引用、越界访问、释放非堆内存、释放内存一部分、重复释放、内存泄漏,其中 "释放后置 NULL" 是避免野指针的关键。
- 笔试题核心考点:值传递导致指针无效、返回栈区指针、realloc 直接赋值、释放后未置 NULL,解决思路是 "传二级指针 / 返回值""用静态 / 堆内存""临时指针接收 realloc 结果"。