当前位置:首页> PHP教程> 经典文章
关键字
文章内容
什么是面向对象编程?
 
 
修改时间:[2009/08/27 11:04]    阅读次数:[629]    发表者:[起缘]
 

译者序
不要将本文简单地视为是对C++特征的一个介绍。它的意义在于,一方面介绍了编程风格的演变,以及这种演变背后的动机
。另一个方面,它特别澄清了基于对象的(OB)和面向对象(OO)的异同,这是具有很大意义的。我们可以看到,
不管是OB还是OO,都不过是一种程序的组织形式。 这在很大程序上指出了OO着眼于解决什么样的问题
(程序如何组织才能有弹性,容易重用和理解),而不解决什么问题(数据结构的设计,算法的设计)等等。

摘要
“面向对象编程”和“数据抽象”已经成为常用的编程术语,然而,很少有人能够就它们的含义取得一致的认识;本文以Ada,C++,Module 2,Simula和Smalltalk等语言为背景对此给出一个非正式的定义。基本的想法是将“支持数据抽象”等同于定义和使用新数据类型的能力,而将“支持面向对象编程”等同于对类层次的表达能力。同时,还讨论了通用编程语言为支持此种编程风格而必须提供的机制。文中虽然采用C++来表述问题,但其讨论的范围并不仅限于这个语言。

1 介绍
并不是所有的语言都是面向对象的。一般认为,APL,Ada,Clu,C++,LOOPS和Smalltalk是面向对象的,我也曾经听说过关于使用C, Pascal,Module-2,和CHILL进行面向对象设计的讨论。那么是否可以尝试使用Fortran和Cobol来进行面向对象设计呢?我认为那也一定是可行的。在很多圈子里,“面向对象”已经成为“优秀”的高科技代名词,在商业出版领域可以看到有以下的三段论:
   Ada是优秀的
   面向对象是优秀的
   所以Ada是面向对象的
本文从通用编程语言的角度出发陈述了“面向对象”技术的概貌:
 第2节比较了数据抽象和面向对象之间的异同,也将它们和其他的编程风格做了区分;同时,指出了为了支持不同的编程风格所需的重要机制。
第3节陈述了为高效地支持数据抽象所需的语言机制。
第4节讨论了支持面向对象所需的设施。
第5节陈述了传统硬件体系结构和操作系统对于数据抽象和面向对象编程施加的限制。

文中例子程序使用C++来书写,这部分是出于介绍C++的目的,部分是因为C++是少数几个同时支持数据抽象,面向对象程序设计和传统编程风格的语言。本文不讨论为支持特定高层语言特性而涉及的并发性和特殊硬件支持。

2.编程风格(Programming Paradigms)
面向对象编程是一种用来针对一类问题编写优质代码的编程技术。一个语言称为是“面向对象”的如果它支持(Support)面向对象风格的编程。
在这里存在一个重要的区别。一个语言称为是“支持”某种风格的编程技术的,如果它提供了便于实施(方便地,安全地和高效地)该种风格编程的手段;反之,如果需要使用额外的技能和手段来获得基于某种风格的编码,则这个语言就是不“支持”该种编程风格的,我们只能说这个语言“使能”(Enable)了某种编程风格。举例来说,人们可以使用Fortran编写结构化程序,使用C语言编写类型安全的程序,在Module-2中使用数据抽象技术,但是,这些任务都具有不必要的困难性,因为这些语言都不“支持”那些编程风格。
对于某种编程风格的支持不仅意味着语言提供明确的并且可以直接使用的编程手段,而且还意味着在编译时间和运行时间提供某种检查,以防止代码无意中偏离了该种风格。类型检查是一个特别明显的例子,二义性检查和运行时间检查也可以扩充语言支持特定编程风格的能力。同时,象标准库和编程环境等等都可以增强这种支持。
      并不一定说一个语言如果支持了某种特性,则它就一定优于其他没有支持该特性的语言。在这里存在着太多的反例。重要的不是一个语言具有多少特性,而是它具有的特性是否能够在特定的领域内足以支持特定的编程风格。

1.所有的特性必须是清晰,优雅地集成进语言的。
2.通过组合使用这些特性必须足以获得解决方案,而不再需要使用其他特性。
3.假冒的和“特殊目的”的特性必须尽可能的少。
4.所有的特性都不能在那些不使用它们的程序中强加上过多的开销。
5.用户只需要了解那些在程序中被明确使用的特性所构成的语言子集就可以编写程序。

最后两点可以概括为“程序员不会被他们不了解的东西伤害”。如果对于一个特性是否有用存在任何疑问,则该特性就最好被抛弃。在语言中加上一个特性要远比从中或者从其文献中去掉一个容易得多。
以下将罗列一些编程风格以及支持它们的核心语言机制,但对此并不打算讨论得过于深入和繁琐。

2.1 过程化编程
最初的(可能也是目前最常用的)编程风格是:
    决定需要那些过程
   使用能够得到的最好的算法
设计的重点在于处理过程和执行运算的算法,语言为此提供了将参数传递给函数以及从函数中返回值的机制。和这种思维方式相关的文献集中讨论了传参的不同方式,区分不同参数的方式,以及各种不同的过程(过程,函数,宏)等等。Fortran是最早的过程语言,Algol60,Algol68,C和Pascal是一些后继的过程语言。
平方根函数是个典型的例子,它简单地产生传入参数的平方根。为此,该函数执行一个简单的数学运算:
 double sqrt(double arg)
 {
      //the code for calculting a square root
 }

 void some_function()
 {
  Double root2 = sqrt(2);
 }
从程序结构的角度来看,函数理清了算法之间的杂乱关系。

2.2 数据隐藏
随着时间的推移,程序设计的重点从重于过程设计转向重于对数据的组织,这反映了程序规模的增长。数据和直接操作数据的一集函数合称为一个模块。程序设计的风格变为:
   决定需要那些模块
   分解程序,使得数据隐藏在不同的模块之中
这种风格被称为“数据隐藏规则”。而在那些不必将数据和与它相关的过程绑定到一起的场合可以只使用过程程序设计风格。特别地,那些用来设计“好的过程”的技术现在可以应用到模块之内的每个过程之上。最常见的例子是定义一个堆栈模块,设计时有以下问题需要解决:
1.为堆栈模块提供一个用户接口(例如,函数 push()和pop() )
2.保证堆栈的表示(例如,一个元素的阵列)只能通过模块的接口来访问
3.保证堆栈在它第一次被访问之前执行过初始化

以下是一个不甚严格的堆栈模块的外部接口:
 
 //declaration of the interface of module stack of charater
 char pop();
 void push(char);
 const stack_size = 100;

假定这个外部定义保存在stack.h文件之中,而其模块内部表示如下:
 #include "stack.h"
 static char v[stack_size];
 static char* p = v;
 char pop()
 {
        //Check for underflow and pop
 }

 void push(char c)
 {
       //check for overflow and push
 }

要将堆栈的表示修改为链表是很方便的,用户不能访问堆栈的内部表示(因为v 和p 已经被声明为static的,因此只能在声明它们的模块内部引用它们)。可以象这样使用这个堆栈模块:
 #include "stack.h"
 void some_function()
 {
  char c = pop(push('c'));
  if( c != 'c' ) error( "impossible" );
 }

Pascal没有提供令人满意的设施来实施这种绑定。将一个名字和程序的其它部分隔离开来的唯一办法是使它局部于一个过程之内,这导致了奇怪的过程嵌套以及对于全局数据的过度依赖。
C语言的表现略好一些,在上面所述的例子之中,可以将数据和与它相关的过程保存在同一个文件之中以形成模块,由此程序员可以控制哪些名字是全局可见的(被声明为static的名字只在本模块内可见)。由此,C语言可以在一定程度上支持模块化;然而C缺乏使用这种机制的一般性框架,同时,通过static控制名字访问显得过于低级。
Pascal的一个后继语言,Module-2,走得更远一些。它形式化了模块这个概念,提供了一些基本的语言构成,如良定义的模块声明,对于名字范围的明确控制(import,export), 模块的初始化机制,以及一组公认的对这些机制的使用方式。
C和Module-2在这个领域内的区别可以概括为,C只是“使能”了将程序分解为模块,而Module-2则“支持”这种技术。

2.3数据抽象
模块化编程发展成为将某种类型的数据集中置于一个类型管理模块的控制之下的编程风格。如果有人需要两个stack,则他可能设计出一个具有如下接口的堆栈管理模块:

 class stack_id; //stack_id is a type
  //no details about stacks or stack_ids are known here
 stack_id create_stack(int size); //make a stack and return its identifier
 destroy_stack(stack_id);
 void push( stack_id,char)
 char pop(stack_id)
相对于以往那些无结构的混乱风格,这当然是一次重大的改进。然而,通过这种方式实现的“类型”又明显地和语言的内建类型有区别。每一个类型管理模块都必须分别定义自己的机制来生成自己的“变量”;这里没有什么明确的方法可以赋予变量以标识符,也不可能让编译器和编程环境了解变量的名字。同时,没有办法让这些变量服从常用的变量作用域规则和参数传递规则。
通过模块机制建立起来的类型在很多重要的方面都和内建类型存在区别,同时,它获得的支持也远比内建类型获得要低级得多。例如:
 void f()
 {
  stack_id s1;
  stack_id s2;
 
  s1 = create_stack(200);
  //Oops: forgot to create s2

  shar c1 = pop(s1,push(s1,'a'));
  if( c1!='c') error("impossible" );
  char c2 = pop(s2,push(s2,'a'))
  if( c2!= 'c') error( "impossible");

  destroy(s2);
  //Oops,forgot to destroy s1
 }

换言之,支持数据隐藏风格的模块概念只是使能了数据抽象,但它不支持这种风格。

Ada, Clu和C++等语言通过允许用户定义和内建类型行为相似的“类型”来解决这个问题。这种“类型”通常称为“抽象数据类型”。于是,编程风格变为:
   决定需要那些类型
   为每一个类型实现一组完整的操作
而在那些不需要为一个类型生成多个对象的场合可以只使用数据隐藏技术。有理数和复数等算术类型是抽象数据类型的常见例子:

 class complex{
  doube re, im;
 public:
  complex(double r, double i) { re =r ;im = i; }
  complex( double r) { re=r; im = 0; } //float->complex conversion

  friend complex operator+(complex,complex);
  friend compelx operator-(complex,complex); //binary minus
  firend complex opeator-(complex);//unary minus
  friend compelx operator*(complex,complex);
  friend complex operator/(complex,complex);
  //...
 }

类complex(用户自定义类型)的声明确定了一个复数的“表示”和一组和它相关的操作。“表示”是私有的,就是说,只能通过在complex类中声明的函数才能访问re和im 。函数可以如下定义:
 complex operator+(complex a1, complex a2)
 {
  return complex( a1.re + a2.re, a1.im + a2.im );
 }
可以象这样使用:
 complex a = 2.3;
 complex b = 1/a;
 complex c = a-b*complex(1,2.3);
 //...
 c = -(a/b)+2;
大多数(但不是全部)模块可以使用“类型”来获得更好的表达。对于那些更加适合表达成为“模块”的概念,程序员可以定义一个只生成单个对象的类型来作为替代。当然,语言也可以在提供自定义类型机制之外再提供一个独立的模块机制。
2.4数据抽象的问题
一个抽象数据类型定义了一类黑盒,一经定义完成,则它和程序的其他部分不再发生交互。除非修改它的定义,否则很难将它用于新的用途。考虑为一个图形系统定义一个类型shape。假定当前系统支持圆,三角形和正方形,同时还有其他的一些相关类:
 class point { /*...*/ };
 class color{ /*...*/ };
shape类可能定义成这样:
 enum kind{ circle,triangle,squre};
 class shape{
  point center;
  color col;
  kind k;
  //representation of shape
 public:
  point where()  { return center; }
  void move(point to) { center = to; draw(); }
  void draw();
  void rotate(int);
  //more operation
 };
为了允许draw,rotate知道当前处理的是何种形状,其中的类型域"k"必须存在(在类Pascal语言中,可使用带标记k的可变记录 ),函数draw可以定义成这样:
 void shape::draw()
 {
  switch( k )
  {
  case circle:
   //draw a circle;
   break;
  case triangle:
   //draw a triangle;
   break;
  case square:
   //draw a square;
   break;
  }
 }
 这是混乱的。象draw这样的函数必须了解当前存在的各种“形状”,因此每当系统新增一个新的“形状”,这些函数就必须被改写。为了定义一个新的“形状”就必须检查,同时也可能修改shape的所有操作。所以除非可以修改源码,否则将不可能在系统中增加新的“形状”。而既然增加一个新的“形状”将导致修改shape所有重要的操作,这就意味着编程需要更高的技巧同时也可能为现存的其他“形状”引入bug。同时,建立在一般类型shape之上的应用框架(或者其中的一部分)可能要求每一个具体的“形状”必须具有定长的表示,这会为如何表示具体的形状带来很大的限制。
2.5 面向对象编程
问题在于没有将各种形状的一般性属性(具有颜色,可以绘画)和特定形状的专有属性(圆具有半径,使用画圆函数执行绘画)区分开来。对这种区分的表达和利用形成了面向对象的编程。只有可以用来直接表达这种区分的语言才是支持面向对象的,其他语言不是。
 Simula的继承机制提供了一个解决方案。首先,指定一个类来定义形状的一般性的属性:
 class shape{
  point center;
  color col;
 public:
  point where(){ return center; }
  void move(point to){ center = to; draw() }
  virtual void draw();
  virtual void rotate(int);
  //.......
 }
调用接口可以确定但实现尚不能确定的函数都被标记成为“virtual”(在Simula和C++中意味着可以被某个子类重新定义)。给定了这些定义以后,我们可以写出操作形状的一般性函数:
 void rotate_all(shape* v, int size, int angle)
  //rotate all members of vector "v" of size "size" "angle" degrees
 {
  for( int i = 0; i < size; i++)  v[i].rotate(angle);
 }
为了定义了一个特定的形状,我们必须声明这是一个“形状”,同时指定它所有的属性(包括虚函数) 
 class circle : public shape{
  int radius;
 public:
 void draw(){ /*...*/ }
 void rotate(int){} //yes, the null function
 }
在C++中,类circle称为从类shape中派生,而类shape则称为是类circle的基类。也可以使用子类(subclass)和超类(superclass)这两个术语。
编程的风格变为:
   决定需要那些类
   为每一个类提供完整的操作
   使用继承明确地获得一般性
而在不需要表达一般性的场合可以只使用数据抽象。通过继承和虚函数可以发掘出的类型之间的共性的多少是衡量面向对象编程技术是否适用于特定应用领域的核心标准。某些领域,例如交互式图形系统,特别适合应用面向对象技术;而另外一些领域,例如经典的算术类型和基于它们的运算系统,则看来使用数据抽象就足够了,面向对象技术在这里不一定是必要的。
在一个系统中的不同类型之间发掘一般性不是一个容易的过程,可以发掘出的一般性的多少取决于系统的设计方法。设计时必须积极地寻找一般性,一方面应当基于已经存在的类型构造新的类型,另一方面可以通过察看不同类型之间表现出的相似性决定是否可以归纳出一个基类。
 文献 Nygarrd[13]和Kerr[9]尝试了不基于特定语言解释面向对象编程;文献Cargill[4]是对面向对象编程的案例研究。
 
3.对数据抽象的支持
为类型定义一组操作同时限制只允许这组操作访问类型的数据是对数据抽象编程的基本支持。随后,程序员很快发现需要进一步的语言机制来方便定义和使用这些新类型。操作符重载是一个很好的例子。

3.1初始化和清除
一旦类型的表示被隐藏了起来,则必须提供一个机制来执行对变量的初始化。一个简单的方案是要求用户在使用一个变量之前先调用一个特定的函数来初始化它。例如:
 class vector{
  int sz;
  int* v;
 public:
  void init(int size); // call init to initialize sz and v before the first use of a 
         //vector
  
  //...
 }

 vector v;
  //don't use v here
 v.init(10);
 //use v here
这容易导致错误并且不够优雅。好一点的方案允许类型的设计者为初始化提供一个特别的函数;给定了这个函数,分配和初始化一个变量变成了同一个操作。这个特定的函数经常被称为构造函数。在某些场合初始化一个对象可能并不是十分简单的,这样就常常需要一个对等的操作来在对象被最后一次使用之后执行清除。在C++中,这样的一个清除函数称为析构函数。考虑一个vector类型:
 class vector{
  int sz;
  int* v;
 public:
  vector(int); //constructor
  ~vector();  //destructor
  int& operator[](int index);
 };
vector的构造函数可以定义为分配空间,象这样:
 vector::vector(int s)
 {
  if( s<=0 ) error("bad vector size' );
  sz = s;
  v = new int[s]; //allocate an array of  "s" integers 
 }
vector的析构函数释放这部分空间
 vector::~vector()
 {
  (译注:此处最好是delete []v;)
  delete v;    //deallocate the memory pointed to by v
 } 
C++不支持垃圾收集,这种允许一个类型自己管理存储空间而不需要用户来干预的技术是一个补偿。存储管理是构造/析构函数经常执行的操作,但是它们也常常用来执行与此无关的事情。

3.2赋值和初始化
对于很多类型而言,控制其初始化和清除过程就已经足够了,但并不是所有的类型都如此。有时候控制拷贝过程也是十分必要的,考虑vector:
 vector v1[100];
 vector v2 = v1; //make a new vector v2 initialized to v1
 v1 = v2; //assign v2 to v1

在这里必须有机制来定义v2初始化和对v1赋值的含义,当然也可以选择提供机制来禁止这种拷贝。理想的情况是,这两种机制都存在。例如:
 class vector{
  int *v;
  int sz;
 public:
  //....
  void operator=(vector&); //assignment
  vector(vector&); //initialization
 };
给出了用户定义的操作来解释vector的赋值和初始化。赋值可以象这样定义:
     ( 译注:由于在上文class vector中operator=(vector&a)声明为void类型,所以这里的定义最好为
 void vector::operator(vector&a) )
 vector::operator=(vector&a) //check size and copy elements
 {
  if( sz != a.sz ) error( "bad vector size for = " );
  for( int = 0; i<sz;i ++) v[i] = a.v[i];
 }
虽然赋值操作可以依赖于一个“旧的 ”的vector对象,但初始化操作就必须有所不同,例如:
 vector::vector(vector& a) // initialize a vector from another vector
 {
  sz = a.sz;
  v = new int[sz];
  for( int i = 0; i < sz; i++ ) v[i]=a.v[i]; //copy elements
 }
在C++中,一个形如X(X&) 的构造函数定义了从X的一个对象出发构造X的另一个对象的初始化过程。除了明确地构造X的对象之外,X(X&)也被用来处理传值的传参过程和函数的返回值。
在C++中,可以通过将赋值声明为私有来禁止对于对象的赋值操作。
 class X{
  void operator=(X&); //only members of x can
  X(X&); //copy an x
  //...
 public:
  //...  
 }
Ada不支持构造,析构,对赋值的重载和用户定义的参数传递和返回机制,这严重限制了用户自定义类型的种类,同时强迫程序员回到“数据隐藏”技术,就是说,用户必须设计和使用类型管理模块而不是真正的类型。
3.3参数化类型
为什么我们要定义一个整数类型的vector呢?要知道,用户常常需要一个对于vector的作者而言类型未知的vector。因此,vector应当采用一种可以将“类型”作为参数来引用的表达方式加以定义:
 class vector<class T>{ //vector of elements of type T
  T* v;
  int sz;
 public:
  vector( int s)
  {
   if( s<= 0 ) error( "bad vector size" );
   v = new T[sz = s ]; //allocate an array of "s" "T"s
  }
 T& opeartor[](int i);
 int size() { return sz; }
 //...
 }
特定类型的vector可以象这样定义和使用:
 vector<int> v1(100); //v1 is a vector of 100 integers
 vector<complex> v2(200); //v2 is a vector of 200 complex numbers
 
 v2[ i ] = complex(v1[x], v1[y]);
Ada,Clu和ML支持参数化类型。不幸的是,C++不支持(译注,现在的C++标准支持参数化类型,称为模板);这里使用的记号只是为了演示;但在必要时,可以使用宏来模拟参数化类型。和那些指定了所有类型的类比起来这样做并没有在运行时引入更多的开销。
一般来说,一个参数化类型总会依赖于参数类型的某些方面。例如,vector的有些操作假定参数类型定义了赋值操作。那么人们如何保证这一点呢?一种方案是要求参数化类型的设计者表明这种依赖关系。例如,“T必须是一种定义了赋值操作的类型”。另一个好一点的办法让参数化类型的规格和参数类型的规格彼此独立,编译器可以检测到对不存在操作的调用,并且可以给出相应的错误提示。例如:
 cannot define vector(non_copy)::operator[](non_copy&) :
  type non_copy does not have operator=
这种技术使得我们可以在“操作”这个级别上处理参数类型和参数化类型之间的依赖性。例如,我们可能定义一个具有排序功能的vector,排序操作可能用到参数类型的<,<= 和=操作。然而,只要不调用vector的排序功能,我们还是可以使用一个没有<操作的类型来参数化vector。
从参数化类型中生成的每一个类型之间是彼此独立的,这是一个问题。例如,vector<char>和vector<complex>之间完全无关。理想的情况是,人们可以表达并且利用从同一个参数化类型中生成的各个类型之间具有的共性,例如,vector<char>和vector<complex>都具有一个和类型无关的size()操作。从vector的定义中推导出size可以被实例类型共用是可能的,但其过程并不简单。解释型的语言或者同时支持参数化类型和继承机制的语言在这个方面具有优势。

3.4 异常处理
随着程序规模的增长,特别是当程序库对外发布后,提供一个处理错误(或者更一般地说,“异常情况”)的标准机制是重要的。Ada,Algol68和Clu各自支持一套处理异常的标准机制。不幸的是,C++不直接支持异常处理(译注,现在的C++标准已经支持异常处理),而必须使用函数指针,“异常对象”,“错误状态”和C的库函数signal和longjump等机制来伪造。这些机制不够一般,同时也不能提供一个处理错误的标准框架。
重新考虑一下vector的例子。当一个越界的索引值被传递给索引(subscribe)操作时,会发生什么?vector的设计者应该可以为此指定一个缺省行为:
 class vector {
  ...
  except vector_range{
  //define an exception called vector_range
   //and specify default code for handling it
    error("global,vector range error" );
    exit( 99 );
  }
 }
vector::opeartor[]()可以触发异常处理代码而不是调用出错函数:
 int& vector::operator[](int i)
 {
  if( 0 < i  || sz <= i ) raise vector_ranger;
  return v[ i ];
 }
这导致堆栈回卷,直到发现一个能够处理vector_range异常的句柄为止。然后执行该异常处理句柄。
可以针对一个特定的代码块来定义异常句柄:
 void f() {
   vector v(10);
   try { //errors here are handled by the local
           //exception handler defined below
    //...
    int i = g(); //g might cause a range error using some vector
    v[ i ] = 7;
   }
   except  {
   vector::vector_ranger:
    error( "f() vector ranger error" );
    return;
   } 
   //error here are handled by the global
   //exception hander defined in vector
   int i = g();
   v[ i ] = 7; ://g might cause a range error using some vector 
     //potential range error
  }
可以有很多种方式来定义异常以及异常处理句柄的行为。这里列出的异常机制概貌是从Clu和Module-2+中变化而来的。这种风格的异常处理可以实现为,直到抛出异常时才执行异常处理代码。也可以容易地使用C的setjmp和longjup模拟出来。
那么,象上文定义的异常处理的语义在C++中是否可以完全伪造出来呢?很不幸,不能。问题在于,当异常发生时,运行栈必须被回卷到安装异常处理句柄的位置,在C++中,这涉及到调用在回卷过程中被销毁对象的析构函数。使用C的longjmp函数是做不到这一点的;一般地说,用户自身也不能做到这一点。

3.5强制
已经证明,用户自定义的强制是非常有用的技术,例如,构造函数complex(double)隐含着一个从double到complex的强制。程序员可以明确地指出强制,或者在必要时,如果没有二义性,编译器也可以暗中引入它:
 complex a = complex(1);
 complex b = 1; //implicit: 1->complex(1)
 a = b + complex(2);
 a = b + 2  //implicit: 2->complex(2)

C++引入用户定义的强制的原因是,在支持算术运算的语言中混合模式的算术表达式是很常见的;同时,参加运算的用户自定义类型(例如,矩阵,字符串,机器地址等)也大多可以很自然地相互映射。
 从程序组织的角度来看,有一种类型的强制可以证明是格外有效的:
 complex a = 2;
 complex b = a+2; //interpereted as operator+(a,complex)
 b = 2+a;     //interpereted as operator+(complex(2),a)
在解释‘+’操作时只需要一个函数,并且对于类型系统而言,两个操作数是被同等看待的。进一步,我们看到,可以在不对整数概念做出任何调整的前提下只通过实现类complex就可以将这两个概念平滑地集成到一起。这和“纯面向对象系统”截然不同,在那里这些操作会被如下解释:
 a+2; ://a.opeartor+(2)
 2+a; ://2.operator(a)
这样就必须修改类integer来使得2.operator(a)合法化。当在一个系统中加入新的功能时,修改已有的代码是必须尽量避免的,一般地说,面向对象的编程技术能够很好地支持这个目标,但在这里,数据抽象技术提供了更好的解决方案。

3.6迭代器(Iterators)
一般认为,支持数据抽象的语言必须提供定义控制结构的手段。特别是,常常需要一个允许用户循环访问一个容器类型中所含元素的机制,同时又不能迫使用户依赖于容器类型的实现细节。如果有一个定义类型的强大机制,同时又能够重载操作符,则就可以在不引入独立的定义控制结构的机制的前提下实现这一目标。
对于vector,用户可以通过下标来确定其顺序,所以可以不必定义迭代器。然而我还是定义了一个来演示这个技术。迭代器可以有很多种风格,我比较喜欢的是通过重载函数操作符:
 class vector_iterator{
  vector & v;
  int i;
 public:
  vector_iterator(vector& r) { i = 0; v = r; }
  int operator()() { return i<v.size() ? v.elem(i++) : 0; }
 };
现在我们可以象这样声明和使用迭代器:
 vector v(sz);
 vector_iterator next(v);
 int i;
 while( i = next() ) print( i );
在同一个时刻一个对象可以激活多个迭代器对象;同时,一个类型可以定义多种不同类型的迭代器以便执行不同的循环操作。迭代器是一种相当简单的控制结构,也可以定义更加一般的控制机制,例如C++标准库提供了co-routine类[15]。
对于很多容器类型,例如vector,可以将迭代机制作为类型自身的一部分来定义以避免引入独立的迭代器。可以将vector定义为具有一个“当前状态”:
 class vector {
  int* v;
  int sz;
  int current;
 public:
  //...
  int next() { return (current++<sz) ? v[current] : 0; }
  int prev() { return ( 0 < --current ) ? v[current] : 0; }
 };
于是可以象这样操作:
 vector v(sz);
 int i;
 while( i = v.next() ) print(i);
和迭代器比起来,这样的方案不够一般;但是在一种重要的特殊情况下它减少了开销:可能我们只需要一种类型的迭代器,并且在同一时刻只会有一个迭代器对象在活动。如果必要,也可以在这个简单的方案之上加上更一般的机制。请注意,使用这种简单的解决方案比起使用迭代器来需要更多的设计远见。迭代器技术也可以设计为同一个迭代器类型能够绑定到不同的容器类型,这样通过一个迭代器就可以访问不同的容器类型。

3.7 实现问题
对数据抽象的支持大多定义为语言特征并且由编译器来实现。但参数化类型最好能够通过一个对于语言的语义有更多理解的连接器来支持;同时异常处理需要运行环境的支持。他们都可以在不牺牲一般性和易用性的前提下获得很好的编译速度和效率。
随着定义类型的能力的增长,程序开始更多地依赖来自一些库中的类型(并不仅限于那些在语言的手册中描述的内容)。这很自然地需要工具来表达程序中哪些部分被插入了库中,而哪些部分是从库中抽取出来的;也需要工具来找出库包含了哪些东西和库中的哪些部分是实际被程序使用了的等等。
对于编译型语言,能够使得代码在修改以后尽量减少编译工作的工具是非常重要的。同时,连接器/加载器能够在加载执行代码时尽量不加载大量的无关和无用代码的能力也是非常关键的。特别要指出来,如果一个类型只有少数几个操作被调用,而库/连接器/加载器却将该类型的所有操作都加载入内存的行为是特别糟糕的。

4. 对面向对象的支持
有两个机制在支持面向对象编程中起了基本的作用,第一个是类的继承机制;第二个是,当在编译时无法确定一个对象的实际类型时,应当能够在运行时基于对象的实际类型来决定调用的具体方法。其中,对于方法调用机制的设计是关键。同时,如上文所述的对数据抽象的支持技术对于支持面向对象也同样是重要的,因为数据抽象的观点,以及为了在语言中优雅地支持它而作的努力在面向对象技术中同样也有效。这两种技术的成功都取决于对类型的设计以及能够高效,方便和灵活地使用这些类型。相对于数据抽象而言,面向对象技术能够设计出更加一般,更加灵活的数据类型。

4.1调用机制
支持面向对象的关键语言特征是针对一个给定的对象如何调用它的方法。例如,给定指针p,如何处理调用p->f(arg)呢?在这里存在一系列的选择。
在C++和Simula这样广泛应用静态类型检查的语言中,可以借助于类型系统来在不同的调用方式之间作出选择。在C++中,有两种函数调用的方式:
【1】普通的方法调用:具体调

 
 
 
 
 
 

威尼斯人唯一官网