Lecture 01 数据抽象与封装概述
抽象与封装
抽象与封装是两个重要的程序设计手段,主要是用来驾驭程序的复杂度,便于大型程序的设计、理解与维护。
对于一个程序实体而言,
- 抽象是指该程序实体的外部可观察到的行为,不考虑该程序实体的内部是如何实现的。(控制复杂度)
- 封装是指把该程序实体内部的具体实现细节对使用者隐藏起来,只对外提供一个接口。(信息保护)
主要的程序抽象与封装机制包括:
- 过程抽象与封装
- 数据抽象与封装
过程抽象与封装
过程抽象
- 用一个名字来代表一段完成一定功能的程序代码,代码的使用者只需要知道代码的名字以及相应的功能,而不需要知道对应的程序代码是如何实现的。
过程封装
- 把命名代码的具体实现隐藏起来(对使用者不可见,或不可直接访问),使用者只能通过代码名字来使用相应的代码。
- 命名代码所需要的数据是通过参数(或全局变量)来获得,计算结果通过返回机制(或全局变量)返回。
实现过程抽象与封装的程序实体通常称为子程序。在C/C++语言中,子程序用函数来表示。
过程抽象与封装是基于功能分解与复合的过程式程序设计的基础。
数据抽象与封装
数据抽象
- 只描述对数据能实施哪些操作以及这些操作之间的关系,数据的使用者不需要知道数据的具体表示形式。
数据封装
- 把数据及其操作作为一个整体来进行实现,其中,数据的具体表示被隐藏起来(使用者不可见,或不可直接访问),对数据的访问(使用)只能通过提供的操作(对外接口)来完成。
与过程抽象与封装相比,数据抽象与封装能够实现更好的数据保护。
数据抽象与封装是面向对象程序设计的基础。
“栈”数据的表示与操作(Example)
栈是一种由若干个具有线性次序关系的元素所构成的复合数据。对栈只能实施两种操作:
- 进栈(push):往栈中增加一个元素
- 退栈(pop):从栈中删除一个元素
- 上述两个操作满足一个重要性质:
- push(…); …pop(…); … ;push(x);pop(y); …
- x == y
- 即,增加/删除操作在线性序列的同一端进行(后进先出,Last In First Out,简称LIFO)
从数据抽象的角度,栈的使用者只需要知道上面的这些信息,不需要知道栈是如何实现的(数组或链表等)。
“栈”的实现 – 非数据抽象和封装途径
定义一个数据类型来表示栈数据
const int STACK_SIZE=100;
struct Stack {
int top;
int buffer[STACK_SIZE];
};
直接操作栈数据
Stack st; //定义栈数据
st.top = -1; //对st进行初始化
//把12放进栈
if (st.top == STACK_SIZE-1) {
cout << “Stack is overflow.\n”; exit(-1);
}
st.top++;
st.buffer[st.top] = 12;
......
//把栈顶元素退栈并存入变量x
if (st.top == -1) {
cout << “Stack is empty.\n”; exit(-1);
}
int x = st.buffer[st.top];
st.top--;
存在的问题
- 操作必需知道数据的具体表示形式。
- 数据表示形式发生变化将会影响操作。
麻烦并易产生误操作,因此不安全。例如,把进栈操作误写成:
st.top--; //书写失误导致误操作
st.buffer[st.top] = 12;
- 忘了初始化:
st.top = -1;
通过过程抽象与封装操作栈数据
先预定义三个函数
void init(Stack &s){
s.top = -1;
}
void push(Stack &s, int i){
if (s.top == STACK_SIZE-1) {
cout << “Stack is overflow.\n”;
exit(-1);
}
else{
s.top++; s.buffer[s.top] = i;
return;
}
}
void pop(Stack &s, int &i){
if (s.top == -1) {
cout <<“Stack is empty.\n”;
exit(-1);
}
else {
i = s.buffer[s.top]; s.top--;
return;
}
}
利用预定义的函数操作栈数据
Stack st; //定义栈数据
int x;
init(st); //对st进行初始化。
push(st,12); //把12放进栈。
......
pop(st,x); //把栈顶元素退栈并存入变量x。
存在的问题
-
数据类型的定义与操作的定义是分开的,二者之间没有显式的联系,push、pop在形式上与下面的函数f没有区别,函数f也能作用于st:
void f(Stack &s) { ...... } f(st); //操作st之后,st可能不再是一个“栈”了!
-
数据表示仍然是公开的,无法防止使用者直接操作栈数据,因此也会面临直接操作栈数据所带来的问题:
st.top--; st.buffer[st.top] = 12;
-
忘了初始化:
init(st);
“栈”的实现 – 数据抽象和封装途径
定义栈数据类型
const int STACK_SIZE=100;
class Stack {
public: //对外的接口(外部可使用的内容)
Stack();
void push(int i);
void pop(int &i);
private: //隐藏的内容,外部不可使用
int top;
int buffer[STACK_SIZE];
};
Stack::Stack(){
top = -1;
}
void Stack::push(int i){
if (top == STACK_SIZE-1) {
cout << “Stack is overflow.\n”;
exit(-1);
}
else{
top++;
buffer[top] = i;
return;
}
}
void Stack::pop(int &i){
if (top == -1) {
cout << “Stack is empty.\n”;
exit(-1);
}
else {
i = buffer[top]; top--;
return;
}
}
使用栈类型数据
Stack st; //会自动地去调用st.Stack()对st进行初始化。
int x;
st.push(12); //把12放进栈st。
......
st.pop(x); //把栈顶元素退栈并存入变量x。
......
st.top = -1; //Error
st.top++; //Error
st.buffer[st.top] = 12; //Error
st.f(); //Error
“栈”的另一种实现(链表表示)-- 数据抽象和封装途径
class Stack{
public:
Stack();
void push(int i);
void pop(int &i);
private:
struct Node {
int content;
Node *next;
} *top;
};
Stack::Stack() {
top = NULL;
}
void Stack::push(int i) {
Node *p=new Node;
if (p == NULL) {
cout << "Stack is overflow.\n";
exit(-1);
}
else {
p->content = i;
p->next = top; top = p;
return;
}
}
void Stack::pop(int &i) {
if (top == NULL) {
cout << "Stack is empty.\n";
exit(-1);
}
else {
Node *p=top;
top = top->next;
i = p->content;
delete p;
return;
}
}
改变栈类型数据的表示对使用者没有影响!
Stack st;
int x;
st.push(12);
st.pop(x);