前置知识: C

动态内存管理

2 minIntermediate2026/6/14

malloc/calloc/realloc/free详解

概述

C语言提供了手动管理动态内存的能力,通过 malloccallocreallocfree 四个函数实现。动态内存从堆(heap)中分配,生命周期由程序员控制,必须手动释放。正确使用动态内存是C编程的核心技能,也是最容易出错的地方。

基础概念

内存区域划分

区域说明生命周期
代码段存放可执行指令程序运行期间
数据段全局变量和静态变量程序运行期间
局部变量、函数参数函数调用期间
动态分配的内存从分配到释放

为什么要动态分配内存

  • 编译时不知道需要多少内存(如用户输入决定数组大小)
  • 需要创建生命周期超出函数调用范围的数据结构(如链表、树)
  • 需要灵活调整内存大小(如动态数组)

快速上手

基本分配与释放

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

int main(void) {
    // 分配10个int的内存
    int *arr = malloc(10 * sizeof(int));
    if (!arr) {
        perror("内存分配失败");
        return 1;
    }

    // 使用内存
    for (int i = 0; i < 10; i++) {
        arr[i] = i * i;
    }

    // 打印
    for (int i = 0; i < 10; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }

    // 释放内存
    free(arr);
    arr = NULL; // 避免悬空指针

    return 0;
}

详细用法

malloc — 分配未初始化内存

#include <stdlib.h>

// void *malloc(size_t size);
// 分配 size 字节的内存,内容未初始化(值不确定)
// 成功返回指针,失败返回 NULL

int *p = (int *)malloc(5 * sizeof(int));
// p 指向的内存内容是不确定的,必须先初始化再使用
for (int i = 0; i < 5; i++) {
    p[i] = 0; // 手动初始化
}
free(p);

calloc — 分配并初始化为零

#include <stdlib.h>

// void *calloc(size_t num, size_t size);
// 分配 num 个大小为 size 的内存块,全部初始化为零
// 适合分配数组

int *arr = (int *)calloc(10, sizeof(int));
// arr 的所有元素已经初始化为0
printf("arr[0] = %d\n", arr[0]); // 输出: 0

free(arr);

realloc — 调整内存大小

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

int main(void) {
    // 初始分配5个元素
    int *arr = malloc(5 * sizeof(int));
    for (int i = 0; i < 5; i++) arr[i] = i;

    // 扩展到10个元素
    int *temp = realloc(arr, 10 * sizeof(int));
    if (!temp) {
        free(arr); // realloc失败时原内存仍有效
        return 1;
    }
    arr = temp;

    // 新增的5个元素值不确定,需要初始化
    for (int i = 5; i < 10; i++) arr[i] = i * 10;

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

    free(arr);
    return 0;
}

free — 释放内存

#include <stdlib.h>

// void free(void *ptr);
// 释放之前分配的内存
// ptr 必须是 malloc/calloc/realloc 返回的指针

int *p = malloc(sizeof(int));
*p = 42;
free(p);   // 释放内存
p = NULL;  // 好习惯:避免悬空指针

// 注意:free(NULL) 是安全的,不会出错
free(NULL); // 合法,什么都不做

常见场景

场景一:动态数组

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

typedef struct {
    int *data;
    size_t size;
    size_t capacity;
} DynamicArray;

// 创建动态数组
DynamicArray da_create(size_t initial_capacity) {
    DynamicArray da = {0};
    da.data = malloc(initial_capacity * sizeof(int));
    da.capacity = initial_capacity;
    da.size = 0;
    return da;
}

// 追加元素
void da_push(DynamicArray *da, int value) {
    if (da->size >= da->capacity) {
        da->capacity = da->capacity ? da->capacity * 2 : 4;
        int *temp = realloc(da->data, da->capacity * sizeof(int));
        if (!temp) return;
        da->data = temp;
    }
    da->data[da->size++] = value;
}

// 获取元素
int da_get(DynamicArray *da, size_t index) {
    return da->data[index];
}

// 释放
void da_free(DynamicArray *da) {
    free(da->data);
    da->data = NULL;
    da->size = da->capacity = 0;
}

int main(void) {
    DynamicArray arr = da_create(4);

    for (int i = 0; i < 20; i++) {
        da_push(&arr, i * i);
    }

    for (size_t i = 0; i < arr.size; i++) {
        printf("arr[%zu] = %d\n", i, da_get(&arr, i));
    }

    da_free(&arr);
    return 0;
}

场景二:链表

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

typedef struct Node {
    int value;
    struct Node *next;
} Node;

// 在链表头部插入节点
Node *list_prepend(Node *head, int value) {
    Node *node = malloc(sizeof(Node));
    if (!node) return head;
    node->value = value;
    node->next = head;
    return node;
}

// 遍历链表
void list_print(Node *head) {
    for (Node *curr = head; curr; curr = curr->next) {
        printf("%d -> ", curr->value);
    }
    printf("NULL\n");
}

// 释放整个链表
void list_free(Node *head) {
    Node *curr = head;
    while (curr) {
        Node *next = curr->next;
        free(curr);
        curr = next;
    }
}

int main(void) {
    Node *list = NULL;
    list = list_prepend(list, 30);
    list = list_prepend(list, 20);
    list = list_prepend(list, 10);

    list_print(list); // 10 -> 20 -> 30 -> NULL
    list_free(list);
    return 0;
}

场景三:读取未知大小的输入

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

// 读取文件全部内容到动态分配的字符串
char *read_file(const char *path) {
    FILE *fp = fopen(path, "r");
    if (!fp) return NULL;

    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    long file_size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    // 分配内存(加1给终止符)
    char *content = malloc(file_size + 1);
    if (!content) { fclose(fp); return NULL; }

    // 读取内容
    size_t bytes_read = fread(content, 1, file_size, fp);
    content[bytes_read] = '\0';

    fclose(fp);
    return content;
}

int main(void) {
    char *text = read_file("input.txt");
    if (text) {
        printf("文件内容:\n%s\n", text);
        free(text);
    }
    return 0;
}

注意事项

内存泄漏

分配的内存没有释放,导致可用内存逐渐减少:

void leak_example(void) {
    int *p = malloc(sizeof(int) * 100);
    // 函数结束时没有 free(p),内存泄漏
}

// 预防:确保每个 malloc 都有对应的 free
void no_leak(void) {
    int *p = malloc(sizeof(int) * 100);
    if (!p) return;
    // 使用 p...
    free(p);
}

悬空指针

访问已释放的内存:

int *p = malloc(sizeof(int));
*p = 42;
free(p);
// *p = 100;  // 未定义行为!p 已释放
p = NULL;     // 设置为 NULL,防止误用

双重释放

对同一块内存调用两次 free:

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

// 预防:free 后立即设为 NULL
free(p);
p = NULL;
free(p);  // free(NULL) 是安全的

检查 malloc 返回值

malloc 可能失败(返回 NULL),必须检查:

int *p = malloc(1000000000 * sizeof(int));
if (!p) {
    fprintf(stderr, "内存分配失败\n");
    return 1;
}

进阶用法

内存池

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

#define POOL_SIZE (1024 * 1024) // 1MB 内存池

typedef struct {
    char buffer[POOL_SIZE];
    size_t offset;
} MemoryPool;

// 初始化内存池
void pool_init(MemoryPool *pool) {
    pool->offset = 0;
}

// 从内存池分配
void *pool_alloc(MemoryPool *pool, size_t size) {
    // 对齐到8字节
    size = (size + 7) & ~7;

    if (pool->offset + size > POOL_SIZE) {
        return NULL; // 内存不足
    }

    void *ptr = pool->buffer + pool->offset;
    pool->offset += size;
    return ptr;
}

// 重置内存池(一次性释放所有分配)
void pool_reset(MemoryPool *pool) {
    pool->offset = 0;
}

int main(void) {
    MemoryPool pool;
    pool_init(&pool);

    // 从池中分配,无需逐个 free
    int *a = pool_alloc(&pool, 10 * sizeof(int));
    char *b = pool_alloc(&pool, 100);
    double *c = pool_alloc(&pool, 5 * sizeof(double));

    if (a) for (int i = 0; i < 10; i++) a[i] = i;
    if (b) strcpy(b, "内存池分配");

    printf("a[5] = %d, b = %s\n", a[5], b);

    // 一次性释放所有
    pool_reset(&pool);
    return 0;
}

自定义分配器

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

// 带统计信息的分配器
typedef struct {
    size_t total_allocated;
    size_t total_freed;
    size_t current_usage;
    size_t peak_usage;
    int alloc_count;
    int free_count;
} AllocStats;

static AllocStats stats = {0};

void *tracked_malloc(size_t size) {
    void *ptr = malloc(size);
    if (ptr) {
        stats.total_allocated += size;
        stats.current_usage += size;
        stats.alloc_count++;
        if (stats.current_usage > stats.peak_usage) {
            stats.peak_usage = stats.current_usage;
        }
    }
    return ptr;
}

void tracked_free(void *ptr, size_t size) {
    if (ptr) {
        free(ptr);
        stats.total_freed += size;
        stats.current_usage -= size;
        stats.free_count++;
    }
}

void print_stats(void) {
    printf("分配次数: %d\n", stats.alloc_count);
    printf("释放次数: %d\n", stats.free_count);
    printf("总分配: %zu 字节\n", stats.total_allocated);
    printf("总释放: %zu 字节\n", stats.total_freed);
    printf("当前使用: %zu 字节\n", stats.current_usage);
    printf("峰值使用: %zu 字节\n", stats.peak_usage);
}

int main(void) {
    int *a = tracked_malloc(100 * sizeof(int));
    char *b = tracked_malloc(256);
    tracked_free(a, 100 * sizeof(int));

    print_stats();
    tracked_free(b, 256);
    return 0;
}

使用 AddressSanitizer 检测内存错误

# 编译时启用 ASan
gcc -fsanitize=address -g -O0 program.c -o program

# 运行程序,ASan 会自动检测并报告内存错误
./program

ASan 可以检测:堆缓冲区溢出、栈缓冲区溢出、使用已释放内存、内存泄漏、双重释放等。