对齐与拆分
让内存块可用且整齐。
对齐与拆分 是 CoddyKit 上的免费 C Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C Academy 课程共包含 4 节课。
为什么对齐很重要
每种类型都有对齐要求:它的地址必须是某个 2 的幂的倍数。double 通常需要 8 字节对齐。
由于 malloc 不知道您要存储什么,它必须返回适用于对齐要求最严格的类型的指针;在 64 位系统上通常是 16 字节。
#include <stdalign.h>
/* the strictest fundamental alignment */
size_t strict = alignof(max_align_t); /* often 16 */align_up 技巧
将大小向上取整到 2 的幂对齐值的下一个倍数,是一种经典的位运算技巧:加上 a - 1,然后屏蔽低位。
这只有在 a 是 2 的幂时才有效,而实际使用的对齐值都是如此。
static size_t align_up(size_t n, size_t a) {
return (n + a - 1) & ~(a - 1);
}
/* align_up(13, 8) == 16, align_up(16, 8) == 16 */验证数学运算
让我们通过一个可运行的程序验证 align_up。它会将几个大小向上取整到 8 字节和 16 字节边界,并打印结果。
#include <stdio.h>
#include <stddef.h>
static size_t align_up(size_t n, size_t a) {
return (n + a - 1) & ~(a - 1);
}
int main(void) {
printf("%zu\n", align_up(13, 8)); /* 16 */
printf("%zu\n", align_up(16, 8)); /* 16 */
printf("%zu\n", align_up(1, 16)); /* 16 */
printf("%zu\n", align_up(33, 16)); /* 48 */
return 0;
}对齐请求的大小
在分配器内部,每个请求的第一步都是将请求大小向上取整到对齐值。这样可以保证下一个 block 也从对齐的位置开始。
填充字节属于内部碎片,是实现通用对齐所付出的代价。
#define ALIGN 16
void *my_alloc(size_t size) {
size = align_up(size, ALIGN);
/* now find a block of this aligned size */
/* ... */
return NULL;
}整 block 重用的问题
上一课中,即使请求很小,我们也会返回整个空闲 block。将一个 4000 字节的空闲 block 分配给 16 字节的请求,会浪费其余空间。
拆分会将 block 分成我们使用的部分和继续保持空闲的剩余部分。
拆分 block
如果选中的 block 远大于所需大小,我们会在请求区域的末尾放置一个新的标头。剩余部分会成为链表中较小的空闲 block。
只有当剩余部分足够容纳一个标头和一些有效载荷时,我们才会进行拆分,否则这个碎片没有用处。
void split(block_t *b, size_t size) {
size_t rem = b->size - size;
if (rem < sizeof(block_t) + ALIGN) return; /* too small */
block_t *nb = (block_t *)((char *)(b + 1) + size);
nb->size = rem - sizeof(block_t);
nb->free = 1;
nb->next = b->next;
b->size = size;
b->next = nb;
}分配、对齐、拆分
现在,完整的分配流程会先对齐大小,找到合适的 block,拆出任何较大的剩余部分,然后将 block 标记为已使用。
这样可以让 block 的大小更加紧凑,并留下可用的空闲空间。
void *my_alloc(size_t size) {
size = align_up(size, ALIGN);
block_t *b = first_fit(size);
if (!b) return NULL;
split(b, size);
b->free = 0;
return (void *)(b + 1);
}保持内存池本身对齐
为了保证正确性,内存池的起始地址和标头大小也应遵守对齐要求,这样每个有效载荷都会落在边界上。
在静态缓冲区上使用 alignas,可以保证整个方案从一开始就是对齐的。
#include <stdalign.h>
alignas(16) static unsigned char pool[4096];
/* pool now begins on a 16-byte boundary */一个可运行的对齐分配示例
该程序从一个对齐的内存池中进行分配,并打印返回地址对 16 取模的结果,显示结果始终为零,也就是始终正确对齐。
#include <stdio.h>
#include <stddef.h>
#include <stdalign.h>
static size_t align_up(size_t n, size_t a){ return (n+a-1)&~(a-1); }
alignas(16) static unsigned char pool[1024];
static size_t off = 0;
void *alloc16(size_t s){ off=align_up(off,16); if(off+s>sizeof(pool)) return NULL; void*p=&pool[off]; off+=s; return p; }
int main(void){
void *a = alloc16(1);
void *b = alloc16(20);
printf("a %% 16 = %lu\n", (unsigned long)((size_t)a % 16));
printf("b %% 16 = %lu\n", (unsigned long)((size_t)b % 16));
return 0;
}拆分与合并
拆分和合并互为相反操作,彼此保持平衡。分配会将大 block 向下拆分;释放会将小 block 向上合并。
二者结合后,同一个内存池就能服务于不断变化的请求大小组合,而不会泄漏空间或永久碎片化。
从示例到实际分配器
现在您已经掌握四个支柱:内存来源、对齐的标头、带合并功能的空闲链表重用,以及拆分。实际的分配器还会加入大小类别分箱、线程缓存和 OS 集成,以提升速度和扩展能力。
但您在这里构建的核心思想支撑着每一种 malloc 实现。
快速检查
思考为什么要在分配过程中拆分 block。
回顾
对齐会使用位运算技巧 (n + a - 1) & ~(a - 1) 将大小向上取整,使每个有效载荷都适用于任何类型。拆分会将过大的空闲 block 切分为已使用部分和空闲剩余部分,而合并会在释放时执行相反的操作。
这四个支柱共同构成了一个完整且正确的内存分配器。
常见问题解答
「对齐与拆分」课时是免费的吗?
是的 — 「对齐与拆分」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C Academy 课程的其余内容,请升级到 CoddyKit PRO。 C Academy 课程共包含 4 节课。
「对齐与拆分」这节课中我会学到什么?
让内存块可用且整齐。 你通过在浏览器中直接运行的动手代码来练习 C Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「对齐与拆分」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C Academy 课中编写并运行代码吗?
能。每节 C Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。