8、动态数组的实现:用malloc创建动态数组、访问与修改元素
说实话,刚学C语言那会儿,我总觉得数组这东西有点死板。你想想看,写死一个 int arr[100],要是数据多了怎么办?少了又浪费。后来我明白了,真正的灵活,得靠动态内存分配。这一章,我们就来聊聊怎么用 malloc 造一个动态数组,以及怎么摆弄它。
8.1 为什么需要动态数组?
静态数组的大小在编译时就定死了。比如你写个学生成绩管理系统,一个班最多50人,你开个 int scores[50]。结果来了个60人的班,程序直接崩。反过来,如果只来了10个人,那40个位置就白白占着内存。
动态数组就不一样了。它的大小在运行时才决定,而且可以随时调整。说白了,就是「用多少,申请多少」。我在项目中遇到过好几次这种情况:一开始不知道数据量,只能先预估一个最大值,结果要么不够用,要么浪费。后来全改成动态数组,省心多了。
malloc 分配内存 + 指针操作 + 手动释放。
8.2 用 malloc 创建动态数组
malloc 的全称是 memory allocation,它从堆上申请一块连续的内存空间,返回一个 void* 指针。我们需要把它强制转换成目标类型的指针。
举个例子,我想创建一个能存10个整数的动态数组:
#include <stdlib.h>
int *arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL) {
// 处理内存分配失败
printf("内存分配失败!\n");
return -1;
}
这里有个细节:sizeof(int) 不能省。我见过有人直接写 malloc(40),觉得 int 就是4字节。但换个平台,int 可能是8字节,代码就废了。所以,永远用 sizeof,这是铁律。
malloc 返回的指针一定要检查是否为 NULL。内存分配失败虽然不常见,但一旦发生,程序直接崩溃。我曾经在一个嵌入式项目里吃过这个亏,堆空间太小,malloc 返回了 NULL,结果程序跑着跑着就挂了,查了好久才找到原因。
8.3 访问与修改元素
动态数组用起来和普通数组几乎一样。因为数组名本质上就是指针,而 arr 也是一个指针。你可以用下标 arr[i] 来访问第 i 个元素,也可以用指针偏移 *(arr + i)。
// 赋值
for (int i = 0; i < 10; i++) {
arr[i] = i * 2;
}
// 读取
for (int i = 0; i < 10; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
你看,语法上完全没区别。但底层不一样:静态数组的内存是在栈上,动态数组在堆上。栈空间很小(一般几MB),堆空间大得多(几个GB)。所以,大数组一定要用动态分配。
arr[i] 而不是 *(arr + i),因为可读性更好。但你要明白,两者完全等价。
8.4 动态数组的释放
用 malloc 申请的内存,必须用 free 释放。否则会造成内存泄漏。内存泄漏的后果是程序占用的内存越来越多,最终系统变慢甚至崩溃。
free(arr);
arr = NULL; // 好习惯:释放后置空
为什么要置空?因为 free 之后,指针仍然指向那块内存(野指针)。如果不置空,后面不小心再 free 一次,或者通过它访问数据,行为是未定义的。我曾经在维护一个老项目时,发现一个 bug 就是重复 free 导致的,排查了两天才定位到。从那以后,我每次 free 完都顺手置空。
8.5 动态数组的扩容
动态数组的好处是能长大。但 malloc 一次分配后,大小就固定了。要扩容,得用 realloc。
int *new_arr = (int*)realloc(arr, 20 * sizeof(int));
if (new_arr == NULL) {
// 扩容失败,原数组仍然有效
printf("扩容失败!\n");
} else {
arr = new_arr;
}
realloc 会尝试在原内存后面扩展,如果后面空间不够,它会重新找一块更大的内存,把原数据拷贝过去,然后释放原内存。所以,一定要用新指针接收返回值,不要直接 arr = realloc(arr, ...),因为如果 realloc 失败,原内存还在,但 arr 变成 NULL 了,你就丢了原数据。
8.6 知识结构图
下面这张图总结了动态数组的核心流程:
8.7 一个完整的例子
把上面这些串起来,写一个完整的动态数组程序:
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("请输入数组大小: ");
scanf("%d", &n);
// 1. 分配
int *arr = (int*)malloc(n * sizeof(int));
if (arr == NULL) {
printf("内存分配失败!\n");
return 1;
}
// 2. 赋值
for (int i = 0; i < n; i++) {
arr[i] = i * 10;
}
// 3. 打印
printf("数组内容: ");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 4. 扩容(演示)
int new_size = n * 2;
int *new_arr = (int*)realloc(arr, new_size * sizeof(int));
if (new_arr == NULL) {
printf("扩容失败,使用原数组\n");
} else {
arr = new_arr;
for (int i = n; i < new_size; i++) {
arr[i] = i * 10;
}
printf("扩容后数组内容: ");
for (int i = 0; i < new_size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 5. 释放
free(arr);
arr = NULL;
return 0;
}
这个例子涵盖了动态数组的完整生命周期:分配、使用、扩容、释放。你可以直接编译运行试试。
8.8 常见陷阱与避坑
- 忘记释放:程序跑完内存泄漏,长时间运行的程序(如服务器)会越来越慢。我曾经维护过一个后台服务,运行一周后内存占用飙到几个G,一查全是 malloc 没 free。
- 越界访问:动态数组不检查边界,你写
arr[100]但只分配了10个,不会报错,但可能覆盖其他数据。这种 bug 极难排查。 - 重复释放:对同一个指针 free 两次,行为未定义。释放后置空可以避免。
- realloc 使用不当:直接用
arr = realloc(arr, ...),失败后原指针丢失。
嗯,动态数组其实不难,但细节很多。你只要记住:分配、检查、使用、释放,这四个步骤缺一不可。多写几次,自然就熟练了。