双向链表,可以在链表的任何位置添加和删除元素,只需要该位置的指针。
如果是头指针,就是在首尾添加和删除。
而且,它删除元素时不需要知道链表头指针,也不需要从头指针遍历到当前位置的前一个位置,只需要当前位置的指针就行。
总的来说,双向链表远比单链表灵活,特别适合需要频繁增加和删除的场景。
随着Linux的流行,Linux内核风格的双向链表(把链表嵌在数据结构里)也流行了起来。
这个实现还是很简单的,代码如下:
定义两个指针,prev和next分别指向前一个和后一个元素,组成双向链表。
整个链表挂在链表头上,链表头也是一个链表元素,但是它没有附加的数据部分。
链表头除了作为整个链表的挂载点之外,就是充当哨兵节点,提示遍历链表时的起始位置。
这种双向链表的for循环,一般是这么写的:
for (l = scf_list_head(h); l != scf_list_sentinel(h); l = scf_list_next(l)) {
// 其他代码
实际上,h的下一个元素(h->next)才是链表的第一个元素,最后一个元素是h->prev,h则是哨兵节点。
为了避免出错,一般把它们写成宏,见下图。
获取链表元素的数据部分的指针,使用宏scf_list_data(l, type, member)。
其中l是链表元素的指针,type是数据部分的类型,member则是链表元素在数据类型里的变量名。
例如可以这么定义一个类型:
typedef struct {
scf_list_t list;
int d;
} scf_word_t;
member就是list,type就是scf_word_t,根据链表元素的指针l以及链表元素在数据类型里的偏移量,就可以算出数据的指针。
scf_list_data()必须写成宏,而不能像scf_list_add_front()和scf_list_add_tail()一样写成内联函数,因为宏可以直接替换源代码,而这里需要直接替换源代码。
在C和C++中,有些场景必须使用宏才可以,而没法使用内联函数。
例如,给一个类添加动态创建功能,这是当年微软MFC的机制之一,也是用宏来实现的,而没法用内联函数。
动态创建,是在代码运行时根据实时获取的类名字符串,来调用类的构造函数创建类的对象。
当然没法把字符串当作构造函数来用:在源代码文件里,new Object()会创建一个类的对象,但是new "Object"()是语法错误。但是在运行时,只能拿得到"Object",需要根据它去找到构造函数Object()。
以后写篇文章说一下C++的动态创建。
根据内嵌链表元素的指针,获取数据结构的指针,也是必须使用宏的场景之一,因为member也是数据结构里的一个成员变量名:既不是指针,也不是字符串,只有在编译阶段才会被解释为成员变量。
宏替换的处理,是在编译之前。
scf_list_data的实现如下:
offsetof(type, member)用于获取member在type里的偏移量,它的实现:
(size_t)(&((type*)0)->member)
就是把整数0转化为type*指针,这样((type*)0)->member的地址(再转化为整数)就是偏移量。
C库本身是带这个宏函数的,不需要自己去写。
想了解更多精彩内容,快来关注闲聊代码