动态内存管理
00:00
malloc/calloc/realloc/free详解
概述
C语言提供了手动管理动态内存的能力,通过 malloc、calloc、realloc 和 free 四个函数实现。动态内存从堆(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 可以检测:堆缓冲区溢出、栈缓冲区溢出、使用已释放内存、内存泄漏、双重释放等。