强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

C/C++ Linux 开发教程(GCC + CMake) / 09 — 内存管理(malloc/free/栈与堆)

内存管理(malloc/free/栈与堆)

1. 进程内存布局

一个 C 程序运行时,内存从低地址到高地址依次划分为以下区域:

高地址  ┌──────────────────┐
        │    栈 (Stack)     │ ← 局部变量、函数参数、返回地址
        │       ↓          │    自动分配/释放,后进先出
        │                  │
        │       ↑          │
        │    堆 (Heap)     │ ← malloc/free 动态分配
        │                  │
        ├──────────────────┤
        │  BSS 段          │ ← 未初始化的全局/静态变量(自动清零)
        ├──────────────────┤
        │  数据段 (Data)    │ ← 已初始化的全局/静态变量
        ├──────────────────┤
        │  代码段 (Text)    │ ← 程序指令(只读)
低地址  └──────────────────┘
#include <stdio.h>

int global_init = 42;       // 数据段(已初始化)
int global_uninit;           // BSS 段(未初始化,自动为 0)
static int static_var = 10;  // 数据段

int main(void)
{
    int local = 100;          // 栈
    static int func_static;   // 数据段/BSS(静态局部变量)
    int *heap = malloc(sizeof(int));  // 堆

    printf("代码段 (main):  %p\n", (void *)main);
    printf("数据段 (global): %p\n", (void *)&global_init);
    printf("BSS (uninit):   %p\n", (void *)&global_uninit);
    printf("栈 (local):     %p\n", (void *)&local);
    printf("堆 (heap):      %p\n", (void *)heap);

    free(heap);
    return 0;
}
区域存储内容分配方式生命周期
代码段程序指令编译时程序运行期(只读)
数据段已初始化全局/静态变量编译时程序运行期
BSS未初始化全局/静态变量编译时程序运行期(自动清零)
动态分配的数据运行时 (malloc)手动管理 (free)
局部变量、函数调用帧运行时函数返回自动释放

2. malloc / calloc / realloc / free

2.1 malloc — 分配内存

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

int main(void)
{
    // 分配 10 个 int 的空间(未初始化)
    int *arr = malloc(10 * sizeof(int));
    if (arr == NULL) {
        perror("malloc");
        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;
}

2.2 calloc — 分配并清零

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

int main(void)
{
    // calloc 分配并初始化为 0
    int *arr = calloc(10, sizeof(int));
    if (!arr) {
        perror("calloc");
        return 1;
    }

    // 所有元素已为 0
    for (int i = 0; i < 10; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);  // 全部为 0
    }

    free(arr);
    return 0;
}

2.3 realloc — 调整大小

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

int main(void)
{
    size_t capacity = 4;
    int *arr = malloc(capacity * sizeof(int));
    if (!arr) return 1;

    size_t size = 0;

    // 动态增长数组
    for (int i = 0; i < 20; i++) {
        if (size >= capacity) {
            capacity *= 2;
            int *new_arr = realloc(arr, capacity * sizeof(int));
            if (!new_arr) {
                free(arr);
                return 1;
            }
            arr = new_arr;
            printf("扩容到 %zu\n", capacity);
        }
        arr[size++] = i;
    }

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

    free(arr);
    return 0;
}

2.4 函数对比表

函数功能初始化原型
malloc(n)分配 n 字节❌ 未初始化void *malloc(size_t size)
calloc(n, size)分配 n×size 字节✅ 全部清零void *calloc(size_t n, size_t size)
realloc(ptr, n)调整已分配内存大小❌ 新增部分未初始化void *realloc(void *ptr, size_t size)
free(ptr)释放内存void free(void *ptr)

⚠️ 注意:

  • malloc 返回的内存包含垃圾值,必须先初始化再使用
  • free(NULL) 是安全的(无操作)
  • 对同一指针 free 两次是未定义行为
  • realloc 可能移动内存块到新位置,必须使用返回的新指针

3. 内存泄漏检测

// leaky.c — 包含内存泄漏
#include <stdio.h>
#include <stdlib.h>

char *create_greeting(const char *name)
{
    char *buf = malloc(256);
    if (!buf) return NULL;
    snprintf(buf, 256, "Hello, %s!", name);
    return buf;
    // 调用者负责 free(buf)
}

int main(void)
{
    char *g1 = create_greeting("Alice");
    char *g2 = create_greeting("Bob");

    printf("%s\n", g1);
    printf("%s\n", g2);

    // 修复:添加 free(g1); free(g2);
    free(g1);
    free(g2);

    return 0;
}

检测工具

# Valgrind —— 最强大的内存检测工具
valgrind --leak-check=full --show-leak-kinds=all ./leaky

# AddressSanitizer (ASan) —— GCC 内置,速度更快
gcc -fsanitize=address -g -o leaky_asan leaky.c
./leaky_asan

4. 野指针与悬空指针

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

int main(void)
{
    // 野指针:未初始化的指针
    int *wild;       // 指向随机地址
    // *wild = 42;   // 未定义行为!

    // 悬空指针:指向已释放的内存
    int *p = malloc(sizeof(int));
    *p = 42;
    free(p);
    // *p = 100;     // 未定义行为!p 是悬空指针

    // 安全模式
    p = NULL;        // free 后立即置空

    // 二次释放
    int *q = malloc(sizeof(int));
    free(q);
    // free(q);      // 未定义行为!

    return 0;
}
问题原因后果预防
野指针声明时未初始化写入随机内存初始化为 NULL
悬空指针使用已释放的内存数据损坏free 后置 NULL
双重释放同一内存 free 两次程序崩溃使用前检查 NULL
内存泄漏分配后未释放内存耗尽确保配对 malloc/free

💡 提示: 养成 malloc 后检查返回值、free 后置 NULL 的习惯。


5. 栈内存 vs 堆内存

#include <stdio.h>

void stack_example(void)
{
    int arr[100];   // 栈分配,函数返回自动释放
    arr[0] = 42;
    printf("栈数组: arr[0] = %d\n", arr[0]);
}

void heap_example(void)
{
    int *arr = malloc(100 * sizeof(int));  // 堆分配
    if (!arr) return;
    arr[0] = 42;
    printf("堆数组: arr[0] = %d\n", arr[0]);
    free(arr);   // 必须手动释放
}

int main(void)
{
    stack_example();
    heap_example();
    return 0;
}
特性栈 (Stack)堆 (Heap)
分配速度极快(移动栈指针)较慢(搜索空闲块)
大小限制通常 1~8 MB可用内存大小
生命周期函数返回自动释放手动 free
碎片化可能产生碎片
适用场景小型、生命周期短的数据大型、生命周期不确定的数据

⚠️ 注意: 栈溢出是真实的风险。以下代码可能导致栈溢出:

void dangerous(void)
{
    int huge_array[10000000];  // 40 MB,超出栈大小限制!
    // 应改为 malloc
}

6. alloca 栈分配

#include <stdio.h>
#include <alloca.h>

int main(void)
{
    int n = 100;

    // alloca 在栈上分配,函数返回自动释放
    int *arr = alloca(n * sizeof(int));
    for (int i = 0; i < n; i++) {
        arr[i] = i;
    }

    printf("alloca 分配 %d 个 int\n", n);
    printf("arr[99] = %d\n", arr[99]);

    return 0;
}
特性mallocalloca
分配位置
需要 free✅ 是❌ 否(自动释放)
可移植性C 标准非标准(大多数编译器支持)
安全性较安全可能栈溢出

⚠️ 注意: alloca 不是 C 标准的一部分,且如果分配过大会导致栈溢出。谨慎使用。


7. 内存对齐(aligned_alloc)

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

int main(void)
{
    // 普通分配
    void *p1 = malloc(64);
    printf("malloc 地址:     %p (对齐: %zu)\n", p1,
           (size_t)(uintptr_t)p1 % 64);

    // 对齐分配(C11)
    void *p2 = aligned_alloc(64, 128);  // 64 字节对齐,大小为 128
    printf("aligned_alloc:    %p (对齐: %zu)\n", p2,
           (size_t)(uintptr_t)p2 % 64);

    free(p1);
    free(p2);

    // alignof 查询
    printf("alignof(char):    %zu\n", alignof(char));
    printf("alignof(int):     %zu\n", alignof(int));
    printf("alignof(double):  %zu\n", alignof(double));

    return 0;
}

💡 提示: SIMD 指令(如 SSE/AVX)通常要求操作数按 16/32 字节对齐,使用 aligned_alloc 可以满足要求。


8. 内存检测工具

8.1 Valgrind

# 内存泄漏检测
valgrind --leak-check=full ./program

# 未初始化内存使用检测
valgrind --track-origins=yes ./program

8.2 AddressSanitizer (ASan)

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

# 同时启用未初始化内存检测
gcc -fsanitize=address,memory -g -o prog prog.c

8.3 工具对比

工具检测能力速度开销使用方式
Valgrind泄漏、越界、未初始化10~50x运行时
ASan越界、UAF、泄漏2x编译时
MSan未初始化内存读取3x编译时
UBSan未定义行为很低编译时

💡 提示: 开发阶段始终使用 -fsanitize=address,undefined 编译,能发现大量隐藏的内存问题。


9. 常见内存错误

9.1 缓冲区溢出

int arr[5] = {0};
arr[5] = 42;  // 越界!写入未知内存

9.2 使用未初始化内存

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

9.3 内存泄漏

void process(void)
{
    int *data = malloc(1000);
    if (some_error) return;  // 泄漏!
    free(data);
}

9.4 返回局部变量地址

int *bad(void)
{
    int x = 42;
    return &x;  // x 在函数返回后无效
}

9.5 错误总结与修复

错误症状修复
越界写数据损坏、崩溃使用 ASan,检查边界
未初始化读行为随机使用 calloc 或手动初始化
内存泄漏内存持续增长使用 Valgrind,确保 free
双重释放崩溃free 后置 NULL
UAF (Use-After-Free)数据损坏free 后置 NULL

10. 实际场景:内存池设计

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

#define BLOCK_SIZE  64
#define BLOCK_COUNT 16

typedef struct Pool {
    char memory[BLOCK_SIZE * BLOCK_COUNT];
    int used[BLOCK_COUNT];
    struct Pool *next;
} Pool;

static Pool *g_pool = NULL;

void pool_init(void)
{
    g_pool = calloc(1, sizeof(Pool));
    g_pool->next = NULL;
}

void *pool_alloc(void)
{
    Pool *p = g_pool;
    while (p) {
        for (int i = 0; i < BLOCK_COUNT; i++) {
            if (!p->used[i]) {
                p->used[i] = 1;
                return &p->memory[i * BLOCK_SIZE];
            }
        }
        if (p->next == NULL) {
            p->next = calloc(1, sizeof(Pool));
        }
        p = p->next;
    }
    return NULL;
}

void pool_free(void *ptr)
{
    Pool *p = g_pool;
    while (p) {
        char *start = p->memory;
        char *end = start + BLOCK_SIZE * BLOCK_COUNT;
        if ((char *)ptr >= start && (char *)ptr < end) {
            int index = (int)(((char *)ptr - start) / BLOCK_SIZE);
            p->used[index] = 0;
            return;
        }
        p = p->next;
    }
}

void pool_destroy(void)
{
    Pool *p = g_pool;
    while (p) {
        Pool *next = p->next;
        free(p);
        p = next;
    }
    g_pool = NULL;
}

int main(void)
{
    pool_init();

    // 分配 5 个块
    void *blocks[5];
    for (int i = 0; i < 5; i++) {
        blocks[i] = pool_alloc();
        memset(blocks[i], 'A' + i, BLOCK_SIZE);
        printf("分配块 %d: %p\n", i, blocks[i]);
    }

    // 释放第 2 个块
    pool_free(blocks[1]);
    printf("释放块 1\n");

    // 重新分配(应该复用块 1 的位置)
    void *new_block = pool_alloc();
    printf("新分配: %p (应该与块 1 相同: %s)\n",
           new_block, new_block == blocks[1] ? "是" : "否");

    pool_destroy();
    return 0;
}

内存池的优势:

  • 分配/释放极快(O(1) 标记位)
  • 无内存碎片
  • 适合频繁分配/释放同大小对象的场景(如游戏引擎、网络服务器)

11. 扩展阅读