哎呀,两位数啦!
老早以前就发过状态说要有链表,要能装任何数据类型。结果那时候没人响应我……其实那时候我是在CSDN上看见有人说Linux的内核中构造的链表能包含所有的数据类型,并且对实现方法给出了简单的介绍才想出个题,看看有没有人能想到这个思路。
其实原理很简单,形象点说就是我们构造一个铁链,只要附上钩子不管是什么样的东西都能挂在上面。对于内存而言不过是一个大小不确定连续内存块罢了,这没啥行不通的~但问题是到了高级语言里要实现这样的链表就有困难了,因为最简单的办法——结构体不允许内存块的大小有变化,就是说,结构体里面包含的数据类型是不能动态改变的。所以,创造一个能动态改变的数据结构来实现这个功能就太复杂了。Linux内核里的办法是创造一个空链表,然后让需要用链表的数据结构包含这个链表。结构体的内存结构(就是指哪个数据放在哪,分别占多少空间,相对位置是怎样的……)是在编译时就确定了的。也就是说,我们可以知道链表节点的地址与链表需要保存的数据之间差了多远,只要知道了链表节点的地址,我们就能算出这个节点想保存的数据的地址在哪,也就能访问到这个数据!
神奇的事情发生了吧!
好,下面定义这个链表:
1 struct list{
2 list *prev;
3 list *next;
4 };
这是个双向环形链表,两个指针域分别指向前一个和后一个节点。
然后用下面这两个宏来初始化每个你要用的链表节点。
#define LIST_HEAD_INIT(name) {&(name),&(name)}
#define LIST_HEAD(name) list name=LIST_HEAD_INIT(name)
比如你可以这样写:
LIST_HEAD(newList);
这样就建立了一个两个指针域都指向自己的新节点。这个宏展开之后是这样的:
首先展开成:
list newList = LIST_HEAD_INIT(newList );
然后右半边再次被展开:
list newList = {&(newList),&(newList)};
其中大括号里面两个参数分别赋给newList 的两个指针域,令它们指向新节点自己。不用宏也行,就用个函数一样的:
void INIT_LIST_HEAD(list *head)
{
head->prev = head;
head->next = head;
}
这个大家应该都会用吧?我就不举例子了。
为了演示我们随便加一个数据结构:
struct added{
int data;
int add;
list node;
};
然后这样初始化这个节点:
added test;
INIT_LIST_HEAD( &test.node );
如果定义好了Insert(list *listHead, list *newNode),就可以将这个新的节点插入链表中
Insert(listHead,&test.node);//listHead是一个双向环形链表的头指针
在双向链表里插入节点不难,我就不写了(其实我是刚发现自己以前试验这个链表的时候也没写……)。
这样还是不行,虽然按计划这个链表已经具有了之前设想的结构,但是还是不能用统一的方法来访问节点的数据。所以就有了下面这些宏:
注意:这些宏都是我根据网上的资料和大概的原理还有网上不能通过编译的宏及其解释模拟出来的,如果你们不加修改的使用,并且出错,那很有可能是我的宏写错了。
#define OFFSETOF(TYPE,MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr,type,member) ((type *) (((size_t)ptr)-OFFSETOF(type,member)))
#define list_entry(ptr,type,member) container_of(ptr,type,member)
前一个宏的名字的意思是“偏移量”顾名思义,这个宏返回TYPE类型的结构体中名字(注意,是名字,不是数据类型)为MEMBER的成员相对于结构体头的偏移量。这个从定义上容易看得出来:首先,“((TYPE *)0)”部分把地址0转换成TYPE 类型;然后通过(TYPE *)0)->MEMBER取得TYPE 类型的MEMBER成员,并&((TYPE *)0)->MEMBER)取得其地址;因为TYPE 结构体头的地址是0,所以这个地址实际上就是类型未知的成员MEMBER相对于结构体头的偏移量。比如32位机中,地址0处是结构added中的成员data,那么4开始的4个位就存储着成员add;这时data与头的偏移量就是0,add的偏移量就是4。然后得到的偏移量被转换成size_t类型,在visual studio里这个就是unsigned int。这么做是防止指针类型不匹配导致的不能做加减运算的现象。
再看第二个宏。这个宏其实和第三个宏完全一样,但是名字不同,而且据说在Linux内核代码中也属于不同的头文件。所以我推测第二个宏应该还有别的作用。但是我在模拟时发现我在网上找到的版本似乎不能正常工作……所以我重新写了一个,不知道是否还具有网上给出的宏的一般性。
第二个宏中PTR是指向type结构体中的list类型指针成员的指针,type是包含了链表节点的数据结构的结构体名(比如,在这篇文章里的"added"),member则是你想得到的type类型结构体里面的名字为member的数据成员(再次注意,是名字不是类型,比如在added中,你可以写“data”但是不能写“int”)。这个宏用((size_t)ptr)把指向结构体里面链表节点成员的指针转换成32位无符号地址,然后((size_t)ptr)-OFFSETOF(type,member),也就是和指针成员相对于结构体头的偏移量做差,得到结构体的地址;然后再强制转换成((type *) (((size_t)ptr)-OFFSETOF(type,member)))结构体类型的指针。由于这个宏展开之后是个表达式,所以它会有返回值,这个返回值就是结构体的指针啦。
至于第三个宏,它除了名字之外跟第二个没区别。
它们是这样用的:
added *tempList;INIT_LIST_HEAD(&tempList.node);
list *ptrList;LIST_HEAD(ptrList);
ptrList = listHead->next;//让ptrList 指向第二个节点
ptrList = ptrList->next;//让ptrList 指向第三个节点
tempList = list_entry(ptrList, added, node);//获取结构体地址
intData = tempList->data;//获取数据成员data
intAdd = tempList->add;//获取数据成员add
以上的代码里,listHead是链表的表头。当然双向环形链表无所谓表头表尾,这不过是个方便访问的指针而已。
其他的我听说还有访问数据,执行各种操作的宏和函数,我也没去细究。有兴趣的可以接着我的这些东西完善下去~或者搞点Linux内核代码看看。当然如果你有,记得给我发一份~