C语言抽象数据类型及不透明指针

一、前言

封装(Encapsulation)是一个在现代程序设计里无处不在的手段。对过程的封装,我们称其为函数(Function),对某个对象的属性及行为的封装我们称其为(Class)。很多高级程序设计语言都提供了足够的特性来支持封装。

封装的目的是信息隐藏(Information Hiding),这也是操作系统及计算机体系结构领域经常提到的最小特权原则(Least Privilege Principle)的一种特殊体现,它规定了程序的目标用户只应该知道他所需要的信息,对于他不该知道的信息或没有必要知道的信息,多一点儿也不让他知道。许多面向对象的程序设计语言都提供了较好的信息隐藏的手段,例如C++,JAVA。他们通过在类(Class)中,对访问权限进行限制,以公有(Public)私有(Private)等标签规定了一套访问规则(当然,还有protected),从而实现了信息隐藏。

在实践中,我们也把上面的技术称为接口(Interface)实现(Implementation)的分离,用户看到的(或者说,能访问的,能调用的……)是接口,而用户看不见的,在接口之下默默运行着的是实现。

利用这样一套机制,我们可以极大地丰富程序设计语言的语义,其方法就是编写许多完备的用户自定义类型,或者说抽象数据类型。而这些抽象数据类型有着这样的特点:它们的含义远比内置类型(比如int,float,char……),而在使用的时候却又应该与内置类型一样方便。它们有着自己的属性与行为,比如一个栈(Stack),可以有栈顶(Top),长度(Length)等属性,可以有压栈(Push),出栈(Pop)等操作。


二、C语言的抽象数据类型(Abstract Data Type,ADT)

那么,在C语言这样的面向过程的程序设计语言中,我们怎么来封装自己的抽象数据类型呢?我们知道,C语言提供了struct关键字,可以让我们将一组各种类型的集合打包成一种新的类型。比如,对于刚才所提到的栈(Stack)类型:


一个简单的用来存储整型的栈:

struct stack1
{
int buf[10];    /*用来存储元素缓冲区,长度为10*/
int top;        /*用来标志栈顶位置*/
};
另一个简单的用来存储任意类型的栈:

struct stack2
{
	void * buf[10];      /*通用指针数组,它包含10个这样的元素,每一个元素是一个void*,即一个通用指针,这些指针指向最终所存储的元素*/
	int element_size;    /*每个元素的字节大小*/
	int top;             /*用来标志栈顶位置*/
}

然而这样的设计并不完美,因为它暴露了所有的实现细节,所以我用的是“打包”而不是“封装”。因为C语言没有提供对于访问权限的控制,我们没有Public、Private等关键字,因而可以任意地,甚至非法地来访问这些属性。例如,引用上面那个关于栈的例子,我们可以这么干:

stack1 my_stack;
my_stack.buf[10] = 100;    /*非法,buf的下标范围为0至9*/
my_stack.top = -10000      /*合法,但无意义,通常top的范围可能是-1至9,-1表示栈为空*/

所以,这里的”抽象数据类型“并不抽象,你都能看到它里面的构造了,你说它还抽象吗?


三、不透明指针(Opaque Pointer)

然而,C语言确确实实有一个特性可以帮助我们实现真正的ADT,那就是不透明指针。C语言有这样一个特性,它允许我们在给出一个结构体(struct)的定义之前,先声明指向它的指针(Pointer),例如下面的代码:(以下代码均在VS2010及GCC 4.8.3上通过了测试)

int main (int argc, char * argv[])
{
	struct some_struct * p;    /*p是一个指向struct some_struct的指针,但我们并不知道struct some_struct是什么,它都有哪些成员*/
	return 0;
} 
该段代码可以顺利地通过编译,说明这样的声明确实是被允许的。然而,当我们继续,进行解引用的时候:
int main (int argc, char * argv[])
{
	struct some_struct * p;   /*p是一个指向struct some_struct的指针,但我们并不知道struct some_struct是什么,它都有哪些成员*/
	p->some_attribute;        /*现在我们对p解引用,希望获得它的some_attribute这个属性*/
	return 0;
}
这时编译器会向我们报错: error: dereferencing pointer to incomplete type (错误:对指向不完整类型的指针进行解引用)

现在好了,用户看不见struct内部的具体细节了。

四、用不透明指针来实现一个ADT

也许你会问:是啊,用户看不见了,可我们编码的人也看不见了,因为我们也不知道那个struct some_struct是什么东西啊?

我刚才说,“在给出一个结构体的定义之前,先声明指向它的指针”,但并不代表我没有定义这个结构体,只是定义在了其他的地方(其他的文件)中。这也是符合将接口与实现分离原则的,所以现在我们按照这个原则,来编写一个下这套接口

接口,通常以头文件的形式声明,它不做定义(definition),只做声明(declaration)

以下为文件"some_struct.h"的内容:

#ifndef _SOME_STRUCT_H_
#define _SOME_STRUCT_H_


typedef struct some_struct * SOME_STRUCT_T;     /*声明一种新的类型叫做SOME_STRUCT_T,它是一个指向struct some_struct的指针*/


extern SOME_STRUCT_T new_some_struct(void);     /*声明一个函数,它创建并初始化一个SOME_STRUCT_T类型的变量*/
extern int get_num (SOME_STRUCT_T s);           /*声明一个函数,它获得某个SOME_STRUCT_T类型变量的num属性*/
extern char * get_str (SOME_STRUCT_T s);        /*声明一个函数,它获得某个SOME_STRUCT_T类型变量的str属性*/

#endif

实现,通常是一个与接口头文件同名的源文件,它对头文件声明的接口进行了实现

以下为文件"some_struct.c"的内容:

#include "some_struct.h"
#include 

struct some_struct                               /*定义了结构体some_struct的具体内容*/
{
	int num;
	char * str;
};

SOME_STRUCT_T new_some_struct(void)              /*函数new_some_struct的具体实现*/
{
	SOME_STRUCT_T new_ss = (SOME_STRUCT_T) malloc (sizeof(*new_ss));    /*为新创建的SOME_STRUCT_T变量分配内存*/
	new_ss->num = 2014;                                                 
	new_ss->str = "Welcome to 2014, and get ready for 2015";
	return new_ss;
}

int get_num (SOME_STRUCT_T s)                    /*函数get_num的具体实现*/
{
	return s->num;
}

char * get_str (SOME_STRUCT_T s)                 /*函数get_str的具体实现*/
{
	return s->str;
}

现在我们可以像这样来使用这套接口了

以下为文件"main.c"的内容:

#include 
#include "some_struct.h"

int main (int argc, char * argv[])
{
	SOME_STRUCT_T s = new_some_struct();      //声明并初始化一个SOME_STRUCT_T类型的变量
	printf ("%d\n", get_num(s));              //打印:2014
	printf ("%s\n", get_str(s));              //打印:Welcome to 2014, and get ready for 2015
	return 0;
} 

好了,现在我们成功地编写了一个名为SOME_STRUCT_T的 抽象数据类型(ADT)以及用户与它交互的一套 接口(Interface)

用户既不知道SOME_STRUCT_T到底是什么东西,也无法通过->或*这两个操作符来获取它内部的内容

用户只能按照我们规定好的一套接口:

new_some_struct()

get_num()

get_str()

来与SOME_STRUCT_T类型的变量进行交互


五、总结

在编程的过程中,我们无时无刻不在和抽象数据类型(ADT)打交道,一个Socket,一个MP4格式的视频,一个哈希表,都是ADT。使用不透明指针(Opaque Pointer)可以让我们编写更易用、更高效的ADT。通过良好的封装及复用,减少开发的工作量,同时提升代码的质量。

除去继承(Inheritance)之外,利用上述方法编写的ADT已经能够帮助我们实现一些基本的面向对象的特性,虽然使用起来并没有C++那么方便,但已经能够极大地丰富C语言的语义。C语言的简单本身就是它的优势之一。当我们在学习C++的时候,从我们知道class,public,private,protected,virtual,template这些关键词的作用,到我们真正理解这些关键字的含义之间还有相当长的一段路要走,对C++的误用远比对C语言的误用要可怕的多。在我们能够确定自己并不需要那么复杂的设计的时候,何不考虑一下简单而高效的C语言呢?

你可能感兴趣的:(C语言)