动态内存管理

在之前的学习中,我们使用的数组、结构体等变量都是在编译时确定大小的,这种内存分配方式称为静态内存分配(或自动分配)。 然而,在实际开发中,很多情况下我们无法提前知道需要多少内存,比如读取用户输入、处理可变长度的数据等。 动态内存管理允许程序在运行时从堆(heap)中请求和释放内存,极大地提高了程序的灵活性。 本章将介绍C语言中动态内存管理的四大函数:malloccallocreallocfree

📦 堆与栈

C程序运行时,内存主要分为几个区域:

  • 栈(Stack):存储局部变量、函数参数等,由编译器自动管理,分配和释放速度快,但大小有限。
  • 堆(Heap):动态分配的内存区域,由程序员手动管理,空间较大,但需要显式释放。
  • 静态存储区:存储全局变量和静态变量,程序启动时分配,结束时释放。
  • 代码区:存储程序的二进制代码。

动态内存管理主要操作的是堆内存。

📤 malloc - 内存分配

malloc 函数从堆中分配一块连续的内存,返回指向这块内存起始地址的指针,如果分配失败则返回 NULL

void *malloc(size_t size);

参数 size 是需要分配的字节数。返回的指针类型为 void*,通常需要强制转换为目标类型。

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

int main() {
    int *p;
    p = (int*)malloc(5 * sizeof(int));  // 分配可存放5个int的内存
    if (p == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }

    // 使用分配的内存
    for (int i = 0; i < 5; i++) {
        p[i] = i * 10;
        printf("%d ", p[i]);
    }
    printf("\n");

    // 释放内存
    free(p);
    p = NULL;  // 避免野指针

    return 0;
}
注意: malloc 分配的内存中的值是未初始化的,包含随机数据。如果需要初始化,可以使用 callocmemset

📤 calloc - 分配并清零

calloc 函数用于分配一块内存,并将其所有字节初始化为0。它接受两个参数:元素个数和每个元素的大小。

void *calloc(size_t nmemb, size_t size);
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = (int*)calloc(5, sizeof(int));  // 分配并清零
    if (p == NULL) {
        printf("分配失败\n");
        return 1;
    }

    for (int i = 0; i < 5; i++) {
        printf("%d ", p[i]);  // 输出 0 0 0 0 0
    }
    printf("\n");

    free(p);
    return 0;
}
区别: malloc 不初始化,速度稍快;calloc 初始化为0,更适合需要清零的场景。

📤 realloc - 调整内存大小

realloc 用于调整已分配内存块的大小,可以扩大或缩小。如果新大小大于原大小,新分配的部分不会初始化;如果缩小,多余部分被释放。

void *realloc(void *ptr, size_t size);

参数 ptr 是之前通过 malloccallocrealloc 返回的指针,size 是新的字节数。

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

int main() {
    int *p = (int*)malloc(3 * sizeof(int));
    if (p == NULL) return 1;

    p[0] = 10; p[1] = 20; p[2] = 30;

    // 扩容到5个元素
    int *temp = (int*)realloc(p, 5 * sizeof(int));
    if (temp == NULL) {
        printf("扩容失败,原内存未被释放\n");
        free(p);
        return 1;
    }
    p = temp;

    p[3] = 40;
    p[4] = 50;

    for (int i = 0; i < 5; i++) {
        printf("%d ", p[i]);
    }
    printf("\n");

    free(p);
    return 0;
}
注意: realloc 可能移动原内存块到新的位置,因此返回的指针可能不同于原指针。一定要使用新指针,并检查是否为 NULL

📤 free - 释放内存

动态分配的内存必须由程序员手动释放,否则会造成内存泄漏。使用 free 函数释放内存,释放后的指针应置为 NULL 以避免野指针。

void free(void *ptr);

释放同一块内存两次会导致未定义行为(通常程序崩溃)。

int *p = (int*)malloc(100);
if (p != NULL) {
    // 使用 p
    free(p);
    p = NULL;   // 好习惯
}

📊 动态数组示例

动态数组允许在运行时根据用户需求决定数组大小,并可随时调整。

#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);
    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");

    // 动态扩容:增加新元素
    int new;
    printf("请输入要追加的元素: ");
    scanf("%d", &new);

    int *temp = (int*)realloc(arr, (n + 1) * sizeof(int));
    if (temp == NULL) {
        printf("扩容失败\n");
        free(arr);
        return 1;
    }
    arr = temp;
    arr[n] = new;
    n++;

    printf("扩容后数组: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    free(arr);
    return 0;
}

🔤 动态字符串示例

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

int main() {
    char *str = NULL;
    char buffer[100];
    int totalLen = 0;

    printf("请输入多行文本(输入空行结束):\n");
    while (1) {
        fgets(buffer, sizeof(buffer), stdin);
        if (buffer[0] == '\n') break;  // 空行结束

        int len = strlen(buffer);
        // 去除换行符
        if (buffer[len-1] == '\n') buffer[len-1] = '\0';

        // 重新分配内存
        char *temp = (char*)realloc(str, totalLen + strlen(buffer) + 1);
        if (temp == NULL) {
            printf("内存分配失败\n");
            free(str);
            return 1;
        }
        str = temp;

        if (totalLen == 0) {
            strcpy(str, buffer);
        } else {
            strcat(str, buffer);
        }
        totalLen += strlen(buffer);
    }

    printf("拼接后的字符串: %s\n", str);

    free(str);
    return 0;
}

⚠️ 内存泄漏与野指针

  • 内存泄漏:动态分配的内存没有释放,且指针丢失,导致这块内存无法被回收。长时间运行的程序(如服务器)如果内存泄漏,最终会耗尽系统内存。
  • 野指针:指向已释放内存的指针。使用野指针可能导致程序崩溃或数据损坏。释放内存后应立即将指针置为 NULL
  • 双重释放:对同一块内存调用两次 free,会导致未定义行为。
预防措施:
  1. 每次 malloc/calloc/realloc 后检查返回值是否为 NULL
  2. 每次 free 后立即将指针设为 NULL
  3. 确保分配和释放成对出现,特别是在函数中分配的内存应在合适的位置释放。
  4. 使用工具(如 Valgrind)检测内存泄漏。

⚠️ 常见错误与注意事项

  • 忘记释放内存:导致内存泄漏。
  • 使用未初始化的动态内存malloc 分配的内存未初始化,直接读取会得到随机值。
  • 释放非动态分配的内存:对栈变量或静态变量调用 free 会导致错误。
  • 越界访问:访问超出分配范围的元素,会破坏堆结构。
  • realloc 使用不当:直接用原指针接收返回值而不检查,若 realloc 失败原指针会丢失,且原内存未释放。
  • 多次释放同一块内存:导致程序崩溃。
  • 内存碎片:频繁分配释放小块内存可能造成内存碎片,影响后续大块分配。

📋 综合示例:动态学生成绩管理系统

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

typedef struct {
    int id;
    char name[50];
    float score;
} Student;

int main() {
    Student *students = NULL;
    int count = 0;
    int choice;

    do {
        printf("\n===== 学生成绩管理系统 =====\n");
        printf("1. 添加学生\n");
        printf("2. 查看所有学生\n");
        printf("3. 删除所有学生\n");
        printf("4. 退出\n");
        printf("请选择: ");
        scanf("%d", &choice);

        switch (choice) {
            case 1: {
                // 添加学生,动态扩容
                Student *temp = (Student*)realloc(students, (count + 1) * sizeof(Student));
                if (temp == NULL) {
                    printf("内存分配失败!\n");
                    break;
                }
                students = temp;

                printf("请输入学号: ");
                scanf("%d", &students[count].id);
                printf("请输入姓名: ");
                scanf("%s", students[count].name);
                printf("请输入成绩: ");
                scanf("%f", &students[count].score);
                count++;
                printf("添加成功!\n");
                break;
            }
            case 2:
                if (count == 0) {
                    printf("暂无学生信息\n");
                } else {
                    printf("\n学号\t姓名\t成绩\n");
                    for (int i = 0; i < count; i++) {
                        printf("%d\t%s\t%.1f\n", students[i].id, students[i].name, students[i].score);
                    }
                }
                break;
            case 3:
                if (students != NULL) {
                    free(students);
                    students = NULL;
                    count = 0;
                    printf("所有学生信息已删除\n");
                } else {
                    printf("没有学生信息\n");
                }
                break;
            case 4:
                printf("退出系统\n");
                break;
            default:
                printf("无效选择\n");
        }
    } while (choice != 4);

    // 程序结束前释放内存
    if (students != NULL) {
        free(students);
    }
    return 0;
}

动态内存管理是C语言高级编程的核心,它赋予了程序运行时灵活分配内存的能力。熟练掌握 malloccallocreallocfree 的使用,并养成良好的内存管理习惯,可以避免内存泄漏和野指针等问题。 下一章我们将学习结构体与联合体,这是组织复杂数据的重要工具。