再论C语言实现的可通用性数据结构(一)——链表1

来源:互联网 发布:请回答1988知乎 编辑:程序博客网 时间:2024/06/04 18:43

        在很久之前就开始学习LINUX的内核了,之前已经很觉得其实现手法之高明,感叹其代码效率之高,并用C语言的某些特性加之引入并实现面向对象(Object-Oriented,下面简称OO)的编程方式达到效率和可重用性的平衡为之惊讶、震撼。而最近由于想搞一个比较好用的数据结构库(一方面想练练手,二方面,有些情况下,用标准库或C++模板未必是最好的选择),重新看了一次,感觉依然不减!而且,还有新收获!

        由于系统内核中经常用到链表、队列等等结构,LINUX是通过把链表主结构抽象出来的做法,使到只需要定义一次链表的相关操作,在不同应用场合都可以使用,这一种可重用性非常好的设计,之前呢写过一份介绍这个结构的日志,但现在去了哪,就不追究了,因为那时忽略了一些细节情况,而这些却是很重要的部分,下边不废话了,直接解说,下边先引入一做代码段,是源于LINUX内核文件中的,源文件位置:/linux-2.6.32.8/linux-2.6.32.8/include/linux/list.h

/*一般头文件开头都会用

*#ifndef……

*#define……

*这个嘛,目的主要是为了防止有同名文件。 下边将会按照程序一样,以注释模式写说明,并用绿色*/

 

#ifndef _LINUX_LIST_H
#define _LINUX_LIST_H

 

……/*引用一段头文件,不写了,下边继续看,下边就是重点(1)*/

 

struct list_head {                                    
 struct list_head *next, *prev;
};

 

#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) /
 struct list_head name = LIST_HEAD_INIT(name)

 

static inline void INIT_LIST_HEAD(struct list_head *list)
{
 list->next = list;
 list->prev = list;
}

/*     

*         说明,上边这一段程序,首先,定义一个list_head结构,字面上是像定义一个链表头,但实际上这是一个链表最

* 基本元素(两个结构)的抽象结构,使用时这可以作为链头也可以作为链表元素!但为何说那个结构既是链头又可以

* 是链表的成员呢?则要看那结构体下边的两个宏和函数,其中第二个会使用到第一个,所以直接讲第二个吧!

* #define LIST_HEAD(name)     struct list_head name = LIST_HEAD_INIT(name),可以看到后半段就是第一个宏了,宏

* 的一个重要作用就是文本替换了,我们尝试将第一个宏直接替换入第二个,得出 #define LIST_HEAD(name)    /

* struct list_head name = { &(name), &(name) },意思是什么呢?

*         看回之前,因为之前做了抽象结构struct list_head,则用struct list_head声明一个变量时,就具有了struct list_head

* 的特质,那 变量可以调用*next和*prev,但问题来了,虽然struct list_head声明了变量,但该变量所包含的*next和*pre

* v还没有宿主,而两那个宏的作用就体现了!当我们直接用宏LIST_HEAD后,则等于用struct list_head声明一个变量,

* 并取该变量的地址用以给*next和*prev使用,这样就等于声明了一个链头,并且初始化把其的*next和*prev指向自身。

*不明白的话可以用编译器执行以用上边那段作为头文件,然后执行以下程序!

*

*程序例1:

* int main()
* {
*    LIST_HEAD(j);

*    printf("%x/n",j.next);      

*       printf("%x/n",j.prev);
* } 

 

* 在编译器中以逐步执行一次这例子,如果不明白的话尝试将#define LIST_HEAD_INIT(name) { &(name), &(name) }转换

* 为#define LIST_HEAD_INIT(name) {},可以看看下边效果图:

* 未转换时:

图片

                                                        图1

变换后:

 

图片 

                                                         图2

* 注意地址变化!(用LINUX上Q-ZONE不方便,只能WINDOWS+.net,观看的LINUX友将就一下)! 

* 之后到了函数static inline void INIT_LIST_HEAD(struct list_head *list),其实明眼人一看都知这是一个链表初始化(它内

* 容是指向自己的,即是初始化及置空)的函数,如果是以下情况的话,其实和之前上边讲的那个宏的作用是差不多:

* int main()
* {
*       struct list_head j;
*       INIT_LIST_HEAD(&j);
*       printf("%x/n",j.next);
*      printf("%x/n",j.prev);
* }

效果图:

图片

                                     图3

 

* 那么为何要搞两组这样的东西呢?笔者觉得是出于两种考虑,一是为方便应用,适应不同情况,如果只有宏的那种方

* 式,那有时如果需要将某一个链表置空、初始化会很麻烦。而二是为代码效率,虽然static inline void INIT_LIST_HEAD

* (struct list_head *list)是内联函数,速度一般的快,但如果在大型工程中,有一种说法就是宏定义的速度会较用函数好,

* 而且在适当的时候,可以化两步为一步,省了一点功夫!

 

*其实LINUX中的链表,那struct list_head 就如一条线(如下图示)!把链表穿起来,这样做法就如一开始所讲的可重用性

*高!

图片

                  图4,此图片转自http://www.th7.cn/Article/cz/li/201002/375437.html)

 

* 上边的仅仅是对于链头的操作,还需要加入其它元素才成一个链表,这则依赖于下列函数:

* static inline void list_add(struct list_head *new1,
*          struct list_head *prev,
*         struct list_head *next)
* {
* next->prev = new1;
* new1->next = next;
* new1->prev = prev;
* prev->next = new1;
* }

*

*这个函数具体使用方法:假设有两个结构要组成一个队列,只要如下这样做就OK。

* typedef struct C
* {
*    struct list_head j;
*     int k;
* }P;
* typedef struct D
* {
*     struct list_head j;
*  int f;
* }Q;


* int main()
* {
* LIST_HEAD(j);
*     P *temp2=(struct C *)malloc(sizeof(struct C));
*     P *p=temp2;
*     p->k=5;
*     Q *temp3=(struct D *)malloc(sizeof(struct C));
*     Q *q=temp3;
*     q->f=6;

*     list_add(&(p->j),&j,&(q->j));
* }

* 这其实就和一般链表的插入操作没分别的,其它操作也非常容易,不详细讲了!

*         如果是在一个大工程中,有N种链表、队列等等,这样只需要定义一组相关操作,就可以应用于全部,很省事很爽

*吧?

*         这样写很有面向对象可重用的意味很酷吧!先别开心,现在,穿起整个链的线我们有了,但有没有发现一个问题?

* 如果访问链表某个节点或遍历链表找某个元素值时,只能够反映出链上*next和*prev的地址,但具体数据很难访问

* 呢? 如果把这用在一个项目中,访问岂不是要很复杂而且很易搞错?要是搞成这样就交去做软件评测,分分钟在背后被

* 评测工程师骂N次,甚至翻工几次,要是给你的项目主管看见你这样的代码,那可是主管不高兴,后果很严重了!

*         这样的不完善代码在一个这么优秀的系统中当然是不可能的,下边就看看LINUX中如何在找到地址时非常快速地把

* 那元数的数据也翻出来!内核中有一个专门处理这方面的宏:#define list_entry(ptr, type, member)    container_of(ptr, ty

* pe, member),这倒看不出什么哦!但在宏list_entry之后执行的是另外一个宏container_of(ptr, type, member),这个宏

*位于别一个头文件:/linux-2.6.32.8/linux-2.6.32.8/include/linux/kernel.h中的,因为那个宏写得比较复杂,在这里用几种颜

*色比较容易看:

*1、 #define container_of(ptr, type, member) (

*2、{   /
*3、 const typeof   ( ( ( type * ) 0 ) ->member  ) *__mptr = (ptr); /
*4、 ((type *)( (char *) __mptr - offsetof ( type,member )  );} )

*

*先借用一个例子:

*struct mylist{void *data;struct list_head list;}; list_head *pos; /*(这里具体指向操作不写了)*/

*list_entry(pos,mylist,list);

 

*         看上去很复杂哦,到底什么意思呢?第1、2行不解释了,第3行,const typeof(((type *)0)->member)*__mptr=(ptr)

* 把例子代入去: const typeof( ( (mylist *) 0 ) ->list)*__mptr=(pos); 先说明,typeof为GCC对C语言扩展,一般编译器没有

* 的,实际作用就是取其后边括号参数中的类型,至于入边((type *)0)->member这种句型什么意思呢?稍后就讲到,而第

* 3行的意思就即是用list的类型去声明一个指针mptr,并且其值就是pos所指向的地方!

*       最重要的地方是第4行, (type *)( (char *) __mptr - offsetof ( type,member )  );} ),其中,offsetof是一个求偏移量的

* 宏,其实现方法为: #define offsetof(type,member) ((size_t) &((TYPE *)0)->MEMBER),将offsetof这个宏展开,则有:

 第四行为:(type *)( (char *) __mptr - ((size_t) &((TYPE *)0)->MEMBER)这个宏看得人有点头晕,其实只需要搞明白
* 后边的那个((size_t) &((TYPE *)0)->MEMBER)
,其实offsetof是一个ANSI C 99下的宏,并且有一位比较出名的C语言专

* 家Nigel Jones  ,他可能在嵌入式方面比较出名(他是一名嵌入式技术顾问),在一篇博客文章中已经解释过这个宏,

* 下边引述一下: 

* ((s *)0) takes the integer zero and casts it as a pointer to s. (将整型常量0转换为指向S类型的指针。)

* ((s *)0)->m dereferences that pointer to point to structure member m. (将指针指向结构成员m。)

* &(((s *)0)->m) computes the address of m.  (取刚先指向的m的地址。)

* (size_t)&(((s *)0)->m) casts the result to an appropriate data type.(将地址化为size_t类型。)

*         其实并不难吧,但很抽像,来看看下边的一个解释

*          由于上边那个宏的括号有点多,其实下边详细解释一下就很容易明白,

* 首先,&((TYPE *)0)->MEMBER)就如上边讲到的,将常量0强制转换为指向某个结构类型的指针(某些说法是将0地址

* 指向了某个结构,其实这种说法也是正确的,因为C语言中的一个特性,ANSI C 99标准中 6.3.2.3-3中说明的那样:

* An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constan

* t.55) If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to co-

* mpare unequal to a pointer to any object or function.)

*上边这段引述之ANSI C99,稍稍翻译第一句就会明了:0为一个整常量,或通过表达式转换为void *,被称为空指

* 针。

* 所以,0(常量/地址)经过&((TYPE *)0)->MEMBER)后变成了指向某个结构的指针的同时,结构内成员变量的地址会

* 以0作为起始地址,所以其结构成员变量就是这个结构起始地址的相对偏移量!因此,&((TYPE *)0)->MEMBER)操作后

* 就会得到一个结构内偏移量的地址!也就等价于当有指针P时,执行P=((Type *)0);,之后输出&(P->member);结果如下

* 图:

 图片

             图5-1

 

图片

 

*上图那过程执行之前存在结构 C,有成员k和D,红色线为执行前,蓝色线为执行后,地址变了从0开始,由于list_head l

* ist位于结构的两个元素之后,所以,从0地址起计算,是地址是10,所以&(p->list)是10,注意这个是一个地址,与执行o

* ffsetof(struct C,list)的结果是不同的,如果不明白为何这是地址(可能是因为看见宏太复杂,突发

* 性发代码瘟),可以想想指针p=(type *)0这是一种什么形式,看看接着这一段,执行一下:

*……

*   int i=1 , s=2;     int *r=&i ,  *t=&s;

*   printf("%x/n",r);  printf("%x/n",t);  printf("%d/n",*r);  printf("%d/n",*t);

*   r=(int *)t;

*   printf("%x/n",r);  printf("%x/n",t);  printf("%d/n",*r);  printf("%d/n",*t);

* 看完之后应该会明白图5紧接着的一段话!

* 图5-2那事还没完,有没有注意绿线的地方在执行时的地址和局部变量查看器中的list一样(那个&p,即存放指针p的地

* 址和list一样)?这就说明了图4中的情况,那个结构C如一颗珍珠一样被“穿”在list_head上。

*         ……花了这么大段讲offsetof,终于可以回正题讲将那条链的地址变实际数据了,先举个不太恰当但比较形象的例

*子,假设一条路,连着几* 幢房,每幢房高度不同,每层只有一个大套间,而且每幢有独立门牌号,当邮差要找某一个

*人时,就会先找到某一幢楼(那条路和门牌就等于我们的list_head所链的那个链表),而楼层高度当然是从一楼起计

*(一楼到某楼就相当于offset),下边有相关图表:

图片

       图6 这是抽象数据的模型!

图片

                               图7  内存中的模型!

 

* 另外, (type *)( (char *) __mptr - ((size_t) &((TYPE *)0)->MEMBER)这个宏当中的那个char *的作用只是为方便计数,将

* ptr转成char *,以字节进行计算!

 

*        就如同上边列举到的这么多东西,这种通用性链表通过把基本的元素抽象出来,之后在访问到每个节点的数据时,

* 就先找到某个结构,之后通过利用偏移量快速转换为数据!这种写链表的方法,既方便管理大量链表的同时效率也很

* 高,而且数据也被充分的封装起来,数据安全性相对较好。这段总结不详细写了,之后再有感觉再添加吧!*/

 

写了几天,终于完成了,下边说明一下本文的作用及一些注意事项:

 

        1、以上本帖主要是以本人理解方式和个人的一些小实验例子来写的,未必对于每个人有用,所以,有相关技术意见的可以提出,但看不明白的话别来找我!而且由于本人才疏学浅,可能有错误之处,如果想应用前请三思而后行。

        2、据本人所了解,某些嵌入式开发行业、公司是禁用offset的,使用类似文中所讲这个链表定义方式时请注意!

        3、本文章是通过参考某些资料后按自己的思维方式写的,为保证本人的文章不会突然飞到网上去献丑或是某些人不尊重劳动力回复都不留就转走,程序话题类文章将会设成禁止转载,只允许堂食,不准打包!

        4、本人欢迎技术讨论,但程序本身并不带政治色彩,所以我希望看客们别讨论政治话题给这文章带上政治色彩!

        5、本文章中只有讲解,没具体例子,迟一点会在另一篇文章中列举或引述一个详细例子以展现这个链表的风味!

原创粉丝点击