简介
这其实是 1994 年的老代码, 在 sys/queue.h 中
queue.h 8.5 (Berkeley) 8/20/94
一共提供了5个数据结构的封装
1. 单链表 list SLIST 省内存,少删除,少插入
2. 双向列表 list LIST, 可惜只能头部插入,
3. 单队列 simple queue 可头尾插入。 头尾移除还是快的。中间就慢了
4. TAILQ 就是本文分析的这个了, 双链表队列, 中间插入也快的。 就是耗点内存
5. 循环链表
示例代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/queue.h>
typedef struct Animal {
LIST_ENTRY(Animal) p;
char name[25];
int age;
}Animal;
typedef LIST_HEAD(AnimalList, Animal) AnimalList;
Animal* newAnimal(const char* name, int age)
{
Animal* a = (Animal*)malloc(sizeof(Animal));
memset(a, 0, sizeof(Animal));
a->age = age;
strncpy(a->name, name, sizeof(a->name));
return a;
}
void freeAnimals(Animal* a)
{
printf("free name = %s age = %d\n", a->name, a->age);
free(a);
}
void showAnimals(AnimalList* list)
{
Animal* iter=NULL;
LIST_FOREACH(iter, list, p) {
printf("name = %s age = %d\n", iter->name, iter->age);
}
}
int main()
{
AnimalList animals;
LIST_INIT(&animals);
// list 好像只提供了头部插入的接口 不能在尾部插入
// 也没有计算列表长度的函数
Animal* a = newAnimal("tiger", 3);
LIST_INSERT_HEAD(&animals, a, p);
/*
千万不要这样, 毕竟是宏展开, newAnimal("tiger", 3) 会被调用多次
LIST_INSERT_HEAD(&animals, newAnimal("tiger", 3), p);
*/
a = newAnimal("dog", 1);
LIST_INSERT_HEAD(&animals, a, p);
a = newAnimal("cat", 4);
LIST_INSERT_HEAD(&animals, a, p);
showAnimals(&animals);
// 计算链表长度
int listn = 0;
Animal* iter=NULL;
LIST_FOREACH(iter, &animals, p) {
++listn;
}
printf("list len = %d\n", listn);
printf("删除 一个动物 %s\n", a->name);
LIST_REMOVE(a, p);
freeAnimals(a);
showAnimals(&animals);
// 清空
Animal* next=NULL;
for (iter = animals.lh_first; iter; ) {
next = iter->p.le_next;
freeAnimals(iter);
iter = next;
}
animals.lh_first = NULL;
showAnimals(&animals);
printf("\n");
a = newAnimal("frog", 1);
LIST_INSERT_HEAD(&animals, a, p);
showAnimals(&animals);
return 0;
}
清空比较麻烦,可以用下面这个宏, 就是多搞了个临时变量,先把next 指针存起来,这样你循环内部就可以把当前指针
free 掉了。
#define SLIST_FOREACH_SAFE(var, head, field, tvar) \
for ((var) = SLIST_FIRST(head); \
(var) && ((tvar) = SLIST_NEXT(var, field), 1); \
(var) = (tvar))
github 上 open62541源码,正是加了这个宏。
注意这里有个逗号运算符
逗号运算符中的所有表达式都会被依次从左到又的执行,但是只保留最右边的表达式结果,作为整个逗号语句的结果。
表达式1 , 表达式2 , 表达式3
比如上面的,表达式3的结果将是整个语句的结果。
而 ((tvar) = SLIST_NEXT(var, field), 1) 的结果就是 1 ,不论 tvar 到底是个啥
源码解析
c语言的结构体变量的内存分布是连续的,首变量的地址就是整个结构体的地址
(c++的结构体要注意,变量的public protected private 属性、继承、可能导致内存分布不连续, 虚函数指针的位置...)
struct A
{
int n1;
int n2;
int n3;
}a;
也就是说
int * pn1 = &a.n1;
A* pa = &a;
pa == pn1
我一旦拿到 n1的地址, 整个A的变量我都可以访问了。
((A*)pn1)->n1;
((A*)pn1)->n2;
((A*)pn1)->n3;
struct qitem
{
int n1;
struct Entry {
qitem* next;
qitem** prev;
}e;
};
// 注意:Head 和 Entry 结构体的布局是一样的。 next==first prev==tail
struct Head {
qitem* first;
qitem** tail; // 始终指向队尾元素 next的地址
}head;
head.first = NULL;
head.tail = &head.first;
qitem item1={1}, item2={2};
// 把它们依次加到末尾
TAILQ_INSERT_TAIL(&head, &item1, e);
TAILQ_INSERT_TAIL(&head, &item2, e);
// 我们把 TAILQ_INSERT_TAIL 展开 它相当于
pitem->next = NULL; // 尾部NULL
pitem->prev = phead->tail;
*phead->tail = pitem;
phead->tail = &pitem->next;
prev指向前一个元素的next的地址
item2.e.prev == &item1.e.next
于是 *item2.e.prev == item1.e.next // 前一个元素的next , 不就是自己么
== &item2
也就是说 任意节点的 *prev 它的结果就是其本身 ---- 知识点1
qitem* pitem2 = &item2;
qitem* me = *pitem2->e.prev;
me == pitem2;
下面来解释
TAILQ_LAST(&head, Head);
head的first指向第一个元素
head的tail指向最后一个元素 next 的地址
而我现在要的是 最后一个元素
在本例中
head->tail == &item2.e.next
光知道你的next地址有啥用呢,我要的是item2的地址啊~~~~~
next 是结构体的首元素
结构体首元素的地址等于结构体的地址
于是 head->tail == &item2.e.next == &item2.e
((struct Entry*)head->tail) == &item2.e
((struct Entry*)head->tail)->prev == item2.e.prev 不就可以访问到了prev了么
根据知识点1, *item2.e.prev == &item2;
TAILQ_LAST(&head, Head) 写成下面这样就行了
*((struct Entry*)head->tail)->prev
TAILQ 实际代码中 struct Entry 是匿名的结构体,
采用 同位序的 struct Head 取代, prev == tail next == first
于是 *((struct Entry*)head->tail)->prev
*((struct Head*)head->tail)->tail
在TAILQ 看来 struct Entry 和 struct Head 是一模一样的东西
同理
TAILQ_PREV(elm, headname, field);
*((struct Head*)(elm->field.prev))->tail