C 语言动态数组
更新时间:2026-08-26。本文是
languages/c/主题入门层第 53 篇。固定数组在编译时就定死大小:用户要存 10 个数你开了 5,存 100 个你开了 50——不是越界就是浪费。动态数组用 malloc 起步、realloc 长大,存多少随运行时数据走。它是 C 里最常用的容器,也是理解"为什么要手动管理内存"的绝佳教材。
本文要回答的问题
- 动态数组怎么实现"边用边长"?
- 扩容一次加多少合适?
- 封装成结构体后怎么用?
一、固定数组的痛
c
#define MAX 100
int scores[MAX]; // 编死 100 个
int count = 0;
count = read_all(scores); // 万一来了 200 个?要么越界写坏内存,要么多开一堆空位。运行前不知道数量的数据,就该上动态数组。
二、第一次实现:手动 malloc + realloc
c
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *arr = malloc(4 * sizeof(int)); // 先给 4 个
if (arr == NULL) return 1;
int len = 0, cap = 4;
for (int i = 0; i < 10; i++) {
if (len == cap) { // 满了就扩容
int *tmp = realloc(arr, cap * 2 * sizeof(int));
if (tmp == NULL) { // 失败:原数组还在
free(arr);
return 1;
}
arr = tmp;
cap *= 2; // 容量翻倍
}
arr[len++] = i * i;
}
for (int i = 0; i < len; i++)
printf("%d ", arr[i]);
printf("\n");
free(arr);
return 0;
}两个关键点:
- 满则扩:
len == cap时翻倍扩容; - 先临时变量再接:
realloc失败不能丢原指针(calloc 与 realloc 讲过)。
三、扩容策略:为什么翻倍
每次扩容都涉及"申请新块 + 拷贝旧数据 + 释放旧块",很贵。如果每次只加 1:
| 策略 | 插 n 个元素总代价 | 说明 |
|---|---|---|
| 每次 +1 | O(n²) | 每插一次就整体搬一次 |
| 翻倍 | O(n)(均摊) | 拷贝次数对数级 |
翻倍扩容是工程标准做法:前期"浪费"的少量空间,换来整体拷贝次数从 n 降到 log n,均摊下来每次插入 O(1)。
四、封装成结构体:Vector 的 C 版
把"指针 + 长度 + 容量"打包成一个结构体,谁用谁爽:
c
typedef struct {
int *data;
int len; // 有效元素个数
int cap; // 已分配容量
} IntVec;
IntVec vec_create(int init_cap) {
IntVec v = {NULL, 0, 0};
v.data = malloc(init_cap * sizeof(int));
if (v.data != NULL) v.cap = init_cap;
return v;
}
void vec_push(IntVec *v, int x) {
if (v->len == v->cap) {
int *tmp = realloc(v->data,
(v->cap ? v->cap * 2 : 4) * sizeof(int));
if (tmp == NULL) return; // 或报错
v->data = tmp;
v->cap = v->cap ? v->cap * 2 : 4;
}
v->data[v->len++] = x;
}
void vec_free(IntVec *v) {
free(v->data);
v->data = NULL;
v->len = v->cap = 0;
}c
IntVec v = vec_create(4);
vec_push(&v, 10);
vec_push(&v, 20);
for (int i = 0; i < v.len; i++)
printf("%d ", v.data[i]);
vec_free(&v);这个模式就是 C++ std::vector 的雏形。封装的收益:调用方不用管扩容细节,只管 push 和遍历。
五、动态数组 vs 固定数组
| 对比项 | 固定数组 | 动态数组 |
|---|---|---|
| 大小 | 编译期定死 | 运行时可变 |
| 内存位置 | 栈/静态区 | 堆 |
| 扩容 | 不行 | realloc |
| 生命周期 | 自动 | 手动 free |
| 性能 | 快、无分配 | 首次分配/扩容有开销 |
经验:数量已知且不大 → 固定数组;数量由输入/运行决定 → 动态数组;频繁插入删除的中间位置 → 该考虑链表了。
六、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 忘判 realloc 失败 | 丢指针、泄漏 | 临时变量 + 判空 |
| 扩容后忘更新 cap | 提前触发扩容 | len/cap 同步维护 |
| 用旧指针访问 | 悬垂 | 只用 v.data |
| push 忘判满 | 越界写 | len==cap 先扩 |
| 忘 vec_free | 泄漏 | 谁创建谁释放 |
七、与本站主线衔接
- realloc 细节与失败处理,见calloc 与 realloc;
- 更复杂的容器(链表/栈/队列/哈希表),见链表与二叉树实现;
- 性能对比与内存池优化,见内存池。
一句话总结
动态数组 = malloc 起步 + 满时翻倍 realloc + 用"指针/长度/容量"结构体封装;翻倍扩容让均摊插入 O(1),len==cap 判断 + 临时变量接 realloc 是防越界、防丢指针的命门——它就是 C 里的 std::vector,容器世界的第一课。