一、前言
封装(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; /*用来标志栈顶位置*/
}
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到底是什么东西,也无法通过->或*这两个操作符来获取它内部的内容
用户只能按照我们规定好的一套接口:
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语言呢?