每个写C的同学想必都会羡慕C++方便的STL,但是无奈标准C是没有这些库的,毕竟C语言不支持泛型,不过好在大多数Linux下都可用sys/queue
,源自BSD:
https://unix.superglobalmegacorp.com/xnu/newsrc/bsd/sys/queue.h.html
这个头文件用宏实现了很多基础数据结构,如:单链表、双链表、队列等。用起来也比较方便,只需要引入一个头文件即可。如著名的的libevent项目就大量使用了这个头文件里的TAILQ. 不过C的宏实现的库并没有C++那么直白好用,还是需要看懂实现才行。
TAILQ使用举例
#include <stdio.h>
#include <stdlib.h>
#include <sys/queue.h>
struct node {
char c;
TAILQ_ENTRY(node) next; // define queue node
};
TAILQ_HEAD(head_s, node); // define queue head
int main()
{
struct head_s queue;
TAILQ_INIT(&queue); // init queue
const char *p = "que";
while (*p) {
struct node *e = malloc(sizeof(struct node));
e->c = *p;
TAILQ_INSERT_TAIL(&queue, e, next); // push element into queue tail
printf("push %p %c\n", e, e->c);
p++;
}
while (!TAILQ_EMPTY(&queue)) {
struct node *first = TAILQ_FIRST(&queue); // get queue head element
printf("pop %p %c\n", first, first->c);
TAILQ_REMOVE(&queue, first, next); // remove queue head element
free(first);
}
printf("OK\n");
return 0;
}
对于C的这些宏API,要通过看源码来理解到底传入的参数是干什么,传入的是类型名字,还是结构体字段名字,还是变量。毕竟,宏仅仅是完成字符串的替换而已。
理解TAILQ宏实现
理解上面代码,可以先看源码的实现,也可用gcc -E
来看下展开后的宏的样子:
struct node {
char c;
struct {
struct node *tqe_next;
struct node **tqe_prev;
} next;
};
struct head_s {
struct node *tqh_first;
struct node **tqh_last;
};
int main()
{
struct head_s queue;
do {
(((&queue))->tqh_first) = ((void *) 0);
(&queue)->tqh_last = &(((&queue))->tqh_first);;
} while (0);
const char *p = "que";
while (*p) {
struct node *e = malloc(sizeof(struct node));
e->c = *p;
do {
(((e))->next.tqe_next) = ((void *) 0);
(e)->next.tqe_prev = (&queue)->tqh_last;
*(&queue)->tqh_last = (e);
(&queue)->tqh_last = &(((e))->next.tqe_next);;;
} while (0);
printf("push %p %c\n", e, e->c);
p++;
}
while (!((&queue)->tqh_first == ((void *) 0))) {
struct node *first = ((&queue)->tqh_first);
printf("pop %p %c\n", first, first->c);
do { ;;
if (((((first))->next.tqe_next)) != ((void *) 0))
(((first))->next.tqe_next)->next.tqe_prev = (first)->next.tqe_prev;
else
{ (&queue)->tqh_last = (first)->next.tqe_prev;; }
*(first)->next.tqe_prev = (((first))->next.tqe_next);;;;
} while (0);
free(first);
}
printf("OK\n");
return 0;
}
可以看出,TAILQ的底层结构是双链表,prev前驱指针是一个二级指针,这个可以在判断是否头结点的指向自己的内存即可,不用特殊处理为NULL,比较巧妙,省掉了头结点的特殊处理。类似比较经典的二级指针使用还有:Linus:利用二级指针删除单向链表 。
TAILQ使用注意事项
使用这些宏API的时候,要注意插入的数据节点的生命周期,注意对应节点是存储在栈区还是堆区。比如:局部变量node插入队列后,如果在调用者访问里面节点就不对了,因为对应的内存已经释放掉了。
一般情况,用C来开发复杂系统时,先考虑Data和Control分离,存储结构式什么样子的,对应的控制字段如何维护等,还要考虑对应对象的声明周期等。