链表的通用数据结构

在日常开发中,经常会使用到链表,链表上承载的数据不同,链表的数据结构定义也会随之变化。本文讨论如何定义一个通用的链表结构。

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;
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值