内存管理
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);
}
malloc 与 calloc 的区别:
malloc不初始化内存,内容是随机的calloc将内存初始化为 0calloc接受两个参数:元素个数和元素大小
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 的注意事项:
- 只能释放由
malloc、calloc、realloc分配的内存 - 不能重复释放同一块内存
- 不能释放 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
- 最佳实践:检查、释放、置空
下一章将学习 文件操作,了解如何读写文件。