客戶程式的職責

出版圈郭志敏發表於2011-08-24

  介面是其實現和其客戶程式之間的一份契約。實現必須提供介面中規定的功能,而客戶程式必須根據介面中描述的隱式和顯式的規則來使用這些功能。程式設計語言提供了一些隱式規則,來支配介面中宣告的型別、函式和變數的使用。例如,C語言的型別檢查規則可以捕獲介面函式的引數的型別和數目方面的錯誤。

  C語言的用法沒有規定的或編譯器無法檢查的規則,必須在介面中詳細說明。客戶程式必須遵循這些規則,實現必須執行這些規則。介面通常會規定未檢查的執行時錯誤(unchecked runtime error)、已檢查的執行時錯誤(checked runtime error)和異常(exception)。未檢查的和已檢查的執行時錯誤是非預期的使用者錯誤,如未能開啟一個檔案。執行時錯誤是對客戶程式和實現之間契約的破壞,是無法恢復的程式bug。異常是指一些可能的情形,但很少發生。程式也許能從異常恢復。記憶體耗盡就是一個例子。異常在第4章詳述。

  未檢查的執行時錯誤是對客戶程式與實現之間契約的破壞,而實現並不保證能夠發現這樣的錯誤。如果發生未檢查的執行時錯誤,可能會繼續執行,但結果是不可預測的,甚至可能是不可重複的。好的介面會在可能的情況下避免未檢查的執行時錯誤,但必須規定可能發生的此類錯誤。例如,Arith必須指明除以零是一個未檢查的執行時錯誤。Arith雖然可以檢查除以零的情形,但卻不加處理使之成為未檢查的執行時錯誤,這樣介面中的函式就模擬了C語言內建的除法運算子的行為(即,除以零時其行為是未定義的)。使除以零成為一種已檢查的執行時錯誤,也是一種合理的方案。

  已檢查的執行時錯誤是對客戶程式與實現之間契約的破壞,但實現保證會發現這種錯誤。這種錯誤表明,客戶程式未能遵守契約對它的約束,客戶程式有責任避免這類錯誤。Stack介面規定了三個已檢查的執行時錯誤:

  (1) 向該介面中的任何例程傳遞空的Stack_T型別的指標;

  (2) 傳遞給Stack_free的Stack_T指標為NULL指標;

  (3) 傳遞給Stack_pop的棧為空。

  介面可以規定異常及引發異常的條件。如第4章所述,客戶程式可以處理異常並採取校正措施。未處理的異常(unhandled exception)被當做是已檢查的執行時錯誤。介面通常會列出自身引發的異常及其匯入的介面引發的異常。例如,Stack介面匯入了Mem介面,它使用後者來分配記憶體空間,因此它規定Stack_new和Stack_push可能引發Mem_Failed異常。本書中大多數介面都規定了類似的已檢查的執行時錯誤和異常。

  在向Stack介面新增這些之後,我們可以繼續進行其實現:

  〈stack.c〉≡

   #include

   #include "assert.h"

   #include "mem.h"

   #include "stack.h"

  

   #define T Stack_T

   〈types 18〉

   〈functions 18〉

  #define指令又將T定義為Stack_T的縮寫。該實現披露了Stack_T的內部結構,它是一個結構,一個欄位指向一個連結串列,連結串列包含了棧上的各個指標,另一個欄位統計了指標的數目。

  〈types 18〉≡

   struct T {

   int count;

   struct elem {

   void *x;

   struct elem *link;

   } *head;

  };

  Stack_new分配並初始化一個新的T:

  〈functions 18〉≡

   T Stack_new(void) {

   T stk;

  

   NEW(stk);

   stk->count = 0;

   stk->head = NULL;

   return stk;

  }

  NEW是Mem介面中一個用於分配記憶體的巨集。NEW(p)為p指向的結構分配一個例項,因此Stack_ new中使用它來分配一個新的Stack_T結構例項。

  如果count欄位為0,Stack_empty返回1,否則返回0:

  〈functions 18〉+≡

   int Stack_empty(T stk) {

   assert(stk);

   return stk->count == 0;

   }

  assert(stk)實現了已檢查的執行時錯誤,即禁止對Stack介面函式中的Stack_T型別引數傳遞NULL指標。assert(e)是一個斷言,聲稱對任何表示式e,e都應該是非零值。如果e非零,它什麼都不做,否則將中止程式執行。assert是標準庫的一部分,但第4章的Assert介面定義了自身的assert,其語義與標準庫類似,但提供了優雅的程式終止機制。assert用於所有已檢查的執行時錯誤。

  Stack_push和Stack_pop分別在stk->head連結串列頭部新增和刪除元素:

  〈functions 18〉+≡

   void Stack_push(T stk, void *x) {

   struct elem *t;

  

   assert(stk);

   NEW(t);

   t->x = x;

   t->link = stk->head;

   stk->head = t;

   stk->count++;

   }

  

  void *Stack_pop(T stk) {

   void *x;

   struct elem *t;

  

   assert(stk);

   assert(stk->count > 0);

   t = stk->head;

   stk->head = t->link;

   stk->count--;

   x = t->x;

   FREE(t);

   return x;

   }

  FREE是Mem用於釋放記憶體的巨集,它釋放其指標引數指向的記憶體空間,並將該引數設定為NULL指標,這與Stack_free的做法同理,都是為了避免懸掛指標。Stack_free也呼叫了FREE:

  〈functions 18〉+≡

   void Stack_free(T *stk) {

   struct elem *t, *u;

  

   assert(stk && *stk);

   for (t = (*stk)->head; t; t = u) {

   u = t->link;

   FREE(t);

   }

   FREE(*stk);

   }

  該實現披露了一個未檢查的執行時錯誤,本書中所有的ADT介面都會受到該錯誤的困擾,因而並沒有在介面中指明。我們無法保證傳遞到Stack_push、Stack_pop、Stack_empty的Stack_T值和傳遞到Stack_free的Stack_T*值都是Stack_new返回的有效的Stack_T值。習題2.3針對該問題進行了探討,給出一個部分解決方案。

  還有兩個未檢查的執行時錯誤,其效應可能更為微妙。本書中許多ADT通過void指標通訊,即儲存並返回void指標。在任何此類ADT中,儲存函式指標(指向函式的指標)都是未檢查的執行時錯誤。void指標是一個類屬指標(generic pointer,通用指標),型別為void *的變數可以容納指向一個物件的任意指標,此類指標可以指向預定義型別、結構和指標。但函式指標不同。雖然許多C編譯器允許將函式指標賦值給void指標,但不能保證void指標可以容納函式指標 。

  通過void指標傳遞任何物件指標都不會損失資訊。例如,在執行下列程式碼之後,

  S *p, *q;

  void *t;

  ...

  t = p;

  q = t;

  對任何非函式的型別S,p和q都將是相等的。但不能用void指標來破壞型別系統。例如,在執行下列程式碼之後,

  S *p;

  D *q;

  void *t;

  ...

  t = p;

  q = t;

  我們不能保證q與p是相等的,或者根據型別S和D的對齊約束,也不能保證q是一個指向型別D物件的有效指標。在標準C語言中,void指標和char指標具有相同的大小和表示。但其他指標可能小一些,或具有不同的表示。因而,如果S和D是不同的物件型別,那麼在ADT中儲存一個指向S的指標,將該指標返回到一個指向型別D的指標中,這是一個未檢查的執行時錯誤。

  在ADT函式並不修改被指向的物件時,程式設計師可能很容易將不透明指標引數宣告為const。例如,Stack_empty可能有下述編寫方式。

  int Stack_empty(const T stk) {

   assert(stk);

   return stk->count == 0;

  }

  const的這種用法是不正確的。這裡的意圖是將stk宣告為一個“指向struct T的常量例項的指標”,因為Stack_empty並不修改*stk。但const T stk將stk宣告為一個“常量指標,指向一個struct T例項”,對T的typedef將struct T *打包到一個型別中,這一個指標型別成為了const的運算元 。無論對Stack_empty還是其呼叫者,const T stk都是無用的,因為在C語言中,所有的標量包括指標在函式呼叫時都是傳值的。無論有沒有const限定符,Stack_empty都無法改變呼叫者的實參值。

  用struct T *代替T,可以避免這個問題:

  int Stack_empty(const struct T *stk) {

   assert(stk);

   return stk->count == 0;

  }

  這個用法說明了為什麼不應該將const用於傳遞給ADT的指標:const披露了有關實現的一些資訊,因而限制了可能性。對於Stack的這個實現而言,使用const不是問題,但它排除了其他同樣可行的方案。假定某個實現預期可重用棧中的元素,因而延遲對棧元素的釋放操作,但會在呼叫Stack_empty時釋放它們。Stack_empty的這種實現需要修改* stk,但因為*stk宣告為const而無法進行修改。本書中的ADT都不使用const。

相關文章