【C语言-指针-动态内存分配】6. 动态内存分配——程序也能“现用现申请”

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 3
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

前面咱们把指针的基础、指针和数组、指针和字符串、函数指针、返回指针的函数全聊完了。今天终于到了指针的“终极实战”——动态内存分配。

说实话,前面学的那些指针知识,如果不用在动态内存上,多少有点“纸上谈兵”的感觉。但你把今天这节课搞明白了,你就会真正体会到指针为什么是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。

回去自己动手,把上面的动态数组例子敲一遍。然后改一改:让它支持删除元素、查找元素、排序。你会发现,用动态内存写出来的程序,比用固定数组灵活太多了。下节课咱们讲链表——那就是动态内存和指针的终极结合。你把这节课搞定了,链表就不远了。