前面咱们把指针的基础、指针和数组、指针和字符串、函数指针、返回指针的函数全聊完了。今天终于到了指针的“终极实战”——动态内存分配。
说实话,前面学的那些指针知识,如果不用在动态内存上,多少有点“纸上谈兵”的感觉。但你把今天这节课搞明白了,你就会真正体会到指针为什么是C语言的灵魂。
咱们先从最根本的问题说起。
先想想之前咱们是怎么存数据的
你之前写程序,要存10个整数,怎么写?
int a[10];
要存100个学生信息,怎么写?
Student students[100];
这种叫静态内存分配。数组的大小在写代码的时候就定死了,编译的时候编译器就知道要给你留多大地方。
但静态分配有个要命的问题:你不知道到底需要多少。
比如你写个学生管理系统,你定义Student students[100],结果学校今年扩招,来了150个学生。完了,数组装不下,程序直接崩。你要是定义[1000],但实际只有50个学生,那950个位置就白白浪费了。
更极端的情况:你写个程序让用户输入一串数字,用户输多少你事先根本不知道。你数组该开多大?开小了不够用,开大了浪费。
这就好比你搬家,不知道有多少东西,却得先决定买多大的箱子。买小了装不下,买大了浪费钱。
那能不能等程序跑起来之后,根据实际需要再决定分配多少内存?
能。这就是动态内存分配——程序运行的时候,想用多少就申请多少。
动态内存分配靠什么?四个函数
C语言提供了一套函数,专门用来做动态内存管理。都在stdlib.h头文件里,总共四个:
-
malloc——申请一块内存 -
calloc——申请一块内存,并自动清零 -
realloc——调整已经申请的内存大小 -
free——释放申请的内存
咱们挨个说,重点在前两个。
malloc——最常用的申请内存函数
malloc是“memory allocation”的缩写。用起来特别简单:
void *malloc(size_t size);
你给它一个数字,告诉它“我要多少字节”,它就给你返回一块内存的首地址。如果申请失败(比如内存不够了),返回NULL。
比如你要申请能存5个int的内存:
int *p = (int*)malloc(5 * sizeof(int));
拆开看:
-
sizeof(int)是每个int占多少字节,通常是4。 -
5 * sizeof(int)就是总共要多少字节,也就是20。 -
malloc(20)向系统申请20个字节。 -
(int*)把返回的void*强转成int*,因为我们要存的是int。 -
p接住这个地址。
现在p就指向了一块可以存放5个int的内存,你可以像用数组一样用它:
p[0] = 10; p[1] = 20; p[2] = 30; p[3] = 40; p[4] = 50;
你看,p就是个指针,但你完全可以用p[0]、p[1]这样下标访问。因为指针和数组本来就是互通的。
完整例子:
#include <stdio.h> #include <stdlib.h> int main() { int n; printf("你要存几个数?"); scanf("%d", &n); // 根据用户输入的数量来申请内存 int *arr = (int*)malloc(n * sizeof(int)); // 检查是否申请成功 if (arr == NULL) { printf("内存申请失败\n"); return 1; } // 用这块内存 printf("请输入%d个整数:\n", n); for (int i = 0; i < n; i++) { scanf("%d", &arr[i]); } printf("你输入的数是:"); for (int i = 0; i < n; i++) { printf("%d ", arr[i]); } printf("\n"); // 用完释放 free(arr); return 0; }
这个程序就能根据用户输入的数量动态分配内存。用户输入5,就申请5个int;输入100,就申请100个int。这才是真正的“按需分配”。
calloc——申请内存并自动清零
calloc跟malloc差不多,但它会把申请到的内存全部清零。参数也稍微不同——它要两个参数:元素的个数和每个元素的大小。
void *calloc(size_t num, size_t size);
比如要申请5个int并清零:
int *p = (int*)calloc(5, sizeof(int));
等价于:
int *p = (int*)malloc(5 * sizeof(int)); for (int i = 0; i < 5; i++) { p[i] = 0; }
但calloc是在系统层面直接给你清零的,效率比你自己循环清高得多。需要初始值为0的场景,就用calloc。
realloc——调整已申请内存的大小
这个函数特别实用。你一开始申请了5个int,后来发现不够用,想扩成10个,怎么办?用realloc。
void *realloc(void *ptr, size_t new_size);
ptr是你原来申请的内存地址,new_size是新的总大小。
int *p = (int*)malloc(5 * sizeof(int)); // ... 用了之后发现不够 p = (int*)realloc(p, 10 * sizeof(int));
realloc会尽量在原地扩容。如果原地空间不够,它会找一块新地方,把原来的数据复制过去,然后把旧地方释放掉。
注意:realloc之后,原来的指针可能失效了,因为可能搬到新地址去了。所以你必须用返回值更新你的指针:
p = (int*)realloc(p, 10 * sizeof(int)); // 正确写法
不能写:
realloc(p, 10 * sizeof(int)); // 错误!p可能变成野指针
free——用完一定要还
动态申请的内存是从“堆”上拿的。堆是整个程序共享的资源。你申请了不还,这块内存就一直被占着,别的程序也用不了。这叫内存泄漏。
int *p = (int*)malloc(100 * sizeof(int)); // ... 用完了 free(p); // 还给系统 p = NULL; // 好习惯:释放后把指针置空,防止野指针
free之后,那块内存就不属于你了,你不应该再去访问它。但指针p本身还存着那个地址,这叫“悬空指针”。访问它会出问题。所以养成习惯:free之后,把指针置为NULL。
一个完整的例子:动态数组
咱们把上面几个函数串起来,写一个“动态数组”——一开始容量小,不够了就自动扩容。
#include <stdio.h> #include <stdlib.h> int main() { int capacity = 2; // 初始容量 int size = 0; // 当前元素个数 int *arr = (int*)malloc(capacity * sizeof(int)); int input; printf("请输入整数(输入-1结束):\n"); scanf("%d", &input); while (input != -1) { // 如果数组满了,扩容为原来的2倍 if (size == capacity) { capacity *= 2; arr = (int*)realloc(arr, capacity * sizeof(int)); printf("(自动扩容到%d)\n", capacity); } arr[size] = input; size++; scanf("%d", &input); } printf("你输入了%d个数:", size); for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } printf("\n"); free(arr); return 0; }
这个例子完美展示了动态内存的威力:你不知道用户会输多少个数,但程序自己会处理——不够了就扩容。
常见的坑,一个比一个狠
坑1:忘记free——内存泄漏
void func() { int *p = (int*)malloc(100 * sizeof(int)); // 用完了,忘了free }
每次调用func都漏掉一块内存,程序跑久了内存被吃光。养成习惯:malloc和free成对出现。
坑2:free之后还继续用——悬空指针
int *p = (int*)malloc(sizeof(int)); *p = 10; free(p); printf("%d\n", *p); // 危险!p指向的内存已经还回去了
free只是把内存还给了系统,但系统不会把那个地址抹掉。你再去访问,读到的是别人写进去的数据,或者直接崩溃。free之后,立刻把指针置为NULL。
坑3:重复free
int *p = (int*)malloc(sizeof(int)); free(p); free(p); // 崩溃!同一块内存free两次
坑4:malloc之后不检查NULL
int *p = (int*)malloc(1000000000 * sizeof(int)); // 申请1G内存 p[0] = 10; // 如果申请失败,p是NULL,这行直接崩溃
虽然现在电脑内存都很大,但申请大块内存的时候还是可能失败。养成检查NULL的习惯。
坑5:realloc之后还用旧指针
前面说过了,realloc可能搬家,旧指针会失效。
坑6:忘了sizeof
int *p = (int*)malloc(10); // 想申请10个int,实际上只申请了10个字节
10个字节连3个int都放不下。永远写malloc(n * sizeof(类型))。
malloc和数组的区别,用一张表说清楚
| 数组 | malloc | |
|---|---|---|
| 大小 | 编译时确定,固定不变 | 运行时确定,可以调整 |
| 内存位置 | 栈(局部数组)或静态区(全局数组) | 堆 |
| 释放 | 自动(函数结束或程序结束) | 手动free |
| 灵活性 | 低 | 高 |
| 访问速度 | 快 | 略慢(多一层指针) |
用哪个? 数据量确定就用数组,简单省心。数据量不确定、需要运行时决定、或者需要动态调整大小,就用动态内存。
最后说两句
动态内存分配是指针最核心的应用。你之前学的指针、数组、函数,在动态内存这里全部串起来了。你会看到malloc返回指针、用指针操作内存、用free释放。所有这些知识在这里汇合。
练到什么程度算过关?
你知道什么时候该用malloc、什么时候该用数组。能写出动态数组的创建、使用、扩容、释放的完整代码。知道free之后要置NULL。知道realloc之后要用返回值更新指针。知道每次malloc之后要检查NULL。
回去自己动手,把上面的动态数组例子敲一遍。然后改一改:让它支持删除元素、查找元素、排序。你会发现,用动态内存写出来的程序,比用固定数组灵活太多了。下节课咱们讲链表——那就是动态内存和指针的终极结合。你把这节课搞定了,链表就不远了。