本文是C语言封装设计的第三篇文章,前两篇请见《C语言面向对象的封装方式》和《C语言面向对象的封装方式(示例)》。
本文介绍C语言中如何封装数据结构,让调用者可以引用这个数据结构,但无法获知这个数据结构的内部构成,也无法读写这个数据结构的成员变量。
解决这个问题,常见的手法是提供一个new_xxx函数,返回一个数据结构的指针。
比如,数据结构为dlist_t,那么在dlist.h和dlist.c中定义如下:
//dlist.h#ifndefDLIST_H#defineDLIST_Htypedefstructdlist_tdlist_t;dlist_t*new_dlist();intdlist_init(dlist_t*plist,uint64_tcapacity);intdlist_destroy(dlist_t*plist);intdlist_append(dlist_t*plist,void*pdata);//.....#endif
dlist.c中的源码为:
//dlist.c#include"dlist.h"#includestdlib.h#includestring.htypedefstructdlist_node_t{structdlist_node_t*pprev;structdlist_node_t*pnext;void*pdata;}dlist_node_t;structdlist_t{dlist_node_t*phead;dlist_node_t*ptail;uint64_tcapacity;//....};dlist_t*new_dlist(){dlist_t*plist=malloc(sizeof(dlist_t));if(!plist)returnNULL;memset(plist,0,sizeof(dlist_t));returnpplist;}intdlist_init(dlist_t*plist,uint64_tcapacity){//...}intdlist_destroy(dlist_t*plist){//...}intdlist_append(dlist_t*plist,void*pdata){//...}//....
这种方法,能够让调用者使用dlist_t这个数据结构,但无法获知这个数据结构的内部构成,也无法读写这个数据结构的成员变量。
但这种方法的不足在于,这个数据结构不能在栈上分配,并且这个对象只能从堆中分配,无法从调用者的内存池中分配。
因此,我们对上面这种方法进行改进,彻底解决这些不足。先探讨一下解决的思路:
要想能够在栈上声明这个数据结构变量,那么这个数据结构必须对调用者完全可见,并且在源码编译过程中就能确定这个对象的尺寸大小。但违背了我们的需求:向调用者隐藏数据结构的内部实现。
因此,要完美解决这些问题,我们需要提供两种数据结构,一种对调用者可见,但没有数据结构的内部实现;另一种对调用者不可见,这是真正的数据结构,有完整的内部实现细节。这两种数据结构的大小完全相同,因此可以相互转化。
具体的做法是:对调用者可见的数据结构,是个char数组,数组的大小为真正数据结构的大小。提供对应的宏,封装这些细节。
我们用这个思路,改造一下上面的代码:
dlist.h中的源码为:
#ifndefDLIST_H#defineDLIST_H#includestdint.h#includestdlib.htypedefstructdlist_tdlist_t;//在栈上声明一个dlist_t结构,varptr_name为指向这个结构的指针#defineDLIST_VAR(varptr_name)DLIST_VAR2(varptr_name,__LINE__)//通过malloc申请一个dlist_t,arptr_name为指向这个结构的指针#defineDLIST_NEW(varptr_name)dlist_t*varptr_name=(dlist_t*)malloc(DLIST_SIZE);//在其它struct中,声明一个dlist_t结构的成员变量#defineDLIST_FIELD_DEF(varname)charvarname[DLIST_SIZE];//引用结构中的dlist_t成员,返回dlist_t*的指针#defineDLIST_FIELD(full_varname)((dlist_t*)(full_varname))//forinternaluse!#defineDLIST_SIZE24#defineDLIST_VAR2(varptr_name,n)\charvarptr_name##n[DLIST_SIZE];\dlist_t*varptr_name=(dlist_t*)varptr_name##n;//public接口函数intdlist_init(dlist_t*plist,uint64_tcapacity);intdlist_destroy(dlist_t*plist);intdlist_append(dlist_t*plist,void*pdata);//.....#endif
dlist.c中的源码为:
//dlist.c#include"dlist.h"#includestring.h#includeassert.h#includestdio.htypedefstructdlist_node_t{structdlist_node_t*pprev;structdlist_node_t*pnext;void*pdata;}dlist_node_t;structdlist_t{dlist_node_t*phead;dlist_node_t*ptail;uint64_tcapacity;//....};intdlist_init(dlist_t*plist,uint64_tcapacity){//printf("sizeof(dlist_t)=%d\n",sizeof(dlist_t));assert(DLIST_SIZE==sizeof(dlist_t));memset(plist,0,sizeof(dlist_t));plist-capacity=capacity;return0;}intdlist_destroy(dlist_t*plist){return0;}intdlist_append(dlist_t*plist,void*pdata){return0;}
我们写一个测试程序,测试一下各种使用场景:
//test_dlist.c#include"dlist.h"typedefstructqueue_t{intlen;intcap;DLIST_FIELD_DEF(list)}queue_t;intmain(){DLIST_VAR(plist)dlist_init(plist,);dlist_destroy(plist);DLIST_NEW(plist2)dlist_init(plist2,);free(plist2);queue_tq;dlist_init(DLIST_FIELD(q.list),);return0;}
通过这种方法,我们就可以实现数据结构的完美封装,调用者可以使用这个数据结构,但不能读写内部成员,并且这个对象可以在栈、堆或用户自己的内存池中分配。
我的