一、回顾与本篇目标
上一篇你学会了结构体——把不同类型的数据打包成一个整体。你还学会了用结构体数组来存储多个学生的信息。
但到目前为止,我们所有的变量和数组都是在编译时就确定大小的。比如 int arr[100],这个 100 是你写代码时就定好的。如果实际只需要存 10 个数据,剩下的 90 个空间就浪费了。如果实际需要存 200 个数据,数组又不够用。
真正灵活的程序需要在运行时根据需要动态地申请和释放内存。比如根据用户输入的学生人数来创建数组,或者在程序运行过程中随时增加数据。这就是动态内存分配要解决的问题。
这一篇是 C 语言中最容易出错的领域,也是区分 C 语言和 JavaScript/Python 最根本的差异之一。在 JS 和 Python 中,你几乎从来不需要关心内存怎么分配、什么时候释放——垃圾回收器帮你搞定一切。在 C 语言中,你申请的内存,你必须自己释放。
本篇的目标:
- 理解栈和堆的区别
- 学会用
malloc申请堆内存 - 学会用
free释放内存 - 了解
calloc和realloc的用法 - 理解内存泄漏和悬空指针的危险
- 学会动态创建结构体和结构体数组
二、栈和堆:两种不同的内存区域
在理解动态内存分配之前,必须先理解 C 语言程序使用的两种内存区域:栈和堆。
栈:自动管理的内存
你之前声明的所有局部变量和数组,都是分配在栈上的:
void some_function() {
int age = 28; // 在栈上分配 4 字节
char name[50]; // 在栈上分配 50 字节
double scores[100]; // 在栈上分配 800 字节
}
// 函数结束时,这些栈内存自动释放
栈的特点:
- 自动管理:变量离开作用域(函数结束、代码块结束)时,栈内存自动释放。你不需要做任何事。
- 速度快:分配和释放只需要移动栈指针,开销极小。
- 空间有限:栈的大小通常只有几 MB(Windows 默认 1MB,Linux 默认 8MB)。如果你在栈上分配了一个超大的数组(比如
int huge[1000000]),程序会崩溃——这叫栈溢出。 - 大小在编译时确定:数组的大小必须是编译时就能确定的常量。
堆:手动管理的内存
堆是程序中一块巨大的内存区域,由程序员手动申请、手动释放:
// 在堆上申请一块内存
int *p = malloc(100 * sizeof(int)); // 申请 100 个 int 的空间
// ... 使用这块内存 ...
// 用完后必须手动释放
free(p);
堆的特点:
- 手动管理:你需要显式地申请(
malloc)和释放(free)。忘了释放会导致内存泄漏。 - 速度较慢:分配和释放需要操作系统介入,比栈慢。
- 空间巨大:堆可以使用的内存远大于栈(受限于系统的物理内存和虚拟内存)。
- 大小在运行时确定:可以根据用户的输入动态决定申请多少内存。
| 特性 | 栈 | 堆 |
|---|---|---|
| 管理方式 | 自动(离开作用域释放) | 手动(malloc/free) |
| 分配速度 | 极快 | 较慢 |
| 空间大小 | 有限(几 MB) | 巨大(几 GB) |
| 生命周期 | 离开作用域即结束 | 由程序员控制 |
| 典型用途 | 局部变量、小数组 | 大数据、动态大小的数据 |
| 对应的 JS/Python | 基本类型(栈) | 对象/数组(堆,但自动回收) |
在 JavaScript 和 Python 中,对象和数组也是分配在堆上的,但垃圾回收器会自动释放不再使用的内存。C 语言把这个责任完全交给了你。
三、malloc:申请内存
malloc 是 C 标准库中最常用的内存分配函数。它的全称是 memory allocation。
基本语法
#include <stdlib.h>
void *malloc(size_t size);
- 参数:要申请的字节数。
- 返回值:一个
void*类型的指针,指向申请到的内存块的首地址。如果申请失败(比如内存不够了),返回NULL。 void*是“通用指针”类型,可以赋值给任何类型的指针变量。
申请一个整数
#include <stdio.h>
#include <stdlib.h>
int main() {
// 在堆上申请一个 int 的空间
int *p = malloc(sizeof(int));
// 检查申请是否成功
if (p == NULL) {
printf("内存分配失败!\n");
return 1;
}
// 像使用普通变量一样使用这块内存
*p = 42;
printf("*p = %d\n", *p); // 42
// 用完后释放
free(p);
return 0;
}
逐行解释:
malloc(sizeof(int)):sizeof(int)是 4(在大多数系统上),所以这行代码在堆上申请了 4 字节的空间。int *p = ...:malloc返回的void*自动转换成int*,赋给p。现在p指向堆上的一块 4 字节内存。if (p == NULL):每次调用 malloc 后都必须检查返回值是否为 NULL。 如果系统内存不足,malloc会返回NULL。不检查就直接使用会导致程序崩溃。*p = 42:通过解引用操作堆上的内存,和操作普通变量一样。free(p):释放malloc申请的内存。
申请一个数组
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("请输入数组大小:");
scanf("%d", &n);
// 在堆上申请 n 个 int 的空间
int *arr = malloc(n * sizeof(int));
if (arr == NULL) {
printf("内存分配失败!\n");
return 1;
}
// 像使用普通数组一样使用
for (int i = 0; i < n; i++) {
arr[i] = i * 10; // arr[i] 等价于 *(arr + i)
}
printf("数组内容:");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 释放整个数组
free(arr);
return 0;
}
关键点:malloc(n * sizeof(int)) 申请了 n × 4 字节的空间,这相当于在堆上创建了一个长度为 n 的 int 数组。数组的大小 n 是运行时由用户输入的,不是编译时写死的常量。 这就是动态内存分配的核心价值。
和普通数组的区别:
int arr[100]:在栈上分配,大小必须编译时确定,函数结束自动释放。int *arr = malloc(n * sizeof(int)):在堆上分配,大小可以运行时确定,需要手动free(arr)。
四、free:释放内存
free 用来释放 malloc(或 calloc、realloc)申请的内存。
int *p = malloc(100 * sizeof(int));
// ... 使用 p ...
free(p); // 释放内存
p = NULL; // 好习惯:释放后把指针设为 NULL,防止误用
free 的三个重要规则:
- 只能释放 malloc/calloc/realloc 返回的指针。不能
free栈上的变量。 - 不能重复释放。同一块内存
free两次会导致未定义行为(通常程序崩溃)。 - 释放后不要再使用。使用已释放的内存是悬空指针问题。
错误的示例
// 错误 1:释放栈上的变量
int x = 10;
// free(&x); // 错误!x 在栈上,不能 free
// 错误 2:重复释放
int *p = malloc(10);
free(p);
// free(p); // 错误!p 已经被释放了
// 错误 3:使用已释放的内存(悬空指针)
int *q = malloc(sizeof(int));
*q = 42;
free(q);
// printf("%d\n", *q); // 未定义行为!q 指向的内存已经被回收
五、内存泄漏
如果使用 malloc 申请了内存,但忘记了调用 free,这块内存就永远无法被回收——这就叫内存泄漏。
// 这是一个有内存泄漏的函数
void leaky_function() {
int *p = malloc(1000 * sizeof(int)); // 申请内存
// 使用 p ...
// 忘记 free(p)!
return; // 函数结束,p 变量消失,但堆上的 4000 字节没有被释放
}
// 每次调用这个函数,就会泄漏 4000 字节
// 如果这个函数被反复调用,程序占用的内存会越来越多
在 JavaScript 和 Python 中,类似的情况不会造成泄漏——垃圾回收器会检测到 p 指向的内存已经没有变量引用了,自动回收它。在 C 语言中,你忘了 free,内存就真的永远丢了。
内存泄漏的后果:程序运行时间越长,占用的内存越大,最终可能耗尽系统内存,导致系统变慢甚至程序崩溃。
六、calloc 和 realloc
除了 malloc,标准库还提供了两个相关的内存分配函数。
calloc:分配并初始化为零
// calloc(元素个数, 每个元素的大小)
int *arr = calloc(10, sizeof(int));
// 等价于 malloc(10 * sizeof(int)),但所有元素被初始化为 0
calloc 和 malloc 的区别:
malloc只分配内存,不初始化(里面是垃圾值)。calloc分配内存并把所有字节初始化为 0。代价是稍微慢一点。- 当你需要一块全零的内存时,用
calloc更安全、更方便。
realloc:调整已分配内存的大小
realloc 可以改变之前用 malloc 或 calloc 分配的内存块的大小:
// 先申请 5 个 int
int *arr = malloc(5 * sizeof(int));
for (int i = 0; i < 5; i++) {
arr[i] = i * 10;
}
// 现在需要扩大到 10 个 int
arr = realloc(arr, 10 * sizeof(int));
// 前 5 个元素的值保持不变,后 5 个元素是未初始化的
// 也可以缩小
arr = realloc(arr, 3 * sizeof(int));
// 只保留前 3 个元素
free(arr);
realloc 的注意事项:
- 可能移动数据:如果原内存后面没有足够的空间扩展,
realloc会找一块新的更大的内存,把旧数据复制过去,然后释放旧内存。所以realloc返回的指针可能和原来的指针不同。 - 不要用原来的指针:
realloc之后,必须使用返回的新指针,原来的指针可能已经失效。 - realloc(NULL, size) 等价于
malloc(size)。 - realloc(ptr, 0) 等价于
free(ptr),返回NULL。
七、动态分配结构体
结合上一篇学的结构体,可以在堆上动态创建结构体:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[50];
int age;
float score;
} Student;
int main() {
// 在堆上创建一个学生
Student *s = malloc(sizeof(Student));
if (s == NULL) {
printf("内存分配失败!\n");
return 1;
}
// 通过指针设置成员
strcpy(s->name, "张三");
s->age = 20;
s->score = 92.5;
// 通过指针读取成员
printf("姓名:%s\n", s->name);
printf("年龄:%d\n", s->age);
printf("成绩:%.1f\n", s->score);
free(s);
return 0;
}
动态结构体数组:
int n;
printf("请输入学生人数:");
scanf("%d", &n);
// 在堆上创建 n 个学生的数组
Student *students = malloc(n * sizeof(Student));
if (students == NULL) {
printf("内存分配失败!\n");
return 1;
}
// 使用这个动态数组
for (int i = 0; i < n; i++) {
printf("学生 %d 姓名:", i + 1);
scanf("%s", students[i].name); // name 是数组,不需要 &
printf("年龄:");
scanf("%d", &students[i].age);
printf("成绩:");
scanf("%f", &students[i].score);
}
// 输出所有学生
for (int i = 0; i < n; i++) {
printf("%s,%d 岁,%.1f 分\n",
students[i].name, students[i].age, students[i].score);
}
free(students);
八、综合演示:可动态增长的学生列表
下面这个示例展示了动态内存分配的完整用法——一个可以根据用户输入动态添加学生的程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[50];
int age;
float score;
} Student;
int main() {
int capacity = 2; // 初始容量
int count = 0; // 当前学生数量
Student *students = malloc(capacity * sizeof(Student));
if (students == NULL) {
printf("内存分配失败!\n");
return 1;
}
printf("===== 学生信息录入 =====\n");
printf("输入 'quit' 作为姓名来结束录入\n");
while (1) {
char input[50];
printf("\n学生 %d 姓名:", count + 1);
scanf("%s", input);
if (strcmp(input, "quit") == 0) {
break;
}
// 如果数组满了,用 realloc 扩容
if (count >= capacity) {
capacity *= 2; // 容量翻倍
Student *temp = realloc(students, capacity * sizeof(Student));
if (temp == NULL) {
printf("内存扩展失败!\n");
free(students);
return 1;
}
students = temp;
printf("(数组已扩容至 %d)\n", capacity);
}
// 录入学生信息
strcpy(students[count].name, input);
printf("年龄:");
scanf("%d", &students[count].age);
printf("成绩:");
scanf("%f", &students[count].score);
count++;
}
// 输出所有学生
printf("\n===== 学生列表(共 %d 人)=====\n", count);
float total = 0;
for (int i = 0; i < count; i++) {
printf("%d. %s,%d 岁,%.1f 分\n",
i + 1, students[i].name, students[i].age, students[i].score);
total += students[i].score;
}
printf("平均分:%.1f\n", total / count);
free(students);
return 0;
}
代码解析:
- 初始容量和动态扩容:程序从
capacity = 2开始,当count >= capacity时,用realloc把容量翻倍。这模拟了 Python 列表和 JavaScript 数组的底层扩容策略。 realloc的安全用法:先用一个临时变量temp接收realloc的返回值。如果成功,再赋给students。如果realloc失败,原来的内存块仍然有效,可以安全地free(students)退出。如果直接写students = realloc(...),失败时原指针就丢失了。- 用户输入
"quit"结束录入,程序输出汇总信息后释放内存。
九、动态内存分配的常见错误
| 错误 | 示例 | 后果 |
|---|---|---|
| 忘记 free | int *p = malloc(100); return; |
内存泄漏 |
| 重复 free | free(p); free(p); |
程序崩溃或未定义行为 |
| 使用已释放的内存 | free(p); *p = 10; |
悬空指针,未定义行为 |
| 忘记检查 NULL | int *p = malloc(...); *p = 10; |
如果 malloc 失败,程序崩溃 |
| 越界访问 | arr[100] = 10;(只申请了 50 个) |
破坏其他内存数据 |
最佳实践总结:
- 每次 malloc 后立即检查返回值是否为 NULL。
- 每次 free 后立即把指针设为 NULL。
- 谁申请,谁释放。 避免在一个函数中申请、在另一个函数中释放,这样很难追踪。
- 用 valgrind 等工具检测内存泄漏。 在 Linux/Mac 上,用
valgrind ./your_program可以检查是否有内存泄漏。
十、本篇动手练习
练习 1:动态数组操作
新建 practice9-1.c,让用户输入一个整数 n,用 malloc 创建大小为 n 的 int 数组,填充 1 到 n,计算总和和平均值,最后 free。
练习 2:字符串动态复制
新建 practice9-2.c,写一个函数 char* my_strdup(const char *src),用 malloc 申请刚好够用的内存,把 src 复制过去,返回新字符串的指针。在 main 中测试并 free。
练习 3:动态图书管理
新建 practice9-3.c,用动态内存分配实现图书管理。定义 Book 结构体,用户可以不断添加书籍,数组自动扩容。用户输入 "quit" 时停止录入,输出所有书籍信息后释放内存。
练习 4:检测内存泄漏
新建 practice9-4.c,故意写一个有内存泄漏的程序(申请内存但不释放)。如果你的系统有 valgrind,用 valgrind --leak-check=full ./practice9-4 运行,观察 valgrind 如何报告内存泄漏。
十一、本篇小结
这一篇你学会了 C 语言中最重要的内存管理机制——动态内存分配:
- 栈和堆的区别:栈自动管理、速度快、空间小、编译时确定大小。堆手动管理、速度较慢、空间大、运行时确定大小。
malloc(size):在堆上申请指定字节数的内存,返回void*指针。必须检查返回值是否为NULL。free(ptr):释放malloc/calloc/realloc申请的内存。释放后把指针设为NULL是良好习惯。calloc(n, size):申请 n 个元素的空间,并全部初始化为 0。realloc(ptr, new_size):调整已分配内存块的大小。可能移动数据,必须使用返回的新指针。- 内存泄漏:申请了内存但忘了释放。程序运行越久占内存越多,最终耗尽系统资源。
- 悬空指针:指向已释放内存的指针。使用悬空指针会导致未定义行为。
- 动态结构体:
malloc(sizeof(Student))在堆上创建结构体,->访问成员。
动态内存分配是 C 语言和高级语言(JS/Python)之间最大的差异之一。掌握它之后,你就能写出真正灵活、可应对不确定数据量的程序。下一篇,我们学习 C 语言中另一个重要概念——文件操作,让程序能够读写硬盘上的文件。
下一篇预告
下一篇——《文件操作——让程序读写硬盘上的数据》:用 fopen 打开文件、fprintf/fscanf 读写文件、fgets/fputs 处理文本、二进制文件的读写。有了文件操作,你的程序就能实现数据持久化——把数据存到硬盘上,下次运行时还能读出来。
C/C++ 零基础入门,每周更新。











暂无评论内容