1 引言
考虑这样一个需求:程序需要处理用户输入的一组整数,但事先不知道有多少个。如果用数组,必须事先指定大小,要么浪费空间,要么空间不足。
c
int arr[1000]; /* 可能太大浪费,也可能太小不够 */
动态内存分配可以解决这个问题:
c
#include <stdio.h> #include <stdlib.h> int main(void) { int n; printf("请输入要存储的数字个数:"); scanf("%d", &n); /* 根据输入动态分配内存 */ int *arr = (int*)malloc(n * sizeof(int)); if (arr == NULL) { printf("内存分配失败\n"); return 1; } /* 使用内存 */ for (int i = 0; i < n; i++) { arr[i] = i * i; } /* 释放内存 */ free(arr); return 0; }这就是动态内存管理的基本模式:分配 → 使用 → 释放。本章我们将深入探讨这些操作背后的机制。
2 堆与栈的区别
2.1 两个重要的内存区域
C程序的内存布局主要分为几个区域,其中与动态内存最相关的是栈和堆:
| 特性 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 分配方式 | 自动分配和释放 | 手动分配和释放(malloc/free) |
| 存放内容 | 局部变量、函数参数、返回地址 | 程序员动态分配的内存 |
| 大小限制 | 较小(通常几MB) | 较大(可达几GB,受系统内存限制) |
| 分配速度 | 非常快(移动栈指针) | 较慢(需要查找空闲块) |
| 生命周期 | 函数执行期间 | 从malloc到free |
| 碎片问题 | 无 | 长时间运行可能产生碎片 |
2.2 栈的特点
c
void func(void) { int a = 10; /* 栈上分配 */ int b = 20; /* 栈上分配 */ char str[100]; /* 栈上分配(固定大小) */ } /* 函数结束,a、b、str 自动释放 */栈内存由系统自动管理
分配释放速度极快
大小固定,过大的局部变量可能导致栈溢出
2.3 堆的特点
c
void func(void) { int *p = (int*)malloc(100 * sizeof(int)); /* 堆上分配 */ if (p != NULL) { /* 使用内存 */ free(p); /* 必须手动释放 */ } } /* 如果忘记free,p指向的内存仍然存在(内存泄漏) */堆内存需要程序员手动管理
可以分配任意大小(受系统限制)
需要手动释放,否则造成内存泄漏
2.4 什么时候用堆
数据大小在编译时未知
需要分配大块内存
数据需要在函数返回后继续存在
需要动态调整内存大小
3 malloc 函数
3.1 函数原型
c
#include <stdlib.h> void *malloc(size_t size);
功能:在堆上分配
size字节的内存参数:要分配的字节数
返回值:成功返回指向分配内存的指针;失败返回
NULL分配的内存内容:未初始化(包含垃圾值)
3.2 基本使用
c
#include <stdio.h> #include <stdlib.h> int main(void) { /* 分配能存放10个int的内存 */ int *p = (int*)malloc(10 * sizeof(int)); /* 总是检查分配是否成功 */ if (p == NULL) { printf("内存分配失败\n"); return 1; } /* 使用内存 */ for (int i = 0; i < 10; i++) { p[i] = i; /* 注意:这里p[i]的值是垃圾值,先赋值再使用 */ } /* 释放内存 */ free(p); return 0; }3.3 常见错误
c
/* 错误1:忘记检查返回值 */ int *p = malloc(1000000000); /* 可能分配失败 */ p[0] = 10; /* 如果p为NULL,程序崩溃 */ /* 错误2:分配大小错误 */ int *p = malloc(10); /* 只分配了10字节,但需要40字节(10*sizeof(int)) */ /* 错误3:使用未初始化的内存 */ int *p = malloc(10 * sizeof(int)); printf("%d\n", p[0]); /* 垃圾值!必须先初始化 */4 calloc 函数
4.1 函数原型
c
#include <stdlib.h> void *calloc(size_t nmemb, size_t size);
功能:分配
nmemb * size字节的内存,并将所有位初始化为 0参数:
nmemb元素个数,size每个元素的大小返回值:成功返回指针,失败返回 NULL
4.2 基本使用
c
#include <stdio.h> #include <stdlib.h> int main(void) { /* 分配能存放10个int的内存,并全部初始化为0 */ int *p = (int*)calloc(10, sizeof(int)); if (p == NULL) { printf("内存分配失败\n"); return 1; } /* 内存已经初始化为0 */ for (int i = 0; i < 10; i++) { printf("%d ", p[i]); /* 全部输出0 */ } printf("\n"); free(p); return 0; }4.3 malloc 与 calloc 对比
| 对比 | malloc | calloc |
|---|---|---|
| 参数 | 一个:总字节数 | 两个:个数和每个大小 |
| 初始化 | 不初始化(垃圾值) | 初始化为0 |
| 性能 | 稍快(不需要清零) | 稍慢(需要清零) |
| 使用场景 | 马上要赋值的数组 | 需要初始化为0的情况 |
c
/* 两种方式等价:分配并清零 */ int *p1 = malloc(10 * sizeof(int)); memset(p1, 0, 10 * sizeof(int)); int *p2 = calloc(10, sizeof(int)); /* 更简洁 */
5 realloc 函数
5.1 函数原型
c
#include <stdlib.h> void *realloc(void *ptr, size_t new_size);
功能:调整之前分配的内存块的大小
参数:
ptr之前分配的内存指针,new_size新的大小(字节)返回值:成功返回指向新内存的指针;失败返回 NULL(原内存不变)
5.2 行为规则
| 情况 | 行为 |
|---|---|
ptr为 NULL | 等价于malloc(new_size) |
new_size为 0 | 等价于free(ptr),返回 NULL |
| 扩大内存 | 可能移动内存位置,原有数据保留 |
| 缩小内存 | 多余部分被释放,剩余数据保留 |
5.3 基本使用
c
#include <stdio.h> #include <stdlib.h> int main(void) { /* 初始分配5个int */ int *arr = (int*)malloc(5 * sizeof(int)); if (arr == NULL) return 1; for (int i = 0; i < 5; i++) { arr[i] = i; } /* 需要扩大到10个int */ int *new_arr = (int*)realloc(arr, 10 * sizeof(int)); if (new_arr == NULL) { /* 分配失败,原内存还在,需要处理错误 */ free(arr); return 1; } /* 更新指针 */ arr = new_arr; /* 继续使用 */ for (int i = 5; i < 10; i++) { arr[i] = i; } free(arr); return 0; }5.4 重要注意事项
c
/* 错误用法:直接使用原指针 */ arr = realloc(arr, 20 * sizeof(int)); /* 如果失败,arr 变成 NULL,原内存丢失! */ /* 正确用法:使用临时变量 */ int *tmp = realloc(arr, 20 * sizeof(int)); if (tmp != NULL) { arr = tmp; /* 成功才更新指针 */ } else { /* 处理失败,arr 仍然指向原内存 */ }6 free 函数
6.1 函数原型
c
#include <stdlib.h> void free(void *ptr);
功能:释放之前动态分配的内存
参数:
ptr必须是之前由 malloc/calloc/realloc 返回的指针返回值:无
6.2 基本使用
c
int *p = (int*)malloc(10 * sizeof(int)); /* 使用 p */ free(p); /* 释放内存 */ p = NULL; /* 好习惯:置空,防止野指针 */
6.3 重要规则
c
/* 规则1:只能释放动态分配的内存 */ int a; free(&a); /* 错误!不是动态分配的 */ /* 规则2:不能重复释放 */ int *p = malloc(100); free(p); free(p); /* 错误!重复释放 */ /* 规则3:释放后不能使用 */ int *p = malloc(100); free(p); *p = 10; /* 错误!悬空指针,未定义行为 */
7 内存泄漏
7.1 什么是内存泄漏
内存泄漏(Memory Leak)是指程序动态分配的内存不再使用后,没有释放,导致这部分内存无法被回收和重用。
c
void leak_example(void) { int *p = (int*)malloc(100 * sizeof(int)); /* 忘记调用 free(p) */ } /* 函数返回,p 丢失,100*sizeof(int) 字节的内存泄漏 */7.2 内存泄漏的后果
程序占用的内存不断增长
系统可用内存减少
长期运行的程序可能耗尽内存而崩溃
对于服务器程序尤其致命
7.3 常见的内存泄漏场景
场景1:忘记释放
c
void func(void) { int *p = malloc(1000); /* 使用 p,但最后没有 free */ }场景2:丢失指针
c
int *p = malloc(100); p = malloc(200); /* 第一个100字节的指针丢失,无法释放 */ free(p); /* 只释放了第二个 */
场景3:异常路径
c
int *p = malloc(100); if (some_condition) { return; /* 直接返回,忘记释放 */ } free(p);场景4:循环内分配
c
for (int i = 0; i < 1000; i++) { int *p = malloc(1000); /* 使用 p,但忘记释放 */ } /* 循环1000次,泄漏1MB */7.4 如何避免内存泄漏
原则1:谁分配,谁释放
明确每个动态内存的责任人
在分配后立即考虑释放时机
原则2:成对出现
c
/* malloc 和 free 应该成对出现 */ int *p = malloc(...); /* ... */ free(p);
原则3:使用后立即释放
c
{ int *p = malloc(...); /* 使用 p */ free(p); /* 用完就释放 */ }原则4:异常路径处理
c
int *p = malloc(100); if (p == NULL) return -1; if (some_condition) { free(p); /* 释放后返回 */ return -1; } /* 正常处理 */ free(p); return 0;原则5:释放后置空
c
free(p); p = NULL; /* 防止悬空指针 */
8 内存泄漏检测
8.1 人工检查
代码审查:检查 malloc/calloc/realloc 与 free 是否成对
静态分析工具:如 cppcheck, splint
8.2 使用 Valgrind(Linux)
Valgrind 是 Linux 下强大的内存调试工具:
bash
# 编译程序(建议加 -g 选项包含调试信息) gcc -g program.c -o program # 用 valgrind 运行 valgrind --leak-check=full ./program
示例输出:
text
==12345== 100 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==12345== at 0x4C2B800: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==12345== by 0x40054E: main (test.c:5)
8.3 使用 AddressSanitizer(GCC/Clang)
AddressSanitizer 是编译器的内存错误检测工具:
bash
# 编译时添加 -fsanitize=address 选项 gcc -g -fsanitize=address program.c -o program # 运行程序,会直接报告内存错误 ./program
8.4 简单的手动追踪
c
#include <stdio.h> #include <stdlib.h> /* 简单的调试宏 */ #define DEBUG_MALLOC(size) \ ({ void *p = malloc(size); \ printf("malloc %zu bytes at %p\n", size, p); \ p; }) #define DEBUG_FREE(p) \ do { printf("free %p\n", p); free(p); } while(0) int main(void) { int *p = DEBUG_MALLOC(100 * sizeof(int)); /* ... */ DEBUG_FREE(p); return 0; }9 综合示例:动态数组
c
#include <stdio.h> #include <stdlib.h> typedef struct { int *data; int size; int capacity; } DynamicArray; /* 初始化动态数组 */ void array_init(DynamicArray *arr, int initial_capacity) { arr->data = (int*)malloc(initial_capacity * sizeof(int)); arr->size = 0; arr->capacity = arr->data ? initial_capacity : 0; } /* 添加元素 */ int array_push(DynamicArray *arr, int value) { if (arr->size >= arr->capacity) { /* 需要扩容 */ int new_capacity = arr->capacity == 0 ? 4 : arr->capacity * 2; int *new_data = (int*)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; } /* 释放数组 */ void array_destroy(DynamicArray *arr) { free(arr->data); arr->data = NULL; arr->size = arr->capacity = 0; } /* 打印数组 */ void array_print(const DynamicArray *arr) { printf("["); for (int i = 0; i < arr->size; i++) { printf("%d ", arr->data[i]); } printf("]\n"); } int main(void) { DynamicArray arr; array_init(&arr, 4); if (arr.data == NULL) { printf("初始化失败\n"); return 1; } /* 添加10个元素(会触发自动扩容) */ for (int i = 0; i < 10; i++) { array_push(&arr, i * i); } array_print(&arr); printf("size=%d, capacity=%d\n", arr.size, arr.capacity); /* 释放内存 */ array_destroy(&arr); return 0; }10 本章小结
本章系统介绍了动态内存管理:
1. 堆与栈的区别
栈:自动管理,速度快,大小有限
堆:手动管理,灵活,大小几乎不限
2. 动态内存函数
| 函数 | 功能 | 特点 |
|---|---|---|
malloc | 分配内存 | 不初始化,速度快 |
calloc | 分配并清零 | 初始化为0,稍慢 |
realloc | 调整大小 | 可能移动内存 |
free | 释放内存 | 必须配对使用 |
3. 使用规则
总是检查返回值是否为 NULL
分配大小要正确计算(
n * sizeof(type))释放后置空指针
不要重复释放
只释放动态分配的内存
4. 内存泄漏
忘记释放不再使用的内存
丢失指向内存的指针
异常路径未释放
长期运行程序危害严重
5. 检测方法
代码审查
Valgrind(Linux)
AddressSanitizer
手动追踪
6. 最佳实践
谁分配谁释放
释放后置空
异常路径处理
封装管理逻辑(如动态数组)