【数据结构】第二站:C语言--单链表(附完整源码和注释)


前言

进入链表,链表根据单/双向,是否循环,是否有哨兵位可分成多种,我们只重点选择几个解析,触类旁通,这篇文章的单链表没有带哨兵位的单链表,实现会有很多困难,也正是因为这种结构有问题,所以很容易取得出题者的青睐,单链表实现函数的参数开始使用二级指针,亲自画图成为理解单链表断言、结构的最快方法。


一、单链表的结构

在这里插入图片描述

typedef int SLTDateType;
typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;

先观察代码,单链表的单个节点就是一个结构体,结构体中存储了下一个节点的地址,上一个节点通过地址可以找到下一个节点,这样就形成了单链表的链式结构,而前后两个节点的内存地址不一定是连续的,就造就了链表逻辑上连续,物理上非连续的性质。

二、单链表增删改查的实现

1.单链表实现各函数

#pragma once

// slist.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDateType;
typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;
 
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
// 分析思考为什么不删除pos位置?
void SListEraseAfter(SListNode* pos);
// 单链表的销毁
void SListDestroy(SListNode* plist);
//pos位置之前插入
void SListInsertbefore(SListNode** pplist, SListNode* pos, SLTDateType x);

2.单链表的初始化

当我们储存单链表的头节点的地址(用A/B),通过该地址访问结构体的成员(next),反复循环直到next指向NULL(链表尾),我们可以得到整条链表,所以当我们想创建一个空的单链表,我们只需要创建一个储存节点地址的指针(A/B)(它指向NULL,意为空)即可。(下图结构体指针均指向链表头)
在这里插入图片描述

3.单链表的头插头删和创建新节点(SlistPushFront/SlistPopFront/SlistCheckCapacity/BuySListNode)

(1)头插

单链表的头插头删比顺序表便捷的多,回顾顺序表的头插头删需要移动整个数组,单链表头插只改变要插入的新节点的成员(next),使其指向头节点,并改变储存头节点地址的指针Head(换新头)即可,见下图。
在这里插入图片描述

void SListPushFront(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	//assert(*pplist);
	SListNode* newNode = BuySListNode(x);
	newNode->next = *pplist;
	*pplist = newNode;
}

这里值得注意的是:1.我们传了指向头节点的指针的指针(二级指针)给函数,这样我们才可以在函数中通过解引用改变指向头节点的指针(也就是换新头操作)。
2.我们不要断言*pplist,因为它是可以指向NULL的(此时链表为空,插入时链表可以为空),而二级指针pplist却不能为空,且容易对类型为SListNode**的NULL指针进行访问。(如下图)
在这里插入图片描述


因为单链表的每个节点都是一个结构体,所以我们要插入新数据(新节点)的时候,我们需要创建一个新的结构体,许多插入都需要创建新节点,于是将节点创造封装成函数(BuySListNode)

SListNode* BuySListNode(SLTDateType x)//x为你要创造的新节点的值data
{
	SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
	if (newNode == NULL)
	{
		perror("BuySListNode malloc fail");
		return NULL;
	}
	newNode->data = x;
	newNode->next = NULL;
	return newNode;
}

创造新节点时不要忘记将新节点的值初始化!
这里或许有人会问:我在BuySListNode函数中直接创造一个结构体,然后用&返回它的地址不是一样创造了一个新节点吗?不要忘记在函数中创造的临时变量有自己的作用域,当函数结束时,临时变量会被销毁(内存还给操作系统),这时我们再访问该地址就会造成内存非法访问。

(2)头删

在这里插入图片描述

void SListPopFront(SListNode** pplist)
{
	assert(*pplist);
	SListNode* del = *pplist;
	*pplist = (* pplist)->next;
	free(del);
}

头删逻辑类似,也需要换头。
注意:1.不要忘记对删除的节点进行内存释放(free),不然会造成内存泄漏,请不要说什么程序结束还是会释放,公司项目程序可不敢轻易结束。
2.删除中 * pplist 是不能为空的,我们不能对空链表进行删除,所以需要断言 *pplist。
3.我们需要先用 SListNode *类型的变量将要删除的头保存,先进行换头,不然释放空间后我们无法再找到头节点的下一个节点。

4.单链表的尾插尾删(SlistPushBack/SlistPopBack)

(1)尾插

单链表想要尾删尾插,我们需要得到尾指针的地址,以至于我们需要从头遍历整个链表来找到它,回顾顺序表的尾删尾插很容易找到尾,可知单链表和顺序表各有优势,顺序表尾操作高效,单链表头操作高效。
在这里插入图片描述
上图为找尾过程的循环判断条件:当cur(tail)的next为NULL时,跳出循环。

void SListPushBack(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SListNode* newNode = BuySListNode(x);
	if (*pplist == NULL)
	{
		*pplist = newNode;
	}
	else
	{
		//找尾
		SListNode* tail = *pplist;//tail就是cur
		while (tail->next)//找尾过程
		{
			tail = tail->next;	
		}
		tail->next = newNode;
	}
}

注意:1.链表为空时,头插操作可以套用正常头插的逻辑,因为NULL也可被指向(作为节点的next),而尾插时我们需要找尾,我们需要对尾指针进行成员访问(->),而NULL是不可以被访问的,所以我们需要加一个判断,当空链表头插时,直接将新节点的地址给指向链表头的指针*pplist(也就是Head指针)。
2.在进行完尾插操作时,不要忘记给尾指针的next置空(NULL)。
3.不要直接让*pplist去亲自找尾,因为这是二级指针的解引用,对它改变将直接影响Head的值。

(2)尾删

尾删逻辑类似,以tail->next->next为循环条件可以不用保存要删的尾指针的前一个指针,直接释放tail->next。

void SListPopBack(SListNode** pplist)
{
	//assert(pplist);
	assert(*pplist);
	SListNode* tail = *pplist;
	while (tail->next->next)
	{
		tail = tail->next;
	}
	/*SListNode* del = tail->next;
	tail->next = NULL;
	free(del);*/
	free(tail->next);
	tail->next = NULL;
}

注意:断言*pplist,链表为空时不存在删除。

5.对单链表进行打印

void SListPrint(SListNode* plist)
{
	//assert(plist);
		SListNode* cur = plist;
		while (cur)
		{
			printf("%d->", cur->data);
			cur = cur->next;
		}
		printf("NULL\n");
}

链表的打印无需对指向头指针的Head指针进行改动,所以我们只要传一级指针就足以访问节点的成员,不过虽然是一级指针,对plist的改动不会反应到实际参数,我们还是尽量不要用函数参数亲自遍历链表,这样可以提升代码的可读性。

6.单链表指定值位置查找(SListFind)

从链表中找到你想要的值,并返回该值所在的节点的地址。

SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	assert(plist);
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data != x)
		{
			cur = cur->next;
		}
		else
		{
			return cur;
		}
	}
	//return NULL;
}

注意:空链表无法查找,如果你认为可以查找,也可以把 “return NULL;” 打开,将assert(plist)断言给注释,全凭你的个人意愿。

7.单链表在pos位置插入/删除(SlistInsert/SlistErase)

指定位置插入删除应和SListFind函数配合使用。

(1)指定位置之后插入

在链表中指定位置(pos)的后面插入新节点。

void SListInsertAfter(SListNode* pos, SLTDateType x)//x为你想要插入的新节点的data
{
	assert(pos);
	SListNode* newNode = BuySListNode(x);
	newNode->next = pos->next;
	pos->next = newNode;
}

注意:pos位置不能为NULL(无法访问)。

(2)指定位置之前插入

在指定位置之前插入情况要稍微复杂一点,我们需要找到pos位置的前一个位置的地址,所以我们需要把头节点的地址给prev,让它从头开始遍历直到prev的next是pos,但是这就有衍生出了一个新的问题:当pos位置就是头节点,就相当于我们需要头插,而头插函数需要二级指针,所以该函数需要传头节点的二级指针。

void SListInsertbefore(SListNode** pplist,SListNode* pos, SLTDateType x)
{
	//assert(pos);
	//assert(pplist);
	//assert(*pplist);
	SListNode* prev = *pplist;
	if (prev == pos)
	{
		SListPushFront(pplist,x);
	}
	while (prev->next!=pos)//从头开始遍历直到prev的next是pos
	{
		prev = prev->next;
	}
	SListNode* newNode = BuySListNode(x);
	prev->next=newNode;
	newNode->next = pos;
}

注意:我们指定位置的插入一般需要和SListFind函数进行配合使用,pos位置就一定会在链表中存在,所以我们无需断言,当函数出错(pos位置不在链表中会有对NULL访问的错误,思考下为什么?),证明使用者没有正确使用函数,成年人应该为自己的行为买单。

(2)删除指定位置之后的值

void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);
	SListNode* del = pos->next;
	/*if (del == NULL)
	{
		return;
	}*/
	pos->next = pos->next->next;
	free(del);
}

注意:1.当pos位置的下一个为NULL我们可以选择温柔结束(return返回),也可以暴力断言assert(pos->next)。
2.因为=运算符先算‘=’右边的式子,我们可以用pos->next->next找到pos后2个节点的地址来赋值给pos的next变量(pos->next),而不会使pos->next地址丢失,导致找不到pos->next->next。

8.单链表的销毁

void SListDestroy(SListNode* plist)
{
	//assert(plist);
	SListNode* cur = plist;
	SListNode* del = plist;
	while (cur)
	{
		cur = cur->next;
		free(del);
		del = cur;
	}
	//plist = NULL;
}

注意:1.链表为空也可销毁(销了相当于没销),为空时plist=cur=NULL,不会进入循环。
2.销毁链表后对指向头节点的指针Head(plist)置空十分有必要,这样可以有效避免非法访问,但是销毁函数将置空工作交给使用者,所以传递了一级指针,肯定不是懒,只是想让使用者清楚的记得这个链表已经被你亲手毁了:)。

三、完整源码

1.Slist.h

#pragma once

// slist.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDateType;
typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;
 
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
// 分析思考为什么不删除pos位置?
void SListEraseAfter(SListNode* pos);
// 单链表的销毁
void SListDestroy(SListNode* plist);
//pos位置之前插入
void SListInsertbefore(SListNode** pplist, SListNode* pos, SLTDateType x);

2.Slist.c

#define _CRT_SECURE_NO_WARNINGS 1


#include"slist.h"

我的节点申请
//SListNode* BuySListNode(SLTDateType x)
//{
//	SListNode s;
//	s.data = x;
//	return &s;
//}


//标准的节点申请
SListNode* BuySListNode(SLTDateType x)
{
	SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
	if (newNode == NULL)
	{
		perror("BuySListNode malloc fail");
		return;
	}
	newNode->data = x;
	newNode->next = NULL;
	return newNode;
}
//对新节点的申请是在BuySListNode函数中的!函数中的变量内存在栈区中(临时变量),函数结束就会被销毁,如果此时返回了临时变量的地址
//很容易导致对已经释放的空间进行访问!!
//值得注意的是,对于所有malloc开辟的空间,我们都要防止开辟失败的情况,所以当开辟失败,我们打印错误信息并且结束程序。
//malloc函数所需要包含的头文件是"stdlib",没有包含会报错访问异常。




// 我的单链表打印
//标准单链表打印
void SListPrint(SListNode* plist)
{
	//assert(plist);
		SListNode* cur = plist;
		while (cur)
		{
			printf("%d->", cur->data);
			cur = cur->next;
		}
		printf("NULL\n");
}
//虽然说对传入函数的变量(形参)进行改动对原变量(实参)没有影响,但是函数对指针变量返回是常有的,因此最好用临时的变量代替来
//进行遍历或者其他操作。(禁止不伦不类)
//注意!!!就算是空链表,也是可以打印的,因此该函数不应该对传来的指针变量进行(assert)断言;


 我的单链表尾插
//void SListPushBack(SListNode** pplist, SLTDateType x)
//{
//	assert(pplist);
//	SListNode * cur = *pplist;
//	SListNode * newNode=BuySListNode(x);
//	if (cur == NULL)
//	{
//		cur = newNode;
//		pplist = &cur;
//	}
//	else
//	{
//		while (cur->next)
//		{
//			cur = cur->next;
//		}
//		cur->next = newNode;
//	}
//}


//标准单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SListNode* newNode = BuySListNode(x);
	if (*pplist == NULL)
	{
		*pplist = newNode;
	}
	else
	{
		//找尾
		SListNode* tail = *pplist;
		while (tail->next)
		{
			tail = tail->next;	
		}
		tail->next = newNode;
	}
}
//这里传二级指针的原因是:当链表为空时结构体指针指向的是空(NULL),此时需要改变链表的头指针使其成为新节点的地址,我们需要保存这个地址
//以便于找到我们链表头的位置,所以传递二级指针变量。
//也正是因为这个原因,二级指针不可能为空,所以我们断言二级指针pplist,在它为空时报错。
//在链表中需要特别注意区分临时指针变量和函数参数指针。


//
 我的单链表的头插
//void SListPushFront(SListNode** pplist, SLTDateType x)
//{
//	assert(pplist);
//	SListNode * newNode = BuySListNode(x);
//	if (*pplist == NULL)
//	{
//		*pplist = newNode;
//	}
//	else
//	{
//		newNode->next = *pplist;
//		*pplist = newNode;
//	}
//}

//标准单链表头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SListNode* newNode = BuySListNode(x);
	newNode->next = *pplist;
	*pplist = newNode;
}
//头插操作与链表的大小无关,注意改变表头就好!


// 我的单链表的尾删
//标准单链表尾删
void SListPopBack(SListNode** pplist)
{
	//assert(pplist);
	assert(*pplist);
	SListNode* tail = *pplist;
	while (tail->next->next)
	{
		tail = tail->next;
	}
	/*SListNode* del = tail->next;
	tail->next = NULL;
	free(del);*/
	free(tail->next);
	tail->next = NULL;
}
//我们断言*pplist,因为当它为空指针时(即链表为空时),链表不存在删除。



// 我的单链表头删
//标准单链表头删
void SListPopFront(SListNode** pplist)
{
	assert(*pplist);
	SListNode* del = *pplist;
	*pplist = (* pplist)->next;
	free(del);
}
//这里可以不对del指针进行置空,因为它是临时变量,出函数还是会被销毁。



// 我的单链表查找
//标准单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	assert(plist);
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data != x)
		{
			cur = cur->next;
		}
		else
		{
			return cur;
		}
	}
	//return NULL;
}
//这里加了plist断言(不允许链表为空),其实可以不加断言,直接return空指针。



// 我的单链表在pos位置之后插入x
// 标准单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);
	SListNode* newNode = BuySListNode(x);
	newNode->next = pos->next;
	pos->next = newNode;
}
//在pos位置之前插入需要知道pos之前一个节点的地址,需要遍历链表,时间复杂度为O(N),效率太低。
//当pos位置的下一个为NULL时同样适用


//在pos位置之前插入
void SListInsertbefore(SListNode** pplist,SListNode* pos, SLTDateType x)
{
	//assert(pos);
	//assert(pplist);
	//assert(*pplist);
	SListNode* prev = *pplist;
	if (prev == pos)
	{
		void SListPushFront(pplist,x);
	}
	while (prev->next!=pos)
	{
		prev = prev->next;
	}
	SListNode* newNode = BuySListNode(x);
	prev->next=newNode;
	newNode->next = pos;


}


// 我的单链表删除pos位置之后的值
// 标准单链表删除pos位置之后的值
// 分析思考为什么不删除pos位置?
void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);
	SListNode* del = pos->next;
	/*if (del == NULL)
	{
		return;
	}*/
	pos->next = pos->next->next;
	free(del);
}
//与在pos位置之前插入同理。
//当pos位置的下一个为NULL我们可以选择温柔结束(return返回),也可以暴力断言(assert)pos->next。
//因为=运算符先算‘=’右边的式子,我们可以用pos->next->next找到pos后2个节点的地址来赋值给pos的next变量(pos->next),
//而不会使pos->next地址丢失,导致找不到pos->next->next。



// 我的单链表的销毁
void SListDestroy(SListNode* plist)
{
	//assert(plist);
	SListNode* cur = plist;
	SListNode* del = plist;
	while (cur)
	{
		cur = cur->next;
		free(del);
		del = cur;
	}
	//plist = NULL;
}
//注意:链表销毁之后不要再对链表进行打印,因为打印函数SListPrint同样是对链表的头节点地址进行访问,
//就算是把plist置空(NULL)也不可以,因为它们都只是通过传递地址进行的访问的函数,你无法改变实参的数据。

本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点:)。

二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保存数据的限制,可以用内核链表来保存任何数据(使用一种链表表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表的实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 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); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) 二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保存数据的限制,可以用内核链表来保存任何数据(使用一种链表表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表的实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 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); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) 二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保存数据的限制,可以用内核链表来保存任何数据(使用一种链表表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表的实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 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); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) C语言下的单链表,可以增加,删除,查找,销毁节点。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值