在本章中,我们将介绍 UNIX 操作系统的内存分配接口。
如何分配和管理内存在 UNIX/C 程序中,通常使用哪些接口?哪些错误需要避免?
内存类型
C 程序分配内存主要分为两种类型:
栈内存 (Stack)
- 管理方式:由编译器隐式管理(申请与释放),也称自动 (Automatic) 内存。
- 生命周期:随函数调用而生,随函数退出而灭。
void func() {
int x; // 在栈上声明一个整数
...
}堆内存 (Heap)
- 管理方式:由程序员显式管理。
- 生命周期:由程序员控制,直到显式释放。适合长期存储或动态大小的数据。
void func() {
// 在堆上申请空间,并将地址存入栈变量 x
int *x = (int *) malloc(sizeof(int));
...
}栈与堆的协作在执行
int *x = malloc(sizeof(int));时:
- 栈:分配一个指针变量
x的空间。- 堆:
malloc在堆上请求空间并返回地址。- 协作:将堆地址存储在栈变量
x中。
堆内存的显式管理虽然灵活,但也带来了巨大的挑战,这是本章讨论的重点。
malloc() 调用
malloc() 用于在堆上申请指定大小的内存空间。它成功就返回一个指向新申请空
间的指针,失败就返回 NULL。
#include <stdlib.h>
...
void *malloc(size_t size); double *d = (double *) malloc(sizeof(double));善用 sizeof()
malloc() 调用通常配合 sizeof() 操作符使用,以确保申请正确大小的空间。
- 编译时操作:
sizeof()在编译时计算大小(如double为 8 字节),因此它是一个操作符而非函数。 - 指针 vs 数组:对指针使用
sizeof()返回的是指针本身的大小(4 或 8 字节),而非其指向的动态分配空间。只有对静态声明的数组(如int x[10]),sizeof()才会返回整个数组的大小。 - 字符串处理:为字符串分配空间应使用
malloc(strlen(s) + 1),以包含结尾的空字符\0。
强制类型转换 (Casting)
malloc() 返回 void * 指针。在 C 中,将结果强制转换为目标类型(如 (double *))并非语法必须,但能提高代码的可读性,明确告知编译器和后续维护者你的意图。
// 示例:分配 10 个整数的空间
int *x = (int *) malloc(10 * sizeof(int)); free() 调用
分配内存相对简单,难点在于知道何时、如何以及是否释放内存。
int *x = malloc(10 * sizeof(int));
...
free(x); - 参数:
free()仅接收由malloc()返回的指针。 - 大小追踪:用户无需传入释放空间的大小,内存分配库会自动追踪并管理每个指针对应的内存块大小。
常见错误
在 C 语言中,正确的内存管理至关重要。即使程序能编译并通过简单测试,也不代表它是正确的。
1. 忘记分配内存
许多函数(如 strcpy)要求目标指针已指向有效的内存空间。
char *src = "hello";
char *dst; // 错误:未分配空间
strcpy(dst, src); // 导致段错误 (Segmentation Fault)修复:使用 malloc(strlen(src) + 1) 为 dst 分配空间,或直接使用 strdup(src)。
2. 分配内存不足(缓冲区溢出)
char *src = "hello";
char *dst = (char *) malloc(strlen(src)); // 错误:漏掉了结尾的 '\0'
strcpy(dst, src); 这种错误可能不会立即导致崩溃,但会覆盖相邻内存,造成难以调试的安全漏洞。
3. 忘记初始化内存
从堆中读取未初始化的数据(Uninitialized Read)会导致程序行为不可预测。
4. 内存泄露 (Memory Leak)
忘记释放不再使用的内存。对于长期运行的程序(如服务器或内核),这会导致内存耗尽。
进程退出时的内存回收当进程退出时,操作系统会回收其占用的所有内存页。因此,短时间运行的程序即使不
free也不会造成物理内存永久丢失,但养成显式释放的习惯依然是好的实践。
5. 悬挂指针 (Dangling Pointer)
在内存释放后继续使用它。这可能导致程序崩溃或数据损坏。
6. 重复释放 (Double Free)
对同一个指针调用两次 free()。其行为是未定义的,通常会导致分配库崩溃。
7. 错误调用 free()
free() 只能接收由 malloc() 等系列函数返回的原始指针。传入任何其他地址都会导致无效释放(Invalid Free)。
底层操作系统支持
malloc() 和 free() 是标准库调用,而非系统调用。它们建立在以下系统调用之上:
brk/sbrk:用于改变程序分断(break)的位置,即堆结束的位置,从而增加或减小堆的大小。mmap():可以创建一个“匿名”内存区域,不与特定文件关联,常用于分配较大的内存块。
Warning不要直接调用
brk或sbrk,应始终通过malloc()和free()进行内存管理。
其他内存 API
calloc(size_t nmemb, size_t size):分配内存并自动将其初始化为零。realloc(void *ptr, size_t size):调整已分配内存块的大小。它通常会申请一块更大的新区域,将旧数据拷贝过去,并返回新指针。
小结
本章介绍了 UNIX/C 中基本的内存分配接口。理解 malloc 与 free 的协作、规避常见的内存错误(如泄露、溢出、悬挂指针)是编写健壮 C 程序的基石。更多高级调试工具和自动化检测方法可参考相关文献(如 Valgrind 等工具的原理)。