写点什么

如何实现一套容器(C 语言版)2

作者:祖维
  • 2022 年 6 月 24 日
  • 本文字数:2930 字

    阅读完需:约 10 分钟

如何实现一套容器(C 语言版)2

经典计算机教材《算法导论》第十章数据结构部分,就有提及链表。其中的伪代码实现,确实精秒。于是我决定讲其里面的伪代码转录成 C 语言,结合上一篇文章《如何设计一道容器 1》中设计的容器接口,让其成为真正能使用的容器。


01 链表的设计


算法导论中的链表是一个双向循环链表,每个节点都有向前向后的指针,大概长这个样子:


因为我要把它套入到容器中,容器是要实现标准的迭代式的访问,是要实现例如 for 循环之类的。所以这里要在上述的循环链表中还加一个边界 tail。修改如下:



当然那个 tail 与 head 物理上同一个内存对象,只不过逻辑上看上是两个节点。


02 代码实现


链表,一般分为两部分,装载数据的数据节点和链表的数据结构。

  • 链表的节点定义如下:

typedef struct _list_node list_node_t;struct _list_node{    // 前一个节点的指针    list_node_t* prev;    // 后一个节点的指针    list_node_t* next;    // 数据内存,这里并不是值只有 1 个字节。    // 这个节点大小需要根据存入的数据大小来申请内存。    T w[1];};
复制代码


  • 链表的定义如下:

typedef struct _list {    // 这里可以看作继承 container 这个基类    container_t container;    list_node_t _sentinel;    int _size;    } list_t;
复制代码


  • 链表的 first 接口的实现:

static iterator_t __list_first (container_t* plist){    return __iterator(list_first(plist)->w, plist);}
复制代码
  • 链表的 last 接口实现:

static iterator_t __list_last (container_t* plist){    return __iterator(list_last(plist)->w, plist);}
复制代码
  • 链表的迭代器移动接口实现:

static int __list_move(iterator_t* it, int step){    list_node_t* pnode = container_of(it->reference, list_node_t, w);    for (int next = step; next; next = step > 0? next - 1: next + 1) {        if (step > 0) pnode = pnode->next;        else if (step < 0) pnode = pnode->prev;    }    it->reference = pnode->w;    return 0;}
复制代码


这里要特别说明,iterator_t *it 中 refrence 字段是指向节点的数据部分,但是在迭代器移动过程中,需要获取 next 与 prev 的指针。但这就需要知道整个 list_node 的地址,于是这里必要用一种 container_of 的技术,从结构体的某个字段的地址,推算出结构体的首地址:

#define offset_of(TYPE, MEMBER) \           ((char*)&(((TYPE *)0)->MEMBER))           #define container_of(ptr, TYPE, MEMBER) \({ \    TYPE* type_ptr =  (((char*)ptr) - offset_of(TYPE, MEMBER)); \    type_ptr; \})
复制代码

container_of 计算过程如下:

  • 链表的插入实现:

static void* __list_insert(container_t* container, iterator_t pos,...){    T data[T_size(container->type_class)];        //从不定参数中读取要插入的数据。    va_list valist;    va_start(valist, pos);    // 此处调用数据对象对应的 不定参数转换函数。    // 把不定参数的值转换到 data 上。    T_vargs(container)(valist, data);    va_end(valist);        list_node_t* insert = container_of(pos.reference, list_node_t, w);        // 节点的大小根据容器中数据对象的大小申请内存    list_node_t* pnew \        = allocate(container->mem_pool, sizeof(list_node_t) \        + T_size(container->type_clazz));    // 赋值
T_setup(container->type_clazz)(pnew->w, data, 0); // 插入 pnew->prev = insert->prev; pnew->next = insert;
insert->prev->next = pnew; insert->prev = pnew;
list_t *plist = container; plist->_size++; return 0;}
复制代码


为什么插入的接口需要不定参数?那是因为,容器是为多种数据服务的。如果接口写死了一种数据类型的插入,那么就不能做到数据和容器分离。耦合度就了。


使用了不定参数作为接口后,容器可以插入 int 或者 float 类型。以前老旧办法是存入 int 类型变量的指针或者 float 类型的指针。但是如此一来,容器就不能直接插入字面量,而必须把字面量转换成变量,再来获取变量的指针。这样做十分不优雅。


使用了不定参数的接口可以如下插入数据:

// int 型的容器container_insert(list, 1);// float 型的容器container_insert(list, 5.4);
复制代码


但这样做也有缺点,那就是只能依靠程序员,严重按照自己定义的容器去插入数据,例如你定义了 int 型的容器,在 container_insert 的时候就必须插入整形。如果你插入了浮点,编译是不会报错的,但是程序的逻辑错了,追查起来比较麻烦。


  • 链表删除

static int __list_remove(container_t* container, iterator_t pos, void* rdata){    // 删除    // 同样需要 container_of 的技术    list_node_t* remove = container_of(pos.reference, list_node_t, w);            remove->prev->next = remove->next;    remove->next->prev = remove->prev;
// 将要删除的值返回出去。 //if (rdata) container->type_def.ty_adapter.bit_cpy(rdata, remove->w); if (rdata) type_value_cpy(rdata, remove->w, T_size(container->type_clazz)); // 回收 deallocate(container->mem_pool, remove); ((list_t*)container)->_size--; return 0;}
复制代码


  • 链表初始化

container_t* list_create(int ty) {    T_clazz __t = T_get(ty);        list_t* list = (list_t*) malloc( sizeof(list_t));    initialize_container(        list,         __list_first,         __list_last,         __list_move,        __list_insert,         __list_remove,         __t,    );
list_first(list) = list_head(list); list_last(list) = list_tail(list); list->_size = 0; return list;}
复制代码


至此双向链表与容器接口的代码已经全部实现。


03 测试


搞了这么多,终于可以试用一下。

int main (){  // 测试 int 类型的容器。  list_t* list = list_create(int_t);  container_insert(list, 1);  container_insert(list, 2);  container_insert(list, 3);  container_insert(list, 4);  container_insert(list, 5);    // 输出容器内的整形 1,2,3,4,5  for (iter it = container_first(list);      !iter_eqaul(it, container_tail(list);      iter_next(it)) {            printf("%d ", iter_drefer(it);  }    // 测试 float 的类型的容器。  list_t list2 = list_create(fl_t);  container_insert(list2, 1.1);  container_insert(list2, 2.2);  container_insert(list2, 3.3);  container_insert(list2, 4.4);  container_insert(list2, 5.5);    // 输出容器内的整形 1.1,2.2,3.3,4.4,5.5  for (iter it = container_first(list);      !iter_eqaul(it, container_tail(list);      iter_next(it)) {            printf("%d ", iter_drefer(it);  }}
复制代码


总结:至此一个简单的 C 语言版本的容器就制作好了。在没有模版机制的加持下,实现一套容器,确实也比较麻烦。但也不是不可以。


以上源码出自:https://github.com/zuweie/boring-code

发布于: 刚刚阅读数: 3
用户头像

祖维

关注

还未添加个人签名 2019.08.15 加入

还未添加个人简介

评论

发布
暂无评论
如何实现一套容器(C 语言版)2_c_祖维_InfoQ写作社区