欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Linux: C语言实现范型数据结构 - 嵌入(侵入)式链表浅谈

最编程 2024-08-14 16:17:05
...

Linux: C语言实现范型数据结构 - 嵌入(侵入)式链表浅谈

0背景

虽然C语言不像C++/Java...等, 从语言本身层面去支持面向对象和范型编程

但Linux内核开发者们依然在内核的开发过程, 大量的使用了面向对象范型编程思想, 下面就将以 内核中把范型概念在C语言基础上应用到数据结构的代表作品 -- 嵌入式链表 作为主题,介绍它的

  • 设计思想
  • 核心实现中的编程技巧
  • 不同链表使用上的对比
  • 应用场景

1.基本概念

1.1 范型数据结构

使用一套数据结构, 管理不同类型的 元素/数据

#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型 的 IntListDoubleList 两个链表

范型数据结构 SLinkedList 模板: 使用DStruct库中的范型数据结构模板存储不同类型, 在定义链表时候只需要指定要存储的数据类型即可简单的定义 存储int型的dstruct::SLinkedList<int> intList1; 链表 和 存储double类型的 dstruct::SLinkedList<double> doubleList1; 链表

SLinkedList 可以指定任意的数据类型 而 IntList / DoubleList 是存储类型固定的数据结构 这是范型数据结构的直观感受

从使用者角度

泛型数据结构 允许程序员在声明定义时才指定存储类型,作为实例化时的参数

从数据结构(或库)开发者角度

核心思想是将算法和数据结构与特定的数据类型解耦,使其能够适用于多种数据类型。

1.2 嵌入式(侵入式) 和 非侵入式

粗略的理解: 数据结构会不会影响或"破坏"数据本身的结构,

  • 非嵌入式: 把数据存储到数据结构容器中
  • 嵌入式: 把数据结构(部分结构)嵌入到数据中
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;
}

ds_embedded_list_with_c.1.png

嵌入式和非嵌入式数据结构的直接区别就是 主体与客体的区别

非嵌入式的主体是数据, 被作用的客体是数据结构

嵌入式的主题是数据结构, 被作用的客体是数据

1.3 数据域 和 "链"域

ds_embedded_list_with_c.2.png

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

ds_embedded_list_with_c.3.png

2.1 数据域与链区 逻辑分离 - 链表操作的高度抽象

抽象出链表的通用操作, 即 数据域为空或者说没有挂载数据的链表的操作

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 视角一: 链表管理

当链表数据结构的管理/操作时中是对数据不感知

  • 判空
  • 插入
  • 删除
  • 遍历

ds_embedded_list_with_c.4.png

2.3 视角二: 数据访问

当需要访问具体链表节点中的数据式 可以通过 视角切换手段 获取数据

  • 成员变量偏移
  • 强制类型转换

ds_embedded_list_with_c.5.png

: 双视角引用的是嵌入式双链表的图片, 详细动画可以观看如下视频

www.bilibili.com/video/BV1Zh…

3. 嵌入式链表的核心实现细节

3.1 dstruct::_SinglyLink使用预览 - MyList

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;
}

ds_embedded_list_with_c.6.png

3.1 链表操作实现

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结构的首地址, 以此来减少求偏移中首地址这个未知量

// 成员地址-相对偏移
#define offset_of(StructType, member) \
	((size_t)&((StructType *)0)->member)

类型转换/视角转换

// 节点视角转linker视角:
#define to_link(nodePtr) (&((nodePtr)->linker))

// Linker视角转节点视角: 通过成员地址 获取完整结构体类型
#define to_node(linkPtr, StructType, member) \
	( \
        (StructType *)( (char *)linkPtr - offset_of(StructType, member) ) \
    )

ds_embedded_list_with_c.7.png

3.3 遍历与数据访问

遍历链表时 - 使用链表视角

访问数据时 - 使用数据节点视角

// 遍历节点 访问数据 并释放节点
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 具体使用对比

ds_embedded_list_with_c.8.png

4.3 *度 与 便利性的 权衡

*度: 嵌入式链表 > 传统链表 > 现代链表

便利性/有好性: 现代链表 > 传统链表 > 嵌入式链表

现代数据结构/容器(std::list/dstruct::SLinkedList...) 给了最大便利性, (多数情况下)让开发者只需要关注业务, 而嵌入式链表给了开发者最大的*度(甚至一个链表中管理各种数据类型), 但同时也提高了数据结构使用的复杂度

5. 应用场景

已经应用的场景及可能的应用场景

5.1 嵌入式链表

Linux 内核

github.com/torvalds/li…

各种场景 - 页表管理/设备管理....

虚拟机中的Object对象管理

ds_embedded_list_with_c.9.png

分配器与内存块管理

github.com/Sunrisepeak…

管理空闲内存块

ds_embedded_list_with_c.10.png

5.2 现代链表 & "传统链表"

现代容器式链表由于友好的接口, 并实现了内存管理 已经应用到各种系统中了,

而"传统"链表(类型固定) 由于即没有现代链表的便利性又没有嵌入式链表的*度, 但其表达了数据结构-链表的基础思想, 也因此多数情况下应用于教学中

6. Other

欢迎Star

完整的测试代码 - HelloWorld项目

测试中用到的数据结构 - DStruct数据结构模板库