Linux: C语言实现范型数据结构 - 嵌入(侵入)式链表浅谈
0背景
虽然C语言不像C++/Java...等, 从语言本身层面去支持面向对象和范型编程
但Linux内核开发者们依然在内核的开发过程, 大量的使用了面向对象 和 范型 的编程思想 , 下面就将以 内核中把范型概念在C语言基础上应用到数据结构的代表作品 -- 嵌入式链表 作为主题,介绍它的
- 设计思想
- 核心实现中的编程技巧
- 不同链表使用上的对比
- 应用场景
1.基本概念
1.1 范型数据结构
使用一套数据结构, 管理不同类型的 元素/数据
cpp
#include <dstruct.hpp>
// g++ -Ilibs/DStruct common/embedded-list.cpp && ./a.out
// C-Style
struct IntList {
int data;
struct IntList *next;
};
void IntList_init(struct IntList *list);
bool IntList_empty(struct IntList *list);
void IntList_add(struct IntList *prev, struct IntList *curr);
void IntList_del(struct IntList *prev, struct IntList *curr);
struct DoubleList {
double data;
struct DoubleList *next;
};
void DoubleList_init(struct DoubleList *list);
bool DoubleList_empty(struct DoubleList *list);
void DoubleList_add(struct DoubleList *prev, struct DoubleList *curr);
void DoubleList_del(struct DoubleList *prev, struct DoubleList *curr);
int main() {
// example1: Generic-Style / C-No-Generic-Style
dstruct::SLinkedList<int> intList1;
struct IntList intList2;
dstruct::SLinkedList<double> doubleList1;
struct IntList doubleList2;
return 0;
}
固定存储类型的数据结构 IntList/DoubleList
: 上面代码通过C语言偏传统数据结构入门教学风格定义了 存储int型 和 存储double型 的 IntList
和 DoubleList
两个链表
范型数据结构 SLinkedList
模板: 使用DStruct
库中的范型数据结构模板 存储不同类型, 在定义链表时候只需要指定要存储的数据类型即可简单的定义 存储int型的dstruct::SLinkedList<int> intList1;
链表 和 存储double类型的 dstruct::SLinkedList<double> doubleList1;
链表
SLinkedList
可以指定任意的数据类型 而 IntList / DoubleList
是存储类型固定的数据结构 这是范型数据结构的直观感受
从使用者角度
泛型数据结构 允许程序员在声明定义时才指定存储类型,作为实例化时的参数
从数据结构(或库)开发者角度
核心思想是将算法和数据结构 与特定的数据类型解耦,使其能够适用于多种数据类型。
1.2 嵌入式(侵入式) 和 非侵入式
粗略的理解: 数据结构会不会影响或"破坏"数据本身的结构,
- 非嵌入式: 把数据存储到数据结构容器中
- 嵌入式: 把数据结构(部分结构)嵌入到数据中
cpp
struct Student {
char *name;
int age;
};
struct StudentListNode {
dstruct::_SinglyLink linker; // 链表链接器
char *name;
int age;
};
int main() {
dstruct::SLinkedList<Student> studentList1;
dstruct::_SinglyLink studentList2;
dstruct::_SinglyLink::init(&studentList2);
// 增加sNode到链表studentList2
StudentListNode sNode;
dstruct::_SinglyLink::add(&studentList2, &(sNode.linker));
return 0;
}

嵌入式和非嵌入式数据结构的直接区别就是 主体与客体的区别
非嵌入式的主体是数据, 被作用的客体是数据结构
嵌入式的主题是数据结构, 被作用的客体是数据
1.3 数据域 和 "链"域

2.嵌入式链表的设计思想 - 双重视角

2.1 数据域与链区 逻辑分离 - 链表操作的高度抽象
抽象出链表的通用操作, 即 数据域为空或者说没有挂载数据的链表的操作
cpp
static void init(_SinglyLink *link);
static bool empty(_SinglyLink *link);
static void add(_SinglyLink *prev, _SinglyLink *curr);
static void del(_SinglyLink *prev, _SinglyLink *curr);
2.2 视角一: 链表管理
当链表数据结构的管理/操作时中是对数据不感知的
- 判空
- 插入
- 删除
- 遍历

2.3 视角二: 数据访问
当需要访问具体链表节点中的数据式 可以通过 视角切换手段 获取数据
- 成员变量偏移
- 强制类型转换

注 : 双视角引用的是嵌入式双链表的图片, 详细动画可以观看如下视频
www.bilibili.com/video/BV1Zh...
3. 嵌入式链表的核心实现细节
3.1 dstruct::_SinglyLink
使用预览 - MyList
cpp
struct MyList {
int a;
dstruct::_SinglyLink linker;
double b;
char c;
};
using MyListNode = MyList;
// ....
int main() {
// 创建&初始化链表
MyList myList;
dstruct::_SinglyLink::init(to_link(&myList));
// 分配node初始化,并添加到链表
for (int i = 0; i < 10; i++) {
// 分配内存
MyListNode *node = (MyListNode *) malloc(sizeof(MyListNode));
// 初始化
node->a = i;
node->b = 0.5 + i;
node->c = 'a' + i;
// 添加到链表(头插法)
dstruct::_SinglyLink::add(to_link(&myList), to_link(node));
}
assert(!dstruct::_SinglyLink::empty(to_link(&myList)));
// 遍历节点 访问数据 并释放节点
auto linkPtr = to_link(&myList)->next; // 获取第一个节点地址
for (; linkPtr != to_link(&myList); linkPtr = linkPtr->next) {
auto nodePtr = to_node(linkPtr, MyListNode, linker); // 1.转成节点指针
printf("%d %f %c\n", nodePtr->a, nodePtr->b, nodePtr->c); // 2.访问数据
}
// 释放链表
linkPtr = to_link(&myList)->next;
while (dstruct::_SinglyLink::empty(to_link(&myList))) {
auto next = linkPtr->next;
free(to_node(linkPtr, MyListNode, linker));
linkPtr = next;
}
return 0;
}

3.1 链表操作实现
cpp
static void init(_SinglyLink *link) {
link->next = link;
}
static bool empty(_SinglyLink *link) {
return link->next == link;
}
static void add(_SinglyLink *prev, _SinglyLink *curr) {
curr->next = prev->next;
prev->next = curr;
}
static void del(_SinglyLink *prev, _SinglyLink *curr) {
prev->next = curr->next;
curr->next = nullptr;
}
3.2 成员变量偏移 与 类型转换
成员变量相对偏移计算
注意: 这里设置了0为
StructType
结构的首地址, 以此来减少求偏移中首地址这个未知量
cpp
// 成员地址-相对偏移
#define offset_of(StructType, member) \
((size_t)&((StructType *)0)->member)
类型转换/视角转换
cpp
// 节点视角转linker视角:
#define to_link(nodePtr) (&((nodePtr)->linker))
// Linker视角转节点视角: 通过成员地址 获取完整结构体类型
#define to_node(linkPtr, StructType, member) \
( \
(StructType *)( (char *)linkPtr - offset_of(StructType, member) ) \
)

3.3 遍历与数据访问
遍历链表时 - 使用链表视角
访问数据时 - 使用数据节点视角
cpp
// 遍历节点 访问数据 并释放节点
auto linkPtr = to_link(&myList)->next; // 获取第一个节点地址
for (; linkPtr != to_link(&myList); linkPtr = linkPtr->next) {
auto nodePtr = to_node(linkPtr, MyListNode, linker); // 1.转成节点指针
printf("%d %f %c\n", nodePtr->a, nodePtr->b, nodePtr->c); // 2.访问数据
}
4. 不同链表对比
4.1 责任划分 - 内存管理 / 类型控制 / 数据结构操作
-
"传统"链表:
-
<数据结构库> : 提供 类型控制 和 数据结构操作
-
<使用者> : 负责数据节点的内存管理(分配和释放)
-
-
现代数据结构/容器:
-
<数据结构库> : 提供了内存管理、类型控制、数据结构的操作的全面实现
-
<使用者> : 只需要关心业务实现
-
-
嵌入式链表:
-
<数据结构库> : 仅提供统一的数据结构操作
-
<使用者> : 需要负责 内存管理 与 类型控制, 给了使用者极大的自由度和权利
-
4.2 具体使用对比

4.3 自由度 与 便利性的 权衡
自由度: 嵌入式链表 > 传统链表 > 现代链表
便利性/有好性: 现代链表 > 传统链表 > 嵌入式链表
现代数据结构/容器(std::list/dstruct::SLinkedList...
) 给了最大便利性, (多数情况下)让开发者只需要关注业务, 而嵌入式链表给了开发者最大的自由度(甚至一个链表中管理各种数据类型), 但同时也提高了数据结构使用的复杂度
5. 应用场景
已经应用的场景及可能的应用场景
5.1 嵌入式链表
Linux 内核
各种场景 - 页表管理/设备管理....
虚拟机中的Object对象管理

分配器与内存块管理
管理空闲内存块

5.2 现代链表 & "传统链表"
现代容器式链表由于友好的接口, 并实现了内存管理 已经应用到各种系统中了,
而"传统"链表(类型固定) 由于即没有现代链表的便利性又没有嵌入式链表的自由度, 但其表达了数据结构-链表的基础思想, 也因此多数情况下应用于教学中
6. Other
欢迎Star 逃