TAILQ是Linux中的一种双向队列(在libevent中有广泛引用),能实现操作队列需要的各种操作:插入元素,删除元素,遍历队列等。这个队列的优点是插入元素很快。
#include
#include
struct item_t {
int value;
TAILQ_ENTRY(item_t) entries; // 链表的指针域,由TAILQ来控制
};
TAILQ_HEAD(item_head_t, item_t) tail_head; // 创建链表
int main(){
TAILQ_INIT(&tail_head); //init head
item_t item1, item2, item3;
item_t *node;
item1.value = 1;
item2.value = 2;
item3.value = 3;
TAILQ_INSERT_TAIL(&tail_head, &item1, entries); //链表状态:1
TAILQ_INSERT_HEAD(&tail_head, &item2, entries); // 链表状态:2 1
TAILQ_INSERT_AFTER(&tail_head, &item2, &item3, entries); // item3插在item2后面; 链表状态: 2 3 1
TAILQ_FOREACH(node, &tail_head, entries) {
printf("%d ", node->value);
}
printf("\n"); // 输出2 3 1
TAILQ_REMOVE(&tail_head, &item2, entries); // 移除item2, 此时链表状态:3 1
TAILQ_FOREACH(node, &tail_head, entries) {
printf("%d ", node->value);
}
printf("\n"); // 输出3 1
}
TAILQ_ENTRY结构体和TAILQ_HEAD结构体基本一致,但是表示的含义不一样。TAILQ_ENTRY结构体用来表示链表节点的指针域(类似于平时编程中的链表指针域,单向链表中一般含有next指针,而双向链表含有pre,next指针)。
TAILQ_HEAD结构是用来表示链表的头节点和尾节点的指针。
#define TAILQ_HEAD(name, type) \
struct name { \
struct type *tqh_first; \
struct type **tqh_last; \
}
#define TAILQ_ENTRY(type) \
struct { \
struct type *tqe_next; /* next element */ \
struct type **tqe_prev; /* address of previous next element */ \
}
注意到,这里的tqe_pre和tqh_last都是二级指针。下图是TAILQ的内部结构(来源:https://blog.csdn.net/ylo523/article/details/43274627)
为什么是二级指针?
next是指向的是下一个元素的地址(由于下一个元素的类型是type,所以是一级指针type*)
pre是指向上一个元素next成员的地址(由于next成员类型是type*,所以需要使用指针type**)
跟我们平常实现的链表有什么不同?
我们平常的双向链表大概是这样子的,
struct item_t {
int value;
item_t *pre, *next;
};
// 简单的3个节点插入过程
item_t head,node1,node2;
head.value=1,node1.value=2,node2.value=3;
head.pre=NULL;head.next=&node1;
node1.pre=&head;node1.next=&node2;
node2.pre=&node1;node2.next=NULL;
指针域都是以及指针,因为只需要存放下一个元素或者是上一个元素的地址;而TAILQ的pre指针存放的是上一个元素某一个成员的地址,而该成员的类型刚好是type*。
#define TAILQ_INIT(head) do { \
(head)->tqh_first = NULL; \
(head)->tqh_last = &(head)->tqh_first; \
} while (/*CONSTCOND*/0)
#define TAILQ_INSERT_TAIL(head, elm, field) do{ \
(elm)->field.tqe_next = NULL; \
(elm)->field.tqe_prev = (head)->tqh_last; \
*(head)->tqh_last = (elm); \
(head)->tqh_last = &(elm)->field.tqe_next; \
}while (0)
初始化:将last指向链表的first域
尾插入:画图之后很好理解。。
#define TAILQ_INSERT_HEAD(head, elm, field) do { \
if (((elm)->field.tqe_next = (head)->tqh_first) != NULL) \
(head)->tqh_first->field.tqe_prev = \
&(elm)->field.tqe_next; \
else \
(head)->tqh_last = &(elm)->field.tqe_next; \
(head)->tqh_first = (elm); \
(elm)->field.tqe_prev = &(head)->tqh_first; \
} while (/*CONSTCOND*/0)
这里的思路与前面的一样,画个图就能看出来,需要注意的是head的可能是空,所以head的tqe_first如果是空的话,则需要单独处理。
#define TAILQ_FOREACH(var, head, field) \
for ((var) = ((head)->tqh_first); \
(var); \
(var) = ((var)->field.tqe_next))
这个就是对TAILQ的进行简单的遍历,容易理解。
逆序遍历比较的复杂,源码如下:
#define TAILQ_FOREACH_REVERSE(var, head, headname, field) \
for ((var) = (*(((struct headname *)((head)->tqh_last))->tqh_last)); \
(var); \
(var) = (*(((struct headname *)((var)->field.tqe_prev))->tqh_last)))
为了理解这个源码,我们需要知道以下:
①假设现在有一个TAILQ,其中某一个节点的指针为node,如何找到node的上一个节点?
结构体在64位下的内存布局如上图所示(64位的指针为8字节)
根据TAILQ结构的性质,我们可以得到该等式是成立的(不考虑空指针的问题)
*(node->prev) == node
由上面的公式得知,把node->prev看成整体,知道一个节点的prev指针就可以获取到该节点的指针(这就是用二级指针的原因)。知道这个性质,我们就可以来求解node节点的前置指针了。
首先: 获取到node节点的前一个节点的next域的地址 node->prev
然后: 以这个地址为起始,将next和prev看成是一个TAILQ_HEAD的结构体,即可获取到node前一个节点的prev指针的地址 item_head_t* p1 = (item_head_t*)(node->prev); item_t *p2 = p1->last;
最后,按上面那个公式的套路来获取指针 *p2
②如何获取尾节点
按照上面的分析,思路是一样的。。