C语言:对象封装

650 阅读7分钟

引言

封装即隐藏属性和实现细节,对外提供有限的公开接口(方法),限制程序对属性的读写和访问级别。封装的意义在于将数据和方法进行有机绑定,而不是分割看待。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语言的封装实现细节。