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;
}
| 特性 | malloc | alloca |
|---|---|---|
| 分配位置 | 堆 | 栈 |
需要 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) 标记位)
- 无内存碎片
- 适合频繁分配/释放同大小对象的场景(如游戏引擎、网络服务器)