文章目录
- 概述
- 简单的链表
- 描述链表的术语
- 简单实现一个单链表
- Linux之SLIST机理分析
- 结构定义
- 单链表初始化
- 单链表插入元素
- 单链表遍历元素
- 单链表删除元素
- Linux之SLIST使用实践
- 纯C中typedef重命名带来的问题
- 预留
概述
本文讲述了数据结构中单链表的基本概念,头指针、头结点、数据域、指针域等链表的描述术语,及单链表操作的简单实现。并在此基础上详细讲讲述 Linux 源码中 SLIST 单链表系列宏的原理和使用方法。
简单的链表
在讲述单链表前,不得不先回顾下线性表的概念。所谓线性表,是零个或多个数据元素的有限序列(序列是指有顺序的排列)。线性表首先是一个序列,也就是说,元素之间是有顺序的,若存在多个元素,则第一个元素没有前驱,最后的元素没有后继,其他的每个元素都是有些只有一个前驱和后继。以学校的小朋友为例,如果大家分散在操场各处,则不能算是线性表。如果一个小朋友去拉两个小朋友的衣服,那就不可以排成一队了;同样,如果一个小朋友后边的衣服,被两个小朋友拉扯,也不算是线性表。另外,线性表强调有限,事实上,在计算机中处理的对象都是有限的,那种无限的数列,只存在于数学的概念中。
描述链表的术语
typedef struct Node {ElemType data;struct Node *next;
} Node;
通常,我们把Node称作一个节点,每个节点包含两个部分。其中存储数据元素信息的域(字段)称作数据域,把存储直接后继位置的域称为指针域,指针域中存储的信息(即下一个节点的内存地址)称作指针或链。链表总得有个头,我们把链表中的第一个节点的存储位置(即第一个节点对象的内存地址,如果有头结点,则是头结点对象的内存地址)叫做头指针。为了更加方便地对链表进行操作,会在单链表的第一个节点前附设一个节点,称作头节点。
头指针是链表的必要元素或者说是固有的,其具有标识作用,所以常用头指针来代表链表本身。无论链表是否为空,头指针均不为空。头指针指向链表的第一个节点的内存,若有头节点,则是指向头结点对象的指针。而,头节点不一定是链表的必要元素。头节点的数据域是不能向其他节点那样存储业务数据的,一般无意义空置,但你也可以在其中存储些自定义的其他数据信息,如存储链表长度。有了头节点,对在链表第一节点前插入节点和删除第一节点这两种操作,就会变得简单,使得其操作与其他节点的操作过程相统一。
下文示例程序中,使用了头结点,
如上,在使用头节点的情况下,头指针、头节点、普通节点之间的关系如上图。头指针Ph等于头节点(对象)在内存中地址,而头节点数据域不实际存储数据元素,只是存储了第一节点的地址。参照下文示例代码main函数中定义的 LinkList 类型的 L 即链表头指针的,它是一个节点类型的指针,结合InitList源码,可得,头指针的赋值过程为,
struct Node *L = (Node*)malloc(sizeof(Node));//如下初始化过程,本质上操作的是头节点的指针域L->next = NULL;
对于LIST的客户端来说,头指针是 Node* 和 void* 并没有什么本质区别,它就是一个地址值,只要能在LIST内部使用头指针找到头结点或第一节点就行,只是为了代码上的优雅和易读写性,头指针被顺便定义成了节点类型的指针类型。
简单实现一个单链表
这是以前从某书中的源码里扒拉出来的,只是做了简单的调整,前几年在项目里,我甚至偶尔直接在其基础上私有化一个单链表用于项目。这里贴出来,并不是说它好或者不好,只是为了有个参考,以更好的理解后续要讲述的Linux中SLIST宏单链表。
#include <iostream>
#include <stdio.h>#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0//Your Data /can be struct
typedef int ElemType;
//线性表链式存储-单链表结构
typedef struct Node {ElemType data;struct Node *next;
} Node;//定义LinkList
typedef struct Node *LinkList; /* 初始化链式线性表 */
int InitList(LinkList *L) {*L = (LinkList)malloc(sizeof(Node)); /* 产生头结点,并使L指向此头结点 */if (!(*L)) /* 存储分配失败 */return ERROR;(*L)->next = NULL; /* 指针域为空 */return OK;
}//若L为空表,则返回TRUE,否则返回FALSE
int ListEmpty(LinkList L) {if (L->next)return FALSE;elsereturn TRUE;
}//将L重置为空表
int ClearList(LinkList *L) {LinkList p, q;p = (*L)->next; /* p指向第一个结点 */while (p) { /* 没到表尾 */q = p->next;free(p);p = q;}(*L)->next = NULL; /* 头结点指针域为空 */return OK;
}//返回L中数据元素个数
int ListLength(LinkList L) {int i = 0;LinkList p = L->next; /* p指向第一个结点 */while (p) {i++;p = p->next;}return i;
}//用e返回L中第i个数据元素的值 //1≤i≤ListLength(L)
int GetElem(LinkList L, int i, ElemType *e) {int j;LinkList p; //声明一结点p p = L->next; //让p指向链表L的第一个结点 j = 1; //j为计数器 //p不为空或者计数器j还没有等于i时,循环继续while (p && j < i) {p = p->next; /* 让p指向下一个结点 */++j;}if (!p || j > i)return ERROR; /* 第i个元素不存在 */*e = p->data; /* 取第i个元素的数据 */return OK;
}//返回L中第1个与e满足关系的数据元素的位序 /若这样的数据元素不存在则返回0
int LocateElem(LinkList L, ElemType e) {int i = 0;LinkList p = L->next;while (p) {i++;if (p->data == e) /* 找到这样的数据元素 */return i;p = p->next;}return 0;
}//在L中第i个位置之前插入新的数据元素e,L的长度加1 //1≤i≤ListLength(L)
int ListInsert(LinkList *L, int i, ElemType e) {int j;LinkList p, s;p = *L;j = 1;while (p && j < i) { /* 寻找第i个结点 */p = p->next;++j;}if (!p || j > i) return ERROR; /* 第i个元素不存在 *//* 生成新结点(C语言标准函数) */s = (LinkList)malloc(sizeof(Node)); s->data = e;s->next = p->next; /* 将p的后继结点赋值给s的后继 */p->next = s; /* 将s赋值给p的后继 */return OK;
}//删除L的第i个数据元素,并用e返回其值,L的长度减1 //1≤i≤ListLength(L)
int ListDelete(LinkList *L, int i, ElemType *e) {int j;LinkList p, q;p = *L;j = 1;while (p->next && j < i) { /* 遍历寻找第i个元素 */p = p->next;++j;}if (!(p->next) || j > i)return ERROR; /* 第i个元素不存在 */q = p->next;p->next = q->next; /* 将q的后继赋值给p的后继 */*e = q->data; /* 将q结点中的数据给e */free(q); /* 让系统回收此结点,释放内存 */return OK;
}//遍历链表 //依次对L的每个数据元素输出
int ListTraverse(LinkList L) {LinkList p = L->next;while (p) {printf("%d ", p->data); //dosmoething..p = p->next;}printf("\n"); return OK;
}int main() {LinkList L;ElemType e;//初始化int i = InitList(&L);//插入新元素for (int j = 1; j <= 5; j++)i = ListInsert(&L, 1, j * 10);//遍历ListTraverse(L);//获取第4个数据//GetElem(L, 3, &e);//删除第3个数据//ListDelete(&L, 3, &e); //...不再赘述...system("pause");
}
上述代码可以直接在C和C++环境中编译和运行,具体测试代码比较简单,没有过多在此涉及。
Linux之SLIST机理分析
进入Linux官网,以HTTTP方式进入 Index of /pub/linux/kernel/ 页面,图个吉利,这里选择下载 linux-6.8.6.tar.xz 版本。解压后可以在相应的目录下找到 linux-6.8.6\drivers\scsi\aic7xxx\queue.h 文件。在Everything中搜索时,可以找到好几个queue.h文件,只有目录 drivers/scsi/aic7xxx 包含的 queue.h 是我们想要的那个。该目录Adaptec AIC-7xxx系列(例如AIC-7870、AIC-7895等)的SCSI(Small Computer System Interface)控制器相关的驱动程序,主要负责与硬件交互,控制SCSI设备,以及提供对SCSI设备的访问和管理功能。该文件中主要包含了单向链表、单向有尾链表(Singly-linked Tail queue 可用作队列)、双向无尾链表、双向有尾链表(Tail queue 可用作队列)、循环链表(Circular queue)的数据结构和操作函数,用于在Linux内核中实现队列和链表的功能。本文仅讲解其中最简单的单链表结构。
/** @brief* A singly-linked list is headed by a single forward pointer. * The elements are singly linked for minimum space and pointer manipulation overhead at the expense of O(n) removal for arbitrary elements. * New elements can be added to the list after an existing element or at the head of the list.* Elements being removed from the head of the list should use the explicit macro for this purpose for optimum efficiency. * A singly-linked list may only be traversed in the forward direction. * Singly-linked lists are ideal for applications with large datasets and few or no removals or for implementing a LIFO queue.
**/#if defined(QUEUE_MACRO_DEBUG) || (defined(_KERNEL) && defined(DIAGNOSTIC))
#define _Q_INVALIDATE(a) (a) = ((void *)-1)
#else
#define _Q_INVALIDATE(a)
#endif/** Singly-linked List definitions.*/
#define SLIST_HEAD(name, type) \
struct name { \struct type *slh_first; /* first element */ \
}#define SLIST_HEAD_INITIALIZER(head) \{ NULL }//条目/列表元素
#define SLIST_ENTRY(type) \
struct { \struct type *sle_next; /* next element */ \
}/** Singly-linked List access methods.*/
#define SLIST_FIRST(head) ((head)->slh_first)
#define SLIST_END(head) NULL
#define SLIST_EMPTY(head) (SLIST_FIRST(head) == SLIST_END(head))
#define SLIST_NEXT(elm, field) ((elm)->field.sle_next)#define SLIST_FOREACH(var, head, field) \for((var) = SLIST_FIRST(head); \(var) != SLIST_END(head); \(var) = SLIST_NEXT(var, field))#define SLIST_FOREACH_SAFE(var, head, field, tvar) \for ((var) = SLIST_FIRST(head); \(var) && ((tvar) = SLIST_NEXT(var, field), 1); \(var) = (tvar))/** Singly-linked List functions.*/
#define SLIST_INIT(head) { \SLIST_FIRST(head) = SLIST_END(head); \
}#define SLIST_INSERT_AFTER(slistelm, elm, field) do { \(elm)->field.sle_next = (slistelm)->field.sle_next; \(slistelm)->field.sle_next = (elm); \
} while (0)#define SLIST_INSERT_HEAD(head, elm, field) do { \(elm)->field.sle_next = (head)->slh_first; \(head)->slh_first = (elm); \
} while (0)#define SLIST_REMOVE_AFTER(elm, field) do { \(elm)->field.sle_next = (elm)->field.sle_next->field.sle_next; \
} while (0)#define SLIST_REMOVE_HEAD(head, field) do { \(head)->slh_first = (head)->slh_first->field.sle_next; \
} while (0)#define SLIST_REMOVE(head, elm, type, field) do { \if ((head)->slh_first == (elm)) { \SLIST_REMOVE_HEAD((head), field); \} else { \struct type *curelm = (head)->slh_first; \\while (curelm->field.sle_next != (elm)) \curelm = curelm->field.sle_next; \curelm->field.sle_next = \curelm->field.sle_next->field.sle_next; \_Q_INVALIDATE((elm)->field.sle_next); \} \
} while (0)
如上代码中的注释部分。
结构定义
SLIST_HEAD 宏定义了一个名称为 name 的结构体,包含一个 type 类型的字段,其含义是指向第一个元素的指针。SLIST_ENTRY宏定义的是单链表中每个元素的结构,其中包含一个指向下一个元素的指针。抛却字段名称不谈,这俩定义是一致的,看起来有点重复,但SLIST_HEAD和SLIST_ENTRY在单链表的表示和用途上是不同的,这样的设计有助于提高代码的清晰性和可维护性。
//你的私有数据结构
typedef struct tagYourData {int a;int b;
} TYourData;
//typedef int TYourData; //also//借助SLIST_ENTRY定义链表结构
struct TLucyItem {TYourData data;SLIST_ENTRY(TLucyItem) linkNode;
};//定义链表头变量
SLIST_HEAD(TslistHead, TLucyItem) slistHead;
结合上文,SLIST_ENTRY宏的功能很明确,也很好理解。哈哈,但是钻个小牛角尖,单词 entry 本意是进入、加入、入口,同时也具有条目、账目、记录等含义。在计算机中,有 data entry: [计]数据输入,entry point: [计]入口点,等含义。那么这里的entry怎么翻译呢?
struct TLucyItem {TYourData data;struct {struct TLucyItem* sle_next;} linkNode;}
结合 SLIST_ENTRY 的实际使用,将结构 TLucyItem 定义展开如上。我给 SLIST_ENTRY 对象取名字 linkNode,含义为链表的连接点,链表连接位置的记录。就这样吧!也许 Entry 这个名字是大神凭借个人喜好采用的。如果不考虑字面意思,这里的 linkNode 代表的是链表结构中的指针域。在链表中,我们通常提到的是数据域和指针域。指针域承担着连接节点的作用,通过指针域,我们可以在链表中进行节点的插入、删除、查找等操作,实现链表的灵活性和可操作性。
链表头变量的展开,如下,
要特别注意的2点是,
SLIST_ENTRY 宏函数、SLIST_HEAD宏函数中的 type 参数,其代表的类型是 TLucyItem,而不是 TYourData 类型,通过代码的展开,很容易理解这一点。即,type不是字节的数据类型,而是包含自己数据类型的链表结构类型。
宏函数SLIST_INSERT_HEAD、SLIST_FOREACH、SLIST_REMOVE_HEAD等函数参数中都传递了head参数,函数内部把head被认做事指针来使用,因此,如果我们使用SLIST_HEAD定义头对象,而不是指针时,相关位置要传递&slistHead才可以。同理,我们在定义链表头时,也可以直接定义头指针,如下,这可能会更利于编码过程,
SLIST_HEAD(TslistHead, TLucyItem) *pslistHead;
单链表初始化
//链表初始化SLIST_INIT(&slistHead);
单链表插入元素
宏函数参数中的,field 不光有田地、场地,处理、应付等含义,它还具有字段的意思,在编程领域其可代表结构体字段。
//第一个元素item = (struct TLucyItem*)malloc(sizeof(struct TLucyItem));item->data.a = 1;item->data.b = 10;SLIST_INSERT_HEAD(&slistHead, item, linkNode);//展开 _INSERT_HEAD//item->linkNode.sle_next = (&slistHead)->slh_first;//(&slistHead)->slh_first = item;
这里要特别注意的是,SLIST_INSERT_HEAD(head, elm, field) 宏函数的 head 参数,要传递的是 &slistHead,即slistHead的地址,而不是直接传递slistHead本身。
宏函数 SLIST_INSERT_AFTER(slistelm, elm, field)
函数参数中 slistelm 是列表中的某已知的节点,elm 是新要插入的节点,本函数的功能是,将结点elm插入到结点slistelm后面。
单链表遍历元素
//遍历单链表SLIST_FOREACH(item, &slistHead, linkNode) {printf("%d, %d \r\n ", item->data.a, item->data.b);}
在SLIST实际使用中,可能要在其基础上进行一些功能扩展,如,保持单链表中元素的唯一性,此时也会使用到遍历操作。
单链表删除元素
SLIST 提供了3个删除元素的函数,具体参见上一节的原码。
//删除elm指定的后一个节点
SLIST_REMOVE_AFTER(elm, field)
//删除头节点指定的节点
SLIST_REMOVE_HEAD(head, field)
//删除elm指定的节点
SLIST_REMOVE(head, elm, type, field)
链表清空方案1,
//链表清空操作while (!SLIST_EMPTY(&slistHead)) {item = SLIST_FIRST(&slistHead); //printf("remove %d, %d \n", item->data.a, item->data.b);SLIST_REMOVE(&slistHead, item, TLucyItem, linkNode);free(item); //同步释放item堆内存}
链表清空方案2,
//链表清空操作 //需要在另外的过程中释放item堆内存while (!SLIST_EMPTY(&slistHead)) {SLIST_REMOVE_HEAD(&slistHead, linkNode);}
上述列表清空操作的代码可以展开为,
需要注意的是,清空方案P2过程中并没有释放链表元素对应的堆内存,不小心地化会造成内存泄漏哈。
Linux之SLIST使用实践
https://www.cnblogs.com/imlgc/archive/2012/05/02/2479654.html
//你的私有数据结构
typedef struct tagYourData {int a;int b;
} TYourData; //借助SLIST_ENTRY定义链表结构/注意没有使用typedef定义结构别名
struct TLucyItem {TYourData data;SLIST_ENTRY(TLucyItem) linkNode;
};int main() {//定义链表头变量 //更建议直接定义成指针SLIST_HEAD(TslistHead, TLucyItem) slistHead;//链表初始化SLIST_INIT(&slistHead);//链表元素项//要动态创建struct TLucyItem* item = NULL;//第一个元素item = (struct TLucyItem*)malloc(sizeof(struct TLucyItem));item->data.a = 1;item->data.b = 10;SLIST_INSERT_HEAD(&slistHead, item, linkNode);//第二个元素item = (struct TLucyItem*)malloc(sizeof(struct TLucyItem));item->data.a = 2;item->data.b = 20;SLIST_INSERT_HEAD(&slistHead, item, linkNode);//遍历单链表SLIST_FOREACH(item, &slistHead, linkNode) {printf("iterator %d, %d \r\n", item->data.a, item->data.b);}//链表删除操作while (!SLIST_EMPTY(&slistHead)) {item = SLIST_FIRST(&slistHead);printf("remove %d, %d \n", item->data.a, item->data.b);SLIST_REMOVE(&slistHead, item, TLucyItem, linkNode);free(item);}system("pause");
}
上述代码运行结果如下,
纯C中typedef重命名带来的问题
在Keil5集成开发环境(STMF429+FreeRTOS)下,标准C99,使用SLIST时,遇到了一些问题。主要代码如下,
//
typedef struct tagLucyItem {TYourData data;SLIST_ENTRY(tagLucyItem) linkNode;
} TLucyItem;
//直接定义成指针会方便些
SLIST_HEAD(TSListHead4Luck, TLucyItem) *s_pListHead; //主要功能代码
int do_something(){//TESTSLIST_INIT(s_pListHead);//开辟堆内存TLucyItem *ptNode = pvPortMalloc(sizeof(TLucyItem));ptNode->data.a = 100; ptNode->data.b = 100;//执行插入操作SLIST_INSERT_HEAD(s_pListHead, ptNode, linkNode);...
}
在编译时,存在如下编译错误,
先谈谈C语言中,为什么喜欢将结构定义typedef为一个别名。
在纯C环境下,我们通常要定义结构体的别名,如上使用typedef定义的TLucyItem类型。如果不这么做,那么任何出现TLucyItem类型名称的地方,都要使用 struct TLucyItem 样式,如前一章节中SLIST的实践代码那样。在C++中,对于结构体类型的定义和使用,可以不用去typedef别名,而是直接使用结构类型名称即可。正是因为这样,出现了上述编译错误。我们宏展开报错的代码,
do { (ptNode)->linkNode.sle_next = (s_pListHead)->slh_first; (s_pListHead)->slh_first = (ptNode); } while (0)
一共两行代码,每行对应一个错误告警。第一个错误显示,右边 slh_first 是 struct TLucyItem* 类型,左边 sle_next 是 struct tagLucyItem*类型,类型不兼容。好吧,这也能报错,在C++中tagLucyItem都可以做构造函数名称啦。一点点改呗,
typedef struct tagLucyItem {TYourData data;SLIST_ENTRY(/*tagLucyItem*/TLucyItem) linkNode;
} TLucyItem;
如上修改 TLucyItem 定义后,果然只剩下第2个错误告警了。我们继续来看看这个错误。右边 TLucyItem* 类型和左边 struct TLucyItem* 类型不兼容,好吧,这也太死板啦,就不能变通一点点。slh_first 是struct TLucyItem*类型,其中关键字struct是在通过SLIST_HEAD宏定义头结构时被宏定义函数体添加的。分析到这里,问题的原因算是确定了,struct Taa 和 Taa 在C编译过程中不兼容。有两种解决方案,
P1、这是不建议的方案。修改SLIST宏实现,将宏实现中 type 参数前的 struct 全部干掉。
P2、去掉上述TLucyItem的别名定义,直接定义它。当在程序内部使用到该结构类型时,统一的加上struct关键字使用它。好在在SLIST使用的过程中节点类型TLucyItem并不会多次使用,这种方案是可行的。不改动引用的源码,所以推荐。
预留
好了,该睡觉了。