一、内核链表的前置概念
到目前为止,我们的顺序表或者链表都以存放整型数据为例,但在实际工作应用中,处理的对象不一定是一个整数,而是任意的数据类型,这就要求我们对顺序表和链表的设计要做一个更加深入的理解。
1、容器
首先要理解,所有的数据结构本质上是一种容器,包括已经学习了的顺序表、链表,以及后续将会学习的栈、队列、二叉树等,所谓容器,指的是只关心其内部数据之间的逻辑关系,并提供这种逻辑关系相对应的操作的集合。容器不关心数据本身的类型,因为对于容器而言,不管是存储一个整数还是存储一个进程,还是一个学生、一本图书,他们都被称为一个数据节点。
如图:
2、通用解决方案
容器提供的是数据处理的通用解决方案,即:提供一套可以处理任意数据类型的通用API,不管是什么数据,都可以统一处理。比如链表,不管处理什么数据,对它们的操作都是统一的:初始化、插入、删除、遍历、销毁等。
目前,有两种常见的方式来获得通用性:
- 创建容器时,让用户提供数据的类型。典型的应用案例是STL(一个C++的类库)。
- 将数据从容器中剥离出去,让容器只提供逻辑,典型应用案例是Linux内核链表。
由于C语言没有类,也不支持重载,受语言本身特性的限制,一般不使用第一种办法来设计通用容器,但在一些小型程序中,C语言也是可以实现通用性的,关键在于:让用户提供数据的类型。而容器本身只处理跟数据逻辑结构相关的操作,凡是涉及具体数据的操作,一律要让用户来提供。
3、下面以双向链表为例,使用上述第一种方法,将其改造成通用的容器。
核心:
①数据域不再固定——链表的节点设计不再固定
②借助工程的模块化和static关键字(限制作用域)
③头文件特性——头文件会在预处理时被展开
二、通用型链表节点的设计
1、初始化
// list.h #ifndef DATATYPE //此条件决定了下面通用结构体的具体类型 #define DATATYPE int #endif typedef DATATYPE datatype; // 此处的节点是通用的 // 原理是将具体数据的类型让渡给用户自己去定义 typedef struct node { //数据域 datatype data; //指针域 struct node *prev; struct node *next; }listnode, *linklist;
以上代码有几处需要着重解释:
- 上述代码必须写在*.h头文件中,而不是*.c源文件中
- 用户使用该容器的时候,定义DATATYPE为其所需要的数据类型
许多人比较困惑的地方在于,既然用户需要提供数据,那为什么不直接让用户定义datatype,而要去定义宏 DATATYPE 呢?原因是 typedef 无法跟宏一样,给用户提供一个默认的数据类型。
接下来,对于跟用户数据无关的操作,无需任何修改,直接就是通用的,比如初始化、判断是否为空:
// 注意以下内容必须放在头文件 list.h 中 // 初始化空链表,与用户实际数据无关 static node * initList() { node * head = (node *)malloc(sizeof(node)); if(head != 0) { head->prev = head; head->next = head; } return head; } // 判断链表是否为空,与用户实际数据无关 static bool isEmpty(node *head) { return head->next == head; }
注意:
通用型算法一律都只能写到头文件 list.h 中,因为编译的时候 datatype 必须结合用户提供的 *.c 源文件才能确定切确的类型,如果单独编辑 list.c,那么在编译产生 list.o 的过程中就无法使用用户所指定的类型。
注意:
通用型算法代码 list.h 的使用方法,就是直接作为头文件放在用户程序中即可,如果用户需要使用链表容器处理其特定的数据,那么就自定义宏 DATATYPE,如:
注意:
为防止头文件被多个C文件包含而造成函数冲突,头文件中的所有函数必须被定义为静态存储类型。
2、增删操作
由于增删操作都涉及用户具体的数据,因此需要对之前的操作作出修改。以增删链表首部第一个节点为例,参考代码如下:
// 根据用户提供的数据,产生一个新节点 static linklist __newNode(datatype *newData) { linklist new = malloc(sizeof(listnode)); if(new != NULL) { new->data = *newData; new->prev = new; new->next = new; } return new; } // 将新节点new插入到链表的首部 void listAdd(linklist head, datatype *newdata) { linklist new = __newNode(newdata); new->prev = head; new->next = head->next; head->next->prev = new; head->next = new; } // 将新节点new插入到链表的尾部 void listAddTail(linklist head, datatype *newdata) { linklist new = __newNode(newdata); new->prev = head->prev; new->next = head; head->prev->next = new; head->prev = new; } // 将指定节点从链表中剔除出去 bool listDel(linklist p) { if(p==NULL || isEmpty(p)) return false; // 将原链表首节点剔除出链表 p->prev->next = p->next; p->next->prev = p->prev; p->prev = p; p->next = p; return true; }
提醒:
不对外的函数接口,一般使用下划线开头,比如 __newNode()
补充:
回调函数:
这类函数是作为参数被其他函数调用
数组名作为参数传递进去本质上是数组首元素地址
函数名作为参数传递进去本质上是函数的地址
关于遍历通用型链表,可以用户自己设计一个适合当前自定义节点的回调函数,专门用来遍历打印使用
3、查找节点
在链表中查找某个节点也是一种常规操作,但查找操作与上述的增删操作有个很大的不同,节点的比对是跟节点本身数据密切相关的,比如整型数据可以直接使用等号来判断是否一致,而字符串则需要通过特定的函数才能判断,至于结构体,则无法使用任何现成的方式去判定,只能由用户根据其实际数据去判定。
因此,查找节点时,节点的判定接口必须由用户提供,链表只提供回调接口。具体代码如下:
// 查找指定的节点,并使用用户提供的钩子函数 equal 判定节点是否 linklist find(linklist head, datatype data, bool (*equal)(datatype, datatype)) { for(linklist tmp=head->next; tmp!=head; tmp=tmp->next) { if(equal(tmp->data, data)) return tmp; } return NULL; }
4、遍历链表
与上述查找算法类似,容器只应提供跟通用性相关的操作,任何涉及用户数据的操作都是不能写的,否则就是去了通用性。之前对链表的遍历,就是将节点中的数据打印出来,这是一种特定的针对整型数据的操作,是不具备通用性的。
注意:
1)在实际应用中,遍历链表时对每个节点的访问操作不一定是将节点内部数据打印出来。
2)对节点的访问方式,应该交给用户去处理,只有用户才知道怎么处理。
容器本身必须且只能提供“挨个访问”每个节点的路径操作,而不能涉及任何数据本身。
3)对节点的操作,需将用户提供的特定操作函数 handle 以参数的方式传入给遍历函数,比如:
// 遍历链表,并使用用户提供的钩子函数 handle 处理节点
void listForEach(linklist head, void (*handle)(datatype *)) { if(isEmpty(head)) return; for(linklist tmp=head->next; tmp!=head; tmp=tmp->next) handle(&tmp->data); }
5、示例代码
double_list.h:
#ifndef DATATYPE #define DATATYPE int #endif typedef DATATYPE datatype; typedef struct node { //数据域 datatype data; //指针域 struct node *next; //后继指针,指向下一个与当前类型一致的成员 struct node *prev; //前驱指针 } listnode, *linklist; //通用型链表初始化 static linklist List_Init() { linklist Head = malloc(sizeof(listnode)); Head->next = Head; Head->prev = Head; return Head; } //通用型链表头插操作 static void HeadInsert(linklist Head, datatype info) { linklist Newnode = malloc(sizeof(listnode)); //数据域 Newnode->data = info; //指针域 Newnode->next = Head->next; Head->next = Newnode; Newnode->next->prev = Newnode; Newnode->prev = Head; } //通用型链表尾插操作 static void TailInsert(linklist Head, datatype info) { linklist Newnode = malloc(sizeof(listnode)); //数据域 Newnode->data = info; //指针域 Newnode->next = Head; Head->prev->next = Newnode; Newnode->prev = Head->prev; Head->prev = Newnode; } //通用型链表遍历操作 static void List_brow(linklist Head, void (*pfunction)(datatype)) { linklist temp = Head->prev; while (temp != Head) { pfunction(temp->data); temp = temp->prev; } } //通用型链表删除节点操作 static void List_Nulldelete(linklist Node) { Node->next->prev = Node->prev; Node->prev->next = Node->next; free(Node); } //通用型链表按条件删除节点操作 static void List_Havedelete(linklist Head, linklist (*fun)(linklist)) { linklist temp = fun(Head); if (temp != NULL) { temp->prev->next = temp->next; temp->next->prev = temp->prev; free(temp); printf("删除成功!\n"); } else printf("删除失败!\n"); } //通用型链表查找节点操作 static void List_Search(linklist Head, linklist (*fun)(linklist)) { linklist temp = fun(Head); if (temp != NULL) { printf("查找成功!\n"); } else printf("查找失败!\n"); } //通用型链表销毁操作 static void List_Destroy(linklist Head) { linklist p = Head; linklist q = p->next; int i = 0; while (q != Head) { p = q; free(p); i++; q = q->next; } free(Head); printf("成功释放%d个节点\n", i); }
double_list.c:
#include <stdio.h> #include <stdlib.h> #include <string.h> struct book { char bookname[64]; char Author[64]; float price; }; #define DATATYPE struct book #include "double_list.h" //打印节点内容 void pridata(datatype binfo) { printf("%s\t%s\t%.1f\n", (binfo).bookname, (binfo).Author, (binfo).price); } //按书名查找节点 linklist Search(linklist Head) { //查找 char buf[32] = {0}; printf("输入你想要查找(删除)的书名:"); scanf("%s", buf); linklist temp = Head->next; int flag = 0; while (temp != Head) { if (strcmp(temp->data.bookname, buf) == 0) { return temp; flag = 1; break; } temp = temp->next; } if (flag == 0) return NULL; } int main() { //创建空表 struct node *head = List_Init(); datatype binfo; //尾插 for (int i = 0; i < 3; i++) { scanf("%s %s %f", binfo.bookname, binfo.Author, &binfo.price); while (getchar() != '\n') ; //清空\n TailInsert(head, binfo); } //按条件删除 List_Havedelete(head, Search); //删除指定节点 //List_Nulldelete(head->prev); //修改节点 //List_Revise(head->prev, Revise); //使用通用型的链表遍历函数,实现每找到一个节点 调用一次回调函数,处理找到的当前节点 List_brow(head, pridata); //查找结点 List_Search(head, Search); //销毁节点 List_Destroy(head); return 0; }
三、内核链表
1、普通链表弊端
普通链表概念简单,操作方便,但存在有致命的缺陷,即:每一条链表都是特殊的,不具有通用性。因为对每一种不同的数据,所构建出来的链表都是跟这些数据相关的,所有的操作函数也都是数据密切相关的,换一种数据节点,则所有的操作函数都需要一一重新编写,这种缺陷对于一个具有成千上万种数据节点的工程来说是灾难性的。
2、内核链表
如前所述,内核链表解决通用性问题,大概分两步:
①设计标准节点
②针对标准节点,设计由标准节点构成的标准链表的所有操作
内核链表的标准节点及其所有操作,都被封装在内核源码中,具体来讲都被封装在一个名为 list.h 的文件中,该文件在内核中的位置是:
kernel/linux/include/list.h
内核中的源码文件 list.h 实际上包含了两部分内容,一是内核链表,二是哈希链表。经过整理的、仅包含内核链表的文件:kernel_list.h
百度网盘下载kernel_list.h文件:
2.1内核链表结构
内核链表分为两个结构体
1)大结构体——包含数据域与指针域(用户自己来设计)
2)小结构体——地址结构体(内核链表定义的类型)
内核链表的双向循环结构其实就是地址结构体形成的双向循环结构。
2.2内核链表的节点设计
- 小结构体
// list.h //内核链表标准节点设计----小结构体 struct list_head { struct list_head *prev; struct list_head *next; };
- 大结构体
//用户链表节点的设计---大结构体设计 struct node // 大结构体 { // 用户数据 datatype data; ... // 标准链表 struct list_head list; // 小结构体 };
2.3内核链表的相关函数
1)内核链表的初始化—— INIT_LIST_HEAD
#define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0)
参数:ptr ——链表头结点中地址结构体的地址
2)插入节点
头插法:
static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); }
尾插法:
static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); }
new : 新插入的节点的地址结构体的地址
head : 链表头结点的地址结构体的地址
3)内核链表的遍历——list_for_each_entry(宏函数 就是一个for循环)
#define list_for_each_entry(pos, head, member) \ for (pos = list_entry((head)->next, typeof(*pos), member); \ &pos->member != (head); \ pos = list_entry(pos->member.next, typeof(*pos), member))
pos: 遍历链表每一个节点的的指针p (p是大结构体的类型)
head: 头结点里面地址结构体的地址 (小结构体的类型)
member : 地址结构体的名字 --》ptr
4)内核链表节点删除——list_del
static inline void list_del(struct list_head *entry) { __list_del(entry->prev, entry->next); entry->next = (void *) 0; entry->prev = (void *) 0; }
entry : 需要被删除的节点的地址结构体的地址
5)内核链表的销毁——先把除了头结点的所有地址是否,最后释放头结点
#define list_for_each_entry_safe(pos, n, head, member) \ for (pos = list_entry((head)->next, typeof(*pos), member), \ n = list_entry(pos->member.next, typeof(*pos), member); \ &pos->member != (head); \ pos = n, n = list_entry(n->member.next, typeof(*n), member))
pos : 指向大的链表节点的指针 (大的结构体类型)
n : 大的链表节点的指针,防止链表断裂 (大的结构体类型)
head : 链表头结点里面地址结构体的地址
member :地址结构体的名字
6)示例代码
#include <stdio.h> #include <stdlib.h> #include "kernel_list.h" //数据域结构体 struct book { char bookname[64]; char Author[64]; float price; }; #define DATATYPE struct book typedef DATATYPE datatype; //构建节点----大结构体 typedef struct node { //数据域 struct book data; //指针域----小结构体 struct list_head list; }Node, *pNode; //借助内核链表宏定义完成双向循环链表空表初始化 pNode List_Init(void) { pNode Head = (pNode)malloc(sizeof(Node)); if(Head == NULL) { perror("malloc faild!"); return NULL; } //做好大结构体中小结构体的链式关系 INIT_LIST_HEAD(&Head->list); return Head; } //使用内核链表完成头插 int Head_Insert(pNode Head, datatype info) { //新节点空间分配 pNode Newnode = (pNode)malloc(sizeof(Node)); if(Newnode == NULL) { perror("malloc faild!"); return -1; //插入失败 } //数据域 Newnode->data = info; //指针域---使用内核链表头插 list_add(&(Newnode->list), &(Head->list)); } //使用内核链表实现遍历 void pridata(datatype binfo) { printf("%s\t%s\t%.1f\n",(binfo).bookname,(binfo).Author,(binfo).price); } void List_BrowRight(pNode Head, void (*pfunction)(datatype)) { pNode p; struct list_head *pos; //pos指向每一个大结构体中的小结构体 list_for_each(pos, &(Head->list)) //循环遍历 { //以上循环只提供小结构体指针的遍历,但是遍历找的是大结构体的数据域,根据每个pos得到大结构体地址 p = list_entry(pos, Node, list); pfunction(p->data); //打印 } } //使用内核链表的删除 void List_Delete(pNode Node) { list_del(&(Node->list)); free(Node); } int main() { pNode head = List_Init(); datatype tempinfo; //头插 for(int i=0;i<3;i++) { scanf("%s %s %f", tempinfo.bookname, tempinfo.Author, &tempinfo.price); while(getchar()!='\n'); Head_Insert(head, tempinfo); } //删除节点 pNode p; struct list_head *pos = (&(head->list))->next; //在小结构体中找到你想删除的节点 p = list_entry(pos, Node, list); //获取该节点的大结构体的地址 List_Delete(p); //删除该节点 //遍历 List_BrowRight(head, pridata); }