当前位置:   article > 正文

Linux中List.h文件的分析和应用

list.h

        本文介绍Linux中List.h文件,并对其详细分析,此文件的双向链表短小精湛,值得借鉴。此文件中还有哈希表的应用。

目录

一、通过函数定义的方式对双向循环链表的操作    
1. List.h源码位置    
2. list_head结构    
3. 头结点的初始化    
4. 双向循环链表的插入操作    
(1)基本插入函数    
(2)首部插入函数    
(3)尾部插入函数    
(4)与非循环单链表的插入操作的区别    
5.双向循环链表的删除操作    
(1)删除    
(2)与非循环单链表删除操作的区别    
6.双向循环链表的替换操作    
(1)替换    
7.双向循环链表的移动操作    
8.双向循环链表的状态检测    
9.双向循环链表的旋转操作    
10.双向循环链表的切分操作    
11.双向循环链表的合并操作    
二、通过宏定义的方式对双向循环链表进行遍历    
1.基本的宏    
(1)list_entry宏    
(2)list_first_entry宏    
2.双向循环链表的遍历    
(1)从左向右遍历    
(2)从右向左遍历    
(3)安全的向前向后遍历    
(4)使用遍历    
(5)list_prepare_entry宏    
(6)下一个结点开始遍历    
(7)当前结点开始遍历    
(8)安全版本    
三、哈希链表的操作    

(注:基于Linux3.5版内核)

一、通过函数定义的方式对双向循环链表的操作

1. List.h源码位置

        List.h文件位于include/linux目录,内容为双向循环链表的操作,对双向循环链表的定义如下:(与单链表相比,双链表的每个结点同时具有前驱指针和后驱指针,操作会更方便)

(1)首节点,存放第一个有效数据的结点。

(2)尾结点,存放最后一个有效数据的结点,在双向循环链表中,头结点的前驱指针指向尾结点,尾结点的后驱指针指向头结点。

(3)头结点:

        1)头结点数据类型与首结点类型一样,首结点什么类型,头结点就是什么类型。

        2)头结点一般不存放有效数据。

        3)头结点是首结点前面的那个结点。

        4)为了方便操作链表而提出的。

(4)头指针,指向头结点的指针,即存放头结点地址。

2. list_head结构

        linux/types.h文件中定义了list_head结构,原型如下:

  1. struct list_head
  2. {
  3. struct list_head *next;
  4. struct list_head *prev;
  5. };

        在实际应用中,此结构经常作为成员与其他数据类型一起组成一个新的结构体,比如类似如下结构:

  1. struct userinfo{
  2. char username[20];
  3. char password[20];
  4. struct list_head list;
  5. };

        在以上述结构为结点的双向循环链表中,userinfo结构的list成员就是用来链接链表的各个成员的。

3. 头结点的初始化

        在下面代码中,前面两个宏实现的功能与后面那个内联函数实现的功能一样。目的都是用来初始化双向循环链表的头指针,让其头指针指向的list_head型结构体变量的前/后驱指针成员指向头指针本身,即为空链表。

  1. 1. #define LIST_HEAD_INIT(name) { &(name), &(name) }
  2. 2. #define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)
  3. 3.
  4. 4. static inline void INIT_LIST_HEAD(struct list_head *list)
  5. 5. {
  6. 6. list->next = list;
  7. 7. list->prev = list;
  8. 8. }

        比如在应用时,需使用上述任意一种方法初始化头指针,这里使用内联函数初始化头指针,如下代码:

  1. struct userinfo  userlist;
  2. INIT_LIST_HEAD(&(userlist.list)); 

         注意:函数参数传递是值传递(一个副本),上述参数传递list_head型结构体变量的地址。详见csdn博客上的文章。

4. 双向循环链表的插入操作(一个参数为待插入元素地址,一个为链表头结点地址)

         对于插入操作,总共定义了三个函数,后两个函数都会调用前一个函数,详见下面的代码。第一个为基础函数,第二个为在首部插入,适用于堆栈;第三个为在尾部插入结点,适用于队列。

  1. /*
  2. * Insert a new entry between two known consecutive entries.
  3. * This is only for internal list manipulation where we know
  4. * the prev/next entries already!
  5. */
  6. static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next)
  7. {
  8. next->prev = new;
  9. new->next = next;
  10. new->prev = prev;
  11. prev->next = new;
  12. }
  13. /*
  14. * list_add - add a new entry
  15. * @new: new entry to be added
  16. * @head: list head to add it after
  17. * Insert a new entry after the specified head.
  18. * This is good for implementing stacks.
  19. */
  20. Static inline void list_add(struct list_head *new, struct list_head *head)
  21. {
  22. __list_add(new, head, head->next);
  23. }
  24. /**
  25. * list_add_tail - add a new entry
  26. * @new: new entry to be added
  27. * @head: list head to add it before
  28. * Insert a new entry before the specified head.
  29. * This is useful for implementing queues.
  30. */
  31. Static inline void list_add_tail(struct list_head *new, struct list_head *head)
  32. {
  33. __list_add(new, head->prev, head);
  34. }

1)基本插入函数

         分析其参数,第一个参数new,是一个类型为list_head的指针,指向要增加的结点,而prev和next也都是list_head类型的指针,顾名思义,就是指向增加的位置的前面和后面的结点。

        在函数内部,首先是把要增加位置的后面的结点的prev元素赋值为new,再把new的next元素指向后面结点,意思是先把后面的结点与新增结点相连;然后是把new的prev元素指向前面结点,再把前面结点的next赋值为new,意思是把前面的结点与新增结点相连。如下图:

 2)首部插入函数 

        与尾部插入函数的区别是传入__list_add函数的第2、3个参数不一样,这样应用非常巧妙。

        根据上述代码,首部插入函数中调用__list_add 函数传入的参数分别为:new, head, head->next。(这三个参数可以另解为:新结点、头结点、首结点)

        这里的head一般指struct  list_head类型变量的地址,如第3中&(userlist.list),类似于非循环单链表的头指针

        首先,当链表为空链表时,head指针( 即上述例子中的&(userlist.list) )的前驱指针和后驱指针都指向了它自己。根据首部插入函数中调用__list_add 函数传入的参数可以很顺利地分析出在首部插入的过程,此时首结点即为new指向的结点。

        其次,当链表至少有一个除头结点外的结点时,也能很顺利地分析出首部插入过程,此时的首结点也为new指向的结点。

3)尾部插入函数

        与首部插入函数的区别是传入__list_add函数的第2、3个参数不一样,这样应用非常巧妙。

        根据上述代码,首部插入函数中调用__list_add 函数传入的参数分别为:new, head->prev, head。(新结点,尾结点,头结点)

        这里的head一般指struct  list_head类型变量的地址,如第3中&(userlist.list),类似与非循环单链表的头指针

        首先,当链表为空链表时,head指针( 即上述例子中的&(userlist.list) )的前驱指针和后驱指针都指向了它自己。根据尾部插入函数中调用__list_add 函数传入的参数可以很顺利地分析出在首部插入的过程,此时尾结点即为new指向的结点。

        其次,当链表至少有一个除头结点外的结点时,也能很顺利地分析出尾部插入过程,此时的尾结点也为new指向的结点。这里需要注意当执行到如下语句时:

new->prev = prev;  

prev->next = new

        new->prev = prev表示插入结点的前驱指针指向prev(根据传入的参数,就是 head->prev),即头结点的前驱指针(指向没有插入时的尾结点),在__list_add函数中第一句 next->prev = new中貌似头结点的前驱指针已指向了new,即改变了指向,但这里的new->prev = prev中的prev即head->prev的值(插入之前尾结点的地址)由函数参数传递而来,是保存在堆栈中的局部变量(指向没有插入时的尾结点,即存放尾结点的地址,值传递)

4)与非循环单链表的插入操作的区别

        非循环单链表插入时,传入的参数可以是头指针和插入位置,双向循环链表的头/尾插入式非循环单链表的特殊情况。

5.双向循环链表的删除操作(参数为待删除元素地址)

1)删除

         对于删除操作,总共定义了四个函数,后三个函数都会调用第一个函数,详见下面的代码。第一个为基础函数,第二、三个删除操作都为不安全删除,第四个为安全删除函数。

  1. 1. static inline void __list_del(struct list_head * prev, struct list_head * next)
  2. 2. {
  3. 3. next->prev = prev;
  4. 4. prev->next = next;
  5. 5. }
  6. 6.
  7. 7. static inline void __list_del_entry(struct list_head *entry)
  8. 8. {
  9. 9. __list_del(entry->prev, entry->next);
  10. 10. }
  11. 11. static inline void list_del(struct list_head *entry)
  12. 12. {
  13. 13. __list_del(entry->prev, entry->next);
  14. 14. entry->next = LIST_POISON1;
  15. 15. entry->prev = LIST_POISON2;
  16. 16. }
  17. 17. static inline void list_del_init(struct list_head *entry)
  18. 18. {
  19. 19. __list_del_entry(entry);
  20. 20. INIT_LIST_HEAD(entry);
  21. 21. }

        先看第一个函数,就是把要删除的这个结点后面的结点的prev指向要删除结点的前面的结点,再把要删除的这个结点的前面的结点的next指向要删除的后面的那个结点,简而言之,就是把要删除的这个的前后相连,就把它自己分离出来了。但是光把前后结点相连还没有彻底完成删除,还要对待删除结点进行一些操作,后面的三个函数的不同点就是处理这个待删除节点的操作时不一样的。先解释第四个函数的操作,INIT_LIST_HEAD(entry),就是前面说到过的那个初始化函数,作用是把entry的prev和next都指向它自身,就不会让它们乱指,也是一种安全的删除方式。再看看第三个函数它是分别将next和prev赋值为LIST_POISON1和LIST_POISON2,这两个是宏定义的常量,在include/linux/poison.h中定义的,源代码如下:

  1. 1. /*
  2. 2. * These are non-NULL pointers that will result in page faults
  3. 3. * under normal circumstances, used to verify that nobody uses
  4. 4. * non-initialized list entries.
  5. 5. */
  6. 6. #define LIST_POISON1 ((void *) 0x00100100 + POISON_POINTER_DELTA)
  7. 7. #define LIST_POISON2 ((void *) 0x00200200 + POISON_POINTER_DELTA)

        大概意思就是一个常人基本不能用的一个地址,就相当于把prev和next屏蔽掉,不能通过它在切入到链表中,但是它属于不安全的方式。第二个函数什么多没做,更加的不安全。

2)与非循环单链表删除操作的区别

        在非循环单链表的删除操作中,传入的参数一般是头指针和删除位置,而这里传入的参数是删除元素的地址,比单链表更高级。

6.双向循环链表的替换操作(两个参数为待替换和被替换的元素地址)

1)替换

        对于替换操作,总共定义了两个函数,后一个函数都会调用第一个函数,详见下面的代码。第一个为基础函数,第二函数调用完第一个函数即完成替换后,还要把被替换的元素的前驱与后驱指针指向它自己,即前面说到的INIT_LIST_HEAD函数初始化。

  1. 1. static inline void list_replace(struct list_head *old,
  2. 2. struct list_head *new)
  3. 3. {
  4. 4. new->next = old->next;
  5. 5. new->next->prev = new;
  6. 6. new->prev = old->prev;
  7. 7. new->prev->next = new;
  8. 8. }
  9. 9. static inline void list_replace_init(struct list_head *old,
  10. 10. struct list_head *new)
  11. 11. {
  12. 12. list_replace(old, new);
  13. 13. INIT_LIST_HEAD(old);
  14. 14. }

        就是把要修改的那个结点的prev和next赋值给新的结点,然后把原来前后的结点对应的next和prev再指向新结点,这就是第一个函数的作用,就是用来修改指向的,而对于修改过的那个结点要做类似于删除的操作,在第二个函数中体现,与上文提到的删除一样,使用INIT_LIST_HEAD()函数,把原来的结点的prev和next都指向自己,安全删除。

7.双向循环链表的移动操作(两个参数分别为待移动结点地址,待移动结点所在的链表。)

        对于移动操作,总共定义了两个函数,详见下面的代码。第一个为把元素移到首节点,第二为把元素移动到尾结点。

  1. 1. static inline void list_move(struct list_head *list, struct list_head *head)
  2. 2. {
  3. 3. __list_del_entry(list);
  4. 4. list_add(list, head);
  5. 5. }
  6. 6. static inline void list_move_tail(struct list_head *list,
  7. 7. struct list_head *head)
  8. 8. {
  9. 9. __list_del_entry(list);
  10. 10. list_add_tail(list, head);
  11. 11. }

        第一个函数是把一个结点移动到头结点之后,使用的方法就是把待移动结点从原来位置分离出来(最终使用__list_del函数),然后在把它增加到头结点后(使用list_add函数);第二个函数是把一个结点移动到最后,使用的方法也是先把待移动结点从原来位置分离出来(使用__list_del函数),然后在把它增加到最后(使用list_add_tail函数)。这个地方就体现出函数功能的独立化的好处了,当然这只是一部分。

8.双向循环链表的状态检测

  1. 1. static inline int list_is_last(const struct list_head *list,
  2. 2. const struct list_head *head)
  3. 3. {
  4. 4. return list->next == head;
  5. 5. }
  6. 6. static inline int list_empty(const struct list_head *head)
  7. 7. {
  8. 8. return head->next == head;
  9. 9. }
  10. 10. static inline int list_empty_careful(const struct list_head *head)
  11. 11. {
  12. 12. struct list_head *next = head->next;
  13. 13. return (next == head) && (next == head->prev);
  14. 14. }
  15. 15. static inline int list_is_singular(const struct list_head *head)
  16. 16. {
  17. 17. return !list_empty(head) && (head->next == head->prev);
  18. 18. }

        第一个函数是判断是否为链表尾部,使用的方法是判断当前结点的next是否指向头结点,因为是双向链表,很容易理解。参数说明:list参数为待判断结点地址的值,head参数为头结点地址的值,即代表哪个链表。

        第二个函数是判断链表是否为空,使用的方法同样容易理解,就是判断头结点的next是否是指向自身的。参数说明: head参数为头结点地址的值,即代表哪个链表。

        第三个函数比第二个函数多一个“careful”,因为它还要检测prev是否也是指向自身。参数说明: head参数为头结点地址的值,即代表哪个链表。

        最后一个是判断链表是否为单个结点,方法是判断头结点的next和prev是否指向同一个地方,当然它还不能是指向自己,也就是不为空。参数说明: head参数为头结点地址的值,即代表哪个链表。

9.双向循环链表的旋转操作(head参数为头结点地址的值,即代表哪个链表。)

  1. 1. static inline void list_rotate_left(struct list_head *head)
  2. 2. {
  3. 3. struct list_head *first;
  4. 4. if (!list_empty(head)) {
  5. 5. first = head->next;
  6. 6. list_move_tail(first, head);
  7. 7. }
  8. 8. }

        将一个非空双向循环链表的头结点的next(next指向的那个结点,即首结点)移到它的前面,也就是链表的最后,使用了list_move_tail函数。达到的操作是把首结点变为尾结点,尾结点变为倒数第二个结点。

10.双向循环链表的切分操作

  1. 1. static inline void __list_cut_position(struct list_head *list,
  2. 2. struct list_head *head, struct list_head *entry)
  3. 3. {
  4. 4. struct list_head *new_first = entry->next;
  5. 5. list->next = head->next;
  6. 6. list->next->prev = list;
  7. 7. list->prev = entry;
  8. 8. entry->next = list;
  9. 9. head->next = new_first;
  10. 10. new_first->prev = head;
  11. 11. }
  12. 12. static inline void list_cut_position(struct list_head *list,
  13. 13. struct list_head *head, struct list_head *entry)
  14. 14. {
  15. 15. if (list_empty(head))
  16. 16. return;
  17. 17. if (list_is_singular(head) &&
  18. 18. (head->next != entry && head != entry))
  19. 19. return;
  20. 20. if (entry == head)
  21. 21. INIT_LIST_HEAD(list);
  22. 22. else
  23. 23. __list_cut_position(list, head, entry);
  24. 24. }

        和前面一样,还是着重分析第一个函数,先从参数下手,list和head分别为切分链表后形成的两个双向链表的头结点(list为新构建的一个头结点,head为待拆分链表的头结点),而entry是指要切分的位置,这三个形参对应的实参都是地址值

        再看函数体,先找到要切分的位置就是entry,把这个位置记录下来,然后把它之前包括它的一段作为一个双向链表,这个双向链表的头结点为list,把它们首尾相连(首为list,尾为entry),此函数体的前五行就是做这个工作的。

        再把head当做后面的一段链表的头结点,与entry后的第一个结点相连,这个节点就作为后面这段链表的第二个结点,因为此时的head还是与最开始的整体链表的尾部是连接好的,所以不用在做首尾相连的工作了,这也是为什么要用head当后半部分链表的头结点了,就是因为会少做一步操作。

        对于第二个函数,就是加上了一些切分链表的条件,首先不能为空,而且如果是单个结点,还需要保证切分点不是头结点也不是头结点后的结点,排除以上情况,如果切分点就是头结点,那么就是说产生两个链表,前面的链表是一个以list为头结点的空链表,而后面的是以head为头结点的整条链表,其实就是相当于初始化定义一个list头结点就可以了。剩下的就是可以正常切分的切分操作了,就可以使用第一个函数了。

11.双向循环链表的合并操作

        就是把一个双向链表A插到另一个双向链表B中,注意插入的是A链表的有效数据结点。

  1. 1. static inline void __list_splice(const struct list_head *list,
  2. 2. struct list_head *prev,
  3. 3. struct list_head *next)
  4. 4. {
  5. 5. struct list_head *first = list->next;
  6. 6. struct list_head *last = list->prev;
  7. 7. first->prev = prev;
  8. 8. prev->next = first;
  9. 9. last->next = next;
  10. 10. next->prev = last;
  11. 11. }

        首先列举出来的这段代码和list.h中其他每个功能的第一个函数一样,都是被后面的函数调用。先看它的参数,list是一个头结点,就是要把它后面的那一串链表结点插入到其他链表中prevnext是要插入的位置(链表)的前后结点

        在函数体中,先用first和last记录这串要插入结点的头和尾,把first和要插入位置的前面prev相连,把last和要插入位置的后面next相连,就像插入一个大结点一样把链表插入到规定位置。

  1. 1. static inline void list_splice(const struct list_head *list,
  2. 2. struct list_head *head)
  3. 3. {
  4. 4. if (!list_empty(list))
  5. 5. __list_splice(list, head, head->next);
  6. 6. }
  7. 7. static inline void list_splice_tail(struct list_head *list,
  8. 8. struct list_head *head)
  9. 9. {
  10. 10. if (!list_empty(list))
  11. 11. __list_splice(list, head->prev, head);
  12. 12. }

        可以观察两个函数体中调用上一函数的参数,第一个函数是把链表插入到head之后head->next之前,就是说把链表插到头结点后,类似于头插法,第二个函数是把链表插入到head->prev之后,head之前,就是说把链表插到链表的最后,类似于尾插法。当然,在插入链表之前,要保证待插的那个链表不是空链表。

        这时就会衍生出一个问题,对于把新链表“带来的那个list头结点该怎么办,源代码作了如下操作:

  1. 1. static inline void list_splice_init(struct list_head *list,
  2. 2. struct list_head *head)
  3. 3. {
  4. 4. if (!list_empty(list)) {
  5. 5. __list_splice(list, head, head->next);
  6. 6. INIT_LIST_HEAD(list);
  7. 7. }
  8. 8. }
  9. 9. static inline void list_splice_tail_init(struct list_head *list,
  10. 10. struct list_head *head)
  11. 11. {
  12. 12. if (!list_empty(list)) {
  13. 13. __list_splice(list, head->prev, head);
  14. 14. INIT_LIST_HEAD(list);
  15. 15. }
  16. 16. }

        看函数名就知道了,就是多了一步init,就是利用INIT_LIST_HEAD函数,把list“释放”了。

二、通过宏定义的方式对双向循环链表进行遍历

1.基本的宏

        在list.h中,涉及遍历的宏非常多。但只要弄清楚一个基本的宏,其他的宏都是进行“封装”。

(1)list_entry宏

        遍历中基本的宏如下:

  1. 1. #define list_entry(ptr, type, member) /
  2. 2. container_of(ptr, type, member)

        从上面代码中可知,遍历基本的宏就是调用宏container_of,它的代码如下:

  1. 1. #define container_of(ptr, type, member) ({ /
  2. 2. const typeof( ((type *)0)->member ) *__mptr = (ptr); /
  3. 3. (type *)( (char *)__mptr - offsetof(type,member) );})

        宏container_of的三个参数:ptr, type, member即结构体成员的指针,结构体类型,结构体成员。

        上述宏中,typeof是GNU C对标准C的扩展,它的作用是根据变量获取变量的类型。这个变量即((type *)0)->member,它的意思是将0转化成type类型的指针变量,即指向type类型的指针,也即存放type类型的地址,而0的值就是0,所以这里的type类型指针变量的值为0x0,这里为什么可以这样实现而不会出错呢?有两点原因,其一,地址0是在编译器编译时已经指定好了的,其二,这里全部都是取址操作,并没内存数据访问,因此不会存在非法访问内存的问题。

        然后再引用member成员,即 ((type *)0)->member ),所以typeof( ((type *)0)->member )即返回member成员的数据类型。

        那么这条语句整体就是将临时的常指针变量__mptr强制转换成member成员的数据类型,再将ptr赋给它(ptr本身就是指向member的指针)。这样__mptr就指向member成员,即存放type型结构体变量对应的member成员的地址,在实际应用中,一般要定义一个type类型的结构体变量,然后才能引用member成员作为宏container_of的第三个参数,宏container_of的第一个参数就为此成员加个取地址符&

        宏container_of的第二条语句中offsetof是定义在linux/include/stddef.h中的一个宏,原型为:

1.	#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)  

        上述先把0强制转换为TYPE类型的指针变量,即存放type类型变量的地址,所以这里type类型变量地址为0,然后以此地址为基础引用member成员,然后再取地址,所以&((TYPE *)0)->MEMBER即为type型变量引用的member成员的地址,由于type型变量地址为0,所以&((TYPE *)0)->MEMBER就是member成员在TYPE数据类型中的偏移量(结构体变量对应的成员的地址等于结构体变量地址即首地址加上相应偏移,而此时结构体变量的地址值为0,所以此时结构体变量对应的成员的地址就等于偏移量)。然后强制转换为无符号长整型。

        注: ->的优先级高于取地址符&

        最后,在宏container_of的第二条语句中,即(char *)__mptr - offsetof(type,member)根据上述分析,__mptr用来存放type型结构体变量对应的member成员的地址的临时常指针变量,(char *)__mptr - offsetof(type,member)type型结构体变量对应的member成员的地址减去member成员在TYPE数据类型中的偏移量,从而得到type型结构体变量的地址,所以container_of宏的功能就是根据一个结构体变量中的一个域成员变量的指针来获取指向整个结构体变量的指针,最典型的应用就是根据链表节点获取链表上的元素对象。

        注:_mptr被强制转换成了(char *),为何要这么做?因为如果member是非char型的变量,比如为int型,并且假设返回值为offset,那么这样直接减去偏移量,实际上__mptr会减去sizeof(int)*offset!这一点和指针加一减一的原理相同。即保证指针相减时是以一个字节为单位进行相减的,保证了程序的正确性。

(2)list_first_entry宏

        获取双向循环链表中第一个有效结点的地址,即类似单链表中首结点的地址。源码如下所示:

  1. 1. #define list_first_entry(ptr, type, member) /
  2. 2. list_entry((ptr)->next, type, member)

        继续使用第一大点中第2小点中的结构体作为例子,结构体如下:

  1. struct userinfo{
  2. char username[20];
  3. char password[20];
  4. struct list_head list;
  5. };

        假如双向循环链表的每个结点都为上述结构类型,并且继续延用第一大点第3小点的例子,如下:

        struct userinfo  userlist;

        INIT_LIST_HEAD(&(userlist.list));

        此时list_first_entry(ptr, type, member)list_entry((ptr)->next, type, member)第一个参数(ptr)->next可以为(userlist.list.next),即为头结点的next,初始化双向链表时,next时指向头接点的,即存放头结点的地址,而在Linux-3.5内核源码的list.h文件中对list_first_entry(ptr, type, member)宏使用时注意事项写明了链表假设不为空链表,也就是说next是存放首结点(存放第一个有效数据的结点) list成员的地址(并不是存放整个结点的地址)

      然后再看宏list_entry((ptr)->next, type, member)的第二个参数,此参数可以为struct userinfo,第三个参数可以写list。这样整个宏就返回首结点的地址,原因在于根据上述分析list_entry宏的作用就是根据结构体域成员的地址而得到整个结构体变量的地址。

注意:

        上述思想与非循环单链表有区别,非循环单链表操作时,都是通过操作每个结点的地址。每个结点的地址采用头指针获得。

         这里的双向循环链表使用更灵活,每个结点中有list_head类型的结构成员,此结构成员有两个指针成员作为连接双向循环链表的指针,这两个指针在初始化时都指向头结点list成员本身,当为非空链表时,就指向双向循环链表的其他结点的list成员了。

        然后通过宏的操作非常巧妙地使用list的地址获取整个结点的地址从而实现对双向循环链表的操作,特别是遍历链表时的核心实体算法。

2.双向循环链表的遍历

(1)从左向右遍历

         上述都为遍历链表核心实体算法,在list.h文件中有两个宏实现遍历链表的for循环。如下代码:

  1. 1. #define list_for_each(pos, head) /
  2. 2. for (pos = (head)->next; prefetch(pos->next), pos != (head); /
  3. 3. pos = pos->next)
  4. 4. #define __list_for_each(pos, head) /
  5. 5. for (pos = (head)->next; pos != (head); pos = pos->next)

        这两个宏有两点不同,1、名字不同,第二个要多两个下划线(--)。2、在for循环的条件中第二个要少一个prefetch(pos->next)。

        第一个宏中多的参数prefetch的作用是预取结点,以提高速率的。所以第一个宏用于结点元素较多的双向循环链表,二个宏实用于元素较少的。

        我接触的应用应该第二个宏使用较多,即__list_for_each(pos, head)。此宏的实体中for循环的初始值为pos=(head)->next,即指向第一个结点;循环条件是pos不是head位置,很典型的遍历链表;循环体即不断地指向下一个结点,使用方法可以参考如下例子:

  1. struct userinfo{
  2. char username[20];
  3. char password[20];
  4. struct list_head list;
  5. };
  1. struct userinfo userlist;
  2. struct list_head *pos;
  3. INIT_LIST_HEAD(&(userlist.list)); 
  4. ………         (省略号,如链表的插入等操作)
  1. //遍历
  2. 1. __list_for_each(pos,&(userlist.list)){
  3. 2. temp=list_entry(pos,struct userinfo,list);
  4. 3. printf("用户名:%s 密码:%s\n",temp->username,temp->password);
  5. 4. }
  1. 上述核心代码代入宏替换后如下:
  2. for (pos =&(userlist.list) )->next; pos != (head); pos = pos->next)
  3. {
  4. temp=list_entry(pos,struct userinfo,list);
  5. printf("用户名:%s 密码:%s\n",temp->username,temp->password);
  6. }

(2)从右向左遍历

  1. 1. #define list_for_each_prev(pos, head) /
  2. 2. for (pos = (head)->prev; pos != (head); pos = pos->prev)

        与向右开始遍历的区别在于prev,这里就不再详细描述。

(3)安全的向前向后遍历

  1. 1. #define list_for_each_safe(pos, n, head) /
  2. 2. for (pos = (head)->next, n = pos->next; pos != (head); /
  3. 3. pos = n, n = pos->next)
  4. 4. #define list_for_each_prev_safe(pos, n, head) /
  5. 5. for (pos = (head)->prev, n = pos->prev; pos != (head); /
  6. 6. pos = n, n = pos->prev)

        比上述(1)、(2)点中的宏的for条件多了一个n,并赋值为pos->next或pos->prev,然后在循环体中用n作为暂存容器,进行向前指或向后指,以防止意外发生(比如防止pos被释放时引起的链表断裂),内核源码就是考虑得全面。

(4)使用遍历

  1. 1. #define list_for_each_entry(pos, head, member) /
  2. 2. for (pos = list_entry((head)->next, typeof(*pos), member); /
  3. 3. &pos->member != (head); /
  4. 4. pos = list_entry(pos->member.next, typeof(*pos), member))
  5. 5. #define list_for_each_entry_reverse(pos, head, member) /
  6. 6. for (pos = list_entry((head)->prev, typeof(*pos), member); /
  7. 7. &pos->member != (head); /
  8. 8. pos = list_entry(pos->member.prev, typeof(*pos), member))

        只用看第一个定义,循环初值为pos=list_entry((head)->next,typeof(*pos),member),通过上面的分析,套入这些参数,就可以知道,就是把pos指向了第一个结点,循环条件中的pos->member!=(head),可以理解成member就是类型为结构体list_head的一个数据单元,当这个单元就是head时,说明循环了一圈,循环体为pos=list_entry(pos->member.next,typeof(*pos),member),就是把pos的成员member的next赋给pos,相当于把pos指向了下一个结点,使循环“启动起来”。它这样写是因为结构体list_head只是一个“桥”,它一般不会单独使用的,而是放在其他结构体中嵌套使用,所以这时它就成为一个结构体中的成员member。第二个宏定义顾名思义,就是倒着遍历。

        (前面我们说过,用在list_for_each宏进行遍历的时候,我们很容易得到pos,我们都知道pos存储的是当前结点前后两个结点的地址。而通过list_entry宏可以获得当前结点的地址,进而得到这个结点中其他的成员变量。而下面两个宏则可以直接获得每个结点的地址,我们接下来看它是如何实现的。为了方便说明以及便于理解,我们用上文中的结构struct stu来举例。pos是指向struct stu结构的指针;list是一个双链表,同时也是这个结构中的成员,head便指向这个双链表;member其实就是这个结构体中的list成员。

        在for循环中,首先通过list_entry来获得第一个结点的地址;&pos->member != (head)其实就是&pos->list!=(head);它是用来检测当前list链表是否到头了;最后在利用list_entry宏来获得下一个结点的地址。这样整个for循环就可以依次获得每个结点的地址,进而再去获得其他成员。理解了list_for_each_entry宏,那么list_for_each_entry_reverse宏就显而易见了。

(5)list_prepare_entry宏

  1. /**
  2. * list_prepare_entry - prepare a pos entry for use in list_for_each_entry_continue()
  3. * @pos: the type * to use as a start point
  4. * @head: the head of the list
  5. * @member: the name of the list_struct within the struct.
  6. * Prepares a pos entry for use as a start point in list_for_each_entry_continue().
  7. */
  8. #define list_prepare_entry(pos, head, member) \
  9. ((pos) ? : list_entry(head, typeof(*pos), member)

(6)下一个结点开始遍历

  1. #define list_for_each_entry_continue(pos, head, member) \
  2. for (pos = list_entry(pos->member.next, typeof(*pos), member); \
  3. &pos->member != (head); \
  4. pos = list_entry(pos->member.next, typeof(*pos), member))
  5. #define list_for_each_entry_continue_reverse(pos, head, member) \
  6. for (pos = list_entry(pos->member.prev, typeof(*pos), member); \
  7. &pos->member != (head); \
  8. pos = list_entry(pos->member.prev, typeof(*pos), member))

(7)当前结点开始遍历

  1. #define list_for_each_entry_from(pos, head, member) \
  2. for (; &pos->member != (head); \
  3. pos = list_entry(pos->member.next, typeof(*pos), member))

(8)安全版本

  1. 1. #define list_for_each_entry_safe(pos, n, head, member) /
  2. 2. for (pos = list_entry((head)->next, typeof(*pos), member), /
  3. 3. n = list_entry(pos->member.next, typeof(*pos), member); /
  4. 4. &pos->member != (head); /
  5. 5. pos = n, n = list_entry(n->member.next, typeof(*n), member))
  6. 6. #define list_for_each_entry_safe_continue(pos, n, head, member) /
  7. 7. for (pos = list_entry(pos->member.next, typeof(*pos), member), /
  8. 8. n = list_entry(pos->member.next, typeof(*pos), member); /
  9. 9. &pos->member != (head); /
  10. 10. pos = n, n = list_entry(n->member.next, typeof(*n), member))
  11. 11. #define list_for_each_entry_safe_from(pos, n, head, member) /
  12. 12. for (n = list_entry(pos->member.next, typeof(*pos), member); /
  13. 13. &pos->member != (head); /
  14. 14. pos = n, n = list_entry(n->member.next, typeof(*n), member))
  15. 15. #define list_for_each_entry_safe_reverse(pos, n, head, member) /
  16. 16. for (pos = list_entry((head)->prev, typeof(*pos), member), /
  17. 17. n = list_entry(pos->member.prev, typeof(*pos), member); /
  18. 18. &pos->member != (head); /
  19. 19. pos = n, n = list_entry(n->member.prev, typeof(*n), member))

        名字和前面的好多宏都是只差一个safe,那就是说是安全一点的遍历,也就是加了一个中间暂存的变量。

三、哈希链表的操作
        list.h后面还有对哈希链表的操作,数据结构中学了哈希表后再分析。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/128102
推荐阅读
相关标签
  

闽ICP备14008679号