在Windows驱动相关编程中,会用到该结构。Windows的源代码中大量使用了该结构。该结构用来组成常见的数据结构——双链表,并且带有头部节点。带头部节点的链表相对于不带头部节点的链表简化了一些链表操作,主要是插入和删除。
LIST_ENTRY
结构如下:
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;
这是一个最简单的双链表节点,只保存了下一个节点指针Flink(Follow Link)
和前一个节点指针Blink(Before Link)
。我们自己使用链表时会在声明时加入数据域,同时指针域为我们自己的节点类型,例如:
typedef struct _NODE {
int data; // 数据域
struct _NODE *Flink; // 后节点指针
struct _NODE *Blink; // 前节点指针
}NODE;
两者一对比,就让我们产生一个疑问,LIST_ENTRY
如何加入数据域?
这也是LIST_ENTRY
设计的巧妙处所在。LIST_ENTRY
结构不单独使用,而是在声明其它结构时,将其作为要组成双链表的结构体的一个成员。例如:
typedef struct _NODE {
int data1; // 数据域1
LIST_ENTRY ListEntry;
int data2; // 数据域2
}NODE;
这样,通过ListEntry
成员可以将结构体连接为一个双链表,如下图所示:
头结点不带数据域,为LIST_ENTRY
结构,使用前要用InitializeListHead
函数初始化。
下面还有一个问题,我们通过LIST_ENTRY
结构结构串联起来,使用时通过前后指针获取得到的都是LIST_ENTRY
结构本身,即NODE
结构中的成员ListEntry
。而实际使用时要得到的是NODE
结构本身的指针,这也就是招聘笔试面试过程中常常遇到的一个问题:知道结构体中某个成员的地址,如何获取结构体的起始地址?
讲如何做之前,先看看Windows中怎么获取。就是使用CONTAINING_RECORD
宏,这个宏的原型如下:
PCHAR CONTAINING_RECORD(
[in] PCHAR Address,
[in] TYPE Type,
[in] PCHAR Field
);
其中,Type
为结构体类型,Field
为Type
结构中的已知成员,Address
为Field
成员的已知地址,宏的结果为Type
结构的起始地址。对于我们的链表示例,获取链表第一个NODE
结构的起始地址用法为:
CONTAINING_RECORD(ListHead.Flink,NODE,ListEntry)
具体的实现原理比较简单,知道成员的地址,算结构体起始地址,只需要知道成员的偏移,因此,将结构体起始对齐到地址0,然后获取成员的地址,即为成员的偏移量,然后减去偏移量即可,该宏可如下实现:
((Type *)(((ULONG)Address) - (ULONG)(&(((Type *)0)->Field))))