九、C语言动态内存管理

C 语言给了你直接向操作系统要内存的权力,但也要求你像个负责任的成年人一样:借了东西,必须记得还。

思维导图




一、 栈与堆的区别

内存主要分为两个区域:。理解它们的区别是防止内存错误的根本。

1.1 对比表

特性
管理方式 自动分配,自动释放 手动申请,手动释放
生命周期 函数返回即销毁 直到 free() 或程序结束
空间大小 很小 (通常几 MB) 很大 (取决于物理内存)
使用场景 局部变量、函数参数 大数组、动态数据结构 (链表)

1.2 代码对比

c 复制代码
void stack_vs_heap() {
    // 1. 栈分配:快,但空间有限,函数结束即亡
    int stack_arr[100]; 
    
    // 2. 堆分配:慢,但空间大,生命周期由你控制
    // 向系统申请 400 字节,返回首地址
    int *heap_arr = (int*)malloc(100 * sizeof(int));
    
    if (heap_arr != NULL) {
        heap_arr[0] = 99; // 像数组一样使用
        free(heap_arr);   // 用完必须还!
    }
}

二、 内存分配三剑客:malloc, calloc, realloc

2.1 malloc:最基础的分配

原型: void* malloc(size_t size);
注意: 它申请的内存里是垃圾值 (未初始化)。
规范: 永远要检查返回值是否为 NULL(防止内存耗尽)。

代码模板:

c 复制代码
int *p = (int*)malloc(10 * sizeof(int)); // 推荐写法:sizeof(类型)
if (p == NULL) {
    fprintf(stderr, "内存申请失败!\n");
    return -1;
}

2.2 calloc:自带清洁功能的分配

原型: void* calloc(size_t num, size_t size);
特点: 它会自动把内存全部初始化为 0
代价: 稍微慢一点点。

代码示例:

c 复制代码
// 申请 10 个 int,且全部清零
int *p = (int*)calloc(10, sizeof(int));
// 此时 p[0] 到 p[9] 都是 0

2.3 free:归还内存

原型: void free(void* ptr);
规则:

1.只能 free 堆内存(malloc/calloc/realloc 出来的)

2.不能重复 free

3.free(NULL) 是安全的(什么都不做)

最佳实践:

c 复制代码
free(p);
p = NULL; // 养成好习惯,防止悬空指针

三、 realloc 的正确用法

当你发现申请的数组不够用了,realloc 可以帮你扩容。

3.1 致命陷阱:直接赋回原指针

错误写法:

c 复制代码
// 如果 realloc 失败返回 NULL,原有的 p 就丢了!内存泄漏!
p = realloc(p, new_size); 

3.2 正确写法:使用临时指针

c 复制代码
// 1. 初始分配
int *arr = malloc(5 * sizeof(int));

// 2. 扩容
int new_size = 10 * sizeof(int);
// 先用临时指针接住返回值
int *temp = realloc(arr, new_size); 

if (temp == NULL) {
    // 扩容失败,arr 里的旧数据还在,可以决定怎么处理
    printf("扩容失败\n");
} else {
    // 扩容成功,更新 arr
    arr = temp; 
}

四、 常见内存错误专题

4.1 内存泄漏

现象: 借了不还。程序运行时间越长,占用内存越多,最后系统崩溃。
复现:

c 复制代码
void leak() {
    int *p = malloc(100);
    return; // 忘了 free(p)!这 100 字节这就没人能用了
}

4.2 释放后使用

现象: 已经还回去了,还想去拿东西。
复现:

c 复制代码
free(p);
// p 变成了悬空指针
*p = 10; // 危险!这块内存可能已经分配给别人了,你这是在改别人的数据!

4.3 重复释放

现象: 同一张支票兑换了两次。
复现:

c 复制代码
free(p);
free(p); // 崩溃!

解法: free(p); p = NULL; 第二次 free(NULL) 是安全的。

4.4 堆越界

复现:

c 复制代码
char *s = malloc(5); // 申请 5 字节
strcpy(s, "Hello");  // 写入 "Hello\0" (6 字节) -> 越界!破坏堆元数据

五、 练习题

题目 1: malloc(0) 会返回什么?是 NULL 吗?

题目 2: 下面代码有内存泄漏吗?

c 复制代码
void func() {
    int *p = malloc(10);
    p = malloc(20); 
    free(p);
}

题目 3: 为什么 realloc(ptr, 0) 等价于 free(ptr)(在某些标准下)?

题目 4: calloc 分配的内存一定全是 0 吗?

题目 5: 下面代码有什么严重问题?

c 复制代码
int *arr = malloc(10 * sizeof(int));
arr = realloc(arr, 20 * sizeof(int));
if (!arr) return;

题目 6: 可以在函数里 malloc,在函数外 free 吗?

题目 7: 栈溢出 (Stack Overflow) 和 堆溢出 (Out of Memory) 有什么区别?

题目 8: free(p) 之后,p 的值会变成 NULL 吗?

题目 9: 结构体包含指针成员时,如何正确释放?

c 复制代码
struct Node { int *data; };
struct Node *p = malloc(sizeof(struct Node));
p->data = malloc(100);

题目 10: 为什么不推荐用 void *p = malloc(100); 然后直接 p[0]

题目 11: 什么是内存碎片?

题目 12: 编写一个函数 create_array(n),动态创建一个长度为 n 的 int 数组并初始化为 0-n。

题目 13: alloca 函数是在哪里分配内存?

题目 14: 为什么说 strcpy 到 malloc 的空间时特别容易出 Bug?

题目 15: 怎么检测内存泄漏?(面试常考)

六、 解析

题 1 解析
答案: 标准规定是"实现定义"。可能返回 NULL,也可能返回一个唯一的指针(可以被 free)。
详解:

尽量避免写 malloc(0)

题 2 解析
答案: 有。
详解:

p 指向了第一次申请的 10 字节。紧接着 p 又指向了新的 20 字节。那这前 10 字节的地址就丢失了,再也无法释放。

题 3 解析
答案: 是的。
详解:

如果 size 为 0,realloc 会释放旧内存并返回 NULL(虽然 C23 标准可能修改此行为,但老代码常利用此特性)。

题 4 解析
答案: 是的。
详解:

calloc 保证所有位都是 0。这对于整数来说就是 0,对指针来说就是 NULL,对浮点数通常是 0.0。

题 5 解析
答案: 内存泄漏风险。
详解:

如果 realloc 失败返回 NULL,赋给 arr 后,原来的内存地址就丢了,变成了无人认领的孤魂野鬼。

题 6 解析
答案: 可以,但这需要良好的文档说明。
详解:

谁申请谁释放是原则。如果函数里申请,必须明确告诉调用者:"这个指针的所有权交给你了,记得 free。"

题 7 解析
答案:

栈溢出:通常是因为递归过深或局部数组太大(如 int a[1000000])。

堆溢出:malloc 申请了超过物理内存限制的空间。

题 8 解析
答案: 不会。
详解:

free 只是告诉操作系统收回这块地。p 依然存着那个地址值(变成了悬空指针)。所以手动 p = NULL 很重要。

题 9 解析
答案: 先释放成员,再释放结构体。
详解:

c 复制代码
free(p->data); // 先释放里面
free(p);       // 再释放外面

顺序反了会导致 p->data 无法访问。

题 10 解析
答案: void* 不能进行指针算术或解引用。
详解:

必须强转为具体类型(如 int*)才能访问。

题 11 解析
答案: 堆内存中即使有总空闲空间,但都不是连续的,导致无法申请大块连续内存。

题 12 解析
答案:

c 复制代码
int* create_array(int n) {
    int *arr = malloc(n * sizeof(int));
    if (arr) {
        for(int i=0; i<n; i++) arr[i] = i;
    }
    return arr;
}

题 13 解析
答案: 栈上。
详解:

alloca 是非标准函数,它在栈上动态分配,函数结束自动释放。优点是快,缺点是容易爆栈。

题 14 解析
答案: 忘了算 \0
详解:

strlen("abc") 是 3。malloc(3) 放不下,必须 malloc(strlen(s) + 1)

题 15 解析
答案:

  1. 静态分析工具:Cppcheck。
  2. 动态分析工具:Valgrind (Linux 神器),AddressSanitizer (编译器自带 -fsanitize=address)。

日期:2025年2月13日

专栏:C语言

相关推荐
毕设源码-朱学姐2 小时前
【开题答辩全过程】以 基于Java的网上书店管理系统为例,包含答辩的问题和答案
java·开发语言
pp起床2 小时前
贪心算法 | part05
算法·贪心算法
Wyn_2 小时前
【心得】医疗设备 - Qt 工程师进阶指南
开发语言·qt·医疗·学习路线
毕设源码-郭学长2 小时前
【开题答辩全过程】以 基于Java的体育馆管理系统的设计与实现为例,包含答辩的问题和答案
java·开发语言
MediaTea2 小时前
Python:迭代器的应用场景
开发语言·python·算法
myron66882 小时前
基于STM32LXXX的模数转换芯片ADC(CS1237-SOP8)驱动C程序设计
c语言·stm32·嵌入式硬件
uesowys2 小时前
Apache Spark算法开发指导-Random forest regression
算法·spark
csbysj20202 小时前
CSS3 按钮:设计与实现的艺术
开发语言
代码无bug抓狂人2 小时前
C语言之合唱队形——动态规划
c语言·开发语言·动态规划