<form id="hz9zz"></form>
  • <form id="hz9zz"></form>

      <nobr id="hz9zz"></nobr>

      <form id="hz9zz"></form>

    1. 明輝手游網中心:是一個免費提供流行視頻軟件教程、在線學習分享的學習平臺!

      什么是面向對象編程?

      [摘要]譯者序不要將本文簡單地視為是對C++特征的一個介紹。它的意義在于,一方面介紹了編程風格的演變,以及這種演變背后的動機。另一個方面,它特別澄清了基于對象的(OB)和面向對象(OO)的異同,這是具有很大...
      譯者序
      不要將本文簡單地視為是對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,可以將迭代機制作為類型自身的一部分來定義以避免引入獨立的迭代器?梢詫ector定義為具有一個“當前狀態”:
      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】普通的方法調用:具體調用那個方法在編譯時間就可以決定(通過查找編譯器的符號表),同時在使用標準過程調用機制基礎上增加一個表示對象身份的指針。如果在某些場合標準過程調用顯得效率不夠高,則可以將函數聲明成為內聯(inline)的,編譯器就嘗試在調用的位置展開函數體。通過這種方式,人們可以既獲得類似宏的展開機制又不犧牲標準函數的語義。這樣的優化措施對于數據抽象也同樣是有價值的。
      【2】虛函數調用:函數調用依賴于對象的實際類型,一般地說,對象的實際類型只能在運行時間才能確定。典型的情況是,指針p具有某個基類B的類型,而p指向的對象是B的某個子類D的(就想上文例子中的shape和circle)。調用機制必須察看編譯器為對象指定的一個表來決定調用那個函數。一旦找到了函數,譬如說是D::f,則可以使用上文所述的方式來調用。在編譯時間,名字f轉換成為函數指針表的一個索引。這樣的調用機制幾乎和普通的方法調用一樣有效,在標準C++中,只需要額外地多做5次內存引用。
      弱類型語言需要采用更加復雜的機制。在象Smalltalk之類的語言中,必須保存類的所有方法的名字以備在運行時間執行查找:
      【3】方法觸發:首先通過p指向的對象找到正確的方法名字的表,然后在這個表中查找是否有一個方法“f”,如果存在則調用之;否則出錯。和靜態類型語言在編譯時間執行函數名查找不同,在這里方法名查找是針對一個實際的對象執行的。
      和虛函數調用比起來,方法觸發是低效的,但是它更加靈活。由于此時通常無法執行靜態參數類型檢查,所以這種調用方式必須和動態類型檢查一起使用。

      4.2 類型檢查
      上文所述的“形狀”這個例子顯示了虛函數的威力。那么方法觸發為我們提供了些什么呢?答案是,我們現在可以在任意對象上嘗試調用任意方法!
      在任意對象上觸發任意方法的能力使得類庫的設計者可以將正確處理數據類型的責任轉嫁到用戶的頭上,當然,這簡化了庫的設計。例如:
      class stack { ://assume class any has a member next
      any* v;
      void push(any* p )
      {
      p->next = v;
      v = p;
      }
      any* pop()
      {
      if( v == 0 ) return error_obj;
      any* r = v;
      v = v->next;
      return r;
      }
      };
      這樣,用戶就必須有責任保證避免以下的類型匹配錯誤:
      stack<any*> cs;

      cs.push( new Saab900 );
      cs.push( new Saab37B );

      plane* p = (plane*)cs.pop();
      p->takeoff();

      p = (plane*)cs.pop();
      p->tackoff(); //Oops! Run time error: a Saab 900 is a car
      // a car does not have a takeoff method;
      消息處理機制可以檢測到程序試圖將一部汽車當作一架飛機,于是觸發了一個錯誤。但是,只有當用戶是程序員本人時這樣的錯誤提示才可能有一些安慰性的價值;由于缺少靜態類型檢查,我們很難保證在最終發布的程序中不包含這樣的錯誤。自然地,在一個沒有靜態類型的,基于方法的語言中這樣的矛盾會更加突出。
      將參數化類型和虛函數結合起來使用可以在庫設計的靈活性,簡單性,以及庫使用的方便性上近似于方法觸發,而同時又不需要放棄靜態類型或在空間和時間上引入開銷,例如:
      stack<plane*> cs;
      cs.push( new Saab900 ); // compile time error:
      // type mismatch: car* passed,plane* expected
      cs.push( new Saab37B);

      plane* p = cs.pop();
      p->takeoff(); //fine: a Saab 37B is a plane.

      p = cs.pop();
      p->takeoff;

      基于靜態類型檢查/虛函數調用的程序風格和基于動態類型檢查/方法觸發的程序風格存在某些差異。例如,在Simula和C++中一個類為其所有子類的對象指定了一個確定的接口,而Smalltalk中的類則為其所有子類的對象指定了一個初始接口。換言之,Smalltalk中的類是一個最小的接口規范,用戶可以任意嘗試其他所有在接口中未指定的方法;而C++類則是一個確定的接口規范,用戶可以確信,只有調用那些在接口中有定義的方法才能通過編譯。
      4.3 繼承
      考慮一個支持方法查找但不支持繼承的語言,這個語言可以被稱為支持面向對象嗎?我認為不。很明顯,我們可以利用方法查找技術來使得對象適應具體的應用環境,并因此能完成很多有趣的事情。然而,為了避免混亂,我們還是需要一個系統的手段來將對象的方法和作為對象表示的數據結構結合在一起。同時,為了使得對象的用戶可以了解到對象的行為方式,又必須有一個系統的手段來表示對象不同的行為之間的共性!斑@個系統而標準的手段”就是繼承。
      考慮一個支持繼承但是不支持虛函數或者方法查找的語言,這個語言可以被稱為是支持面向對象的嗎?我認為不:上文所述的“形狀”這個例子在這個語言中沒有很好的解決方案。然而,相對于只支持數據抽象的語言,這樣的語言要強大很多。這個觀點來自于這樣的觀察:很多基于Simula和C++的程序都使用繼承來組織其結構,但在其中沒有使用虛函數。表達共性的能力是一個特別強大的工具,例如,不使用聯合我們就可以解決各種“形狀”需要一個公用表示的問題。但是,由于缺少虛函數,人們必須借助于一個“類型域”來表達對象的具體類型,這導致代碼的組織缺少模塊性。
      類繼承是一種特別有效的編程工具, 繼承不僅可以用來支持面向對象編程,而且還有著更廣泛的應用。在面向對象的編程實踐中,我們可以使用基類來表達一般的概念,而使用子類來表達各種特例。這樣的方式只是部分展示了繼承的強大功能,然而那些所有函數都是虛函數的(或者都是基于方法查找的)語言很強調這樣的觀點。如果能夠正確地控制從基類中繼承而來的內容,那么類繼承可以是一個從已有類型中產生新類型的強大工具。子類和基類之間的關系并不總能概括為特例和一般,“分解”(factoring)能夠更加準確地表達子類和基類之間的關系。
      繼承是又一個我們無法簡單地預言應該如何使用才算是合理的編程工具,并且在今天(即便Simula誕生已經超過20年)要簡單地說斷定說哪些使用方式是誤用也還為時過早。

      4.4多繼承
      假設A是B的基類,則B繼承了A的所有屬性;就是說,除了自身特有的一些屬性之外,B就是一個A。在這樣的解釋之下,很明顯, 讓B同時繼承兩個基類A1和A2很可能是有用的。這稱為多繼承。
      舉一個經典的例子。假定類庫提供了兩個類displayed和task,分別用來表示顯示管理對象和調度管理對象,則程序員就可以產生象這樣的類:
      class my_displayed_task : public displayed, public task{
      // my stuff
      };

      class my_task : public task { //not displayed
      // my stuff;
      }

      class my_displayed : public displayed { // not a task
      // my stuff;
      };
      如果只使用單繼承,那么程序員就只能使用這三個選擇中的兩個。這或者將導致代碼的重復,或者將導致缺少彈性-或者常常兩者兼有。在C++中,這樣的例子可以使用多繼承解決,并且相對于單繼承不引人明顯開銷(時間和空間),也不犧牲靜態類型檢查。
      二義性可以在編譯時間解決:
      class A{ public : f(); ... }
      class B{ public : f(); ... }
      class C: public A, public, B{ ... };

      void g() {
      C* p;
      p->f(); //error:ambiguous
      }
      在這一點上,C++有別于LISP的一個支持多繼承的面向對象方言。LISP通過依賴聲明的順序來解決名字沖突,或者將來自不同基類的同名的方法看作是等價的,又或者將基類中的同名方法組合成為一個最高層類中的一個更復雜的方法。
      而在C++中,我們可以通過一個額外的功能來解決名字的沖突:
      class C : public A, public B{
      public:
      f()
      {
      //C's own stuff
      A::f();
      B::f();
      }
      ....
      }
      除了這樣用來直接表達獨立的多繼承(independent multiple inheritance)的概念之外,看來還需要更一般的機制來表達在一個多繼承格(multiple inheritance lattice)中類之間的依賴關系。在C++中,可以使用虛基類來表達一個類對象中的一個子對象被所有其他的子對象共享:
      class W{ ... }
      class Bwindow //window with border
      : public virtual W
      { ... }

      class Mwindow
      : public virtual W
      { ... }

      class BMW //window with border and menu
      : public Bwindow,public Mwindow
      { ... };

      這樣,在一個BMW對象中,只有一個W子對象被Bwindow和Mwindow子對象共享。LISP方言提供了方法組合的概念來減少在編程中使用如此復雜的類層次,但C++沒有。

      4.5 封裝
      考慮類的一些成員(不管是數據成員還是函數成員)需要被保護起來以防止未授權的訪問。那么該如何合理地界定哪些函數可以訪問這些成員呢?對于面向對象的編程語言來說,最明顯的答案是“定義在該對象上的所有成員函數”。但由此可以有一個不太明顯的推論,即實際上我們不能完整地確定一個集合,其中包括了所有有權訪問這些受保護成員的函數;因為總可以從這些具有保護成員的類中派生出新的類,并且在派生類上定義新的成員函數。這樣的方案一方面在很大的程度上防止了意外地訪問保護成員(因為我們不會“意外”地派生出一個新類),在另一方面,又為使用類層次建立應用提供了彈性(因為我們可以通過派生一個新類來賦予自己訪問保護成員的能力)。
      不幸的是,對于一個面向數據抽象的語言而言,其答案是不同的:“必須在類的聲明中羅列出所有有權訪問保護成員的函數”,但對于這些函數本身沒有特別的要求,特別是,這些函數不必是類的成員函數。在C++中,有權訪問私有成員的非成員函數稱為友元函數。在上文中定義的類complex具有友元函數。有時候,將一個函數聲明為多個類的友元函數也是很有用的。如果想理解一個類型的意義,特別是想修改它的時候,有一個成員函數和友元函數的完整列表是非常有好處的。
      以下的例子演示了在C++中封裝的幾個選擇:
      class B{
      //class member are default private
      int i;
      void f1();
      protected:
      int i2;
      void f2();
      public:
      int i3;
      void f3();
      friend void g(B*); //any function can be designated as a friend
      }
      私有和保護成員不能被外界直接訪問:
      void h(B* p)
      {
      p->f1(); //error:B::f1 is private
      p->f2(); //error:B::f2 is protected
      p->f3(); //fine:B::f1 is public
      }
      保護成員可以在派生類中訪問,但私有成員不能:
      class D: public B{
      public:
      void g()
      {
      f1(); //erro: B:f1 is private
      f2(); //fine: B:f2 is protected, but D is derived from B
      f3(); //fine: B:f3 is public
      }
      }
      友元函數可以象成員函數一樣訪問私有和保護成員:
      void g(B* p)
      {
      p->f1(); //fine: B::f1 is private, but g() is a friend of B
      p->f2(); //fine: B::f2 is protected, but g() is a friend of B
      p->f3();// fine: B::f1 is public
      }
      隨著程序規模,用戶的數量的增長,同時如果用戶在地理上分布比較分散,成員保護機制的重要性就會大大增加。文獻Snyder[17]和Stroustrup[18]進一步討論了保護問題。
      4.6 實現問題
      為了支持面向對象編程,主要需要改進運行時間系統和編程環境。在一定程度上,這是因為面向對象需要的語言機制已經由數據抽象引入了,所以不再需要很多額外的特征。
      面向對象技術進一步模糊了編程語言及其環境之間的界限,因為各種一般的或是具體的用戶定義類型越來越多地充斥在程序之中。這需要進一步發展運行時間系統,庫工具,調試器,性能測量工具和監控工具。理想的情況是這些工具被集成到一個環境之中去。Smalltalk是這方面的一個好例子。

      5 限制
      為了定義了一個能夠充分利用數據隱藏,數據抽象和面向對象技術的語言,我們面對的主要問題是,任何一個通用的程序語言都必須能夠:
      1.在傳統的計算機上運行
      2.和傳統的操作系統并存
      3.在時間效率上堪輿傳統的程序語言媲美
      4.用于主要的應用領域
      這意味著,這個語言必須能夠高效地執行算術運算(在浮點運算方面要堪輿媲美Fortran);其訪問內存的方式必須能夠用于設備驅動程序;同時必須能夠遵從傳統操作系統制定的古怪標準來生成調用。進一步,傳統語言必須能夠調用使用面向對象語言書寫的函數,而面向對象的語言也必須能夠調用使用傳統語言書寫的函數。
      此外,如果一個面向對象的程序語言依賴于不能在傳統系統結構下有效實現的技術,則它不可能成為一個通用的語言。除非得到特別的支持,否則方法觸發機制的一般性實現將會成為負擔。
      類似的,垃圾收集可能成為性能和移植性的瓶頸。大多數面向對象的語言都采用垃圾收集機制來簡化程序員的工作,同時也減少語言本身和編譯器的復雜性。然而,就算我們可以在一些非關鍵的領域內使用垃圾收集,但一旦需要,我們就應該能夠保留對存儲器的控制權。另一方面,一個程序語言選擇放棄垃圾收集,轉而為類型管理自身的存儲提供便利的表達手段也是切實可行的。C++就是一個例子。異常處理和并發特征是另外一個潛伏著問題的地方。任何依賴于連接器的支持才能有效實現的機制有可能存在移植問題。在一個語言中擁有“低級”特征的另一個方法是可以在主要的應用領域中使用一個獨立的“低級”語言。

      6. 結論
      基于繼承的編程叫做是面向對象的編程方法,基于用戶自定義類型的編程叫做是基于數據抽象的編程方法。除了很少的一些例外情況之外,面向對象編程可以視為是數據抽象的一個超集。只有得到了正確的支持這些技術才能是有效的;對數據抽象的支持主要來自語言本身,而面向對象則需要來自編程環境的進一步支持。為了通用性,支持數據抽象和面向對象的語言必須能夠高效地利用硬件。

      7. 致謝
      本文一個較早的版本在斯德哥爾摩的 Association of Simula Users會議上面世。在那里的進行的討論導致了對本文的風格和內容的做了很多改進。Brain Kernighan和Ravi Sethi給出了很多建設性的意見。同時感謝所有為增強C++作出了貢獻的人。

      8.參考文獻
      略,請參閱原文



      日韩精品一区二区三区高清