
C 语言是不包含面向对象特性的,但是我们可以使用一些 tricks 来实现面向对象,借助 C 语言的宏,我们甚至可以实现泛型。
本文以双向循环链表为例,讲解如何实现面向对象风格的泛型链表。
Table of contents
传统实现
一般情况下我们会这样定义链表:
typedef struct list_node { struct list_node *prev, *next; int data;} list_node;typedef struct list { list_node *_head; size_t _size;} list;static list *init_list() { list *obj = (list *) malloc(sizeof(list)); obj->_head = (list_node *) malloc(sizeof(list_node)); obj->_head->prev = obj->_head; obj->_head->next = obj->_head; obj->_size = 0; return obj;}static void free_list(list *self) { while (self->_size > 0) { list_pop_back(self); } free(self->_head); free(self);}然后实现插入、删除算法等
static void list_push_back(list *self, int value) { list_node *new_node = (list_node *) malloc(sizeof(list_node)); new_node->data = value; new_node->prev = self->_head->prev; new_node->next = self->_head; self->_head->prev->next = new_node; self->_head->prev = new_node; self->_size++;}
static void list_pop_back(list *self) { list_node *removal = self->_head->prev; removal->prev->next = self->_head; self->_head->prev = removal->prev; free(removal); self->_size--;}测试一下代码:
int main() { list *l = init_list(); for (int i = 0; i < 5; i++) { list_push_back(l, i); } assert(l->_size == 5); for (list_node *p = l->_head->next; p != l->_head; p = p->next) { printf("%d ", p->data); } printf("\n"); for (int i = 0; i < 5; i++) { list_pop_back(l); } assert(l->_size == 0); free_list(l);}可以看到期望的输出:
0 1 2 3 4基于上面的代码,将其改造成面向对象风格。
面向对象
C 语言的结构体 struct 是不支持成员方法的,但是我们可以使用函数指针实现。
比如上面的链表,将 push_back 和 pop_back 写成函数指针的形式为:
typedef struct list { list_node *_head; size_t _size; void (*push_back)(struct list *self, int value); void (*pop_back)(struct list *self);} list;初始化链表应改写为:
static list *init_list() { list *obj = (list *) malloc(sizeof(list)); obj->_head = (list_node *) malloc(sizeof(list_node)); obj->_head->prev = obj->_head; obj->_head->next = obj->_head; obj->_size = 0; obj->push_back = list_push_back; obj->pop_back = list_pop_back; return obj;}这样的话,我们就可以像 C++ 那样调用成员方法了:
int main() { list *l = init_list(); for (int i = 0; i < 5; i++) { l->push_back(l, i); } assert(l->_size == 5); for (list_node *p = l->_head->next; p != l->_head; p = p->next) { printf("%d ", p->data); } printf("\n"); for (int i = 0; i < 5; i++) { l->pop_back(l); } assert(l->_size == 0); free_list(l);}泛型
C 语言实现泛型最简单的方法个人认为是宏。虽然可读性很差,并且不方便调试,但是实现起来简单。
我们对上面的代码进行改写,将函数都写进宏内部:
#define DECLARE_LIST(T) \ typedef struct list_node { \ struct list_node *prev, *next; \ T data; \ } list_node; \ typedef struct list { \ list_node *_head; \ size_t _size; \ void (*push_back)(struct list * self, T value); \ void (*pop_back)(struct list * self); \ } list; \ static void list_push_back(list *self, int value) { \ list_node *new_node = (list_node *)malloc(sizeof(list_node)); \ new_node->data = value; \ new_node->prev = self->_head->prev; \ new_node->next = self->_head; \ self->_head->prev->next = new_node; \ self->_head->prev = new_node; \ self->_size++; \ } \ static void list_pop_back(list *self) { \ list_node *removal = self->_head->prev; \ removal->prev->next = self->_head; \ self->_head->prev = removal->prev; \ free(removal); \ self->_size--; \ } \ static list *init_list() { \ list *obj = (list *)malloc(sizeof(list)); \ obj->_head = (list_node *)malloc(sizeof(list_node)); \ obj->_head->prev = obj->_head; \ obj->_head->next = obj->_head; \ obj->_size = 0; \ obj->push_back = list_push_back; \ obj->pop_back = list_pop_back; \ return obj; \ } \ static void free_list(list *self) { \ while (self->_size > 0) { \ self->pop_back(self); \ } \ free(self->_head); \ free(self); \ }#define CREATE_LIST(obj) list *obj = init_list();#define FREE_LIST(obj) free_list(obj);
DECLARE_LIST(int)
int main() { CREATE_LIST(l); for (int i = 0; i < 5; i++) { l->push_back(l, i); } assert(l->_size == 5); for (list_node *p = l->_head->next; p != l->_head; p = p->next) { printf("%d ", p->data); } printf("\n"); for (int i = 0; i < 5; i++) { l->pop_back(l); } assert(l->_size == 0); FREE_LIST(l);}代码由三个宏构成,DECLARE_LIST 定义链表,CREATE_LIST 创建链表对象,FREE_LIST 销毁对象。
但是这个代码有致命的缺陷:它只能定义一个类型的链表。
如果使用两次 DECLARE_LIST 会出现符号重定义的错误,我们还需要对它进行改造。
这里使用了一点宏的拼接技巧,解决了符号重定义的问题。
#define DECLARE_LIST(list, T) \ typedef struct list##_node { \ struct list##_node *prev, *next; \ T data; \ } list##_node; \ typedef struct list { \ list##_node *_head; \ size_t _size; \ void (*push_back)(struct list * self, int value); \ void (*pop_back)(struct list * self); \ } list; \ static void _##list##_push_back(list *self, int value) { \ list##_node *new_node = (list##_node *)malloc(sizeof(list##_node)); \ new_node->data = value; \ new_node->prev = self->_head->prev; \ new_node->next = self->_head; \ self->_head->prev->next = new_node; \ self->_head->prev = new_node; \ self->_size++; \ } \ static void _##list##_pop_back(list *self) { \ list##_node *removal = self->_head->prev; \ removal->prev->next = self->_head; \ self->_head->prev = removal->prev; \ free(removal); \ self->_size--; \ } \ static list *_##list##_init_list() { \ list *obj = (list *)malloc(sizeof(list)); \ obj->_head = (list##_node *)malloc(sizeof(list##_node)); \ obj->_head->prev = obj->_head; \ obj->_head->next = obj->_head; \ obj->_size = 0; \ obj->push_back = _##list##_push_back; \ obj->pop_back = _##list##_pop_back; \ return obj; \ } \ static void _##list##_free_list(list *self) { \ while (self->_size > 0) { \ self->pop_back(self); \ } \ free(self->_head); \ free(self); \ }#define CREATE_LIST(list, obj) list *obj = _##list##_init_list();#define FREE_LIST(list, obj) _##list##_free_list(obj);
DECLARE_LIST(flist, float)
int main() { CREATE_LIST(flist, l); for (int i = 0; i < 5; i++) { l->push_back(l, i); } assert(l->_size == 5); for (flist_node *p = l->_head->next; p != l->_head; p = p->next) { printf("%f ", p->data); } printf("\n"); for (int i = 0; i < 5; i++) { l->pop_back(l); } assert(l->_size == 0); FREE_LIST(flist, l);}现在我们就有了一个面向对象风格的泛型链表了。
更进一步
不妨趁着学数据结构的时候试着将常用的数据结构也用 C 语言实现一遍,这样在库函数少之又少的 C 语言里,我们也可以优雅的使用高级的数据结构了。
我的实现:wlonestar/libcoo
因为学业,没有足够的时间,目前只实现了一部分容器,供大家参考。
以上。