内容简介
最近在看一个开源库的代码,用 C 实现了一种通用的双向链表结构。设计思想是将prev
和next
抽取成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 头插
申请一个IntNode
将IntNode::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
了,代码没有丝毫的违和感。要是想再丝滑点,可将Node
和XList
使用模板类实现,实现如下类似的代码:
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_free
,x_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 库的代码,里面有一些有意思的宏,就学习了下。尤其是变参宏的写法。然后得到宏这个锤子后,就到处都想捶捶,哈哈。