面向对象

来源:互联网 发布:mysql desc asc 编辑:程序博客网 时间:2024/05/09 06:38

面向对象程序设计的重点在于对象,是对象构成了程序,而不是函数和数据。一个对象可以表达真实生活中的某个概念,诸如汽车或太阳系,也可以表达更为抽象的东西,像数字栈或者排序引擎等。每一个对象都有一套定义明确的能力,如一辆汽车可以启动、停止和转弯,而一个栈则可以推入或者弹出数据等。

OOP是由创建这些对象并使得他们协调工作而构成的。你可以使用OOP创建一个完全实用的程序,也可以提供一个对象的集合以向别人提供服务。你所创建的对象的用户(或客户)可以是你自己,也可以是你公司的其他人,或者是你永远也不会见到的某个人。

要使用对象进行编程并不需要OOP语言,使用C同样可以做到这一点。但是像c++这样的语言会使OOP更容易一些,因为他们直接支持对象的创建和使用。更正式地说,OOP语言通常支持一些关键的特性。每一个对象方法都有自己的一套特性集合,他们稍有不同。下面是中要的几点:

1. 抽象性(Abstraction)

抽象性就是为对象创建一个定义明确的接口。正确的抽象机制是将对象的实现和它的接口分离开。在C中,抽象性包括把一个数据机构和函数接口进行打包。

例如我们需要一个整数栈,这个栈的大小具有固定的最大值。如果我们不在乎抽象性的话,可以使用一个数组直接访问它:

    main ()

{

int stack_items[STACKSIZE], stack_top = 0, x;

/*.....*/

stack_intems[stack_top++] = x;/*push onto the stack*/

/*.....*/

x = stack_intems[--stack_top];/*pop off the stack*/

}

这里stack_items 是一个数组,保存了一个整数栈,栈顶元素有stack_top表示。我们将数值X推入栈中,然后再从栈中弹出一个值,并将它赋给X。

抽象的方法会在栈的实现代码和使用栈的代码之间添加一个函数层:

void init (Stack *s);/*initialize the stack*/

void push (Stack *s, int i);/*push an element onto the stack*/

int pop (Stack *s);/*Pop an element off the stack*/

void cleanup (Stack *s);/*clean up the stack when you are done with it*/

这些函数为我们的栈定义了四个功能。函数init()建立了一个新的空栈。函数push()将一个元素推入栈顶; 函数pop()则将栈顶元素弹出并返回它的值;当我们使用完栈并且它可以被删除时,就调用cleanup()函数。这些函数共同为栈创建了接口。每个函数都以Stack* 作为它的第一个参数。Stack容纳了栈所需要的数据,

声明了函数接口后,我们可以实现下面的数据和代码:

typedef struct 

{

int items[STACKSIZE];

init top;

}Stack;

void init (Stack* s) {s->top = 0;}

void push (Stack* s, int i) {s->item[s->top++] = i};

int pop (Stack* s, int i) {return s->items[--s->top;]}

void cleanup (Stack* s) {/* nothing for static stack */}

结构Stack的一个实例保留了栈的数据,这个Stack实例的用户将它传递给了每一个接口函数。注意push()和pop()访问栈中项目的方法和前面非抽象性的版本中的方法一样。

我们无论何时访问栈都使用函数接口:

main ()

{

int x;

Stack stack1;

/*......*/

init (&stack1);

/*......*/

push (&stack1, x);

/*......*/

x = pop(&stack1);

/*......*/

cleanup (&stack1);

}

这个例子说明了如何调用函数接口来代替对数据直接进行的操作。

抽象机制的一个优点就是对操作数据的代码局部化了。如果我们需要修改处理数据的方法,只需要在一个地方进行更改就可以了,而不必修改程序中所有直接访问数据的地方。

例如,起初我们可能要求栈大小不会超过某个值,所以我们只需像上面例子中那样静态分配内存。现在要求改变了,要求我们在必要的时候增加数组的大小。如果我们使用的是抽象机制(像我们在例子中所做的那样),这样的改变时轻而易举的,只需要将数组类型从静态的改动为动态的即可。函数cleanup()会释放动态数组。如果我们没有使用抽象机制,就需要找到所有访问数组的地方,然后更改这些代码,这样就显得很凌乱。


2. 封装性(Encapsulation)

封装性是指保持抽象机制实现细节的专有性。前一节解释过,抽象机制要求你认真考虑对象的接口。决定用户应该看到什么的另一个方面,是要确保用户看到的东西即是他能看到的全部。正确的封装机制不仅鼓励而且强迫隐藏实现细节。它是的你的代码更可靠,而且更容易维护,因为你清除地知道自己所实现的抽象机制将做些什么。

封装有很多形式。在某些语言中,是不能访问内部细节的。而在另一些语言中则允许你访问专有的信息,但是要进行额外的访问时很困难的。

在C中,你需要将细节隐藏在一个分离的文件中才能保持它的专有性。我们可以将Stack接口函数的实现细节放在自己的文件中:

/*in stack.h: only the declarations (stack user sees these)*/

void init (Stack* s);

void push (Stack *s, int i);

int pop (Stack *s);

void cleanup (Stack *s);

/*in stack.c: the definitions (stack user never sees these)*/

void init (Stack* s) { s->top = 0}

/*.......*/

这里,文件stack.h仅仅包含了Stack的接口函数的声明,而在stack.c中对他们进行定义,Stack的用户是看不到这个文件的。而且,我们可以在这个文件中将Stack独自需要的函数声明为static型。

隐藏Stack的数据对技巧的要求更高。我们可以使用一个栈标示符(ID), 并且在一个分离的代码文件中存储实际的数据:

/*in stack.h: the user sees only an ID*/

typedef int Stack;

/*in stack.c: we store the stacks by stack ID*/

struct 

{

int items[STACKSIZE];

int top;

}stacks[NUMSTACKS];

这里Stack实际上只是一个整数,在Stack的代码文件中,这个整数是数组stacks的索引值,实际的数据存储在数组中。显然,这种方法仅仅是为了隐藏代码的实现就需要做很多工作。C++对这种封装性的原始形式做了很大的改进。

3. 层次性(Hierarchy)

我们可以再用一个好的抽象机制,将其作为其他抽象机制的基础,这样我们就创建了强大的抽象机制的层次。Stack抽象机制使用了整数和整数数组。这些简单的抽象机制看起来并不是那么强大,但是使用它们,我们却可以创造出一个新的更为有用的抽象机制。一旦我们实现了Stack,我们可以使用它作为其他许多抽象机制的基础。对代码合理的重复使用可以节省大量的时间和经历。

我们已经看到多(在实现一个对象时使用其他对象)的一种层次化方法是合并(Composition), 因为我们将较小的对象合并为大的对象。在上例中,栈是由一个存储栈元素的数组和存储栈大小的整数合并而成的。OOP语言中的另一种层次化方法是派生(derivation或称为继承,inheritance)。派生不仅允许一个抽象不止一次地重用已实现的代码,还可以多次重用与另一个抽象接口。例如,假设我们想创建另一种类型的栈,他除了具有栈的一般功能外,还要求能返回一些统计数字,如栈中元素的平均值(mean), 中值(medium)以及最频值(mode)等。如果我们由Stack 合并成StatStack,那么就必须在StatStack中声明接口函数,然后只需要调用Stack中相应的函数即可。

typedef struct

{

Stack basic_stack;

}Statstack;

/*new to StatStack*/

int mean (StatStack* s) {/****/}

int medium (StatStack* s) {/*****/}

int mode (StatStack* s) {/******/}

/*in Stack*/

void ss_init (StatStack* s) {init(&s->basic_stack);}

void ss_push(StatStack* s, int i) {push (&s->basic_stack, i);}

int ss_pop (StatStack* s)  {return pop(&s->basic_stack);}

void ss_cleanup(StatStack* s) {cleanup (&s->basic_stack);},

这里,我们使用Stack来合并成StatStack,它有3个函数,分别为mean(), median()和mode(), 它们返回有关元素的不同统计值。对于Stack中已有的像push()这样的函数,StatStack也有自己的版本,只不过是在basic_stack域上调用了Stack中的相应函数罢了。

在StatStack中有大量的工作是用来重复Stack中的接口的。相反地,我们真正想要的是让StatStack继承Stack的接口,所以要做的知识添加新函数,如下所示:

/*StatStack is a Stack puls:*/

int mean(StatStack* s) {/*...*/}

int medium (StatStack* s){/*...*/}

int mode (StatStack* s) {/*...*/}

这就是派生允许我们做的。在C中模拟派生也是可以的,但是很少这样做。

4. 多态性 (Polymorphism)

如果代码在被不同类型的实例使用时是透明的,那么就说它具有多态性。典型的例子是一组类,他们分别代表不同的平面图形,如矩形、椭圆形等,每一个图形都知道如何自我绘制、计算自己的面积等,当然他们实现这些的方法都不一样,但是它们共享有这些能力。多态性允许我么根据一般的图形编写代码,并且使它共享有这些能力。多态性允许我们根据一般的图形编写代码,并且使它对任何实际图形都适用。


原创粉丝点击