
✨ 把代码写进星轨,
用逻辑丈量宇宙。
| 导航 | 链接 |
|---|---|
| 个人主页 | 🏠 星轨初途 |
| 基础语言专栏 | 💻 C语言 、 📚 数据结构 |
| C++ 进阶专栏 | 🏆 C++学习(竞赛类) 、 ⚙️ C++专栏(开发类) |
| 刷题实战专栏 | 🚀 算法及编程题分享 |

文章目录
- 前言
- [一、 灵魂拷问:为什么要有动态内存分配?](#一、 灵魂拷问:为什么要有动态内存分配?)
- [二、 硬核拆解:动态内存四大金刚](#二、 硬核拆解:动态内存四大金刚)
-
- [1. `malloc`:动态申请内存](#1.
malloc:动态申请内存) - [2. `free`:释放回收内存](#2.
free:释放回收内存) - [3. `calloc`:malloc+初始化为0](#3.
calloc:malloc+初始化为0) - [4. `realloc`:动态扩容](#4.
realloc:动态扩容)
- [1. `malloc`:动态申请内存](#1.
- 三、常见的6大动态内存错误
-
- [1. 对 `NULL` 指针的解引用操作](#1. 对
NULL指针的解引用操作) - [2. 对动态开辟空间的越界访问](#2. 对动态开辟空间的越界访问)
- [3. 对非动态开辟内存使用 `free` 释放](#3. 对非动态开辟内存使用
free释放) - [4. 使用 `free` 释放一块动态开辟内存的一部分](#4. 使用
free释放一块动态开辟内存的一部分) - [5. 对同一块动态内存多次释放(Double Free)](#5. 对同一块动态内存多次释放(Double Free))
- [6. 动态开辟内存忘记释放(内存泄漏)](#6. 动态开辟内存忘记释放(内存泄漏))
- [1. 对 `NULL` 指针的解引用操作](#1. 对
- [四、 降维打击:4道经典笔试题深度分析](#四、 降维打击:4道经典笔试题深度分析)
-
- [笔试题 1:传值的迷思](#笔试题 1:传值的迷思)
- [笔试题 2:返回栈空间地址(野指针的诱惑)](#笔试题 2:返回栈空间地址(野指针的诱惑))
- [笔试题 3:修复了传参,但没修彻底](#笔试题 3:修复了传参,但没修彻底)
- [笔试题 4:释放后不置空的惨案(Use After Free)](#笔试题 4:释放后不置空的惨案(Use After Free))
- [五、 进阶黑科技:柔性数组(Flexible Array)](#五、 进阶黑科技:柔性数组(Flexible Array))
-
- [1. 柔性数组的声明与避坑](#1. 柔性数组的声明与避坑)
- [2. 核心机制:柔性数组的三大特点](#2. 核心机制:柔性数组的三大特点)
- [3. 代码实战:柔性数组怎么用?](#3. 代码实战:柔性数组怎么用?)
- [5. 灵魂拷问:为什么不用指针 `int* a`,而非要搞个柔性数组?](#5. 灵魂拷问:为什么不用指针
int* a,而非要搞个柔性数组?)
- [六、 终局视角:C/C++ 程序内存区域划分总结](#六、 终局视角:C/C++ 程序内存区域划分总结)
前言
嗨(。◕ˇ∀ˇ◕)!今天我们直接进入正题!
在C/C++的底层开发世界里,内存管理绝对是一道分水岭。不会动态内存管理,你的程序永远只能在"温室"里运行,处理点小打小闹的固定数据;掌握了它,你就能真正触碰到操作系统的脉搏,让代码拥有处理海量未知数据的能力。
今天,星轨带你硬核拆解C语言的动态内存管理,不仅把四大内存函数掰碎了揉烂了讲,还会把常考的4道 经典笔试题,以及6大 最容易踩坑的内存错误讲解清楚!准备好发车了吗?

一、 灵魂拷问:为什么要有动态内存分配?
我们以前学过的内存开辟方式,主要是在栈区(Stack):
c
int val = 20; // 在栈空间上开辟四个字节
char arr[10] = {0}; // 在栈空间上开辟10个字节的连续空间
但在栈上分配内存有两个致命弱点:
- 空间开辟大小是固定的。
- 数组在申明时必须指定长度。在C99标准之前,一旦确定了大小就不能在运行期间动态调整。
如果程序需要处理用户输入的未知量级数据,栈空间不仅大小受限(通常只有几MB),生命周期也随函数结束而销毁。这时候,我们就必须向**堆区(Heap)**借内存了,这就是动态内存分配的意义所在。
二、 硬核拆解:动态内存四大金刚
C语言在 <stdlib.h> 中为我们提供了四个极其重要的内存管理函数:malloc、free、calloc、realloc。下面我们逐一拆解,看看它们到底是怎么工作的。
1. malloc:动态申请内存
链接:malloc
函数原型: void* malloc(size_t size);
功能: 向堆区申请一块连续可用的空间(大小为 size 字节),并返回指向这块空间的指针。注意,malloc 不会对这块内存进行初始化,里面存放的是未知的垃圾数据。
malloc 函数核心特性总结
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个
NULL指针,因此malloc的返回值一定要做检查。 - 返回值的类型是
void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候由使用者自己来决定。 - 如果参数
size为 0,malloc的行为在标准中是未定义的,取决于编译器。
💡 黄金开发经验:
永远不要相信你一定能申请到内存!
malloc开辟失败会返回NULL。如果不做检查直接解引用,你的程序会直接段错误(Crash)!
代码实战:
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 申请能存放 10 个整型的内存空间 (40 字节)
int* p = (int*)malloc(10 * sizeof(int));
// 🚨 第一步:永远记得检查返回值!
if (p == NULL) {
perror("malloc failed"); // 打印错误原因
return 1;
}
// 正常使用这块内存
for (int i = 0; i < 10; i++) {
p[i] = i;
printf("%d ", p[i]);
}
// 用完记得还给系统(见下一个函数)
free(p);
p = NULL;
return 0;
}
2. free:释放回收内存
链接:free
函数原型: void free(void* ptr);
功能: 将之前通过 malloc/calloc/realloc 借来的内存空间还给操作系统。
free 函数核心特性总结:
free函数用来释放动态开辟的内存。- 如果参数
ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。 - 如果参数
ptr是NULL指针,则函数什么事都不做。
🚨 致命踩坑点:释放后不置空等于留了一把"作案钥匙"!
free(p)只是把堆内存还回去了,但指针变量p里面依然存着那块内存的地址 !如果不把p置为NULL,它就成了一个彻头彻尾的野指针。
代码实战:
c
int* p = (int*)malloc(100);
if(p != NULL)
{
// ... 业务处理 ...
}
free(p); // 内存还给系统了
// p[0] = 10; // ❌ 极其危险!如果这里不小心用到p,属于非法访问野指针!
p = NULL; // ✅ 优雅做法:彻底切断联系,哪怕后面误用,也是空指针报错,比野指针乱改数据强得多!
3. calloc:malloc+初始化为0
链接🔗:calloc
函数原型: void* calloc(size_t num, size_t size);
功能: 为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节都初始化为 0。
- 与malloc区别
与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为 O。
💡 硬核场景:什么时候用
calloc?如果你需要一个干净的计数器数组,或者用来存储哈希映射,用
calloc比malloc + memset更加简洁高效。
代码实战:
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 申请 10 个整型空间,并自动全部初始化为 0
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc failed");
return 1;
}
// 打印验证:你会发现全是 0,而如果用 malloc 这里全是乱码
for (int i = 0; i < 10; i++)
{
printf("%d ", p[i]); // 输出: 0 0 0 0 0 0 0 0 0 0
}
free(p);
p = NULL;
return 0;
}
4. realloc:动态扩容
链接🔗:realloc
函数原型: void* realloc(void* ptr, size_t size);
功能: 将 ptr 指向的内存块大小调整为 size 字节。
🚨 致命踩坑点:
realloc的底层扩容机制(原地 vs 异地)🔹 原地扩容 :如果原内存块后面有足够的闲置空间,直接在后面追加,返回原地址。
🔹 异地扩容 :如果后面空间不够,它会在堆区另找一块足够大的新空间,把旧数据自动拷贝 过去,然后自动
free掉旧空间,最后返回新空间的地址!千万不要直接用原指针接收返回值! 如果申请失败返回
NULL,原指针就被覆盖了,旧数据直接丢失,导致内存泄漏!
原地扩容和异地扩容区别如图

代码:
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 1. 先申请 5 个整型
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL) return 1;
// 2. 假设空间不够了,需要扩容到 10 个整型
// ❌ 错误写法:p = (int*)realloc(p, 10 * sizeof(int));
// ✅ 正确写法:使用临时指针 tmp 探路
int* tmp = (int*)realloc(p, 10 * sizeof(int));
if (tmp != NULL)
{
p = tmp; // 只有扩容成功,才把新地址交给 p
printf("扩容成功!\n");
}
else
{
printf("扩容失败,但原有的 5 个整型数据还在 p 里,没有丢失!\n");
}
free(p);
p = NULL;
return 0;
}
三、常见的6大动态内存错误
很多萌新在用指针时凭直觉写代码,结果程序跑着跑着就崩了。课件里提到的这6种死法,你中招过几个?
1. 对 NULL 指针的解引用操作
死因: 忘记检查 malloc 的返回值,直接拿来用。
c
void test() {
int *p = (int *)malloc(INT_MAX / 4);
*p = 20; // ❌ 如果申请失败 p 是 NULL,这里直接段错误(Crash)!
free(p);
}
2. 对动态开辟空间的越界访问
死因: 以为堆区无限大,循环条件写错导致越界。
c
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++)
{
*(p + i) = i; // ❌ 当 i=10 时,越界访问了不属于你的第11个位置!
}
free(p);
}
3. 对非动态开辟内存使用 free 释放
死因: 试图用堆区的钥匙去开栈区的锁。
c
void test()
{
int a = 10;
int *p = &a; // p指向的是栈区变量
free(p); // ❌ 致命错误!free只能释放堆区内存,程序直接崩溃!
}
4. 使用 free 释放一块动态开辟内存的一部分
死因: 指针位置移动了,free 找不到内存块的起始头信息。
c
void test() {
int *p = (int *)malloc(100);
p++; // 改变了指针的位置
free(p); // ❌ p不再指向动态内存的起始位置,释放失败崩溃!
}
5. 对同一块动态内存多次释放(Double Free)
死因: 释放完了不置空,后面手滑又释放一次。
c
void test() {
int *p = (int *)malloc(100);
free(p);
// ... 一大堆代码过后
free(p); // ❌ 重复释放已经被系统回收的内存,崩溃!
}
6. 动态开辟内存忘记释放(内存泄漏)
死因: 函数结束了,但是堆区的内存没还,服务端的终极杀手!
c
void test() {
int *p = (int *)malloc(100);
if (NULL != p) {
*p = 20;
}
// ❌ 忘记 free(p)!由于 p 是局部变量,出函数后 p 销毁,这100字节再也找不回来了!
}
四、 降维打击:4道经典笔试题深度分析
下面4道题,专门考察你对"生命周期"、"传值与传址"、"野指针"的理解。
笔试题 1:传值的迷思
c
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
💡 硬核剖析:程序崩溃 + 内存泄漏!
GetMemory里的p只是str的一份临时拷贝(传值调用)。p申请了堆空间,但完全没改变外面的str。出函数后p销毁(内存泄漏),外部的str依然是NULL。strcpy试图向NULL拷贝字符串,触发空指针解引用,Crash!
笔试题 2:返回栈空间地址(野指针的诱惑)
c
char *GetMemory(void) {
char p[] = "hello world";
return p;
}
void Test(void) {
char *str = NULL;
str = GetMemory();
printf(str);
}
💡 硬核剖析:打印出乱码!
数组
p存放在栈区 。GetMemory执行完毕后,它的栈帧被系统销毁 。str虽然拿到了地址,但那块内存已经还给操作系统了,str变成了野指针。强行访问可能会读到被覆盖的脏数据,打印乱码。
笔试题 3:修复了传参,但没修彻底
c
void GetMemory(char **p, int num) {
*p = (char *)malloc(num);
}
void Test(void) {
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
💡 硬核剖析:能正常打印 "hello",但存在内存泄漏!
这一次学聪明了,用了二级指针(传址调用),外部的
str确实成功指向了开辟的100字节堆内存,所以strcpy和printf都能正常工作。
但是!Test函数结束前,忘记调用free(str)了!这100个字节永远留在了堆区,造成内存泄漏。
笔试题 4:释放后不置空的惨案(Use After Free)
c
void Test(void) {
char *str = (char *)malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL) { // 陷阱就在这里!
strcpy(str, "world");
printf(str);
}
}
💡 硬核剖析:非法访问(Use After Free)!
free(str)只是把堆内存还给了操作系统,但str这个变量本身的值(那个地址)并没有被清空 !所以
if(str != NULL)条件必然成立!接着执行strcpy,就是在向一块已经不属于你的内存强行写入数据,这是极其危险的野指针访问,随时会导致程序崩溃或数据损坏。
解法 :free(str);之后必须立马接一句str = NULL;。
五、 进阶黑科技:柔性数组(Flexible Array)
在 C99 标准中,引入了一个极其巧妙的特性:结构体中的最后一个元素允许是未知大小的数组,这就叫柔性数组成员。
1. 柔性数组的声明与避坑
c
typedef struct st_type
{
int i;
int a[0]; // 柔性数组成员
} type_a;
🚨 致命踩坑点:编译器
上面使用
int a[0];的写法在某些编译器下可能会报错无法编译!如果你遇到了这种情况,请果断改成下面这种标准写法,省略数组大小:
ctypedef struct st_type { int i; int a[]; // 柔性数组成员(标准写法) } type_a;
2. 核心机制:柔性数组的三大特点
根据底层规范,柔性数组有着严格的使用准则(这也正是课件图片中强调的重点):
🔹 特点 1:结构中的柔性数组成员前面必须至少一个其他成员。 (它绝对不能是结构体里的光杆司令)。
🔹 特点 2:sizeof 返回的这种结构大小不包括柔性数组的内存。 (比如上面 sizeof(type_a) 的结果通常是 4 字节,仅仅是成员 i 的大小,它给你制造了一种"数组不占空间"的幻觉)。
🔹 特点 3:包含柔性数组成员的结构用 malloc() 函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
3. 代码实战:柔性数组怎么用?
既然 sizeof 不包含它,那它的空间怎么来?答案就是根据特点 3 ,必须配合 malloc 动态分配!
c
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type {
int i;
int a[]; // 柔性数组成员
} type_a;
int main() {
// 💡 黄金开发经验:开辟空间 = 结构体本身大小 + 你预期的数组总大小
// 这里我们期望数组能存 100 个 int
type_a *p = (type_a *)malloc(sizeof(type_a) + 100 * sizeof(int));
if (p == NULL) {
perror("malloc failed");
return 1;
}
// 正常使用这块连续的内存
p->i = 100;
for (int j = 0; j < 100; j++) {
p->a[j] = j;
}
printf("结构体大小: %zu\n", sizeof(type_a)); // 输出 4
printf("数组第10个元素: %d\n", p->a[9]); // 输出 9
// ✅ 释放空间,干脆利落,一次搞定!
free(p);
p = NULL;
return 0;
}
5. 灵魂拷问:为什么不用指针 int* a,而非要搞个柔性数组?
很多同学会想:我直接在结构体里放一个指针 int* a,然后再对 a 单独 malloc 一次不香吗?
对比"结构体套指针再二次 malloc"的方案,柔性数组有两个极其优雅的"降维打击"优势:
优势:
-
方便内存释放(防泄漏)
如果是结构体里面套指针,你需要先
free内部指针的内存,再free结构体本身。如果你把这个结构体当作接口返回给别人用,用户极其容易忘记释放内部成员,从而造成严重的内存泄漏。而柔性数组让结构体和数据的内存是一次性连续分配 的,释放时只需要一次干脆利落的free(p),绝不留后患! -
有利于提升访问速度
连续的内存分配不仅极大减少了内存碎片的产生,还能极大提高 CPU Cache(缓存)的命中率。因为结构体本身和数组数据在物理内存上是紧挨着的,CPU 在抓取结构体时,会顺带把后面的数组数据也加载到高速缓存中。在追求极致性能的底层组件开发里,这种内存局部性的优化是神级细节!
六、 终局视角:C/C++ 程序内存区域划分总结
为了把内存彻底吃透,我们需要在脑海中建立完整的物理空间模型:

- 栈区(Stack): 存放局部变量、函数参数等。编译器自动分配释放,效率极高,空间有限。
- 堆区(Heap): 程序员用
malloc/calloc/realloc分配,用free释放。不释放就会泄漏。 - 数据段(静态区/Static): 存放全局变量、静态数据。程序结束后由操作系统回收。
- 代码段(Text): 存放可执行的机器指令代码和只读常量(如字符串字面量)。
嗨ヾ(o´∀`o)ノ!今天的内容就到这里啦!如果觉得有帮助,别忘了点赞收藏!我们下一篇不见不散啦!φ(>ω<*)
