天天看點

《C語言接口與實作:建立可重用軟體的技術》一2.4 客戶程式的職責

本節書摘來自異步社群《c語言接口與實作:建立可重用軟體的技術》一書中的第2章,第2.4節,作者 傅道坤,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

接口是其實作和其客戶程式之間的一份契約。實作必須提供接口中規定的功能,而客戶程式必須根據接口中描述的隐式和顯式的規則來使用這些功能。程式設計語言提供了一些隐式規則,來支配接口中聲明的類型、函數和變量的使用。例如,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接口添加這些之後,我們可以繼續進行其實作:

define指令又将t定義為stack_t的縮寫。該實作披露了stack_t的内部結構,它是一個結構,一個字段指向一個連結清單,連結清單包含了棧上的各個指針,另一個字段統計了指針的數目。

stack_new配置設定并初始化一個新的t:

new是mem接口中一個用于配置設定記憶體的宏。new(p)為p指向的結構配置設定一個執行個體,是以stack_new中使用它來配置設定一個新的stack_t結構執行個體。

如果count字段為0,stack_empty傳回1,否則傳回0:

assert(stk)實作了已檢查的運作時錯誤,即禁止對stack接口函數中的stack_t類型參數傳遞null指針。assert(e)是一個斷言,聲稱對任何表達式e,e都應該是非零值。如果e非零,它什麼都不做,否則将中止程式執行。assert是标準庫的一部分,但第4章的assert接口定義了自身的assert,其語義與标準庫類似,但提供了優雅的程式終止機制。assert用于所有已檢查的運作時錯誤。

stack_push和stack_pop分别在stk->head連結清單頭部添加和删除元素:

free是mem用于釋放記憶體的宏,它釋放其指針參數指向的記憶體空間,并将該參數設定為null指針,這與stack_free的做法同理,都是為了避免懸挂指針。stack_free也調用了free:

該實作披露了一個未檢查的運作時錯誤,本書中所有的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指針可以容納函數指針[1]。

通過void指針傳遞任何對象指針都不會損失資訊。例如,在執行下列代碼之後,

對任何非函數的類型s,p和q都将是相等的。但不能用void指針來破壞類型系統。例如,在執行下列代碼之後,

我們不能保證q與p是相等的,或者根據類型s和d的對齊限制,也不能保證q是一個指向類型d對象的有效指針。在标準c語言中,void指針和char指針具有相同的大小和表示。但其他指針可能小一些,或具有不同的表示。因而,如果s和d是不同的對象類型,那麼在adt中存儲一個指向s的指針,将該指針傳回到一個指向類型d的指針中,這是一個未檢查的運作時錯誤。

在adt函數并不修改被指向的對象時,程式員可能很容易将不透明指針參數聲明為const。例如,stack_empty可能有下述編寫方式。

const的這種用法是不正确的。這裡的意圖是将stk聲明為一個“指向struct t的常量執行個體的指針”,因為stack_empty并不修改stk。但const t stk将stk聲明為一個“常量指針,指向一個struct t執行個體”,對t的typedef将struct t 打包到一個類型中,這一個指針類型成為了const的操作數[2]。無論對stack_empty還是其調用者,const t stk都是無用的,因為在c語言中,所有的标量包括指針在函數調用時都是傳值的。無論有沒有const限定符,stack_empty都無法改變調用者的實參值。

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

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

繼續閱讀