程序设计范型
要支持一种范型,不仅在于某些能直接用于该种范型的显见形式的语言功能,还在于一些形式上更加细微的,对无意中偏离了这种范型的情况做编译时或者运行时的检查。类型检查是这类检查中的最明显例子。
程序设计范型发展:
1.过程式程序设计
原始的程序设计范型是:确定你需要哪些过程,采用你能找到的最好的算法。
double sqrt(double arg) { //计算平方根的代码 } void fun() { double root = sqrt(2); // }
附:C++中,一个char变量具有某种自然的大小,正好能保存给定机器里的一个字符(通常是一个字节),int变量正好适合给定机器里的整数运算。C++能在基本类型之间完成任何所有有意义的相互转换。
2.模块程序设计
程序设计重点从有关过程转到对数据的组织。一集相关的过程与被它们所操作的数据组织在一起,通常被称作一个模块。
程序设计的范型变成了:确定你需要的哪些模块;将程序分为一些模块,使数据隐藏于模块当中。这一范型被作为数据隐藏原来而广为人知。
典型例子,定义一个堆栈:
【1】为堆栈提供一个用户界面(例如函数pushu()和pop())。
【2】保证堆栈的表示(例如,一个元素的数组)只能够通过用户界面访问。
【3】保证堆栈在被使用之前已经做了初始化。
namespace Stack //界面 { void push(char); char pop(); } void f() { Stack::push('c'); if(Stack::pop() != 'c') error("impossible"); }
这里Stack::限定词表面push()和pop()是来自Stack名字空间。这些名字的其他使用不会与之混乱。
Stack的定义可以通过程序的另一个单独编译的部分提供:
namespace Stack{ const int max_size=200; char v[max_size]; int top = 0; void push(char c){/*检查上溢并压入c*/} char pop(){/*检查下溢并弹出*/} }
该Stack模块的关键点是,用户完成隔离于Stack的数据表示之外,隔离的方式通过写出Stack::push()和Stack::pop()代码实现的。用户不必知道Stack是用数组实现的,这个方式实现可以修改而不会影响用户的代码。
模块化是一切成功大型程序的一个最基本特征。
3.用户定义类型
基于模块的程序设计,趋向于以一个类型的所有数据为中心,在某个类型管理模块的控制之下工作。例如,如果希望有很多堆栈——而不是像前面只用一个由Stack模块提供的堆栈——我们就可能会定义一个堆栈管理器,具有如下界面:
namespace Stack{ struct Rep; typedef Req& stack; stack create(); void destroy(stack s); void push(stack s ,cha c); char pop(stack s); }
以某种方式实现Stack。可以是先预先分配几个堆栈,而让Stack::create()递交出到某个尚未使用的堆栈的引用。而Stack::destroy()则将一个堆栈表示标记为未使用,以是Stack::create()能够重用它
namespace Stack{ //表示 const int max_size=200; struct Rep{ char v[max_size]; int top; }; const int max=16; //最大堆栈数 Rep statcks[max]; boolean used[max]; typedef Rep& stack; } void Stack::push(stack s, char c){} char Stack::pop(stack s){} Stack::stack Stack::create()(){ //找一个未使用的Rep,将它标记为已使用,将它初始化并返回引用 } Stack::detroy(stack s){/*标记s为未被使用*/}
这里围绕这一个表示类型包装起一组界面函数。结果得到的“堆栈类型”具有怎样的行为,部分在于我们如何定义这些界面函数,部分在于我们如何将Stack的表示展现给它的用户,部分在于这个表示本身的设计情况。
这种方法不是最理想的。这里给用户提供了一个“假类型”的表示,它会因为表示类型的不同而出现很大的变化——与此同时,又要隔离用户于表示之外。例如:如果我们选择采用另外一种数据结构去表示一个堆栈,那么Stack::stack的初始化和赋值规则就可能发生剧烈的变化。C++解决这个问题的方式是通过允许用户直接定义类型,这种类型的行为方式与内部类型完全一样。这样的类型常常被称作抽象数据类型或者用户定义类型。
现在程序设计范型变成了:确定你需要哪些类型,为每个类型提供完整的一组操作。在那些对于每个类型都只需要一个对象的地方,采用模块方式实现数据隐藏风格的程序设计也就足够了。
例子:复数
class complex{ double re, im; public : complex (double r ,double i){re=r;im=i;}//两个标量构造一个复数 complex(doble r){re=r; im = 0;}//一个标量构造一个复数 complex(d){re=im = 0;}//默认复数 friend complex operator+(complex,complex); //... }
现在考虑一个遵循用户定义的Stack类型。
class Stack{ char* v; int top; int max_size; public : class Underflow(); //用作异常 class Overflow();//用作异常 class Bad_size();//用作异常 Stack(int s); //构造函数,如果一个对象出了其作用域,需要做某些清理时,就应该去生命构造函数的对应物——它被称为析构函数 ~Stack();//析构函数 void push(char c); char pop(); }
Stack::Stack(int s){ top=0; if(s<0 || 10000<s) throw Bad_size(); max_size=s; v= new char[s];//在自由存储中为元素分配存储 } Stack::~Stack(){ delete[] v;//释放元素存储,使空间可能重新使用 }
在将Stack从用一个模块实现的“假类型”转变成一个真正的类型的过程中,一个性质被丢掉了:表示方式没有和用户界面分离,反而变成了使用Stack的程序片段里将要包含这部分。这个表示完全是私用的,因此只能通过成员函数访问,然而它却出现在了那里。如果这种表示出现了某种显著变化,那些使用它的代码需要重新编译。这是为做出在行为方式完全像内部类型的具体类型时所做出的一个代价。
但是,如果我们需要将堆栈用户与堆栈表示的修改完全隔离,前面的这个Stack就不够好了。针对这个问题的解决方案能得到界面与表示的完全分离,这时就需要放弃的是真正的局部变量。
定义界面:
class Stack{ public : class Underflow{};//用于异常 class Overflow{};//用于异常 virtual void push(char c)=0; virtual char pop()=0; }
virtual在Simula和C++里的意思是“可以在今后由这个类所派生的类里重新定义”。派生类提供了Stack界面的一个具体实现。有些古怪的 =0 语法形式说明在Stack派生的某些类中必须定义这些函数。
该Stack以如下方式使用:
void f(Stack& s_ref) { s_ref.push('c'); if(s_ref.pop() != 'c') throw Bad_pop(); }
f()如何使用Stack界面,完全忽略实现的细节。
实现Stack:
class Array_stack : public Stack{//Array_stack实现Stack(数组) //... } class Array_stack : public Stack{//List_stack实现Stack(List) //...List }
4.面向对象的程序的设计
考虑在某个图形系统中定义一个类型Shape。假定当时这个系统需要支持圆形、三角形和正方形,再假定我们已经有了
class Point{/*...*/} class Color{/*...*/}
我们可能这样定义形状(shape) 类
enum Kind{circle,triangle,square}; class Shape{ Kind k;//类型域 point center; Color col; //... public : void draw(); void rotate(int); //... }
函数draw()可能具有下面样子:
void Shape::draw(){ switch(k){ case circle : //画圆 break; case circle : //画三角形 break; case circle : //画正方形 break; } }
这真是个悲剧,draw()必须知道现存的所有形状的类型。
这里的问题在于:没有一种方式来区分所有形状的共有特征,与某个特定形状种类的特殊特征。表达这种区分并由此获益就定义了面向对象的程序设计。继承机制(C++从Simula借来的)提供了一种解决方案。
现在程序设计范型是:确定你需要哪些类;为每一个类提供完整的一组操作;利用继承去明确地表示共性。
5.通用型程序设计
有人需要使用堆栈,但是未必总是需要字符的堆栈。堆栈是一个一般性概念,与字符的概念并没有关系。
这个程序设计范型是:确定你需要哪些算法,将他们参数化,使它们能够对各种各样适当的类型和数据结构工作。
template<class T> class Stack{ T* v; int max_size; int top; public : class Underflow(); class Overflow(); Stack(int s); ~Stack(); void push(T); T pop();
}
一般来说,列在这里的所有范型相互补充,相互支持。
并不是所有的东西都能通过语言的某些内部特征表述。语言特征的存在是为了支持各种各样的程序设计风格和技术,因此,学习一种语言的工作就应该集中于去把握对语言而言固有的和自然的风格——而不是去理解该语言的所有语言特征的细枝末节。
注:参考来源于《C++语言程序设计》(《The C++ Programming Language》)