union obj{
union obj* free_list_link;
char client_data[1];
};
原书中对这个 union 结构的解释是:由于 union 之故,从其第一个字段观之,obj 可视为一个指针,指向相同形式的另一个 obj。从其第二个字段观之,obj 可视为一个指针,指向实际区块。一物二用的结果是,不会为了维护链表所必须的指针而造成内存的另一种浪费。
上半句:从其第一个字段观之,obj 可视为一个指针,指向相同形式的另一个 obj 毋庸置疑,而后半句却是令人感到困惑,因为第二个字段本身是一个长度为 的 char 类型数组 client_data ,它为何能指向一个实际大小可能不止 的区块呢?况且,使用 union 结构为什么就能够避免内存的浪费呢?
回答上面的问题之前,首先回顾一下 union 联合结构的知识:
union
“联合”是一种特殊的类,也是一种构造类型的数据结构。在一个“联合”内可以定义多种不同的数据类型, 一个被说明为该“联合”类型的变量中,允许装入该“联合”所定义的任何一种数据(但仅为其中的一种),这些数据共享同一段内存,以达到节省空间的目的。 这是一个非常特殊的地方,也是联合的特征。另外,同struct一样,联合默认访问权限也是公有的,并且,也具有成员函数。
简而言之,union 所有的成员共用一个空间,并且同一时间只能储存其中一个成员变量的值。这也就导致 union 的实际字节大小为联合中类型字节数最多的变量的类型长度的整数倍。例如:
#include <iostream>
union obj {
union obj* free_lists_link;
char client_data[1];
};
int main(){
std::cout << "size of union obj:" << sizeof(obj) << std::endl;
return 0;
}
结果为 。因为 位系统里,指针的大小为 个字节,而第二个字段 char 数组大小为 个字节。
为什么节省内存?
首先回顾一下数据结构中链表的知识,如果要使用链表形式来记录内存中存在的内存碎片,常规方法下得在每块内存碎片中牺牲额外的内存来保存一个指针,用于指向下一个节点,这实际上造成了另一种额外负担,因为对于本身大小就有限的内存碎片而言,保存指针的开销实际上也显得尤为昂贵。
// 常规方法下,使用结构体来声明链表节点,例如
struct node{
struct node* next;
char data;
};
这种方式下,对于一块内存而言,无论是否空闲,指针和数据域是同时存在的。回到二级空间配置器的例子上来,free-lists 本身只是为了记录空闲的空间碎片,对于已分配使用的空间碎片而言,不应该再分配指针域造成额外负担。于是使用 union 的思想就产生了。
- 对于空闲内存碎片,union 使用第一个字段来维护 free-lists 链表;
- 而对于已分配使用的内存碎片,union 使用第二个字段来记录数据。
那么问题来了,char client_data[1] 声明了一个长度为 的 char 数组为什么能指向实际的内存区块。
事实上,从原书的后半句:从其第二个字段观之,obj 可视为一个指针,指向实际区块 已经可以看出端倪:我们并不需要给出内存碎片实际的大小的 client_data 数组,而是需要给出这块内存碎片的内存首地址,而这个数组 client_data 很好的实现了这样的目的。
你可能还会疑惑,仅凭内存首地址,理论上还需要知道内存块长度才能安全地对内存碎片进行使用。事实上,free-lists 本身就是一个按照小额区块实际大小划分的等价类 ,况且实际内存管理中,系统使用 cookie 域(也是内存块上分配的额外空间)用于记录内存的大小,所以内存碎片的大小是已知的。
正如原书上的图2-4所示: