本文是 C 语言封装设计的第三篇文章,前两篇请见《C 语言面向对象的封装方式》和《 C语言面向对象的封装方式(示例)》。
本文介绍 C 语言中如何封装数据结构,让调用者可以引用这个数据结构,但无法获知这个数据结构的内部构成,也无法读写这个数据结构的成员变量。
解决这个问题,常见的手法是提供一个 new_xxx 函数,返回一个数据结构的指针。
比如,数据结构为 dlist_t, 那么在 dlist.h 和 dlist.c 中定义如下:
// dlist.h
#ifndef DLIST_H#define DLIST_H
typedef struct dlist_t dlist_t;
dlist_t* new_dlist();
int dlist_init(dlist_t* plist, uint64_t capacity);int dlist_destroy(dlist_t* plist);
int dlist_append(dlist_t* plist, void* pdata);// .....
#endif
复制代码
dlist.c 中的源码为:
// dlist.c
#include "dlist.h"
#include <stdlib.h>
#include <string.h>
typedef struct dlist_node_t {
struct dlist_node_t* pprev;
struct dlist_node_t* pnext;
void* pdata;
} dlist_node_t;
struct dlist_t {
dlist_node_t* phead;
dlist_node_t* ptail;
uint64_t capacity;
// ....
};
dlist_t* new_dlist() {
dlist_t* plist = malloc(sizeof(dlist_t));
if (!plist) return NULL;
memset(plist, 0, sizeof(dlist_t));
return pplist;
}
int dlist_init(dlist_t* plist, uint64_t capacity) {
// ...
}
int dlist_destroy(dlist_t* plist) {
// ...
}
int dlist_append(dlist_t* plist, void* pdata) {
// ...
}
// ....
复制代码
这种方法,能够让调用者使用 dlist_t 这个数据结构,但无法获知这个数据结构的内部构成,也无法读写这个数据结构的成员变量。
但这种方法的不足在于,这个数据结构不能在栈上分配,并且这个对象只能从堆中分配,无法从调用者的内存池中分配。
因此,我们对上面这种方法进行改进,彻底解决这些不足。先探讨一下解决的思路:
要想能够在栈上声明这个数据结构变量,那么这个数据结构必须对调用者完全可见,并且在源码编译过程中就能确定这个对象的尺寸大小。但违背了我们的需求:向调用者隐藏数据结构的内部实现。
因此,要完美解决这些问题,我们需要提供两种数据结构,一种对调用者可见,但没有数据结构的内部实现;另一种对调用者不可见,这是真正的数据结构,有完整的内部实现细节。这两种数据结构的大小完全相同,因此可以相互转化。
具体的做法是:对调用者可见的数据结构,是个 char 数组,数组的大小为真正数据结构的大小。提供对应的宏,封装这些细节。
我们用这个思路,改造一下上面的代码:
dlist.h 中的源码为:
#ifndef DLIST_H
#define DLIST_H
#include <stdint.h>
#include <stdlib.h>
typedef struct dlist_t dlist_t;
// 在栈上声明一个dlist_t结构,varptr_name 为指向这个结构的指针
#define DLIST_VAR(varptr_name) DLIST_VAR2(varptr_name, __LINE__)
// 通过malloc 申请一个 dlist_t,arptr_name 为指向这个结构的指针
#define DLIST_NEW(varptr_name) dlist_t* varptr_name = (dlist_t*)malloc(DLIST_SIZE);
// 在其它struct 中,声明一个dlist_t结构的成员变量
#define DLIST_FIELD_DEF(varname) char varname[DLIST_SIZE];
// 引用结构中的dlist_t成员,返回dlist_t*的指针
#define DLIST_FIELD(full_varname) ((dlist_t*)(full_varname))
// for internal use!
#define DLIST_SIZE 24
#define DLIST_VAR2(varptr_name, n) \
char varptr_name ## n [DLIST_SIZE]; \
dlist_t* varptr_name = (dlist_t*)varptr_name ## n;
// public 接口函数
int dlist_init(dlist_t* plist, uint64_t capacity);
int dlist_destroy(dlist_t* plist);
int dlist_append(dlist_t* plist, void* pdata);
// .....
#endif
复制代码
dlist.c 中的源码为:
// dlist.c
#include "dlist.h"
#include <string.h>
#include <assert.h>
#include <stdio.h>
typedef struct dlist_node_t {
struct dlist_node_t* pprev;
struct dlist_node_t* pnext;
void* pdata;
} dlist_node_t;
struct dlist_t {
dlist_node_t* phead;
dlist_node_t* ptail;
uint64_t capacity;
// ....
};
int dlist_init(dlist_t* plist, uint64_t capacity) {
//printf("sizeof(dlist_t) = %d\n", sizeof(dlist_t));
assert(DLIST_SIZE == sizeof(dlist_t));
memset(plist, 0, sizeof(dlist_t));
plist->capacity = capacity;
return 0;
}
int dlist_destroy(dlist_t* plist) {
return 0;
}
int dlist_append(dlist_t* plist, void* pdata) {
return 0;
}
复制代码
我们写一个测试程序,测试一下各种使用场景:
// test_dlist.c
#include "dlist.h"
typedef struct queue_t {
int len;
int cap;
DLIST_FIELD_DEF(list)
} queue_t;
int main() {
DLIST_VAR(plist)
dlist_init(plist, 1024);
dlist_destroy(plist);
DLIST_NEW(plist2)
dlist_init(plist2, 1024);
free(plist2);
queue_t q;
dlist_init(DLIST_FIELD(q.list), 1024);
return 0;
}
复制代码
通过这种方法,我们就可以实现数据结构的完美封装,调用者可以使用这个数据结构,但不能读写内部成员,并且这个对象可以在栈、堆或用户自己的内存池中分配。
我的微信号 "实力程序员",欢迎大家关注我。
评论