APR分析-环篇
APR中少见对数据结构的封装,好像唯一例外的就是其对循环链表,即环(RING)的封装。
在大学的时候学的不是计算机专业,但大三的时候我所学的专业曾开过一门好像叫“计算机软件开发基础”的课,使用的是清华的一本教材,课程的内容包括数据结构。说实话听过几节课,那个老师讲的还不错,只是由于课程目标所限,没讲那么深罢了。当然我接触数据结构要早于这门课的开课时间。早在大一下学期就开始到计算机专业旁听“数据结构”,再说一次实话,虽号称名校名专业,但是那个老师的讲课水平却不敢恭维。
言归正传!简单说说环(RING):环是一个首尾相连的双向链表,也就是我们所说的循环链表。对应清华的那本经典的《数据结构》一书中线性表一章的内容,按照书中分类其属于线性表中的链式存储的一种。环是很常见也很实用的数据结构,相信在这个世界上环的实现不止成千上万,但是APR
RING(按照APR RING源代码中的注释所说,APR RING的实现源自4.4BSD)却是其中较独特的一个,其最大的特点是其所有对RING的操作都由一组宏(大约30个左右)来实现。在这里不能逐个分析,仅说说一些让人印象深刻的方面吧。
1、如何使用APR RING?
我们先来点感性认识!下面是一个典型的使用APRRING的样例:
假设环节点的结构如下:
struct elem_t { /* APR RING链接的元素类型定义*/
APR_RING_ENTRY(elem_t) link; /*链接域*/
int foo; /*数据域*/
};
APR_RING_HEAD(elem_head_t, elem_t);
int main() {
struct elem_head_t head;
structelem_t *el;
APR_RING_INIT(&head, elem_t, link);
/*使用其他操作宏插入、删除等操作,例如*/
el = malloc(sizeof(elem_t);
el->foo = 20051103;
APR_RING_ELEM_INIT(el, link);
APR_RING_INSERT_TAIL(&h, el, elem_t, link);
}
2、APR RING的难点--“哨兵”
环是通过头节点来管理的,头节点是这样一种节点,其next指针指向RING的第一个节点,其prev指针指向RING的最后一个节点,即尾节点。但是通过察看源码发现APR
RING通过APR_RING_HEAD宏定义的头节点形式如下:
#define APR_RING_HEAD(head, elem) /
struct head{ /
struct elem *next; /
struct elem *prev; /
}
如果按照上面的例子进行宏展开,其形式如下:
struct elem_head_t {
struct elem_t *next;
struct elem_t *prev;
};
而一个普通的元素elem_t展开形式如下:
struct elem_t {
struct{ /
struct elem_t*next; /
struct elem_t*prev; /
} link;
int foo;
};
通过对比可以看得出头节点仅仅相当于一个elem_t的link域。这样做的话必然带来对普通节点和头节点在处理上的不一致,为了避免这种情况的发生,APR
RING引入了“哨兵(sentinel)”节点的概念。我们先看看哨兵节点在整个链表中的位置。
sentinel->next =链表的第一个节点;
sentinel->prev =链表的最后一个节点;
但是察看APR RING的源码你会发现sentinel节点只是个虚拟存在的节点,这个虚拟节点既有数据域(虚拟出来的,不能引用)又有链接域,好似与普通节点并无差别。在APR
RING的源文件中使用了下面这幅图来说明sentinel的位置,同时也指出了sentinel和head的关系-- head即为sentinel虚拟节点的link域。
普通节点
+->+-------+<--
|struct |
|elem |
+-------+
|prev |
| next|
+-------+
| etc. |
. .
. .
sentinel节点
+->+--------+<--
|sentinel|
|elem |
+--------+
|ring |
| head |
+--------+
再看看下面APR_RING_INIT的源代码:
#define APR_RING_INIT(hp, elem, link) do{ /
APR_RING_FIRST((hp)) = APR_RING_SENTINEL((hp), elem, link); /
APR_RING_LAST((hp)) = APR_RING_SENTINEL((hp), elem, link); /
} while (0)
你会发现:初始化RING实际上是将head的next和prev指针都指向了sentinel虚拟节点了。从sentinel的角度来说相当于其自己的link域的next和prev都指向了自己。所以判断APRRING是否为空只需要判断RING的首个节点是否为sentinel虚拟节点即可。APR_RING_EMPTY宏就是这么做的:
#define APR_RING_EMPTY(hp, elem,link) /
(APR_RING_FIRST((hp)) ==APR_RING_SENTINEL((hp), elem, link))
那么如何计算sentinel虚拟节点的地址呢?
我们这样思考:从普通节点说起,如果我们知道一个普通节点的首地址(elem_addr),那么我们计算其link域的地址(link_addr)的公式就应该为link_addr
= elem_addr + offsetof(elem_t, link);前面我们一直在说sentinel虚拟节点看起来和普通节点没什么区别,所以它仍然符合该计算公式。前面我们又说过head_addr是sentinel节点的link域,这样的话我们将head_addr输入到公式中得到head_addr
= sentinel_addr + offsetof(elem_t, link),做一下变换即可得到sentinel_addr= head_addr - offsetof(elem_t, link)。看看APR
RING源代码就是这样实现的:
#define APR_RING_SENTINEL(hp, elem, link) /
(struct elem *)((char *)(hp) -APR_OFFSETOF(struct elem, link))
至此APR RING使用一个虚拟sentinel节点分隔RING的首尾节点,已达到对节点操作一致的目的。
3、使用时注意事项
这里在使用APR RING时有几点限制:
a)在定义RING的元素结构时,需要把APR_RING_ENTRY放在结构的第一个字段的位置。
b)链接一种类型的元素就要使用APR_RING_HEAD宏定义该种类型RING的头节点类型。学过C++或者了解泛型的人可能都会体味到这里的设计有那么一点范型的味道。比如:
模板:APR_RING_HEAD(T_HEAD,T) ----链接----> T类型元素
实例化:APR_RING_HEAD(elem_head_t,elem_t) ---链接---->elem_t类型元素
4、APR RING不足之处
1)缺少遍历接口
浏览APR RING源码后发现缺少一个遍历宏接口,这里提供一种正向遍历实现:
#define APR_RING_TRAVERSE(ep, hp, elem,link) /
for ((ep) = APR_RING_FIRST((hp)); /
(ep) != APR_RING_SENTINEL((hp), elem, link); /
(ep) = APR_RING_NEXT((ep), link))
大家还可以模仿写出反向遍历的接口APR_RING_REVERSE_TRAVERSE。
网友评论