首先上一篇博客介绍了Linux下的两个经典宏,它可以根据结构体中的成员变量地址,计算出结构体地址。有了它,就可以实现可复用的高效双链表。这次我再Windows环境下给予的实现,看完觉得会受益匪浅。
Linux中双向链表的使用思想
它是将双向链表节点嵌套在其它的结构体中;在遍历链表的时候,根据双链表节点的指针获取"它所在结构体的指针",从而再获取数据。
我举个例子来说明,可能比较容易理解。假设存在一个社区中有很多人,每个人都有姓名和年龄。通过双向链表将人进行关联的模型图如下:
person代表人,它有name和age属性。为了通过双向链表对person进行链接,我们在person中添加了list_head属性。通过list_head,我们就将person关联起来了。
struct person { int age; char name[20]; struct list_head list; };
下面是Windows下的实现:
//1.节点定义。虽然名称list_head,但是它既是双向链表的表头,也代表双向链表的节点。 struct list_head { struct list_head *next, *prev; }; //2.初始化节点:将list节点的前继节点和后继节点都是指向list本身。 static inline void INIT_LIST_HEAD(struct list_head *list) { list->next = list; list->prev = list; } /* 3.添加节点 list_add(newhead, prev, next)的作用是添加节点:将newhead插入到prev和next节点之间。 list_add(newhead, head)的作用是添加newhead节点:将newhead添加到head之后,是newhead称为head的后继节点。 list_add_tail(newhead, head)的作用是添加newhead节点:将newhead添加到head之前,即将newhead添加到双链表的末尾。 */ static inline void list_add(struct list_head *newhead,struct list_head *prev,struct list_head *next) { next->prev = newhead; newhead->next = next; newhead->prev = prev; prev->next = newhead; } //将newhead添加到head之后 static inline void list_add(struct list_head *newhead, struct list_head *head) { list_add(newhead, head, head->next); } //将newhead添加到head之前,也就是末尾 static inline void list_add_tail(struct list_head *newhead, struct list_head *head) { list_add(newhead, head->prev, head); } /* 4.删除节点 list_del(prev, next) 的作用是从双链表中删除prev和next之间的节点。 list_del(entry) 的作用是从双链表中删除entry节点。 list_del_init(entry) 的作用是从双链表中删除entry节点,并将entry节点的前继节点和后继节点都指向entry本身。 */ static inline void list_del(struct list_head * prev, struct list_head * next) { next->prev = prev; prev->next = next; } //删除自身 static inline void list_del(struct list_head *entry) { list_del(entry->prev, entry->next); } static inline void list_del_init(struct list_head *entry) { list_del_entry(entry); INIT_LIST_HEAD(entry); }
//5.替换节点:list_replace(old, newhead)的作用是用newhead节点替换old节点。 static inline void list_replace(struct list_head *old,struct list_head *newhead) { newhead->next = old->next; newhead->next->prev = newhead; newhead->prev = old->prev; newhead->prev->next = newhead; } //6. 判断双链表是否为空:list_empty(head)的作用是判断双链表是否为空。它是通过区分"表头的后继节点"是不是"表头本身"来进行判断的。 static inline int list_empty(const struct list_head *head) { return head->next == head; }
/*7. 获取节点 list_entry(ptr, type, member) 实际上是调用的container_of宏。 它的作用是:根据"结构体(type)变量"中的"域成员变量(member)的指针(ptr)"来获取指向整个结构体变量的指针。 */ // 获得结构体(TYPE)的变量成员(MEMBER)在此结构体中的偏移量。 #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) //根据"结构体(type)变量"中的"域成员变量(member)的指针(ptr)"来获取指向整个结构体变量的指针 #define container_of(PT,TYPE,MEMBER) ((TYPE *)((char *)(PT) - offsetof(TYPE,MEMBER))) #define list_entry(ptr, type, member) container_of(ptr, type, member) /*8.遍历节点 list_for_each(pos, head)和list_for_each_safe(pos, n, head)的作用都是遍历链表。但是它们的用途不一样! list_for_each(pos, head)通常用于获取节点,而不能用到删除节点的场景。 list_for_each_safe(pos, n, head)通常删除节点的场景。 */ #define list_for_each(pos, head) for (pos = (head)->next; pos != (head); pos = pos->next) #define list_for_each_safe(pos, afterpos, head) for (pos = (head)->next, n = pos->next; pos != (head); pos = n, n = pos->next)
</pre><h2><span style="font-family:verdana,arial,helvetica,sans-serif; font-size:14px; line-height:21px">测试使用方法:</span></h2><span style="font-family:verdana,arial,helvetica,sans-serif; font-size:14px; line-height:21px"></span><pre name="code" class="cpp">#include <stdio.h> #include <stdlib.h> #include <string.h> #include "list.h" struct Person { int age; char name[20]; struct list_head list; }; void main() { Person *pperson; Person person_head; //为了建立一个空的循环双链表表头,不存放任何数据 list_head *pos, *next; int i; // 初始化双链表的表头 INIT_LIST_HEAD(&person_head.list); // 添加节点 for (i=0; i<5; i++) { pperson = (Person*)malloc(sizeof(Person)); pperson->age = (i+1)*10; sprintf(pperson->name, "%d", i+1); // 将节点链接到链表的末尾 list_add_tail(&(pperson->list), &(person_head.list)); } // 遍历链表 printf("==== 1st iterator d-link ====\n"); list_for_each(pos, &person_head.list) { pperson = list_entry(pos, struct Person, list); printf("name:%-2s, age:%d\n", pperson->name, pperson->age); } // 删除节点age为20的节点 printf("==== delete node(age:20) ====\n"); list_for_each_safe(pos, next, &person_head.list) { pperson = list_entry(pos, struct Person, list); //获取双链表结构体的地址 if(pperson->age == 20) { list_del_init(pos); free(pperson); } } // 再次遍历链表 printf("==== 2nd iterator d-link ====\n"); list_for_each(pos, &person_head.list) { pperson = list_entry(pos, struct Person, list); printf("name:%-2s, age:%d\n", pperson->name, pperson->age); } // 释放资源 list_for_each_safe(pos, next, &person_head.list) { pperson = list_entry(pos, struct Person, list); list_del_init(pos); free(pperson); } }
结果:
==== 1st iterator d-link ==== name:1 , age:10 name:2 , age:20 name:3 , age:30 name:4 , age:40 name:5 , age:50 ==== delete node(age:20) ==== ==== 2nd iterator d-link ==== name:1 , age:10 name:3 , age:30 name:4 , age:40 name:5 , age:50
到这里就把Linux的双链表实现,当然Linux实现的代码比这多多了。我只是把精髓提取出来。再一次感叹下,高手写的代码就是不一样啊!