引言
封装即隐藏属性和实现细节,对外提供有限的公开接口(方法),限制程序对属性的读写和访问级别。封装的意义在于将数据和方法进行有机绑定,而不是分割看待。C++和Java通过private, protocted, public三个关键字控制访问级别,protocted需要语言级别的支持,C语言不支持。所以这里仅讨论C语言的private和public访问控制。
数据和方法的绑定
C语言虽没有C++和Java的类特性,但它有struct和指针,我们可借助struct和指针完成数据和方法的绑定。
// file.h 对外提供的接口头文件
typedef struct tagFile
{
FILE* fp;
void (*open)(struct tagFile* self, const char* fileName);
int (*isOpen)(struct tagFile* self);
void (*close)(struct tagFile* self);
void (*seek)(struct tagFile* self, long offset, int from);
size_t (*read)(struct tagFile* self, void *buffer, size_t size, size_t count);
size_t (*write)(struct tagFile* self, const void * ptr, size_t size, size_t count);
} File;
File* newFile();
void deleteFile(File* file);
我们以File接口为例,阐述基于struct的接口封装实现细节。实现File接口的封装,我们首先通过struct实现文件操作的封装,将文件的句柄FILE*和文件的操作(open,isOpen,close等)进行关联绑定;然后通过newFile和deleteFile方法实现对象的创建和释放。在File接口封装中,对象操作接口open等的第一个参数为self表示实施操作的对象自身,这是借鉴了python的类定义实现方式。有兴趣的同学可以阅读相关python书籍。
// file.c 接口的实现
static void open(struct tagFile* self, const char* fileName)
{
FILE* fp = NULL;
if (0 == fopen_s(&fp, fileName, "w"))
{
self->fp = fp;
return;
}
self->fp = NULL;
return;
}
static int isOpen(struct tagFile* self)
{
if (NULL != self->fp)
{
return 0;
}
return -1;
}
static void close(struct tagFile* self)
{
if (NULL != self->fp)
{
fclose(self->fp);
}
self->fp = NULL;
}
static void seek(struct tagFile* self, long offset, int from)
{
if (NULL != self->fp)
{
fseek(self->fp, offset, from);
}
}
static size_t read(struct tagFile* self, void *ptr, size_t size, size_t count)
{
if (NULL != self->fp)
{
size_t ret = fread(ptr, size, count, self->fp);
return ret;
}
return 0;
}
static size_t write(struct tagFile* self, const void * ptr, size_t size, size_t count)
{
if (NULL != self->fp)
{
size_t ret = write(ptr, size, count, self->fp);
}
return 0;
}
File* newFile()
{
File* file = (File*)malloc(sizeof(File));
if (NULL != file)
{
file->fp = NULL;
file->open = open;
file->close = close;
file->seek = seek;
file->read = read;
file->write = write;
file->isOpen = isOpen;
}
return file;
}
void deleteFile(File * file)
{
if (NULL != file)
{
free(file);
}
file = NULL;
}
采用此种方式进行面向对象封装,对象的创建和释放是面向对象实现的关键,此例中,newFile负责File对象的创建,在创建的具体实现中,open,close等函数指针的赋值是实现数据和操作方法绑定的关键所在,file->open = open就是为File对象指定其对应的open实现,而open指针和fp在同一个struct中,这自然而然的就实现了数据和操作方法的绑定了;deleteFile负责申请对象的释放操作。
实现了数据和方法的绑定,我们就可以使用类似C++调用方式进行编程了,请大家参考:
// 面向对象的编程
File* file = newFile();
if (NULL != file)
{
file->open(file, "a.txt");
if (0 == file->isOpen(file))
{
file->write(file, "abc....", sizeof("abc...."), 1);
char buffer[512] = { 0 };
file->read(file, buffer, 5, 1);
file->close(file);
}
deleteFile(file);
file = NULL;
}
数据的隐藏
通过struct,我们实现了数据和方法绑定,但是你发现没有?struct tagFile中fp外部可见,如此以来struct tagFile中的数据细节也就变成的公开,这不是我们所期望的,我们真正期望的是外部不知道struct tagFile中的任何数据细节,即实现数据的隐藏,所以我们要做的就是限制外部对struct tagFile中fp查看和访问。这就是本部分讨论的数据隐藏。
幸运的是C语言提供了前置声明的语法规则,我们可以借助前置声明实现数据隐藏。这里通过Pimpl前置声明惯用手法实现数据的隐藏。Plmp的缩写就是Pointer to Implementor,顾名思义就是将真正的实现细节的Implementor从struct定义的头文件中分离出去,公有接口通过一个指针指向隐藏的实现,是促进接口和实现分离的重要机制。
struct PrivateFileDesc;
typedef struct tagFile
{
struct PrivateFileDesc* fileDesc;
void (*open)(struct tagFile* self, const char* fileName);
int (*isOpen)(struct tagFile* self);
void (*close)(struct tagFile* self);
void (*seek)(struct tagFile* self, long offset, int from);
size_t (*read)(struct tagFile* self, void *buffer, size_t size, size_t count);
size_t (*write)(struct tagFile* self, const void * ptr, size_t size, size_t count);
} File;
采用Pimpl,我们对File接口重新封装,即可实现fp的数据隐藏。Pimpl中的FileDesc仅仅是一个指向实现的指针,我们仅知道FileDesc是一个文件描述的struct,而无法感知具体包含哪些详细的数据。这样就完美的实现数据的隐藏。
struct PrivateFileDesc
{
FILE* fp
};
static void open(struct tagFile* self, const char* fileName)
{
FILE* fp = NULL;
if (0 == fopen_s(&fp, fileName, "w"))
{
if (NULL != self->fileDesc)
{
self->fileDesc->fp = fp;
}
return;
}
self->fileDesc->fp = NULL;
return;
}
static int isOpen(struct tagFile* self)
{
if ((NULL != self->fileDesc) && (NULL != self->fileDesc->fp))
{
return 0;
}
return -1;
}
static void close(struct tagFile* self)
{
if ((NULL != self->fileDesc) && (NULL != self->fileDesc->fp))
{
fclose(self->fileDesc->fp);
self->fileDesc->fp = NULL;
}
}
static void seek(struct tagFile* self, long offset, int from)
{
if ((NULL != self->fileDesc) && (NULL != self->fileDesc->fp))
{
fseek(self->fileDesc->fp, offset, from);
}
}
static size_t read(struct tagFile* self, void *ptr, size_t size, size_t count)
{
if ((NULL != self->fileDesc) && (NULL != self->fileDesc->fp))
{
size_t ret = fread(ptr, size, count, self->fileDesc->fp);
return ret;
}
return 0;
}
static size_t write(struct tagFile* self, const void * ptr, size_t size, size_t count)
{
if ((NULL != self->fileDesc) && (NULL != self->fileDesc->fp))
{
size_t ret = write(ptr, size, count, self->fileDesc->fp);
}
return 0;
}
File* newFile()
{
File* file = (File*)malloc(sizeof(File));
if (NULL != file)
{
file->fileDesc = (struct PrivateFileDesc*) malloc(sizeof(struct PrivateFileDesc));
file->fileDesc->fp = NULL;
file->open = open;
file->close = close;
file->seek = seek;
file->read = read;
file->write = write;
file->isOpen = isOpen;
}
return file;
}
void deleteFile(File * file)
{
if (NULL != file)
{
if (NULL != file->fileDesc)
{
free(file->fileDesc);
}
file->fileDesc = NULL;
free(file);
}
file = NULL;
}
方法的隐藏
不仅数据需要隐藏,函数方法同样需要隐藏。因为接口中有些函数方法仅仅是为其他共有接口服务的,没有对外暴露的必要,而且暴露了可能会引起或存在其他不必要的隐患。
对函数方法的隐藏,可以借鉴数据的隐藏,我们这里也采用Pimpl前置声明惯用手法。有所不同的是数据隐藏前置声明struct FileDesc的实现包含的是一个fp数据,而方法隐藏时,声明的实现包含的就是一个个函数指针了。
struct PrivateFileDesc;
struct PrivateFuncs;
typedef struct tagFile
{
struct PrivateFileDesc* fileDesc;
struct PrivateFuncs* funcs;
void (*open)(struct tagFile* self, const char* fileName);
void (*close)(struct tagFile* self);
void (*seek)(struct tagFile* self, long offset, int from);
size_t (*read)(struct tagFile* self, void *buffer, size_t size, size_t count);
size_t (*write)(struct tagFile* self, const void * ptr, size_t size, size_t count);
} File;
File* newFile();
void deleteFile(File* file);
采用Pimpl对需要隐藏的函数重新封装,isOpen由外部可见的共有接口变成内部可见的私有接口,外部就无法感知isOpen的存在了,可见的仅是一个struct PrivateFuncs。具体可参考下述实现。
struct PrivateFileDesc
{
FILE* fp
};
struct PrivateFuncs
{
int (*isOpen)(struct tagFile* self)
};
static void open(struct tagFile* self, const char* fileName)
{
if (self->funcs->isOpen(self))
{
return;
}
FILE* fp = NULL;
if (0 == fopen_s(&fp, fileName, "w"))
{
if (NULL != self->fileDesc)
{
self->fileDesc->fp = fp;
}
return;
}
self->fileDesc->fp = NULL;
return;
}
static int isOpen(struct tagFile* self)
{
if ((NULL != self->fileDesc) && (NULL != self->fileDesc->fp))
{
return 0;
}
return -1;
}
static void close(struct tagFile* self)
{
if ((NULL != self->fileDesc) && (NULL != self->fileDesc->fp))
{
fclose(self->fileDesc->fp);
self->fileDesc->fp = NULL;
}
}
static void seek(struct tagFile* self, long offset, int from)
{
if ((NULL != self->fileDesc) && (NULL != self->fileDesc->fp))
{
fseek(self->fileDesc->fp, offset, from);
}
}
static size_t read(struct tagFile* self, void *ptr, size_t size, size_t count)
{
if ((NULL != self->fileDesc) && (NULL != self->fileDesc->fp))
{
size_t ret = fread(ptr, size, count, self->fileDesc->fp);
return ret;
}
return 0;
}
static size_t write(struct tagFile* self, const void * ptr, size_t size, size_t count)
{
if ((NULL != self->fileDesc) && (NULL != self->fileDesc->fp))
{
size_t ret = write(ptr, size, count, self->fileDesc->fp);
}
return 0;
}
File* newFile()
{
File* file = (File*)malloc(sizeof(File));
if (NULL != file)
{
file->fileDesc = (struct FileDesc*) malloc(sizeof(struct PrivateFileDesc));
file->fileDesc->fp = NULL;
file->funcs = (struct PrivateFuncs*) malloc(sizeof(struct PrivateFuncs));
file->funcs->isOpen = isOpen;
file->open = open;
file->close = close;
file->seek = seek;
file->read = read;
file->write = write;
}
return file;
}
void deleteFile(File * file)
{
if (NULL != file)
{
if (NULL != file->fileDesc)
{
free(file->fileDesc);
}
file->fileDesc = NULL;
if (NULL != file->funcs)
{
free(file->funcs);
}
free(file);
}
file = NULL;
}
总结
封装的目的包括两个层次:第一个层次是数据和方法的绑定,第二个层次是隐藏属性和实现细节。本博客从封装的概念入手,详细介绍了基于C语言的封装实现细节。