跳到主要内容

内存管理

C 语言提供了手动管理内存的能力,这是 C 语言强大但也容易出错的特性之一。正确理解和管理内存对于编写高效、稳定的 C 程序至关重要。

程序的内存布局

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

区域说明管理方式
代码段存储程序的可执行指令只读,由系统加载
数据段存储已初始化的全局变量和静态变量程序启动时分配,结束时释放
BSS 段存储未初始化的全局变量和静态变量程序启动时初始化为 0
动态分配的内存程序员手动管理
局部变量、函数参数、返回地址编译器自动管理

内存地址从低到高的排列:

低地址
┌─────────────┐
│ 代码段 │ 只读,存储程序代码
├─────────────┤
│ 数据段 │ 已初始化的全局/静态变量
├─────────────┤
│ BSS 段 │ 未初始化的全局/静态变量
├─────────────┤
│ 堆 │ 向上增长,动态分配
│ ↓ │
│ │
│ ↑ │
│ 栈 │ 向下增长,自动管理
├─────────────┤
│ 命令行参数 │
└─────────────┘
高地址

栈内存

自动变量

局部变量存储在栈上,函数调用时自动分配,返回时自动释放:

void function(void) {
int a = 10; // 栈上分配
int arr[100]; // 栈上分配 400 字节
char buffer[1024]; // 栈上分配 1KB
} // 函数返回时自动释放

栈的特点

  • 分配速度快:只需移动栈指针
  • 自动管理:无需手动释放
  • 空间有限:通常几 MB
  • 生命周期确定:与函数调用同步

栈溢出

递归过深或局部数组过大会导致栈溢出:

void recursive(int depth) {
int large_array[10000]; // 每次调用分配 40KB
if (depth > 0) {
recursive(depth - 1); // 递归调用
}
}

int main(void) {
recursive(100); // 可能栈溢出
return 0;
}

堆内存

动态内存分配函数

<stdlib.h> 提供了动态内存分配函数:

函数说明
malloc(size)分配 size 字节的未初始化内存
calloc(n, size)分配 n * size 字节的内存,初始化为 0
realloc(ptr, size)调整已分配内存的大小
free(ptr)释放动态分配的内存

malloc

malloc 分配指定字节数的内存,返回 void* 指针:

#include <stdlib.h>

int* ptr = malloc(sizeof(int)); // 分配一个 int 大小的内存
if (ptr == NULL) {
printf("内存分配失败\n");
return -1;
}

*ptr = 42;
printf("值: %d\n", *ptr);
free(ptr); // 释放内存
ptr = NULL; // 避免悬空指针

分配数组:

int* arr = malloc(10 * sizeof(int));  // 分配 10 个 int
if (arr != NULL) {
for (int i = 0; i < 10; i++) {
arr[i] = i * i;
}
free(arr);
arr = NULL;
}

calloc

calloc 分配并初始化内存为 0:

int* arr = calloc(10, sizeof(int));  // 分配 10 个 int,初始化为 0
if (arr != NULL) {
// 所有元素已经是 0
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]); // 输出 10 个 0
}
free(arr);
}

malloccalloc 的区别:

  • malloc 不初始化内存,内容是随机的
  • calloc 将内存初始化为 0
  • calloc 接受两个参数:元素个数和元素大小

realloc

realloc 调整已分配内存的大小:

int* arr = malloc(5 * sizeof(int));
if (arr == NULL) return -1;

for (int i = 0; i < 5; i++) {
arr[i] = i;
}

// 扩展到 10 个元素
int* temp = realloc(arr, 10 * sizeof(int));
if (temp == NULL) {
free(arr);
return -1;
}
arr = temp;

// 新元素未初始化
for (int i = 5; i < 10; i++) {
arr[i] = i;
}

free(arr);

realloc 的行为:

  • 如果原位置后有足够空间,直接扩展
  • 如果空间不足,分配新内存,复制数据,释放旧内存
  • 返回的指针可能与原指针不同
  • 如果 realloc 失败,原内存仍然有效

安全的 realloc 模式:

int* safe_realloc(int* ptr, size_t new_size) {
int* new_ptr = realloc(ptr, new_size);
if (new_ptr == NULL) {
free(ptr); // 失败时释放原内存
return NULL;
}
return new_ptr;
}

free

free 释放动态分配的内存:

int* ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr); // 释放内存

// 释放后
ptr = NULL; // 避免悬空指针

free 的注意事项:

  • 只能释放由 malloccallocrealloc 分配的内存
  • 不能重复释放同一块内存
  • 不能释放 NULL 指针(实际上释放 NULL 是安全的,什么都不做)
  • 释放后指针仍然指向原地址,需要手动置为 NULL

常见内存错误

内存泄漏

分配的内存没有释放,导致内存逐渐耗尽:

void leak_example(void) {
int* ptr = malloc(sizeof(int));
*ptr = 42;
// 没有 free,内存泄漏!
}

void loop_leak(void) {
for (int i = 0; i < 1000; i++) {
int* ptr = malloc(1024 * 1024); // 每次分配 1MB
// 没有 free,泄漏 1GB!
}
}

正确的做法:

void no_leak(void) {
int* ptr = malloc(sizeof(int));
if (ptr != NULL) {
*ptr = 42;
free(ptr);
}
}

重复释放

同一块内存被释放两次:

int* ptr = malloc(sizeof(int));
free(ptr);
free(ptr); // 错误!重复释放

预防方法:释放后立即置为 NULL:

int* ptr = malloc(sizeof(int));
free(ptr);
ptr = NULL;
free(ptr); // 安全,free(NULL) 什么都不做

使用已释放的内存

int* ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);

printf("%d\n", *ptr); // 错误!使用已释放的内存

越界访问

int* arr = malloc(5 * sizeof(int));
arr[5] = 10; // 错误!越界访问
arr[-1] = 10; // 错误!越界访问
free(arr);

分配失败未检查

int* ptr = malloc(1000000000000);  // 可能失败
*ptr = 42; // 如果 ptr == NULL,段错误

正确做法:

int* ptr = malloc(huge_size);
if (ptr == NULL) {
fprintf(stderr, "内存分配失败\n");
return -1;
}

动态数据结构

动态数组

实现可变大小的数组:

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

DynamicArray* array_create(size_t initial_capacity) {
DynamicArray* arr = malloc(sizeof(DynamicArray));
if (arr == NULL) return NULL;

arr->data = malloc(initial_capacity * sizeof(int));
if (arr->data == NULL) {
free(arr);
return NULL;
}

arr->size = 0;
arr->capacity = initial_capacity;
return arr;
}

void array_free(DynamicArray* arr) {
if (arr != NULL) {
free(arr->data);
free(arr);
}
}

int array_push(DynamicArray* arr, int value) {
if (arr->size >= arr->capacity) {
size_t new_capacity = arr->capacity * 2;
int* new_data = realloc(arr->data, new_capacity * sizeof(int));
if (new_data == NULL) return -1;

arr->data = new_data;
arr->capacity = new_capacity;
}

arr->data[arr->size++] = value;
return 0;
}

int array_get(DynamicArray* arr, size_t index) {
if (index >= arr->size) return -1;
return arr->data[index];
}

链表

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

Node* list_create(int data) {
Node* node = malloc(sizeof(Node));
if (node != NULL) {
node->data = data;
node->next = NULL;
}
return node;
}

void list_append(Node** head, int data) {
Node* new_node = list_create(data);
if (new_node == NULL) return;

if (*head == NULL) {
*head = new_node;
return;
}

Node* current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = new_node;
}

void list_free(Node* head) {
Node* current = head;
while (current != NULL) {
Node* next = current->next;
free(current);
current = next;
}
}

内存池

对于频繁分配释放的场景,可以使用内存池提高效率:

#define POOL_SIZE 1024

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) {
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;
}

RAII 模式模拟

C 语言没有析构函数,但可以通过一些技巧模拟 RAII:

使用 cleanup 属性(GCC/Clang)

void cleanup_free(void** ptr) {
if (ptr && *ptr) {
free(*ptr);
*ptr = NULL;
}
}

void function(void) {
__attribute__((cleanup(cleanup_free))) int* ptr = malloc(sizeof(int));
*ptr = 42;
// 函数结束时自动调用 cleanup_free(&ptr)
}

使用宏简化

#define SCOPE_MALLOC(type, name, size) \
type* name __attribute__((cleanup(cleanup_free))) = malloc(size)

void function(void) {
SCOPE_MALLOC(int, ptr, sizeof(int));
*ptr = 42;
// 自动释放
}

内存调试工具

Valgrind

Linux 下常用的内存检测工具:

gcc -g program.c -o program
valgrind --leak-check=full ./program

检测内存泄漏、越界访问等问题。

AddressSanitizer

编译器内置的内存检测工具:

gcc -fsanitize=address -g program.c -o program
./program

检测越界、使用已释放内存等问题。

自定义内存检测

#ifdef DEBUG
#define MALLOC(size) debug_malloc(size, __FILE__, __LINE__)
#define FREE(ptr) debug_free(ptr, __FILE__, __LINE__)

void* debug_malloc(size_t size, const char* file, int line) {
void* ptr = malloc(size);
printf("malloc: %p (%zu bytes) at %s:%d\n", ptr, size, file, line);
return ptr;
}

void debug_free(void* ptr, const char* file, int line) {
printf("free: %p at %s:%d\n", ptr, file, line);
free(ptr);
}
#else
#define MALLOC(size) malloc(size)
#define FREE(ptr) free(ptr)
#endif

内存管理最佳实践

1. 检查分配结果

int* ptr = malloc(size);
if (ptr == NULL) {
// 处理错误
return -1;
}

2. 释放后置空

free(ptr);
ptr = NULL;

3. 避免内存泄漏

void function(void) {
int* ptr = malloc(sizeof(int));
if (ptr == NULL) return;

// 使用 ptr...

free(ptr); // 确保所有路径都释放
}

4. 使用 const 保护数据

void process(const int* data, size_t size) {
// data 指向的内容不会被修改
}

5. 避免魔法数字

int* arr = malloc(10 * sizeof(int));  // 好
int* bad = malloc(40); // 不好,假设 int 是 4 字节

6. 使用 sizeof 运算符

int* ptr = malloc(sizeof(*ptr));  // 好,类型改变时代码自动适应
int* bad = malloc(sizeof(int)); // 可以,但如果 ptr 类型改变需要同步修改

小结

本章介绍了 C 语言的内存管理:

  • 内存布局:代码段、数据段、BSS、堆、栈
  • 栈内存:自动管理,空间有限
  • 堆内存:手动管理,灵活但需谨慎
  • 动态内存函数:malloc、calloc、realloc、free
  • 常见错误:内存泄漏、重复释放、越界访问
  • 动态数据结构:动态数组、链表
  • 内存池:提高分配效率
  • 调试工具:Valgrind、AddressSanitizer
  • 最佳实践:检查、释放、置空

下一章将学习 文件操作,了解如何读写文件。