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

文章目录

概述

本文讲述了数据结构中单链表的基本概念,头指针、头结点、数据域、指针域等链表的描述术语,及单链表操作的简单实现。并在此基础上详细讲讲述 Linux 源码中 SLIST 单链表系列宏的原理和使用方法。

简单的链表

在讲述单链表前,不得不先回顾下线性表的概念。所谓线性表,是零个或多个数据元素的有限序列(序列是指有顺序的排列)。线性表首先是一个序列,也就是说,元素之间是有顺序的,若存在多个元素,则第一个元素没有前驱,最后的元素没有后继,其他的每个元素都是有些只有一个前驱和后继。以学校的小朋友为例,如果大家分散在操场各处,则不能算是线性表。如果一个小朋友去拉两个小朋友的衣服,那就不可以排成一队了;同样,如果一个小朋友后边的衣服,被两个小朋友拉扯,也不算是线性表。另外,线性表强调有限,事实上,在计算机中处理的对象都是有限的,那种无限的数列,只存在于数学的概念中。

描述链表的术语

cpp 复制代码
typedef struct Node {
    ElemType data;
    struct Node *next;
} Node;

通常,我们把Node称作一个节点,每个节点包含两个部分。其中存储数据元素信息的域(字段)称作数据域,把存储直接后继位置的域称为指针域,指针域中存储的信息(即下一个节点的内存地址)称作指针或链。链表总得有个头,我们把链表中的第一个节点的存储位置(即第一个节点对象的内存地址,如果有头结点,则是头结点对象的内存地址)叫做头指针 。为了更加方便地对链表进行操作,会在单链表的第一个节点前附设一个节点,称作头节点

头指针是链表的必要元素或者说是固有的,其具有标识作用,所以常用头指针来代表链表本身。无论链表是否为空,头指针均不为空。头指针指向链表的第一个节点的内存,若有头节点,则是指向头结点对象的指针。而,头节点不一定是链表的必要元素。头节点的数据域是不能向其他节点那样存储业务数据的,一般无意义空置,但你也可以在其中存储些自定义的其他数据信息,如存储链表长度。有了头节点,对在链表第一节点前插入节点和删除第一节点这两种操作,就会变得简单,使得其操作与其他节点的操作过程相统一。

下文示例程序中,使用了头结点,

如上,在使用头节点的情况下,头指针、头节点、普通节点之间的关系如上图。头指针Ph等于头节点(对象)在内存中地址,而头节点数据域不实际存储数据元素,只是存储了第一节点的地址。参照下文示例代码main函数中定义的 LinkList 类型的 L 即链表头指针的,它是一个节点类型的指针,结合InitList源码,可得,头指针的赋值过程为,

cpp 复制代码
 struct Node *L = (Node*)malloc(sizeof(Node));
 //如下初始化过程,本质上操作的是头节点的指针域
 L->next = NULL; 

对于LIST的客户端来说,头指针是 Node* 和 void* 并没有什么本质区别,它就是一个地址值,只要能在LIST内部使用头指针找到头结点或第一节点就行,只是为了代码上的优雅和易读写性,头指针被顺便定义成了节点类型的指针类型。

简单实现一个单链表

这是以前从某书中的源码里扒拉出来的,只是做了简单的调整,前几年在项目里,我甚至偶尔直接在其基础上私有化一个单链表用于项目。这里贴出来,并不是说它好或者不好,只是为了有个参考,以更好的理解后续要讲述的Linux中SLIST宏单链表。

cpp 复制代码
#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;
    else
        return 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内核中实现队列和链表的功能。本文仅讲解其中最简单的单链表结构。

cpp 复制代码
/** @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在单链表的表示和用途上是不同的,这样的设计有助于提高代码的清晰性和可维护性。

cpp 复制代码
//你的私有数据结构
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怎么翻译呢?

cpp 复制代码
	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才可以。同理,我们在定义链表头时,也可以直接定义头指针,如下,这可能会更利于编码过程,

cpp 复制代码
SLIST_HEAD(TslistHead, TLucyItem) *pslistHead;

单链表初始化

cpp 复制代码
    //链表初始化
    SLIST_INIT(&slistHead);

单链表插入元素

宏函数参数中的,field 不光有田地、场地,处理、应付等含义,它还具有字段的意思,在编程领域其可代表结构体字段。

cpp 复制代码
    //第一个元素
    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后面。

单链表遍历元素

cpp 复制代码
    //遍历单链表
    SLIST_FOREACH(item, &slistHead, linkNode) {
        printf("%d, %d \r\n ", item->data.a, item->data.b);
    }

在SLIST实际使用中,可能要在其基础上进行一些功能扩展,如,保持单链表中元素的唯一性,此时也会使用到遍历操作。

单链表删除元素

SLIST 提供了3个删除元素的函数,具体参见上一节的原码。

cpp 复制代码
//删除elm指定的后一个节点
SLIST_REMOVE_AFTER(elm, field)
//删除头节点指定的节点
SLIST_REMOVE_HEAD(head, field)
//删除elm指定的节点
SLIST_REMOVE(head, elm, type, field)

链表清空方案1,

cpp 复制代码
    //链表清空操作
    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,

cpp 复制代码
    //链表清空操作 //需要在另外的过程中释放item堆内存
    while (!SLIST_EMPTY(&slistHead)) {
        SLIST_REMOVE_HEAD(&slistHead, linkNode);
    }

上述列表清空操作的代码可以展开为,

需要注意的是,清空方案P2过程中并没有释放链表元素对应的堆内存,不小心地化会造成内存泄漏哈。

Linux之SLIST使用实践

https://www.cnblogs.com/imlgc/archive/2012/05/02/2479654.html

cpp 复制代码
//你的私有数据结构
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时,遇到了一些问题。主要代码如下,

cpp 复制代码
//
typedef struct tagLucyItem {
    TYourData data;
    SLIST_ENTRY(tagLucyItem) linkNode;
} TLucyItem;
//直接定义成指针会方便些
SLIST_HEAD(TSListHead4Luck, TLucyItem) *s_pListHead; 

//主要功能代码
int do_something(){
	//TEST
	SLIST_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别名,而是直接使用结构类型名称即可。正是因为这样,出现了上述编译错误。我们宏展开报错的代码,

cpp 复制代码
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都可以做构造函数名称啦。一点点改呗,

cpp 复制代码
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并不会多次使用,这种方案是可行的。不改动引用的源码,所以推荐。

预留

好了,该睡觉了。

相关推荐
XiaoLeisj20 分钟前
【递归,搜索与回溯算法 & 综合练习】深入理解暴搜决策树:递归,搜索与回溯算法综合小专题(二)
数据结构·算法·leetcode·决策树·深度优先·剪枝
Jackey_Song_Odd1 小时前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
乐之者v2 小时前
leetCode43.字符串相乘
java·数据结构·算法
A懿轩A3 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
️南城丶北离4 小时前
[数据结构]图——C++描述
数据结构··最小生成树·最短路径·aov网络·aoe网络
✿ ༺ ོIT技术༻4 小时前
C++11:新特性&右值引用&移动语义
linux·数据结构·c++
菜鸡中的奋斗鸡→挣扎鸡11 小时前
滑动窗口 + 算法复习
数据结构·算法
axxy200012 小时前
leetcode之hot100---240搜索二维矩阵II(C++)
数据结构·算法
Uu_05kkq13 小时前
【C语言1】C语言常见概念(总结复习篇)——库函数、ASCII码、转义字符
c语言·数据结构·算法
1nullptr15 小时前
三次翻转实现数组元素的旋转
数据结构