背景
写这篇博客的起因是在B站看到了一个视频,于是突然想起我之前写的一个动态内存分配器中也使用了类似的方法,于是写篇博客记录一下。
在动态内存分配器中定义了如下的一个结构体:
typedef uint64_t sf_header;
typedef sf_header sf_footer;
/*
* Structure of a block.
* The first field of this structure is actually the footer of the *previous* block.
*/
typedef struct sf_block {
sf_footer prev_footer; // NOTE: This actually belongs to the *previous* block.
sf_header header; // This is where the current block really starts.
union {
/* A free block contains links to other blocks in a free list. */
struct {
struct sf_block *next;
struct sf_block *prev;
} links;
/* An allocated block contains a payload (aligned), starting here. */
char payload[0]; // Length varies according to block size.
} body;
} sf_block;
可以看到,在结构体 sf_block
中定义了一个联合体 body
,之所以要定义为联合体而不是直接定义为 char payload[0];
,是因为该动态内存分配器需要支持显式空闲链表(双向链表)来提升性能。问题是,联合体 body
中为什么要定义一个长度为0的数组呢?
解释
在实际使用中,动态内存分配器中块的大小是未知的,而结构体类型在定义后它的大小就确定了,为了能够使用结构体类型来描述这种大小未知的块,可以在结构体中定义一个长度为0的数组,为了进一步说明,看一个例子。
// 申请大小为1024个字节的动态内存,注意这个大小是指块中的负载大小,而不包括头部和脚部
sf_block* blk = (sf_block*) sbrk(sizeof(sf_footer) + sizeof(sf_header) + 1024);
// 访问这1024个字节
// blk->body.payload[0],访问第一个字节
// blk->body.payload[1],访问第二个字节
// blk->body.payload[2],访问第三个字节
// 负载的首地址为 blk->body.payload 或者 &blk->body
那为什么要使用 char payload[0];
,而不是直接在结构体中定义一个 payload
的指针呢?使用 char payload[0];
至少有以下三点好处
- 省了一个指针的内存占用(在我的例子中由于需要支持显式空闲链表而并没有体现这个好处)
- 因为是结构体,内存是分配在一起的而不是分开的,减少cache失效
- 分配和释放内存是很消耗性能的操作,如果用指针将需要两次分配两次释放,而这个只需要一次