对数据类型封装和数据抽象的简单理解
请特别关注程序设计技术,而不是各种语言特征。
--《C++程序设计语言》 Bjarne Stroustrup
本文是《C++程序设计语言》(Bjarne Stroustrup )的第二章的读书笔记,例子来源于这本书的第二章。
在程序设计之中,我们倾向于将数据结构(也可以说是数据类型)以及一组对其操作的相关过程组织在一起,在逻辑上可以称将其为模块。此时程序分为一些模块,模块包括一组对数据的操作,数据隐藏于模块之中。以下以栈的设计为例,使用C和C++进行设计,简单理解模块化设计中的数据封装和数据抽象。
对于C 语言我们可以对栈这一数据类型进行如下简单的设计:
typedef struct Stack { int elem[MAX_SIZE]; int top; } Stack; Stack* createStack(); void destroyStack(Stack*); void push(Stack*,int); int pop(Stack*);
由于struct结构可以自由的有外界进行访问,因此可能对数据进行破坏。我们可以引入类似于win32编程使用到的句柄的设计。即:
typedef void* HStack; HStack createStack(); void destroyStack(HStack hStack); void push(HStack hStatck,int) { struct Stack* stack = (struct Stack*)hStack; ........ } int pop(HStack hStack) { struct Stack* stack = (struct Stack*)hStack; ........ }
只有在实现该数据类型相关的操作,才知道Stack的内在结构,可以进行强制到转换。因此该模块的用户无需知道struct Stack的结构,改变struct Stack结构也不会影响到API用户的使用。
客户端代码:
void f() { HStack h= createStack(); push(h,2); int i = pop(h); destroyStack(h); }
因此C语言在进行类似的数据类型封装时,通常提供的一组操作中通常包含了初始化和回收的操作,通常需要用户有意识的进行调用。这让这一数据类型有点“伪类型”的感觉,C语言的特征无法让封装的数据类型进行自动的初始化和销毁。
C++提供类这一用户自定义类型对数据类型封装进行支持。在进行程序设计时,通过确定需要哪些类型,为每个类型提供完整的操作。对栈的定义如下:
class Stack { public: Stack(int max_size); ~Stack(); void push(int); int push(); private: int* m_elem; int m_top; int m_max_size; };
构造函数Stack(int) 在建立这个类的对象时被调用,处理初始化问题。如果该类的一个对象出了其作用域,进行某些清理的时候,通过调用其析构函数。
像上面的Stack这种类型的定义,我们可以称为就具体类型,涉及到具体的实现。在类型不常改变,或者类型用于局部变量的情况下,这种设计方法足够解决问题。在一些情况下我们希望有抽象类型。抽象类型可以将用户与实现细节隔离,得到更好的灵活性。此时用户面向抽象类型编程,而不是面向具体类型编程。 C++可以通过类的继承和抽象类实现这一方式。如:
class Stack { public: virtual void push(int) = 0; virtual int pop(int) = 0; }; // 使用数组实现栈 class ArrayStack : public Stack {}; // 使用链表实现栈 class ListStack : public Stack{}; // 用户面向抽象类编程: void f(Stack& s) { s.push(2); int i = s.pop(); }
通过定义Stack这一抽象类(在C++中可以理解为具有纯虚成员函数的类。 如:virtual void push(int) = 0;),为这一抽象类提供不同实现获得灵活性,用户面向这一抽象类编程(在C++中,通常是该类型对象的引用或者指针,才能实现多态)。