19. C语言动态内存分配

前面的文章里,我们用的数组、字符串、结构体,都有一个共同特点:大小在编译时就确定了。比如 int arr[100];,这 100 个元素的数组在程序运行前就已经被分配好了空间,不管你实际用了 5 个还是 80 个,内存都占那么多。

但现实中的程序往往不知道运行时会有多少数据——用户可能输入 3 个成绩,也可能输入 300 个;日志文件可能一行,也可能十万行。如果总是“多预留一些”,内存会大量浪费;“预留少了”又会溢出。这就需要一种能在程序运行时按需申请内存的机制。

C 语言把这把钥匙交给了你:动态内存分配。它强大,但也危险——申请了要记得还,还了就别再用。今天我们就来掌握这套“手动内存管理”的艺术。


一、程序的内存布局:栈、堆与静态区

在正式学 malloc 之前,先搞清楚程序运行时内存的几个主要区域,这对理解“变量住哪里”很有帮助。

一个典型的 C 程序,其内存布局(从高地址到低地址)大致是:

内存区域存放内容特点
栈(Stack)局部变量、函数参数、返回地址自动管理,函数调用时分配,返回时释放;空间有限(通常几 MB)
堆(Heap)动态分配的内存手动管理,程序通过 malloc 申请,用 free 释放;空间较大
静态数据区全局变量、static 局部变量、字符串字面量整个程序运行期存在
代码区程序的机器指令只读

以前我们写的局部变量都在栈上,来去匆匆,不需要你操心。但动态分配的内存来自——这块区域的大小只受系统物理内存和虚拟内存的限制,你可以按需索取。代价是:你必须自己管理它的生命周期。忘了归还,它就一直占着,这就是内存泄漏


二、malloc:申请一块内存

malloc(memory allocation)是最基础的动态内存分配函数,定义在 <stdlib.h> 里:

void *malloc(size_t size);
  • 参数 size:要申请的字节数。
  • 返回值:一个 void* 指针,指向申请到的内存块的首地址;如果分配失败(比如内存不足),返回 NULL

使用流程

#include <stdlib.h>

int *p = (int*)malloc(sizeof(int));   // 申请一块 int 大小的内存
if (p == NULL) {
    // 处理分配失败
    printf("内存分配失败!\n");
    return 1;
}
*p = 42;          // 正常使用
free(p);          // 用完后释放

几个要点:

  1. 为什么用 sizeof 不同类型大小不同,用 sizeof 让编译器自动计算,不依赖手动猜测。
  2. 为什么强制类型转换? malloc 返回 void*,在 C 里可以隐式转换成任意指针类型,强转主要是为了代码清晰,以及兼容 C++(如果以后被 C++ 编译器使用的话)。C 社区有争议,但写了更清晰。
  3. 为什么检查 NULL 内存请求可能失败(尤其是申请大块内存时),不检查就使用会因空指针解引用而崩溃。

三、动态分配数组

malloc 最常见的用途是创建运行时才能确定大小的数组:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n;
    printf("请输入学生数量:");
    scanf("%d", &n);

    int *scores = (int*)malloc(n * sizeof(int));
    if (scores == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }

    // 像普通数组一样使用
    for (int i = 0; i < n; i++) {
        scores[i] = i * 10;   // 赋值
    }
    for (int i = 0; i < n; i++) {
        printf("%d ", scores[i]);
    }
    printf("\n");

    free(scores);   // 释放
    return 0;
}

注意 n * sizeof(int) 计算总字节数。释放后,指针 scores 仍然存着那个地址,但内存已经不属于你了,再访问它会导致未定义行为。


四、calloc:申请并自动清零

calloc 也是申请内存,但它会把分配到的所有字节初始化为 0,并且参数写法不同:

void *calloc(size_t nmemb, size_t size);
  • nmemb:元素个数
  • size:每个元素的大小
int *arr = (int*)calloc(10, sizeof(int));   // 10 个 int,全部为 0

malloc 分配到的内存里是垃圾值,如果忘了初始化就直接读,很容易出问题。calloc 自动清零,代价是稍微慢一点(因为要擦写内存)。给结构体数组、或者需要初始化为 0 的场景,推荐用 calloc


五、realloc:给已分配的内存“扩容”

有时候之前申请的 100 个位置用满了,想扩容到 200 个。realloc 可以调整动态内存的大小:

void *realloc(void *ptr, size_t new_size);
  • ptr:之前通过 malloc/calloc/realloc 返回的指针。
  • new_size:新的大小(字节)。
  • 返回值:新内存块的地址(可能和原来一样,也可能不一样)。

重要:realloc 可能搬移数据!

如果原地址后面没有足够的连续空闲空间,realloc 会:

  1. 在别处找一块够大的新区域。
  2. 把旧数据复制过去。
  3. 自动释放旧区域。
  4. 返回新地址。

所以,永远用返回值更新原指针,而且先用临时变量接住,防止失败时丢失原指针:

int *arr = (int*)malloc(5 * sizeof(int));
// ... 使用 arr,满了

int *tmp = (int*)realloc(arr, 10 * sizeof(int));
if (tmp == NULL) {
    // 扩容失败,但 arr 仍然有效!可以继续用原来的
    printf("扩容失败\n");
} else {
    arr = tmp;   // 用新地址
}

如果 realloc 第一个参数是 NULL,它的行为等同于 malloc(new_size)


六、free:归还内存

free 把通过 malloccallocrealloc 申请的内存归还给堆,以供后续分配。

free(ptr);
ptr = NULL;   // 好习惯:释放后置空,防止误用

规则

  • 只能 free 动态分配的内存,栈上的变量绝对不能 free
  • 不能重复 free 同一块内存(free(NULL) 是安全的,不会报错)。
  • free 后指针变成“悬垂指针”,再访问就是未定义行为。

七、常见动态内存错误(避坑手册)

1. 忘记调用 free——内存泄漏

void func(void) {
    int *p = (int*)malloc(100 * sizeof(int));
    // 使用 p,但函数结束前没有 free(p)
}  // p 本身消失,但内存还在堆里没人回收

每次 func 被调用,就泄露 400 字节。程序长时间运行,内存被慢慢蚕食。对于长期运行的程序(服务器、数据库),内存泄漏是致命的。

2. 使用已释放的内存(悬垂指针)

int *p = (int*)malloc(sizeof(int));
*p = 10;
free(p);
*p = 20;   // 未定义行为!p 指向的内存已经不属于你

释放后把指针置为 NULL,至少在解引用 NULL 时会立即崩溃(更容易定位)。

3. 多次 free 同一块内存

int *p = (int*)malloc(sizeof(int));
free(p);
free(p);   // 未定义行为!

同样,free 后置 NULL 可以避免这种情况,因为 free(NULL) 是安全的。

4. 越界写入动态数组

int *arr = (int*)malloc(5 * sizeof(int));
arr[5] = 100;   // 越界!只分配了 0-4 共 5 个元素

和普通数组一样,动态分配的数组也不检查越界,但后果可能更隐蔽——可能覆盖了堆的管理结构,导致后续 free 时崩溃。

5. 使用未初始化的动态内存(malloc 后直接读)

int *p = (int*)malloc(sizeof(int));
printf("%d\n", *p);   // 垃圾值

要么用 calloc,要么 malloc 后立即赋值。


八、实战:简易动态数组

我们把动态内存知识整合成一个可用的“动态数组”模块——能自动扩容的整数数组。

darray.h

#ifndef DARRAY_H
#define DARRAY_H

typedef struct {
    int *data;        // 指向堆上的数组
    int capacity;     // 当前容量
    int size;         // 实际存放的元素个数
} DArray;

DArray* darray_create(int initial_capacity);
void darray_append(DArray *da, int value);
int  darray_get(DArray *da, int index);
void darray_free(DArray *da);

#endif

darray.c

#include <stdlib.h>
#include "darray.h"

DArray* darray_create(int initial_capacity) {
    DArray *da = (DArray*)malloc(sizeof(DArray));
    if (da == NULL) return NULL;
    da->data = (int*)malloc(initial_capacity * sizeof(int));
    if (da->data == NULL) {
        free(da);
        return NULL;
    }
    da->capacity = initial_capacity;
    da->size = 0;
    return da;
}

void darray_append(DArray *da, int value) {
    if (da->size >= da->capacity) {
        // 扩容为原来的 2 倍
        int new_cap = da->capacity * 2;
        int *tmp = (int*)realloc(da->data, new_cap * sizeof(int));
        if (tmp == NULL) return;  // 扩容失败,保持现状
        da->data = tmp;
        da->capacity = new_cap;
    }
    da->data[da->size] = value;
    da->size++;
}

int darray_get(DArray *da, int index) {
    // 注意:实际项目应加边界检查
    return da->data[index];
}

void darray_free(DArray *da) {
    free(da->data);   // 先释放内部数组
    free(da);         // 再释放结构体本身
}

main.c

#include <stdio.h>
#include "darray.h"

int main(void) {
    DArray *da = darray_create(4);
    if (da == NULL) {
        printf("创建动态数组失败\n");
        return 1;
    }

    for (int i = 0; i < 20; i++) {
        darray_append(da, i * i);
    }

    printf("动态数组内容: ");
    for (int i = 0; i < da->size; i++) {
        printf("%d ", darray_get(da, i));
    }
    printf("\n容量: %d, 大小: %d\n", da->capacity, da->size);

    darray_free(da);
    return 0;
}

输出:

动态数组内容: 0 1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361 
容量: 32, 大小: 20

初始容量为 4,插入到第 5 个元素时自动扩容为 8,再满扩到 16、32。这个“动态数组”就是很多高级语言中 vectorArrayList 的底层原理。


九、小结

动态内存分配让程序摆脱了编译时固定大小的桎梏。你今天学到了:

  • 堆与栈的本质区别:栈自动管理,堆手动管理。
  • malloccallocrealloc 的使用场景和差异。
  • free 的规则:配对使用,释放后置 NULL。
  • 五大常见动态内存错误及其防范方法。
  • 一个完整的动态数组实现,体感“自动扩容”的原理。

现在你已经有了指针的深厚功底,又能自由操控内存——这几乎是 C 语言最具威力的组合。下一步,我们将用这些知识构建真正灵活的数据结构:链表。数组再能扩容,也逃不开“插入中间要移动大量元素”的宿命。而链表能让你在 O(1) 时间内完成插入和删除,它是动态数据结构的真正起点。


课后小练习

  1. 写一个程序,用 malloc 创建一个包含 N 个 double 的数组(N 由用户输入),然后读取 N 个数存入数组,计算平均值并输出。最后 free
  2. calloc 实现和上题同样的功能,然后解释 callocmalloc 的区别。
  3. 下面的代码有 bug,找出并修复:
    int* create_array(int size) {
        int arr[size];
        return arr;
    }
    int main(void) {
        int *p = create_array(10);
        p[0] = 5;
        free(p);
        return 0;
    }
    
  4. (小挑战)在“简易动态数组”的基础上,增加一个 darray_remove_last 函数,删除最后一个元素(size 减 1 即可,不需缩容)。再加一个 darray_insert 函数,在指定索引处插入一个元素(后面的元素要后移)。如果空间不够,自动扩容。

我们下期见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值