在日常开发中,经常会使用到链表,链表上承载的数据不同,链表的数据结构定义也会随之变化。本文讨论如何定义一个通用的链表结构。
1. 在链表节点中定义前向指针或后向指针。
每次都需要重新定义链表的结构,重新定义对链表的操作。这是最传统的、最简单的方法。
2. 使用宏简化链表的定义和操作。
将常用的定义封装成宏定义,减少冗余代码。
单链表的指针结构定义如下所示,使用时需要将LIST_ENTRY作为type的成员变量。请注意le_prev类型为二级指针,而不是直接指向struct type的指针。在单链表插入、删除操作时需要前结点的指针,通常需要遍历单链表,以获取当前结点的前一个结点。指针le_prev指向前一个结点的le_next变量,通过le_prev可以直接修改前一个结点的le_next变量。
#define LIST_ENTRY(type) \
struct { \
struct type *le_next; /* next element */ \
struct type **le_prev; /* address of previous next element */ \
}
如下是链表头结点的定义:
#define LIST_HEAD(name, type) \
struct name { \
struct type *lh_first; /* first element */ \
}
获取第一个结点的宏定义,head为头结点。
#define LIST_FIRST(head) ((head)->lh_first)
获取le_next指针变量,elm为结点指针,field为LIST_ENTRY成员变量名称。
#define LIST_NEXT(elm, field) ((elm)->field.le_next)
下面的宏实现链表的遍历:
#define LIST_FOREACH(var, head, field) \
for ((var) = LIST_FIRST((head)); \
(var); \
(var) = LIST_NEXT((var), field))
链表的插入
#define LIST_INSERT_AFTER(listelm, elm, field) do { \
if ((LIST_NEXT((elm), field) = LIST_NEXT((listelm), field)) != NULL)\
LIST_NEXT((listelm), field)->field.le_prev = \
&LIST_NEXT((elm), field); \
LIST_NEXT((listelm), field) = (elm); \
(elm)->field.le_prev = &LIST_NEXT((listelm), field); \
} while (0)
在头结点后插入结点
577 #define LIST_INSERT_HEAD(head, elm, field) do { \
578 QMD_LIST_CHECK_HEAD((head), field); \
579 if ((LIST_NEXT((elm), field) = LIST_FIRST((head))) != NULL) \
580 LIST_FIRST((head))->field.le_prev = &LIST_NEXT((elm), field);\
581 LIST_FIRST((head)) = (elm); \
582 (elm)->field.le_prev = &LIST_FIRST((head)); \
583 } while (0)
链表的删除
#define LIST_REMOVE(elm, field) do { \
if (LIST_NEXT((elm), field) != NULL) \
LIST_NEXT((elm), field)->field.le_prev = \
(elm)->field.le_prev; \
*(elm)->field.le_prev = LIST_NEXT((elm), field); \
} while (0)
通过以上宏定义,简化了代码。通过新增le_prev指针简化了单链表的插入操作。
3. Linux内核定义格式
在Linux内核中经常使用链表作为存储结构,所以定义了一套通用的双向链表定义和使用格式。在定义链表结点时,将前向指针和后向指针独立出来作为单独的数据结构
struct list_head {
struct list_head *next, *prev;
};
插入、删除操作都在list_head上进行操作。
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
在使用时,将list_head放到宿主的数据结构定义中,通过对list_head的操作实现对链表的操作。还有一个最重要的问题没有解决,链表操作的是list_head指针,如何得到宿主数据结构的地址?通过下面的宏定义获取宿主的地址。
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
代码通过container_of获取宿主地址,其中ptr指向list_head的指针,type为宿主的数据结构类型,member为list_head在type中的成员名称。再将container_of定义展开。
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
offsetof宏中取TYPE中MEMBER成员的地址,而TYPE的地址为0,所以offsetof定义了MEMBER在TYPE类型中的偏移。
将list_head的地址减去list_head在宿主结构中的偏移,就得到了宿主结构的地址,这就是上述container_of宏完成的功能。container_of先定义了list_head的指针,并使用ptr进行赋值,然后减去list_head的偏移量,就得到了宿主结构的地址。
我们就得到了一个通用的链表定义和操作的通用机制,相对于传统的链表定义,Linux的链表操作多了一个偏移运算。
FreeBSD实现时,也采用了类似的方法,链表的偏移是放在链表的头结点中。
struct list_node {
struct list_node *list_next;
struct list_node *list_prev;
};
struct list {
size_t list_size;
size_t list_offset;
struct list_node list_head;
};