前面的文章里,我们用的数组、字符串、结构体,都有一个共同特点:大小在编译时就确定了。比如 int arr[100];,这 100 个元素的数组在程序运行前就已经被分配好了空间,不管你实际用了 5 个还是 80 个,内存都占那么多。
但现实中的程序往往不知道运行时会有多少数据——用户可能输入 3 个成绩,也可能输入 300 个;日志文件可能一行,也可能十万行。如果总是“多预留一些”,内存会大量浪费;“预留少了”又会溢出。这就需要一种能在程序运行时按需申请内存的机制。
C 语言把这把钥匙交给了你:动态内存分配。它强大,但也危险——申请了要记得还,还了就别再用。今天我们就来掌握这套“手动内存管理”的艺术。
一、程序的内存布局:栈、堆与静态区
在正式学 malloc 之前,先搞清楚程序运行时内存的几个主要区域,这对理解“变量住哪里”很有帮助。
一个典型的 C 程序,其内存布局(从高地址到低地址)大致是:
| 内存区域 | 存放内容 | 特点 |
|---|---|---|
| 栈(Stack) | 局部变量、函数参数、返回地址 | 自动管理,函数调用时分配,返回时释放;空间有限(通常几 MB) |
| 堆(Heap) | 动态分配的内存 | 手动管理,程序通过 malloc 申请,用 free 释放;空间较大 |
| 静态数据区 | 全局变量、static 局部变量、字符串字面量 | 整个程序运行期存在 |
| 代码区 | 程序的机器指令 | 只读 |
以前我们写的局部变量都在栈上,来去匆匆,不需要你操心。但动态分配的内存来自堆——这块区域的大小只受系统物理内存和虚拟内存的限制,你可以按需索取。代价是:你必须自己管理它的生命周期。忘了归还,它就一直占着,这就是内存泄漏。
二、malloc:申请一块内存
malloc(memory allocation)是最基础的动态内存分配函数,定义在 <stdlib.h> 里:
void *malloc(size_t size);
- 参数
size:要申请的字节数。 - 返回值:一个
void*指针,指向申请到的内存块的首地址;如果分配失败(比如内存不足),返回NULL。
使用流程:
#include <stdlib.h>
int *p = (int*)malloc(sizeof(int)); // 申请一块 int 大小的内存
if (p == NULL) {
// 处理分配失败
printf("内存分配失败!\n");
return 1;
}
*p = 42; // 正常使用
free(p); // 用完后释放
几个要点:
- 为什么用
sizeof? 不同类型大小不同,用sizeof让编译器自动计算,不依赖手动猜测。 - 为什么强制类型转换?
malloc返回void*,在 C 里可以隐式转换成任意指针类型,强转主要是为了代码清晰,以及兼容 C++(如果以后被 C++ 编译器使用的话)。C 社区有争议,但写了更清晰。 - 为什么检查
NULL? 内存请求可能失败(尤其是申请大块内存时),不检查就使用会因空指针解引用而崩溃。
三、动态分配数组
malloc 最常见的用途是创建运行时才能确定大小的数组:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n;
printf("请输入学生数量:");
scanf("%d", &n);
int *scores = (int*)malloc(n * sizeof(int));
if (scores == NULL) {
printf("内存分配失败!\n");
return 1;
}
// 像普通数组一样使用
for (int i = 0; i < n; i++) {
scores[i] = i * 10; // 赋值
}
for (int i = 0; i < n; i++) {
printf("%d ", scores[i]);
}
printf("\n");
free(scores); // 释放
return 0;
}
注意 n * sizeof(int) 计算总字节数。释放后,指针 scores 仍然存着那个地址,但内存已经不属于你了,再访问它会导致未定义行为。
四、calloc:申请并自动清零
calloc 也是申请内存,但它会把分配到的所有字节初始化为 0,并且参数写法不同:
void *calloc(size_t nmemb, size_t size);
nmemb:元素个数size:每个元素的大小
int *arr = (int*)calloc(10, sizeof(int)); // 10 个 int,全部为 0
malloc 分配到的内存里是垃圾值,如果忘了初始化就直接读,很容易出问题。calloc 自动清零,代价是稍微慢一点(因为要擦写内存)。给结构体数组、或者需要初始化为 0 的场景,推荐用 calloc。
五、realloc:给已分配的内存“扩容”
有时候之前申请的 100 个位置用满了,想扩容到 200 个。realloc 可以调整动态内存的大小:
void *realloc(void *ptr, size_t new_size);
ptr:之前通过malloc/calloc/realloc返回的指针。new_size:新的大小(字节)。- 返回值:新内存块的地址(可能和原来一样,也可能不一样)。
重要:realloc 可能搬移数据!
如果原地址后面没有足够的连续空闲空间,realloc 会:
- 在别处找一块够大的新区域。
- 把旧数据复制过去。
- 自动释放旧区域。
- 返回新地址。
所以,永远用返回值更新原指针,而且先用临时变量接住,防止失败时丢失原指针:
int *arr = (int*)malloc(5 * sizeof(int));
// ... 使用 arr,满了
int *tmp = (int*)realloc(arr, 10 * sizeof(int));
if (tmp == NULL) {
// 扩容失败,但 arr 仍然有效!可以继续用原来的
printf("扩容失败\n");
} else {
arr = tmp; // 用新地址
}
如果 realloc 第一个参数是 NULL,它的行为等同于 malloc(new_size)。
六、free:归还内存
free 把通过 malloc、calloc、realloc 申请的内存归还给堆,以供后续分配。
free(ptr);
ptr = NULL; // 好习惯:释放后置空,防止误用
规则:
- 只能
free动态分配的内存,栈上的变量绝对不能free。 - 不能重复
free同一块内存(free(NULL)是安全的,不会报错)。 free后指针变成“悬垂指针”,再访问就是未定义行为。
七、常见动态内存错误(避坑手册)
1. 忘记调用 free——内存泄漏
void func(void) {
int *p = (int*)malloc(100 * sizeof(int));
// 使用 p,但函数结束前没有 free(p)
} // p 本身消失,但内存还在堆里没人回收
每次 func 被调用,就泄露 400 字节。程序长时间运行,内存被慢慢蚕食。对于长期运行的程序(服务器、数据库),内存泄漏是致命的。
2. 使用已释放的内存(悬垂指针)
int *p = (int*)malloc(sizeof(int));
*p = 10;
free(p);
*p = 20; // 未定义行为!p 指向的内存已经不属于你
释放后把指针置为 NULL,至少在解引用 NULL 时会立即崩溃(更容易定位)。
3. 多次 free 同一块内存
int *p = (int*)malloc(sizeof(int));
free(p);
free(p); // 未定义行为!
同样,free 后置 NULL 可以避免这种情况,因为 free(NULL) 是安全的。
4. 越界写入动态数组
int *arr = (int*)malloc(5 * sizeof(int));
arr[5] = 100; // 越界!只分配了 0-4 共 5 个元素
和普通数组一样,动态分配的数组也不检查越界,但后果可能更隐蔽——可能覆盖了堆的管理结构,导致后续 free 时崩溃。
5. 使用未初始化的动态内存(malloc 后直接读)
int *p = (int*)malloc(sizeof(int));
printf("%d\n", *p); // 垃圾值
要么用 calloc,要么 malloc 后立即赋值。
八、实战:简易动态数组
我们把动态内存知识整合成一个可用的“动态数组”模块——能自动扩容的整数数组。
darray.h
#ifndef DARRAY_H
#define DARRAY_H
typedef struct {
int *data; // 指向堆上的数组
int capacity; // 当前容量
int size; // 实际存放的元素个数
} DArray;
DArray* darray_create(int initial_capacity);
void darray_append(DArray *da, int value);
int darray_get(DArray *da, int index);
void darray_free(DArray *da);
#endif
darray.c
#include <stdlib.h>
#include "darray.h"
DArray* darray_create(int initial_capacity) {
DArray *da = (DArray*)malloc(sizeof(DArray));
if (da == NULL) return NULL;
da->data = (int*)malloc(initial_capacity * sizeof(int));
if (da->data == NULL) {
free(da);
return NULL;
}
da->capacity = initial_capacity;
da->size = 0;
return da;
}
void darray_append(DArray *da, int value) {
if (da->size >= da->capacity) {
// 扩容为原来的 2 倍
int new_cap = da->capacity * 2;
int *tmp = (int*)realloc(da->data, new_cap * sizeof(int));
if (tmp == NULL) return; // 扩容失败,保持现状
da->data = tmp;
da->capacity = new_cap;
}
da->data[da->size] = value;
da->size++;
}
int darray_get(DArray *da, int index) {
// 注意:实际项目应加边界检查
return da->data[index];
}
void darray_free(DArray *da) {
free(da->data); // 先释放内部数组
free(da); // 再释放结构体本身
}
main.c
#include <stdio.h>
#include "darray.h"
int main(void) {
DArray *da = darray_create(4);
if (da == NULL) {
printf("创建动态数组失败\n");
return 1;
}
for (int i = 0; i < 20; i++) {
darray_append(da, i * i);
}
printf("动态数组内容: ");
for (int i = 0; i < da->size; i++) {
printf("%d ", darray_get(da, i));
}
printf("\n容量: %d, 大小: %d\n", da->capacity, da->size);
darray_free(da);
return 0;
}
输出:
动态数组内容: 0 1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361
容量: 32, 大小: 20
初始容量为 4,插入到第 5 个元素时自动扩容为 8,再满扩到 16、32。这个“动态数组”就是很多高级语言中 vector 或 ArrayList 的底层原理。
九、小结
动态内存分配让程序摆脱了编译时固定大小的桎梏。你今天学到了:
- 堆与栈的本质区别:栈自动管理,堆手动管理。
malloc、calloc、realloc的使用场景和差异。free的规则:配对使用,释放后置 NULL。- 五大常见动态内存错误及其防范方法。
- 一个完整的动态数组实现,体感“自动扩容”的原理。
现在你已经有了指针的深厚功底,又能自由操控内存——这几乎是 C 语言最具威力的组合。下一步,我们将用这些知识构建真正灵活的数据结构:链表。数组再能扩容,也逃不开“插入中间要移动大量元素”的宿命。而链表能让你在 O(1) 时间内完成插入和删除,它是动态数据结构的真正起点。
课后小练习
- 写一个程序,用
malloc创建一个包含 N 个double的数组(N 由用户输入),然后读取 N 个数存入数组,计算平均值并输出。最后free。 - 用
calloc实现和上题同样的功能,然后解释calloc和malloc的区别。 - 下面的代码有 bug,找出并修复:
int* create_array(int size) { int arr[size]; return arr; } int main(void) { int *p = create_array(10); p[0] = 5; free(p); return 0; } - (小挑战)在“简易动态数组”的基础上,增加一个
darray_remove_last函数,删除最后一个元素(size 减 1 即可,不需缩容)。再加一个darray_insert函数,在指定索引处插入一个元素(后面的元素要后移)。如果空间不够,自动扩容。
我们下期见!

850

被折叠的 条评论
为什么被折叠?



