在C语言里,数组长度并不是运行时随便改的变量。用 int a[10] 这种形式定义的数组,其大小在编译阶段就写死在栈帧布局里,编译器生成访问指令时直接按偏移量算地址。想给这种数组“加长度”,语言层面没有提供任何语法支持。真正能动态变长的,是借助堆内存和指针模拟出来的连续存储空间。理解这一点,才能选对扩容方案。

为什么普通数组不能加长度
C语言标准规定,数组类型的长度必须是整型常量表达式(VLA除外,但VLA也不能事后改长度)。当你写 int buf[8]; 时,编译器在栈上预留固定字节,函数返回后整块回收。如果你试图把 buf 传给 realloc,那等于让系统去释放一块它从没用 malloc 分配过的地址,后果是未定义行为。
很多资料把“数组名退化成指针”挂在嘴边,但这不代表数组本身可伸缩。指针只保存首地址,背后那块内存是谁分配的、有多大,完全看你怎么申请。因此所谓“数组加长度”,实质是:放弃旧空间、拿到更大的新空间、把数据搬过去、更新指针。
方案一并用malloc与realloc
最直观的做法是一开始就用堆内存,而不是栈数组。先用 malloc 要一块初始空间,不够了调用 realloc 扩。realloc 若成功会返回新地址,原数据自动拷贝;失败返回 NULL,此时旧指针仍有效,别直接覆盖。
下面示例实现一个可增长的整型数组,每次满就翻倍:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr = (int *)malloc(sizeof(int) * 2);
int cap = 2, len = 0;
for (int i = 0; i < 10; i++) {
if (len == cap) {
cap *= 2;
int *tmp = (int *)realloc(arr, sizeof(int) * cap);
if (tmp == NULL) {
printf("扩容失败n");
free(arr);
return 1;
}
arr = tmp;
}
arr[len++] = i;
}
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
free(arr);
return 0;
}
这种写法好处是语义清晰、连续内存访问快;缺点是翻倍策略若不当会浪费内存,且频繁 realloc 有拷贝开销。务必检查返回值,并在程序结束或出错路径上 free,否则内存泄漏。
方案二结构体加柔性数组
柔性数组(flexible array member)是C99引入的特性:结构体最后一个成员写成 int data[]; 不写长度。它为“头部元数据+变长数据”提供单块分配的可能,比分开 malloc 两次更安全。
示例用一个结构体保存容量和数组本体,扩容时整体 realloc 结构体:
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int len;
int data[];
} Vec;
Vec *vec_create(int cap) {
Vec *v = (Vec *)malloc(sizeof(Vec) + sizeof(int) * cap);
v->len = 0;
return v;
}
Vec *vec_push(Vec *v, int val, int *cap) {
if (v->len == *cap) {
*cap *= 2;
v = (Vec *)realloc(v, sizeof(Vec) + sizeof(int) * (*cap));
}
v->data[v->len++] = val;
return v;
}
int main() {
int cap = 2;
Vec *v = vec_create(cap);
for (int i = 0; i < 6; i++) {
v = vec_push(v, i, &cap);
}
for (int i = 0; i < v->len; i++) {
printf("%d ", v->data[i]);
}
free(v);
return 0;
}
柔性数组让数据与头信息共存一块内存,缓存局部性更好,也不会出现“指针和缓冲区分开分配导致一方释放另一方悬空”的问题。但它仍受 realloc 拷贝成本限制,不适合超大量频繁伸缩。
方案三用链表绕开连续内存
如果业务根本不要求下标随机访问,可以用单向链表把节点串起来。每个节点单独 malloc,插入就是改指针,不存在整体搬移。这样“加长度”成本降到 O(1)。
下面是最简链表节点与追加逻辑:
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int val;
struct Node *next;
} Node;
void append(Node **head, int val) {
Node *n = (Node *)malloc(sizeof(Node));
n->val = val;
n->next = NULL;
if (*head == NULL) {
*head = n;
return;
}
Node *p = *head;
while (p->next) p = p->next;
p->next = n;
}
int main() {
Node *head = NULL;
for (int i = 0; i < 5; i++) {
append(&head, i);
}
for (Node *p = head; p; p = p->next) {
printf("%d ", p->val);
}
while (head) {
Node *t = head->next;
free(head);
head = t;
}
return 0;
}
链表完全规避了数组扩容的搬移痛点,但失去连续性,遍历缓存命中率低,且每个节点有额外指针开销。只在插入多、查询少时划算。
三种方案怎么选
归纳起来,若你需要真数组般的连续存储且容量变化可预估,malloc+realloc 最直接;想把元信息和数据绑死、减少管理负担,柔性数组更优雅;若访问模式本就不依赖下标,链表最省心。无论哪种,都请记住:栈数组不可加长,所有“加长度”的本质都是堆上重分配。
| 方案 | 连续性 | 扩容成本 | 适用场景 |
|---|---|---|---|
| malloc+realloc | 有 | 中(拷贝) | 通用动态数组 |
| 柔性数组 | 有 | 中(拷贝) | 结构化变长包 |
| 链表 | 无 | 低(O(1)) | 频繁插入 |
写C时把“数组”和“指针指向的堆块”区分开,就不会再问栈数组怎么加长度,而是自然过渡到正确的内存管理思路上。