当前位置: 首页 > news >正文

语言基础/单向链表的构建和使用(含Linux中SLIST的解析和使用)

文章目录

  • 概述
  • 简单的链表
    • 描述链表的术语
    • 简单实现一个单链表
  • 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并不会多次使用,这种方案是可行的。不改动引用的源码,所以推荐。

预留

好了,该睡觉了。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【webpack】wabpack5 常用配置
  • 【ORACLE】minus() 函数
  • [数据集][目标检测]快递包裹检测数据集VOC+YOLO格式5382张1类别
  • 聚星文社——Ai推文工具
  • docker-harbor 仓库上传下载镜像以及仓库之间的镜像复制
  • 自学Python网站
  • 前端网格布局display: grid;
  • 论文期刊介绍
  • 7. Java 中 HashMap 的扩容机制是怎样的?
  • iOS开发进阶(二十三):iOS 常见面试题汇总
  • uniapp h5手机如何打开本地跑的前端项目进行本地调试
  • 亿发工单管理系统助力五金行业智造升级:高效生产新篇章
  • [数据集][目标检测]竹子甘蔗发芽缺陷检测数据集VOC+YOLO格式2953张3类别
  • NGINX高性能web服务器
  • 20240826日报
  • 【5+】跨webview多页面 触发事件(二)
  • Android 架构优化~MVP 架构改造
  • android图片蒙层
  • es的写入过程
  • Java精华积累:初学者都应该搞懂的问题
  • mysql外键的使用
  • Node项目之评分系统(二)- 数据库设计
  • PAT A1050
  • Redis提升并发能力 | 从0开始构建SpringCloud微服务(2)
  • Solarized Scheme
  • vue+element后台管理系统,从后端获取路由表,并正常渲染
  • webpack入门学习手记(二)
  • Yii源码解读-服务定位器(Service Locator)
  • 服务器从安装到部署全过程(二)
  • 后端_MYSQL
  • 计算机常识 - 收藏集 - 掘金
  • 利用jquery编写加法运算验证码
  • 每个JavaScript开发人员应阅读的书【1】 - JavaScript: The Good Parts
  • 入门级的git使用指北
  • 微信端页面使用-webkit-box和绝对定位时,元素上移的问题
  • 译自由幺半群
  • - 转 Ext2.0 form使用实例
  • Java总结 - String - 这篇请使劲喷我
  • linux 淘宝开源监控工具tsar
  • #Linux(权限管理)
  • #微信小程序:微信小程序常见的配置传旨
  • #我与Java虚拟机的故事#连载08:书读百遍其义自见
  • (01)ORB-SLAM2源码无死角解析-(66) BA优化(g2o)→闭环线程:Optimizer::GlobalBundleAdjustemnt→全局优化
  • (JS基础)String 类型
  • (Redis使用系列) SpirngBoot中关于Redis的值的各种方式的存储与取出 三
  • (STM32笔记)九、RCC时钟树与时钟 第一部分
  • (二)linux使用docker容器运行mysql
  • (翻译)Quartz官方教程——第一课:Quartz入门
  • (附源码)ssm本科教学合格评估管理系统 毕业设计 180916
  • (附源码)ssm智慧社区管理系统 毕业设计 101635
  • (九)信息融合方式简介
  • (蓝桥杯每日一题)平方末尾及补充(常用的字符串函数功能)
  • (四)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (一)UDP基本编程步骤
  • (原创) cocos2dx使用Curl连接网络(客户端)