K&R C语言8.7节中有一个内存动态分配的代码,涉及到向上取整,最开始不懂为什么这样写,后来弄明白之后想把思路分享出来:
nunits = (nbytes + sizeof(Header) - 1) / sizeof(Header) + 1;
union Header base; // 定义 base 变量
union Header *freep = NULL; // 空闲列表指针,初始为 NULL
void *malloc(unsigned nbytes) {
Header *p, *prevp;
Header *morecore(unsigned);
unsigned nunits;
// 第一次调用 malloc 时初始化
if ((prevp = freep) == NULL) {
base.s.ptr = freep = prevp = &base;
base.s.size = 0;
}
// 计算所需的单元数,包括头部
nunits = (nbytes + sizeof(Header) - 1) / sizeof(Header) + 1;
// 遍历空闲列表找到合适的块
for (p = prevp->s.ptr; ; prevp = p, p = p->s.ptr) {
if (p->s.size >= nunits) { // 找到足够大的块
if (p->s.size == nunits) // 恰好大小相等
//当前块的下一个块赋给上一个块的下一个块,跳过了当前块
prevp->s.ptr = p->s.ptr;
else { // 分割块
p->s.size -= nunits;
//指针前移,指向分割后的新分配块的新位置
p += p->s.size;
p->s.size = nunits;
}
freep = prevp;
//+1跳过头部信息,指向实际数据区
return (void *)(p + 1);
}
if (p == freep) // 没有找到合适的块
if ((p = morecore(nunits)) == NULL)
return NULL;
}
}
代码:
nunits = (nbytes + sizeof(Header) - 1) / sizeof(Header) + 1;
其中nunits
是计算所需的内存块大小,nbytes
是用户请求的字节数,sizeof(Header)
是头部的大小,通常内存分配中除了用户请求的数据以外,还包括一些元数据,用于管理这些内存,其中包含指向下一个空闲块的指针以及该内存块的大小,它们被定义在头部Header
中。且头部的大小通常被视为满足这种对齐要求。
这也就解释了为什么内存块数目的计算需要除头部的大小。
再看等式右边,(nbytes + sizeof(Header) - 1) / sizeof(Header)
,正常求内存块的大小一般会是nbytes / sizeof(Header)
,但是这种方式会被向下取整,与预期不符。
比如用户请求17
个字节的内存大小,Header
大小为8
,17 / 8 = 2
,实际上按照对齐要求,应该分配3
个内存块才对。那么就考虑给nbytes
加上一个Header
的大小,即(nbytes + sizeof(Header)) / sizeof(Header)
本例中就为(17 + 8) / 8 = 3
,看到这里可能会想那为什么还要-1
呢,不-1
同样可以按照向下取整计算出正确的结果。
这个问题就要考虑到后面的+1
操作了,+1
是为了在请求到能容纳用户所需数据大小的块数目之外,还要保留一个Header
的块,但是当用户请求的数据大小是Header
大小的整数倍的话,比如用户请求24
个字节,Header
大小为8,那么按照不-1
的情况来计算,(nbytes + sizeof(Header)) / sizeof(Header) + 1
就是(24 + 8) / 8 + 1 = 5
,实际上我们只需要4
个内存块(三个数据块和一个Header
块),-1
就排除了这种情况的发生。
所以最终的公式就可以归纳为:
int n = (N - 1) / M + 1;