本系统仿照Linux中的队列实现是一个双向链表,个人认为Linux中的双向链表实现简直太妙了。
在学习数据结构课程时,都学过双向链表这种数据结构,基本上都是下面这种结构:
struct student { struct student* next; int id; char name[30]; struct student* pre; };
很多节点组成的双向链表结构大致如下:
我特意在每个结构体周围花了虚线的边框,表示整个结构体。(并不表示结构体本身和成员有空隙)。
会发现,这样的链表有一下很重要的特点:
但是会发现,这样的链表有很大的局限性:
但是在Linux中,实现了一种更加通用的链表结构,简直太妙了。
在Linux中的双向链表的实现是这样的(听说,我也没看过Linux源代码)。
下面是链表的节点的结构:
/*结点中不需要数据成元,只要求前驱和后继结点指针*/ struct list_elem { struct list_elem* prev; // 前躯结点 struct list_elem* next; // 后继结点 };
乍一看,总感觉怪怪的,怪就对了:节点中没有放数据。
没有数据的链表有什么用呢?这些节点串起来一点用也没有啊?
常规链表中,把数据放在链表节点里面。Linux中,把链表节点放到数据里面。
直接上代码演示吧,如下是一个学生节点:
struct student { int id; char name[30]; struct list_elem tag; };
看到这里,你应该大概懂了,数据节点怎样组成链表了,基本上如下图所示:
我们可以知道,这样的链表有一下几个特性:
这个时候问题来了,数据节点是串起来了,但是怎么访问呢?
一般我们会遍历链表,可是最终我们需要的是数据啊,这样的链表只能访问到student节点的成员tag啊,如何访问到student节点本身呢?
现在的问题是如何通过结构的成员访问到结构体本身?
这就提现了C语言的强大之处了,C语言可以直接操作内存(当然是虚拟内存),所以,只要计算出链表节点(struct list_elem tag
)在整个节点(struct student
)的地址偏移量即可。
那么怎么应该计算呢?计算偏移量似乎也要一个特定的结构体才行啊?不同的结构体(数据节点)计算偏移量过程并不通用啊?
这个时候,就充分体现出C语言中的宏的厉害之处了!
/* Desciprtion: offset计算某个结构体的成员到该结构体起始地址的偏移量 Parameters: struct_type: 结构体的类型 member: 结构体成员 Return: 结构体成员到该结构体的偏移量 使用方法: 例如有一个struct student结构体,有个成员为tag,计算tag到student的偏移量:offset(struct student, tag) */ #define offset(struct_type, member) (int)(&((struct_type*)0)->member)
上面就是使用C语言的宏来实现的计算偏移量,例如这里要计算student结构体中tag成员到结构体本身的偏移量:
int offset = offset(struct student, tag);
几点说明:
(struct_type*)0
struct_type
的结构体,很巧妙。struct_type
类型的,但是我们需要一个struct_type
类型的结构体,所以我们把0地址处当成一个结构体,借用一下,然后计算成员地址的偏移量。并没有改变0地址处的内容。struct_type
类型的结构体,至于内容如何我们并不关心,我们只关心地址。(struct_type*)0->member
offset(struct student, tag)
,在宏的内部,会直接替换成(struct student*) 0 -> tag
(int)(&((struct_type*)0)->member)
计算出了结构体成员的地址偏移量,然后就可以通过结构体成员来访问到结构体自身啦。
/* Description: 给出某个结构体的类型、成员,以及需要计算的目标结构体成员地址,得到目标结构体的自身地址 Parameters: struct_type: 结构体类型,比如struct student struct_menber: 结构体成员名称(这里是变量的名称) elem_ptr: 目标结构体成员 地址 使用方法: 比如已经得到结构体student1的成员tag的地址tag1_ptr了,那么需要计算student1结构体的地址: struct student* s = elem2entry(struct student, tag, tag1_ptr); */ #define elem2entry(struct_type, struct_member_name, elem_ptr) \ (struct_type*)((int)elem_ptr - offset(struct_type, struct_member_name))
比如,要计算student1结构体中tag成员的地址tag1_ptr了,计算student1结构体的起始地址:
struct list_elem* tag1_ptr = .....;// 得到了结构体成员tag的地址 struct student* target_student = elem2entry(struct student, tag, tag1_ptr);
简单说明:
tag1_ptr
,然后减去struct student
类型的结构体中的成员地址偏移量,就可以算出来tag1_ptr
所指向的的结构体成员的结构体起始地址f了。解决了如何根据结构体成员取出结构体,那么剩下的问题就非常容易了。
形成链表,就是把所有的链表节点串起来即可。由于链表节点是通用的(是包含在数据节点中),所以对链表的操作,也都是通用的。
这里说一下,我这里使用的是有头和尾节点的双向链表,也就是结构如下所示:
主要就是对链表增删改查了:
#define offset(struct_type,member) (int)(&((struct_type*)0)->member) #define elem2entry(struct_type, struct_member_name, elem_ptr) \ (struct_type*)((int)elem_ptr - offset(struct_type, struct_member_name)) /********** 定义链表结点成员结构 *********** *结点中不需要数据成元,只要求前驱和后继结点指针*/ struct list_elem { struct list_elem* prev; // 前躯结点 struct list_elem* next; // 后继结点 }; /* 链表结构,用来实现队列 */ struct list { /* head是队首,是固定不变的,不是第1个元素,第1个元素为head.next */ struct list_elem head; /* tail是队尾,同样是固定不变的 */ struct list_elem tail; }; /* 初始化双向链表list */ void list_init (struct list* list) { list->head.prev = NULL; list->head.next = &list->tail; list->tail.prev = &list->head; list->tail.next = NULL; } /* 把链表元素elem插入在元素before之前 */ void list_insert_before(struct list_elem* before, struct list_elem* elem) { /* 将before前驱元素的后继元素更新为elem, 暂时使before脱离链表*/ before->prev->next = elem; /* 更新elem自己的前驱结点为before的前驱, * 更新elem自己的后继结点为before, 于是before又回到链表 */ elem->prev = before->prev; elem->next = before; /* 更新before的前驱结点为elem */ before->prev = elem; } /* 添加元素到列表队首,类似栈push操作 */ void list_push(struct list* plist, struct list_elem* elem) { list_insert_before(plist->head.next, elem); // 在队头插入elem } /* 追加元素到链表队尾,类似队列的先进先出操作 */ void list_append(struct list* plist, struct list_elem* elem) { list_insert_before(&plist->tail, elem); // 在队尾的前面插入 } /* 使元素pelem脱离链表 */ void list_remove(struct list_elem* pelem) { pelem->prev->next = pelem->next; pelem->next->prev = pelem->prev; } /* 将链表第一个元素弹出并返回,类似栈的pop操作 */ struct list_elem* list_pop(struct list* plist) { struct list_elem* elem = plist->head.next; list_remove(elem); return elem; } /* 从链表中查找元素obj_elem,成功时返回true,失败时返回false */ bool elem_find(struct list* plist, struct list_elem* obj_elem) { struct list_elem* elem = plist->head.next; while (elem != &plist->tail) { if (elem == obj_elem) { return true; } elem = elem->next; } return false; } /* 把列表plist中的每个元素elem和arg传给回调函数func, * arg给func用来判断elem是否符合条件. * 本函数的功能是遍历列表内所有元素,逐个判断是否有符合条件的元素。 * 找到符合条件的元素返回元素指针,否则返回NULL. */ struct list_elem* list_traversal(struct list* plist, function func, int arg) { struct list_elem* elem = plist->head.next; /* 如果队列为空,就必然没有符合条件的结点,故直接返回NULL */ if (list_empty(plist)) { return NULL; } while (elem != &plist->tail) { if (func(elem, arg)) { // func返回ture则认为该元素在回调函数中符合条件,命中,故停止继续遍历 return elem; } // 若回调函数func返回true,则继续遍历 elem = elem->next; } return NULL; } /* 返回链表长度 */ uint32_t list_len(struct list* plist) { struct list_elem* elem = plist->head.next; uint32_t length = 0; while (elem != &plist->tail) { length++; elem = elem->next; } return length; } /* 判断链表是否为空,空时返回true,否则返回false */ bool list_empty(struct list* plist) { // 判断队列是否为空 return (plist->head.next == &plist->tail ? true : false); }