0Pricing
C Academy · 课时

对齐与拆分

让内存块可用且整齐。

对齐与拆分 是 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. malloc 的工作原理
  2. 简单的碰撞分配器
  3. 空闲列表与复用
  4. 对齐与拆分
← 返回 C Academy