malloc(memory allocate)是 C 标准库中用于动态分配内存的函数,当你调用 malloc(size) 时,它会向操作系统请求一块连续的、指定大小的内存空间,并返回一个指向这块内存起始地址的指针。

理解 malloc 的实现,需要从两个层面来看:
- 应用程序层面:我们如何使用
malloc,以及它做了什么。 - 操作系统/标准库内部层面:
malloc是如何管理内存、找到可用内存并返回给我们的。
应用程序如何使用 malloc
这是最直接的层面。malloc 的函数原型在 stdlib.h 中:
void* malloc(size_t size);
size_t size: 你需要分配的内存大小,以字节为单位。void*: 返回一个通用指针,你可以将其转换为任何类型的指针(如int*,char*,struct*等)。
使用示例:
#include <stdio.h>
#include <stdlib.h> // 必须包含此头文件
int main() {
int n = 10;
// 1. 声明一个整型指针
int *numbers;
// 2. 调用 malloc 分配 10 个 int 大小的内存
// sizeof(int) 获取一个 int 所占的字节数
// malloc 返回 void*,我们将其转换为 int*
numbers = (int*) malloc(n * sizeof(int));
// 3. **至关重要的检查**:malloc 可能会失败(例如内存不足),此时返回 NULL
if (numbers == NULL) {
printf("内存分配失败!\n");
return 1; // 返回错误码
}
// 4. 使用分配好的内存
for (int i = 0; i < n; i++) {
numbers[i] = i * 10;
}
// 5. 打印内容
for (int i = 0; i < n; i++) {
printf("%d ", numbers[i]);
}
printf("\n");
// 6. **必须释放内存**:使用完毕后,调用 free 将内存归还给系统
free(numbers);
return 0;
}
核心要点:

- 检查返回值:
malloc可能会因为内存不足而失败,此时返回NULL,直接使用NULL指针会导致程序崩溃(段错误)。 - 手动管理:
malloc分配的内存在程序结束前不会自动释放,你必须调用free()函数来释放它,否则会导致内存泄漏。 - 大小计算:计算所需大小时,通常使用
元素个数 * sizeof(元素类型)。 free的参数:free的参数必须是malloc返回的原始指针,如果你对指针进行了算术运算(如p+1),free(p+1),结果是未定义的。
malloc 的内部实现原理
这才是问题的核心。malloc 并不是一个简单的系统调用,它是一个复杂的内存管理器,其内部工作流程通常如下:
内存池
malloc 不会每次都向操作系统申请内存,频繁地与操作系统交互(系统调用)开销很大。malloc 会一次性向操作系统申请一大块内存,作为自己的“内存池”或“堆”,之后,所有的内存分配和释放都在这个内存池内部进行。
这块内存池通常被称为 Heap。
内存块管理
为了管理这块内存池,malloc 会将内存划分为一系列的“块”(Blocks),每个块包含两部分信息:
- 元数据:关于这块内存本身的信息,
- 块的大小(包括元数据本身)
- 块是否已被分配
- 指向下一个块的指针(用于链表管理)
- 有效载荷:实际分配给用户使用的内存区域。
元数据通常存储在有效载荷的前面(或后面),形成一个链表。
分配算法
当调用 malloc(size) 时,malloc 内部会执行以下步骤:
步骤 1:调整请求大小
- 用户请求的
size可能不够存放元数据。malloc会将size调整为一个对齐后的最小值,以确保有效载荷的起始地址满足特定类型(如int,double)的对齐要求,提高访问效率。 - 会加上元数据的大小,得到最终的块大小。
步骤 2:搜索空闲块
malloc 会遍历它的空闲块链表,寻找一个足够大的空闲块来满足请求,主要有三种搜索策略:
-
首次适应法:
- 从链表头部开始,找到第一个大小足够的空闲块就立即返回。
- 优点:速度快,倾向于把小的空闲块留在链表尾部,有利于大块内存的分配。
- 缺点:容易在链表头部产生很多小的“碎片”。
-
最佳适应法:
- 遍历整个链表,找到大小最合适(即大于等于请求大小的最小空闲块)的块。
- 优点:内存利用率高,产生的碎片小。
- 缺点:速度慢,且容易产生大量无法利用的“小碎片”。
-
最差适应法:
- 总是使用链表中最大的空闲块。
- 优点:可以避免产生太多的小碎片。
- 缺点:大块内存被迅速切小,导致后续需要大块内存分配时失败。
现代的 malloc 实现(如 glibc 中的 ptmalloc)会结合多种策略,并根据使用情况进行优化。
步骤 3:处理找到的块
根据找到的空闲块大小,有两种情况:
-
情况 A:找到的块大小 略大于 请求大小
- 为了避免浪费,
malloc会将这块内存分割。 - 它会把这块内存分成两部分:
- 一部分是恰好满足请求大小的块,将其标记为“已分配”,返回给用户。
- 剩余的部分作为一个新的空闲块,放回空闲链表中。
- 为了避免浪费,
-
情况 B:找到的块大小 远大于 请求大小
- 为了提高效率,
malloc可能不会进行分割,而是直接将整个大块返回给用户,虽然会有一些内部碎片,但减少了链表操作的开销。
- 为了提高效率,
-
情况 C:没有找到足够大的空闲块
- 如果遍历完整个空闲链表都找不到合适的块,
malloc就会向操作系统申请更多的内存(例如通过sbrk或mmap系统调用)。 - 获得新内存后,将其加入内存池,然后再次尝试分配。
- 如果遍历完整个空闲链表都找不到合适的块,
返回指针
malloc 返回的是有效载荷区域的起始地址,而不是包含元数据的整个块的起始地址,用户只能看到和使用这个指针,对元数据部分一无所知。
释放内存 (free) 的工作原理
free(ptr) 的工作流程相对简单但同样重要:
- 定位块:
free接收用户传来的指针ptr,通过这个指针,malloc内部可以计算出整个内存块(包括元数据)的起始地址。 - 标记为空闲:它找到对应的块,将其状态从“已分配”改为“空闲”。
- 合并空闲块(关键!):
free不会立即把内存还给操作系统,它会检查相邻的内存块是否也是空闲的。- 如果是,它会将当前块与前一个或后一个空闲块合并,形成一个更大的空闲块。
- 这个合并操作是解决内存碎片问题的关键,它能减少内存浪费,提高后续
malloc的成功率。
- 归还内存(可选):当某个空闲块变得足够大,并且在一段时间内没有被使用,
malloc可能会选择将其归还给操作系统,以减少程序的内存占用。
一个简化的 malloc 实现示例
下面是一个极度简化的、用于演示核心思想的 my_malloc 和 my_free 的实现。它不安全,不高效,仅用于教学。
#include <stdio.h>
#include <unistd.h> // for sbrk
// 简化的块结构(元数据)
typedef struct block {
size_t size; // �的大小(包括这个结构体)
int free; // 是否空闲
struct block *next; // 指向下一个块
} Block;
Block *heap_list = NULL; // 空闲块链表的头指针
// 简化的 malloc
void* my_malloc(size_t size) {
if (size == 0) return NULL;
// 1. 调整大小,并加上元数据大小
size_t total_size = sizeof(Block) + size;
// 简单对齐到 8 字节
total_size = (total_size + 7) & ~7;
// 2. 遍历链表寻找合适的空闲块
Block *current = heap_list;
while (current) {
if (current->free && current->size >= total_size) {
// 找到合适的块,这里不分割,直接返回
current->free = 0;
return (void*)(current + 1); // 返回有效载荷的地址
}
current = current->next;
}
// 3. 没找到,向操作系统申请内存
Block *new_block = (Block*) sbrk(total_size);
if (new_block == (void*)-1) {
return NULL; // sbrk 失败
}
new_block->size = total_size;
new_block->free = 0;
new_block->next = NULL;
// 将新块加入链表
if (heap_list == NULL) {
heap_list = new_block;
} else {
current = heap_list;
while (current->next) {
current = current->next;
}
current->next = new_block;
}
return (void*)(new_block + 1); // 返回有效载荷的地址
}
// 简化的 free
void my_free(void *ptr) {
if (ptr == NULL) return;
// 1. 找到对应的块
Block *block_to_free = (Block*)ptr - 1;
// 2. 标记为空闲
block_to_free->free = 1;
// 3. 合并相邻的空闲块 (简化版,只合并下一个)
Block *next_block = block_to_free->next;
if (next_block && next_block->free) {
block_to_free->size += next_block->size;
block_to_free->next = next_block->next;
}
}
int main() {
int *p = (int*) my_malloc(sizeof(int) * 5);
if (p) {
p[0] = 100;
printf("p[0] = %d\n", p[0]);
my_free(p);
}
return 0;
}
| 特性 | 描述 |
|---|---|
| 核心功能 | 在堆上动态分配指定大小的连续内存。 |
| 返回值 | 指向分配内存起始地址的 void* 指针,失败时返回 NULL。 |
| 内存管理 | 手动管理,程序员必须调用 free() 释放内存,否则造成内存泄漏。 |
| 内部机制 | 通过内存池(堆)减少系统调用开销;使用链表管理空闲内存块;通过分割和合并技术来管理内存。 |
| 关键算法 | 首次适应、最佳适应等,用于在空闲块链表中查找合适的块。 |
| 性能考虑 | 分配速度、内存利用率、内部/外部碎片是 malloc 实现需要权衡的关键点。 |
现代的 malloc 实现(如 glibc 的 ptmalloc)非常复杂,它引入了多线程锁、多级分离的空闲链表、mmap 机制来处理大块内存等高级优化,但其核心思想仍然是:管理一个内存池,高效地分割和合并内存块。
