C++之内核链表和企业链表

作为数据结构中的基础结构之一,链表的优点就是可以不要求连续的存储空间,但是没有了随机访问,在按索引增删改查,似乎效率没有数组快,但是链表也有它优势的地方,linux内核和企业上,对于链表的时候,和我们一般学的方法有所不同,这里详细讲解一下。


废话说到这里,开始上干货。


内核链表

在Linux内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在[include/linux/list.h]实现的一个相当精彩的链表数据结构。事实上,内核链表就是采用双循环链表机制。

内核链表有别于传统链表就在节点本身不包含数据域,只包含指针域。故而可以很灵活的拓展数据结构。

头节点
#pragma
#include
#include
#include
#include

using namespace std;

struct list_head {
	struct list_head *prev;
	struct list_head *next;
};

struct list_head headNode;

//初始化函数
void INIT_LIST_HEAD(struct list_head *node) {
	node->next = node;
	node->prev = node;
}

首先,我们可以发现,链表节点只有指针域,没有数据。它相当于把头节点,内嵌到数据域中,就好像晾衣服的挂钩一样,下面会进行实现,这里先看一下链表的基础操作,因为是双向循环链表,所以操作和循环链表类似。

基本操作
void __list_add(struct list_head *new_, struct list_head *prev, struct list_head *next)
{
	prev->next = new_;
	new_->next = next;
	next->prev = new_;
	new_->prev = prev;
}

void __list_del(struct list_head *prev, struct list_head *next) {
	prev->next = next;
	next->prev = prev;
}


//头部添加
void list_add(struct list_head *new_, struct list_head *head){
	__list_add(new_, head, head->next);
}

//尾部添加
void list_add_tail(struct  list_head *new_, struct list_head *head){
	__list_add(new_, head->prev, head);
}

//节点删除
void list_del(struct list_head *entry) {
	__list_del(entry->prev, entry->next);
}

主要是三个函数,头部添加,尾部添加,和节点删除,这里内核的写法,是尽可能把冗余得到代码都封装成了函数,整体看起来非常简介,很有借鉴意义。

  • 循环链表的头部添加和尾部添加,基本是一样的,需要修改插入位置前驱节点的后继指针,后继节点的前驱指针,以及该节点的前后指针。
  • 节点删除,只需要修改该节点前驱节点的后继指针和后继节点的前驱指针即可,这里没有对malloc的节点进行free操作,需要我们自己进行。
循环遍历
#define list_for_each(pos, head) \
		for(pos = (head)->next; pos != (head); pos = pos->next)   //替换语句

struct list_head *pos = NULL;
list_for_each(pos, &headNode) {

    userTmp = container_of(pos, struct userInfo, node);
    printf("id = %d name = %s \n", userTmp->id, userTmp->name);
}

这里我们采用宏函数,来进行遍历,宏函数的使用是内核链表一个很精妙的地方,这里我们先从最简单的开始。

这里宏函数就会无脑的替换掉成for循环的语句,然后输出结果

数据域
struct userInfo
{
	int id;
	char name[10];
	struct list_head node;
};

数据的定义如上,这里我们简单定义一个整形和一个字符数组。我们可以发现指针域的定义是放在数据下面的,这样的话,我们如果需要把list_head指针转换成userInfo的话,就需要在list_head指针的地址上减去一个数据的偏移量。那么内核链表核心的代码来了,具体如下所示

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

#define container_of(ptr, type, member) ({			\
	const typeof(((type *)0)->member) * __mptr = (ptr);	\
	(type *)((char *)__mptr - offsetof(type, member)); })

上述两个宏函数,分别是

  1. 偏移量 = 结构体成员地址 - 结构体地址(0)= 结构体成员地址
  2. 结构体地址 = 结构体成员地址 - 偏移量地址

这里我们拆解来看

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

/*
	1. ((struct userInfo*)0) 对地址0强制转换成struct userInfo
	2. ((struct userInfo*)0)->node 访问结构体中的node成员
	3. &((struct userInfo*)0)->node 获取成员node的地址,表示node成员在结构体中的偏移量
	4. ((size_t))&((struct userInfo*)0)->node 在32位系统中,size_t表示无符号int, 在64系统中表示无符号long类型
*/

这就相当于将地址0,强转成结构体userInfo,然后调用它的成员变量的地址,而这个结构体成员变量的地址,就是偏移量,最后在强转成size_t,就得到了最后的偏移量。

#define container_of(ptr, type, member) ({			\
	const typeof(((type *)0)->member) * __mptr = (ptr);	\
	(type *)((char *)__mptr - offsetof(type, member)); })
/*
	1. typeof( ((struct userInfo*)0)->node ) 拿到成员的数据类型
	2. const typeof( ((struct userInfo*)0)->node ) *__mptr = &(userInfo->node) 
	:  const struct list_head *__mptr = &(userInfo->node)  创建了一个链表的指针域
	3. (char*)__mptr - offserof(struct userInfo, node)      需要对地址做加减,所以需要强制转换为char*类型
	4. 这样就获取到了结构体的起始地址。
*/

typeof函数,可以拿到成员的数据类型,用于定义__mptr指针,指向结构体成员的地址,而char*的强转,是因为地址上的加减运算,需要强转。最后减去偏移量,就得到了数据域的地址。

不过这里我们需要注意的是,win上C++是没有typeof函数,同时也不支持({ }) 形式 ,这个要修改.且offset宏定义和编译器再带的宏函数是冲突的,修改情况如下:

// 将offsetof改成了offsetof2
#define offsetof2(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)  

// C++编译器不支持({ }) 形式 ,这个要修改
#define container_of(ptr, type, member) \
		(type*)( (char*)(ptr) - offsetof2(type, member)) 

接下来,我们在写一个添加节点的代码

void addUser(int id,const char *name)
{
	struct userInfo *userTmp = (struct userInfo *)malloc(sizeof(struct userInfo));

	userTmp->id = id;
	memcpy(userTmp->name, name, strlen(name)+1);

	list_add(&(userTmp->node), &headNode);
}

如上所示,传入id和name之后,会自动创建数据域结构体,并增长添加指针域节点即可。

访问的话,如下所示

list_for_each(pos, &headNode) {
    userTmp = container_of(pos, struct userInfo, node);
    printf("id = %d name = %s \n", userTmp->id, userTmp->name);
}

最后,以下是测试代码

int main() {

	INIT_LIST_HEAD(&headNode);
	struct list_head *pos = NULL;
	struct userInfo *userTmp = NULL;

	//因为是全局变量,所以这里直接调用addUser
	addUser(1, "fang");
	addUser(2, "wangyu");
	addUser(3, "hongjie");
	
	list_for_each(pos, &headNode) {
		userTmp = container_of(pos, struct userInfo, node);
		printf("id = %d name = %s \n", userTmp->id, userTmp->name);
	}

	return 0;
}

企业链表

我们来看一下企业链表的示意图
C++之内核链表和企业链表_第1张图片
大家有发现企业链表和内核链表的区别吗,这里因为没有画内核链表的示意图,但是通过结构体我们可以发现,内核链表的指针域是放在下面的,而数据是放在上面的。而对于企业链表则是指针域在上面,数据域在下面,这样有什么好处呢?

相信大家已经知道了,那就是不需要在计算偏移量了。

这里只需要修改两个东西

struct userInfo
{
    struct list_head node;
	int id;
	char name[10];
};

除此之外,还需要强转指针域的指针,转成数据域

userTmp = (userInfo*)pos;

这样你再次测试,就没有问题了。

你还可以尝试一下,将指针域的定义放在数据域的中间,如果你不使用偏移量,而直接强转的话,你会发现不会得到好的结果。

你可能感兴趣的:(数据结构,算法与思维,链表,数据结构)