一种通用链表设计

内容简介

最近在看一个开源库的代码,用 C 实现了一种通用的双向链表结构。设计思想是将prevnext抽取成struct Node,针对Node编写通用的双向链表类。当需构造特定类型的链表时,首先在其中定义一个Node类型的成员,然后使用CONTAINER_OF宏借助Node的成员的关系得到特定类型链表关系,无需再为特定类型链表编写专用函数。

本文以IntNode为例,演示通用Node双向链表类和CONTAINER_OF宏的使用。其实,写这篇文章就是为了介绍CONTAINER_OF这个宏。

设计实现

Node 节实现

struct Node {
    Node *prev;
    Node *next;
};

通用的双向链表类实现

本文的重点不在于双向链表,直接附上一种双向循环链表XList的实现。技巧点在于利用一个常驻Node节点作为head,简化代码。基础的数据结构没事可以看看,磨磨脑子。其实开源库中代码的XList也是宏定义,没啥亮点,为了提高可读性,直接使用 C++的class实现。

XList 构造和析构

XList::XList() {
    head_ = x_new(Node); // 常驻头结点
    head_->next = head_;
    head_->prev = head_;
    size_ = 0; // 链表初始大小
}

XList::~XList() {
    x_delete(head_);
    head_ = nullptr;
}

XList 头插和尾插

void XList::PushFront(Node *node) {
    node->prev = head_;
    node->next = head_->next;
    head_->next->prev = node;
    head_->next = node;
    ++size_;
}

void XList::PushBack(Node *node) {
    node->prev = head_->prev;
    node->next = head_;
    head_->prev->next = node;
    head_->prev = node;
    ++size_;
}

XList 删除

void XList::Remove(Node *node) {
    node->next->prev = node->prev;
    node->prev->next = node->next;
    node->next = nullptr;
    node->prev = nullptr;
    --size_;
}

XList 空判断

bool XList::IsEmpty() { return head_ == head_->next; }

XList 迭代器

Node *XList::begin() { return head_->next; }
Node *XList::end() { return head_; }

XList 首尾节点

Node *XList::front() { return head_->next; }
Node *XList::back() { return head_->prev; }

XList 大小获取

int XList::size() const { return size_; }

CONTAINER_OF 宏

首先定义一个 OFFSET_OF 宏,如下:

#define OFFSET_OF(_type, _member) ((size_t) &((_type *)0)->_member)

&((_type *)0)->_member这种写法很奇怪,看上去觉得空指针访问_member会崩溃,但实际上编译器会将&((_type *)0)->_member直接优化成取地址,不存在访问空间。由于首地址为 0,因此OFFSET_OF的返回结果为_member相对于_type 结构的地址偏移。

#define CONTAINER_OF(_member_addr, _type, _member) ((_type*)((char*)(_member_addr) - OFFSET_OF(_type, _member)))

_member_type的成员变量,当_member_addr_member的地址时,CONTAINER_OF就返回的是其所在的_type 结构的首地址。为了更人性化点,将CONTAINER_OF定义为LIST_ENTRY

#define LIST_ENTRY(_member_addr, _type, _member) CONTAINER_OF(_member_addr, _type, _member)

IntNode中定义一个Node类型的成员,如下:

struct IntNode {
    Node node;
    int sn;
};

如下代码就可以通过XList::front得到其所在IntNode的地址,是不是很赞?有没有泛型的感觉?

IntNode *tmp = LIST_ENTRY(list.front(), IntNode, node);

测试代码

IntNode 头插

申请一个IntNodeIntNode::node从头插入到list中。

XList list;
for (int i = 0; i < 5; ++i) {
    auto *node = (IntNode *) x_malloc(sizeof(IntNode));
    node->sn = i + 1;
    list.PushFront(&node->node);
}

IntNode 遍历

遍历list,然后通过LIST_ENTRY宏使用Node *p得到其所在的IntNode地址,打印IntNode::sn

XList list;
for (Node *p = list.begin(); p != list.end(); p = p->next) {
    IntNode *tmp = LIST_ENTRY(p, IntNode, node);
    printf("sn = %d\n", tmp->sn);
}

IntNode 删除

遍历list,然后通过LIST_ENTRY宏使用list.front()得到其所在的IntNode地址,释放IntNode

int size = list.size();
for (int i = 0; i < size; ++i) {
    IntNode *tmp = LIST_ENTRY(list.front(), IntNode, node);
    list.Remove(&tmp->node);
    x_free(tmp);
}

扩展

也就是 C 能写出这种奇淫技巧。要是 C++写的,IntNode直接继承Node就可以复用XList了,代码没有丝毫的违和感。要是想再丝滑点,可将NodeXList使用模板类实现,实现如下类似的代码:

XList<IntNode*> list;
IntNode *node = x_new(IntNode2);
list.PushBack(node);
IntNode *node = list.front();

要是想更完美,搞个迭代器实现begin()end(),用于区分front()back(),代码就更优雅了。

事实上 C++的std::list已经实现了该功能。C++重点在于设计,在组建大型工程时,可以写出很优雅的代码,同时会增加代码的体积。上面 C 实现的方式虽然不怎么优雅,但是基本没有多余的代码。linux 内核中也采用类似的方式实现了一个通用 list。

PostScript

其中,x_malloc/x_freex_new/x_delete是为了统计内存做的一个宏,大致定义如下。以后有机会给大家分享下。之前试过重载new/delete运算符方式,但是代码中要是用到 STL ,STL 中的new/delete也会被重载,在delete时候内存会出问题,无法完美的替换。然后,退而求其次定义了一个x_new/x_delete,虽然不是很美观,但很可靠。不过,这也仅是用来 debug 的,debug 完了以后就可以取消。用了一个AUTO_MEMORY_TRACKER进行控制。

#ifdef AUTO_MEMORY_TRACKER

int add_c_memory(void *p, size_t size, const char *filename, int line);
int del_c_memory(void *p);

int add_cpp_memory(void *p, size_t size, const char *filename, int line);
int del_cpp_memory(void *p);

#define x_malloc(_size) \
    ({void* _xp = malloc(_size);add_c_memory(_xp,_size,get_filename_from_file(__FILE__),__LINE__);_xp;})
#define x_calloc(_num, _size) \
    ({void* _xp = calloc(_num, _size);add_c_memory(_xp,_num*_size,get_filename_from_file(__FILE__),__LINE__);_xp;})
#define x_free(_ptr)  do { \
    if (del_c_memory(_ptr) == 0) free(_ptr); } while(0)

#define x_new(_type, ...) \
    ({auto* _xp = new _type(__VA_ARGS__);add_cpp_memory(_xp,sizeof(_type),get_filename_from_file(__FILE__),__LINE__);_xp;})
#define x_delete(_ptr)  do { \
    if (del_cpp_memory(_ptr) == 0) delete(_ptr); } while(0)

#else

#define       x_malloc(_size)   malloc(_size)
#define x_calloc(_num, _size)   calloc(_num,_size)
#define          x_free(_ptr)   free(_ptr)

#define     x_new(_type, ...)   new _type(__VA_ARGS__)
#define        x_delete(_ptr)   delete(_ptr)

#endif

是不是觉得最近文中代码宏用的比较多?因为最近在看 C 库的代码,里面有一些有意思的宏,就学习了下。尤其是变参宏的写法。然后得到宏这个锤子后,就到处都想捶捶,哈哈。

推荐阅读

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Pintos是一个操作系统的教学项目,它里面有许多基础的数据结构。其中之一就是通用链表(generic list)。 链表是一个常见的数据结构,用于存储一系列的元素。在Pintos中,通用链表一种灵活且可扩展的数据结构,可以存储任意类型的元素。 Pintos中的通用链表由两个主要的结构组成:链表节点和链表本身。每个链表节点包含了一个指向前一个节点的指针和一个指向后一个节点的指针,以及一个用于存储元素的指针。链表本身则包含了一个指向头节点和尾节点的指针,以及用于记录链表长度的变量。 通过使用这些结构,Pintos的通用链表提供了一系列的操作来管理链表中的元素。比如,它可以实现在链表头部和尾部插入元素、删除元素以及在指定位置插入或删除元素等功能。此外,通过遍历链表,我们可以对链表中的每个元素进行操作,比如查找、更新和打印。 Pintos的通用链表使用简单而高效的方法来处理链表操作。通过使用指针连接节点,我们可以轻松地插入和删除元素,并且时间复杂度为O(1)。而遍历链表的操作也只需要O(n)的时间复杂度,其中n是链表的长度。 总之,Pintos的通用链表为操作系统的开发提供了一个方便和高效的数据结构。它可以存储任意类型的元素,并提供了丰富的操作来管理和操作这些元素。无论是在Pintos项目中还是在实际操作系统的开发中,通用链表都是一个非常有用的工具。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值