| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 3953 人关注过本帖, 1 人收藏
标题:数据抽象
取消只看楼主 加入收藏
lz1091914999
Rank: 14Rank: 14Rank: 14Rank: 14
来 自:四川
等 级:贵宾
威 望:37
帖 子:2011
专家分:5959
注 册:2010-11-1
结帖率:94.64%
收藏(1)
已结贴  问题点数:100 回复次数:11 
数据抽象
如果你正在设计一个栈,目前这个栈只存放int数据,那么你会如何设计它呢?

其实栈的结构并不复杂,我们在头脑中就能建立一个栈的模型,当然我们在纸上把它的结构画出来也许会更好一点:
图片附件: 游客没有浏览图片的权限,请 登录注册

这张图就是一个以数组实现的栈的模型,栈底的索引是0,Pointer是栈顶指针,它指向索引5这个位置,说明现在栈里刚好有5个元素,当Pointer指向索引0时,栈为空,表示栈里已经没有元素了,当Pointer指向索引9时(这个位置不属于我们,因为它超出了范围,而且在图里也没有,但可以猜到这个位置在索引8的上一个位置,在这里我们并没有使用这块内存,仅仅只是当做一个标致),表示栈已经满了(当然我们可以动态增加,比如利用realloc)。
一个栈我想应该实现这些功能吧:
1、入栈(Push):把一个元素添加到栈顶的位置,并使栈顶指针向上移动一个位置。
2、出栈(Pop):把一个元素从栈顶取出,并使栈顶指针向下移动一个位置。
3、获得栈顶元素(Peek):把一个元素从栈顶取出,并不移动栈顶指针的位置。
注意:入栈时要检查栈是否已经满了,如果栈已经满了,输出信息提示用户,并不做任何操作。出栈和获得栈顶元素时要检查栈是否为空,如果栈为空,输出信息提示用户,不做任何操作并返回0(返回0的原因是大多时候它并不会造成一些错误,而且测试条件时也为假)。
可能还需要这样一些功能:
1、获得栈元素的数量(GetSize):这个很简单,我们只需要返回栈顶指针对应的索引值就行了,它刚好是栈元素的数量。
2、获得栈底指针(GetBottomPointer):返回这个数组的名字,它代表这个栈的底指针。
3、获得栈顶指针(GetTopPointer):返回栈顶指针即可。
2和3可以用在遍历栈时用到,比如:
程序代码:
int* bPointer = GetBottomPointer(&stack);
int* tPointer = GetTopPointer(&stack);
while (bPointer != tPointer) // (1)
    printf("%d", *bPointer++);
// or
while (tPointer != bPointer) // (2)
    printf("%d", *--tPointer);
(1)可以从栈底到栈顶遍历这个栈。
(2)可以从栈顶到栈底遍历这个栈。
看来一共有6个函数:
1、void Push(Stack*)。
2、int Pop(Stack*)。
3、int Peek(Stack*)。
4、int GetSize(Stack*)。
5、int* GetTopPointer(Stack*)。
6、int* GetBottomPointer(Stack*)。
好了,可以写代码了:
程序代码:
/**

 * Stack.h

 * Code by Kenier Lee.

 */

#ifndef _KENIER_LEE_STACK_H
#define _KENIER_LEE_STACK_H

#define STACK_SIZE 1024 /* Size of the Stack */

typedef struct StackTag Stack; /* Prepare type definition*/

struct StackTag {
   int storage[STACK_SIZE]; /* Array of int */
   int* tp;                 /* Top pointer */
};

void Push(Stack*, int);
int  Pop(Stack*);
int  Peek(Stack*);
int  GetSize(Stack*);
int* GetTopPointer(Stack*);
int* GetBottomPointer(Stack*);

#endif /* _KENIER_LEE_STACK_H */
这里栈的大小是1024,Stack里有一个字段是tp,它表示栈顶指针。
程序代码:
/**

 * Stack.c

 * Code by Kenier Lee.

 */

#include "Stack.h"
#include <stdio.h>

void InitStack(Stack* stack) {
   stack-> tp = stack->storage;
}

void Push(Stack* stack, int value) {
   int size = GetSize(stack);
   if (size == STACK_SIZE) /* Full */
      printf("Error: The stack is full.\n");
   else
      *stack->tp++ = value;
}

int Pop(Stack* stack) {
   int value = 0;
   if (stack->tp == stack->storage) /* Empty */
      printf("Error: The stack is empty.\n");
   else
      value = *--stack->tp;
   return value;
}

int Peek(Stack* stack) {
   int value = 0;
   if (stack->tp == stack->storage) /* Empty */
      printf("Error: The stack is empty.\n");
   else
      value = *stack->tp;
   return value;
}

int GetSize(Stack* stack) {
   return stack->tp - stack->storage;
}

int* GetTopPointer(Stack* stack) {
   return stack->tp;
}

int* GetBottomPointer(Stack* stack) {
   return stack->storage;
}
下面是测试代码,记得链接Stack.o
程序代码:
/**

 * StackTest.c

 * Code by Kenier Lee.

 */

#include "Stack.h"
#include <stdio.h>

int main(void) {
   Stack stack;
   int i, *tPointer, *bPointer;

   InitStack(&stack);

   for (i = 0; i < 10; ++i)
      Push(&stack, i);
   for (i = 0; i < 10; ++i)
      printf("%d ", Pop(&stack));
   printf("\n");

   for (i = 0; i < 10; ++i)
      Push(&stack, i);
   printf("Sizeof stack: %d\n", GetSize(&stack));
   tPointer = GetTopPointer(&stack);
   bPointer = GetBottomPointer(&stack);
   while (bPointer != tPointer)
      printf("%d ", *bPointer++);
   printf("\n");

   bPointer = GetBottomPointer(&stack);
   while (tPointer != bPointer)
      printf("%d ", *--tPointer);
   printf("\n");

   for (i = 0; i < 10; ++i)
      printf("%d ", Pop(&stack));
   printf("\n");
   printf("%d\n", Pop(&stack));
   printf("%d\n", Peek(&stack));

   return 0;
} /* Output:
9 8 7 6 5 4 3 2 1 0
Sizeof stack: 10
0 1 2 3 4 5 6 7 8 9
9 8 7 6 5 4 3 2 1 0
9 8 7 6 5 4 3 2 1 0
Error: The stack is empty.
0
Error: The stack is empty.
0

*/
这样的函数调用:
InitStack(&stack);
Push(&stack, i);
Pop(&stack);
Peek(&stack);
GetSize(&stack);
GetTopPointer(&stack);
GetBottomPointer(&stack);
每个函数调用都需要传递Stack结构体的指针,更危险的是我们可以随意修改Stack里的所有字段,比如我们修改的tp字段的值,这样的修改显然是致命的,而我们无法阻止这些操作。在最开始调用任何函数之前必须先调用InitStack函数,不然可能会出现一些对话框,提示该内存不能为"write"或"read"(在Linux下面是段错误),这表示我们使用了不属于我们程序的内存,因为未初始化的tp的值是未知的。最后我们来看看Stack的C++版本:
程序代码:
/**

 * Stack.h

 * Code by Kenier Lee.

 */

#ifndef _KENIER_LEE_STACK_H
#define _KENIER_LEE_STACK_H

class Stack {
public:
   enum { STACK_SIZE = 1024 }; // Size of the Stack
   Stack();
   void push(int);
   int pop();
   int peek();
   int getSize();
   int* getTopPointer();
   int* getBottomPointer();
private:
   int storage[STACK_SIZE]; // Array of int
   int* tp; // Top pointer
};

#endif // _KENIER_LEE_STACK_H
不需要typedef,我们也不能直接修改tp的值了,因为它是私有的,我们能使用的只有从public:到private:之前的名字。
程序代码:
/**

 * Stack.cpp

 * Code by Kenier Lee.

 */

#include "Stack.h"
#include <iostream>
using namespace std;

Stack::Stack() {
   tp = storage;
}

void Stack::push(int value) {
   int size = getSize();
   if (size == STACK_SIZE) /* Full */
      cout << "Error: The stack is full." << endl;
   else
      *tp++ = value;
}

int Stack::pop() {
   int value = 0;
   if (tp == storage) /* Empty */
      cout << "Error: The stack is empty." << endl;
   else
      value = *--tp;
   return value;
}

int Stack::peek() {
   int value = 0;
   if (tp == storage) /* Empty */
      cout << "Error: The stack is empty." << endl;
   else
      value = *tp;
   return value;
}

int Stack::getSize() {
   return tp - storage;
}

int* Stack::getTopPointer() {
   return tp;
}

int* Stack::getBottomPointer() {
   return storage;
}
这是定义之前声明的那些成员函数,其中还有一个构造函数Stack(),它与类名相同,在创建Stack对象时它将被自动调用,它可以替代InitStack,而且比InitStack好,因为不用担心它不会被调用。
程序代码:
/**

 * StackTest.cpp

 * Code by Kenier Lee.

 */

#include "Stack.h"
#include <iostream>
using namespace std;

int main() {
   Stack stack; // Constructor called
   int i, *tPointer, *bPointer;

   for (i = 0; i < 10; ++i)
      stack.push(i);
   for (i = 0; i < 10; ++i)
      cout << stack.pop() << " ";
   cout << endl;

   for (i = 0; i < 10; ++i)
      stack.push(i);
   cout << "Sizeof stack: " << stack.getSize() << endl;
   tPointer = stack.getTopPointer();
   bPointer = stack.getBottomPointer();
   while (bPointer != tPointer)
      cout << *bPointer++ << " ";
   cout << endl;

   bPointer = stack.getBottomPointer();
   while (tPointer != bPointer)
      cout << *--tPointer << " ";
   cout << endl;

   for (i = 0; i < 10; ++i)
      cout << stack.pop() << " ";
   cout << endl;
   cout << stack.pop() << endl;
   cout << stack.peek() << endl;
} /* Output:
9 8 7 6 5 4 3 2 1 0
Sizeof stack: 10
0 1 2 3 4 5 6 7 8 9
9 8 7 6 5 4 3 2 1 0
9 8 7 6 5 4 3 2 1 0
Error: The stack is empty.
0
Error: The stack is empty.
0

*/
现在我们又看到了这样的调用:
stack.push(i);
stack.pop();
stack.peek();
stack.getSize();
stack.getTopPointer();
stack.getBottomPointer();
我感觉这样要清晰得多,很容易看出这些操作是指对于那个对象的,并且如果你现在写这样的代码:stack.tp++ 或 stack.tp = 0 ... 等等修改tp字段值的操作都会被编译器看作是错误,因为tp被声明为私有的(只能在这个类的成员函数中使用),这样就安全多了。Stack stack;这句话被执行的时候,该类的构造函数Stack()就自动调用了,它完成了初始化的工作,不用担心初始化会被遗忘。

C++提供了一种,把函数与自定义数据类型绑定在一起的特性,这被称做数据抽象,而且上面的代码量明显要比前一个C版本的要少,语法更清楚,数据抽象的用途远不止这些,更多的特性,如果以后我有时间再与大家一一分享吧!

不知道看了这些代码之后同鞋们有没有兴趣去学习 C Plus Plus呢?

[ 本帖最后由 lz1091914999 于 2012-2-6 22:10 编辑 ]
搜索更多相关主题的帖子: 模型 设计 如何 元素 
2012-02-06 21:34
lz1091914999
Rank: 14Rank: 14Rank: 14Rank: 14
来 自:四川
等 级:贵宾
威 望:37
帖 子:2011
专家分:5959
注 册:2010-11-1
收藏
得分:0 
回复 3楼 beyondyf
我开始也是学的C,之后又研究了Java,发现抽象换来的是简单和清晰,但在效率上会有所损失,但学了C++之后,又发现抽象不仅换来了简单和清晰,在效率上与C也相差无几。所以以后我还是用C++吧,我准备把英文学好后去学Qt,杨大哥有性趣吗?

My life is brilliant
2012-02-06 22:18
lz1091914999
Rank: 14Rank: 14Rank: 14Rank: 14
来 自:四川
等 级:贵宾
威 望:37
帖 子:2011
专家分:5959
注 册:2010-11-1
收藏
得分:0 
回复 6楼 beyondyf
恩,其实我是一个比追求效率的人吧,虽然Java很方便,而且语法的清晰程度也令人痴迷,但是我并不愿意牺牲效率去换得这些,当然利用Java做一些小工具也是不错的。
其实我一点也不懂设计模式,只是看了Bruce(看我的头像)的书后觉得这方面很有用。以后杨大哥想做Android的开发吗?

My life is brilliant
2012-02-06 22:41
lz1091914999
Rank: 14Rank: 14Rank: 14Rank: 14
来 自:四川
等 级:贵宾
威 望:37
帖 子:2011
专家分:5959
注 册:2010-11-1
收藏
得分:0 
回复 8楼 beyondyf
恩,不过Qt比MFC好太多了,你可以去了解一下,我可买不起你那手机,毕竟我现在还在用爸爸妈妈的钱呢!

My life is brilliant
2012-02-06 23:37
lz1091914999
Rank: 14Rank: 14Rank: 14Rank: 14
来 自:四川
等 级:贵宾
威 望:37
帖 子:2011
专家分:5959
注 册:2010-11-1
收藏
得分:0 
回复 11楼 beyondyf
恩,不过一个人学英文真的是很难,加上我自己记性差,唉不知道什么时候才能学会啊?这跟以前学编程的时候不一样啊,可能就是不能马上看到效果吧,而编程就能马上看到效果,呵呵。

My life is brilliant
2012-02-06 23:59
lz1091914999
Rank: 14Rank: 14Rank: 14Rank: 14
来 自:四川
等 级:贵宾
威 望:37
帖 子:2011
专家分:5959
注 册:2010-11-1
收藏
得分:0 
回复 13楼 pangding
Thank you very much!

My life is brilliant
2012-02-07 00:29
lz1091914999
Rank: 14Rank: 14Rank: 14Rank: 14
来 自:四川
等 级:贵宾
威 望:37
帖 子:2011
专家分:5959
注 册:2010-11-1
收藏
得分:0 
回复 18楼 BlueGuy
其实C那么容易出错的很大一部分原因都是没有得到恰当的初始化造成的,因为我们可能使用到一个未知的内存块,造成段错误,并且稍加修改就能产生这样一个版本:
程序代码:
/**

 * Stack2.h

 * Code by Kenier Lee.

 */

#ifndef _KENIER_LEE_STACK_H
#define _KENIER_LEE_STACK_H

#define STACK_SIZE 1024 /* Size of the Stack */

typedef struct StackTag Stack; /* Prepare type definition */

struct StackTag {
   int  storage[STACK_SIZE];  /* Array of int */
   int* tp;                   /* Top pointer */

   void (*Push)(Stack*, int);
   int  (*Pop)(Stack*);
   int  (*Peek)(Stack*);
   int  (*GetSize)(Stack*);
   int* (*GetTopPointer)(Stack*);
   int* (*GetBottomPointer)(Stack*);
};

void InitStack(Stack*);

#endif /* _KENIER_LEE_STACK_H */

/**

 * Stack2.c

 * Code by Kenier Lee.

 */

#include "Stack2.h"
#include <stdio.h>

void Push(Stack*, int);
int  Pop(Stack*);
int  Peek(Stack*);
int  GetSize(Stack*);
int* GetTopPointer(Stack*);
int* GetBottomPointer(Stack*);

void InitStack(Stack* stack) {
   stack->tp               = stack->storage;
   stack->Push             = Push;
   stack->Pop              = Pop;
   stack->Peek             = Peek;
   stack->GetSize          = GetSize;
   stack->GetTopPointer    = GetTopPointer;
   stack->GetBottomPointer = GetBottomPointer;
}

void Push(Stack* stack, int value) {
   int size = GetSize(stack);
   if (size == STACK_SIZE) /* Full */
      printf("Error: The stack is full.\n");
   else
      *stack->tp++ = value;
}

int Pop(Stack* stack) {
   int value = 0;
   if (stack->tp == stack->storage) /* Empty */
      printf("Error: The stack is empty.\n");
   else
      value = *--stack->tp;
   return value;
}

int Peek(Stack* stack) {
   int value = 0;
   if (stack->tp == stack->storage) /* Empty */
      printf("Error: The stack is empty.\n");
   else
      value = *stack->tp;
   return value;
}

int GetSize(Stack* stack) {
   return stack->tp - stack->storage;
}

int* GetTopPointer(Stack* stack) {
   return stack->tp;
}

int* GetBottomPointer(Stack* stack) {
   return stack->storage;
}

/**

 * Stack2Test.c

 * Code by Kenier Lee.

 */

#include "Stack2.h"
#include <stdio.h>

int main(void) {
   Stack stack;
   int i, *tPointer, *bPointer;

   InitStack(&stack); // (1)

   for (i = 0; i < 10; ++i)
      stack.Push(&stack, i);
   for (i = 0; i < 10; ++i)
      printf("%d ", stack.Pop(&stack));
   printf("\n");

   for (i = 0; i < 10; ++i)
      stack.Push(&stack, i);
   printf("Sizeof stack: %d\n", stack.GetSize(&stack));
   tPointer = stack.GetTopPointer(&stack);
   bPointer = stack.GetBottomPointer(&stack);
   while (bPointer != tPointer)
      printf("%d ", *bPointer++);
   printf("\n");

   bPointer = stack.GetBottomPointer(&stack);
   while (tPointer != bPointer)
      printf("%d ", *--tPointer);
   printf("\n");

   for (i = 0; i < 10; ++i)
      printf("%d ", stack.Pop(&stack));
   printf("\n");
   printf("%d\n", stack.Pop(&stack));
   printf("%d\n", stack.Peek(&stack));

   return 0;
} /* Output:
9 8 7 6 5 4 3 2 1 0
Sizeof stack: 10
0 1 2 3 4 5 6 7 8 9
9 8 7 6 5 4 3 2 1 0
9 8 7 6 5 4 3 2 1 0
Error: The stack is empty.
0
Error: The stack is empty.
0

*/
这里把函数与自定义数据类型绑定在了一起,看起来非常成功,但这里有几个危险:
1、(1)这句话在任何时候都有可能被遗忘,我以前也经常这样,导致我几天都没找到问题所在。。。
2、tp的值可能被修改,特别是那些认为自己聪明的程序员,想利用它达到某些特殊的效果。
还有很多程序员不愿意使用stack.GetTopPointer和stack.GetBottomPointer来获得顶和底指针,而喜欢直接用stack.tp和stack.storage,看起来没有任何错误,因为这两个字段可以直接使用,而且节省函数调用开销。想一想,如果有一点我修改了实现,我认为用tp作为栈顶指针的名字是不好的,storage也如此,那么任何直接使用了tp和storage的代码都需要重新修改。并且我认为C并不能做到面向对象,只能做到不完全的基于对象。

My life is brilliant
2012-02-07 12:39
lz1091914999
Rank: 14Rank: 14Rank: 14Rank: 14
来 自:四川
等 级:贵宾
威 望:37
帖 子:2011
专家分:5959
注 册:2010-11-1
收藏
得分:0 
回复 21楼 BlueGuy
在C中,初始化被遗忘导致错误是程序界公认的,不仅是我可能犯这个错误。我知道版主喜欢OpenGL,那么我们就来画一些图形吧:
1、圆形(Circle)
2、正方形(Square)
3、三角形(Triangle)

实现画(Draw)这些形状和清除(Erase)这些形状。

C版本的:
程序代码:
#include <stdio.h>

enum Shape { CIRCLE, SQUARE, TRIANGLE } currentShape;

void DrawCircle() { printf("Draw a circle.\n"); }
void EraseCircle() { printf("Erase a circle.\n"); }

void DrawSquare() { printf("Draw a square.\n"); }
void EraseSquare() { printf("Erase a square.\n"); }

void DrawTriangle() { printf("Draw a triangle.\n"); }
void EraseTriangle() { printf("Erase a triangle.\n"); }

void Draw() {
   switch (currentShape) {
   case CIRCLE:   DrawCircle();   return;
   case SQUARE:   DrawSquare();   return;
   case TRIANGLE: DrawTriangle(); return;
   default: return; // Error here
   }
};

void Erase() {
   switch (currentShape) {
   case CIRCLE:   EraseCircle();   return;
   case SQUARE:   EraseSquare();   return;
   case TRIANGLE: EraseTriangle(); return;
   default: return; // Error here
   }
}

int main(void) {
   currentShape = TRIANGLE;
   Draw();
   currentShape = CIRCLE;
   Draw();
   currentShape = SQUARE;
   Draw();

   // Do other things...

   currentShape = TRIANGLE;
   Erase();
   currentShape = CIRCLE;
   Erase();
   currentShape = SQUARE;
   Erase();

   return 0;
} /* Output:
Draw a triangle.
Draw a circle.
Draw a square.
Erase a triangle.
Erase a circle.
Erase a square.
*/
我设置了一个状态机(currentShape)来表示当前形状,当调用Draw之前必须设置需要绘制的形状,然后Draw通过判断形状来调用对应的函数,Erase函数也一样。
我们来看看C++版本的:
程序代码:
#include <iostream>
using namespace std;

class Shape {
public:
   virtual void draw() = 0;
   virtual void erase() = 0;
};

class Circle : public Shape {
public:
   void draw() { cout << "Draw a circle." << endl; }
   void erase() { cout << "Erase a circle." << endl; }
};

class Square : public Shape {
public:
   void draw() { cout << "Draw a Square." << endl; }
   void erase() { cout << "Erase a Square." << endl; }
};

class Triangle : public Shape {
public:
   void draw() { cout << "Draw a Triangle." << endl; }
   void erase() { cout << "Erase a Triangle." << endl; }
};

void draw(Shape& shape) { shape.draw(); }

void erase(Shape& shape) { shape.erase(); }

int main() {
   Circle   c;
   Square   s;
   Triangle t;

   draw(c);
   draw(s);
   draw(t);

   // Do other things...

   erase(c);
   erase(s);
   erase(t);
} /* Output:
Draw a circle.
Draw a Square.
Draw a Triangle.
Erase a circle.
Erase a Square.
Erase a Triangle.
*/
在这个版本中,draw和erase并没有判断,参数都被向上转型(Upcasting)到了他们的基类Shape,并且可以知道Shape的draw和erase函数都使用了后期绑定(虚函数),调用时它会自动计算到正确的函数地址,并调用这个正确的函数。

如果我现在想增加一个梯形(Trapezoidal),C版本需要这样改:
在枚举Shape中增加一个成员TRAPEZOIDAL,然后分别为Trapezoidal实现Draw和Erase函数,然后在Draw函数的switch中增加一个TRAPEZOIDAL判断条件,最后在Erase函数的switch中增加一个TRAPEZOIDAL判断条件。

在C++中需要这样改:
增加一个梯形类Trapezoidal,它继承至Shape,实现该类的画(draw)和擦除(Erase)函数。

区别是显而易见的,其实这才是真正的面象对象:
1、继承建立公共接口。
2、后期绑定。
基于对象是指:
1、数据结构与函数捆绑(在C中可以通过在结构中增加函数指针域来完成)。
2、构造(创建这个对象时就初始化,这在C中只能靠自己)。
3、继承(代码重用,复用接口,建立类层次)。
C似乎只能勉强支持第1点,所以我说它只能完成不完全的基于对象。

[ 本帖最后由 lz1091914999 于 2012-2-7 16:54 编辑 ]

My life is brilliant
2012-02-07 16:49
lz1091914999
Rank: 14Rank: 14Rank: 14Rank: 14
来 自:四川
等 级:贵宾
威 望:37
帖 子:2011
专家分:5959
注 册:2010-11-1
收藏
得分:0 
回复 22楼 BlueGuy
版主,这你就误会了,没有C会有C++吗?我在这里只是想分享一下我以前学习的经验,并不是在贬低C,我只想讨论一下用C做设计的局限性。众所周知,用C写算法是再好不过的,但做设计,C++的确比C好,找准机会用适当的东西才是最重要的,也祝版主C旅途愉快!

My life is brilliant
2012-02-07 17:01
lz1091914999
Rank: 14Rank: 14Rank: 14Rank: 14
来 自:四川
等 级:贵宾
威 望:37
帖 子:2011
专家分:5959
注 册:2010-11-1
收藏
得分:0 
回复 26楼 BlueGuy
我以前没接触过ADT,这里能不能请教一下版主呢?而且我很想知道如何用C来做单根继承呢?谢谢啦!

My life is brilliant
2012-02-07 17:37
快速回复:数据抽象
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.025481 second(s), 9 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved