天天看點

《C語言接口與實作:建立可重用軟體的技術》一第2章 接口與實作2.1 接口

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

c語言接口與實作:建立可重用軟體的技術

子產品分為兩個部分,即子產品的接口與實作。接口規定了子產品做什麼。接口會聲明辨別符、類型和例程,提供給使用子產品的代碼。實作指明子產品如何完成其接口規定的目标。對于給定的子產品,通常隻有一個接口,但可能有許多實作提供了接口規定的功能。每個實作可能使用不同的算法和資料結構,但它們都必須合乎接口的規定。

客戶程式(client)是使用子產品的一段代碼。客戶程式導入接口,實作則導出接口。客戶程式隻需要看到接口即可。實際上,它們可能隻有實作的目标碼。多個客戶程式共享接口和實作,因而避免了不必要的代碼重複。這種方法學也有助于避免bug,接口和實作編寫并調試一次後,可以經常使用。

接口僅規定客戶程式可能使用的那些辨別符,而盡可能隐藏不相關的表示細節和算法。這有助于客戶程式避免依賴特定實作的具體細節。客戶程式和實作之間的這種依賴性稱之為耦合(coupling),在實作改變時耦合會導緻bug,當依賴性被與實作相關的隐藏或隐含的假定掩蓋時,這種bug可能會特别難于改正。設計完善且陳述準确的接口可以減少耦合。

對于接口與實作相分離,c語言隻提供了最低限度的支援,但通過一些簡單的約定,我們即可獲得接口/實作方法學的大多數好處。在c語言中,接口通過一個頭檔案指定,頭檔案的擴充名通常為.h。這個頭檔案會聲明客戶程式可能使用的宏、類型、資料結構、變量和例程。客戶程式用c預處理器指令#include導入接口。

以下例子說明了本書中的接口使用的約定。下述接口

聲明了6個整數算術運算函數。該接口的實作需要為上述每一個函數提供定義。

該接口命名為arith,接口頭檔案命名為arith.h。在接口中,接口名稱表現為每個辨別符的字首。這種約定并不優美,但c語言幾乎沒有提供其他備選方案。所有檔案作用域中的辨別符,包括變量、函數、類型定義和枚舉常數,都共享同一個命名空間。所有的全局結構、聯合和枚舉标記則共享另一個命名空間。在一個大程式中,在本來無關的子產品中,很容易使用同一名稱表示不同的目的。避免這種名稱沖突(name collision)的一個方法是使用字首,如子產品名。一個大程式很容易有數千全局辨別符,但通常隻有幾百個子產品。子產品名不僅提供了适當的字首,還有助于使客戶程式代碼文檔化。

arith接口中的函數提供了标準c庫缺失的一些有用功能,并對除法和模運算提供了良定義的結果,而标準則将這些操作的行為規定為未定義(undefined)或由具體實作來定義(implementation-defined)。

arith_min和arith_max函數分别傳回其整型參數的最小值和最大值。

arith_div傳回x除以y獲得的商,而arith_mod則傳回對應的餘數。當x和y都為正或都為負時,arith_div(x,y)等于x/y,而arith_mod(x,y)等于x%y。然而當兩個操作數符号不同時,由c語言内建運算符所得出的傳回值取決于具體編譯器的實作。當y為零時,arith_div和arith_mod的行為與x/y和x%y相同。

c語言标準隻是強調,如果x/y是可表示的,那麼(x/y)y + x%y必須等于x。當一個操作數為負數時,這種語義使得整數除法可以向零舍入,也可以向負無窮大舍入。例如,如果-13/5的結果定義為-2,那麼标準指出,-13%5必須等于-13 - (-13/5)5 = -13 - (-2)5 = -3。但如果-13/5定義為-3,那麼-13%5的值必須是-13 - (-3)5 = 2。

因而内建的運算符隻對正的操作數有用。标準庫函數div和ldiv以兩個整數或長整數為輸入,并計算二者的商和餘數,在一個結構的quot和rem字段中傳回。這兩個函數的語義是良定義的:它們總是向零舍入,是以div(-13,5).quot總是等于-2。arith_div和arith_mod同樣是良定義的。它們總是向數軸的左側舍入,當其操作數符号相同時向零舍入,當其符号不同時向負無窮大舍入,是以arith_div(-13,5)傳回-3。

arith_div和arith_mod的定義可以用更精确的數學術語來表達。arith_div(x,y)定義為不超過實數z的最大整數,而zy=x。因而,對x=-13和y=5(或者x = 13和y= -5),z為-2.6,是以arith_div(-13,5)為-3。arith_mod(x,y)定義為等于x - yarith_div(x,y),是以arith_mod(-13,5)為-13 -5*(-3) - 2。

arith_ceiling和arith_floor函數遵循類似的約定。arith_ceiling(x,y)傳回不小于x/y的實數商的最小整數,而arith_floor(x,y)傳回不大于x/y的實數商的最大整數。對所有操作數x和y來說,arith_ceiling傳回數軸在x/y對應點右側的整數,而arith_floor傳回x/y對應點左側的整數。例如:

即便簡單如arith這種程度的接口仍然需要這麼費勁的規格說明,但對大多數接口來說,arith的例子很有代表性和必要性(很讓人遺憾)。大多數程式設計語言的語義中都包含漏洞,某些操作的精确含義定義得不明确或根本未定義。c語言的語義充滿了這種漏洞。設計完善的接口會塞住這些漏洞,将未定義之處定義完善,并對語言标準規定為未定義或由具體實作定義的行為給出明确的裁決。

arith不僅是一個用來顯示c語言缺陷的人為範例,它也是有用的,例如對涉及模運算的算法,就像是哈希表中使用的那些算法。假定i從零到n - 1,其中n大于1,并對i加1和i減1的結果模n。即,如果i為n-1,i+1為0,而如果i為0,i-1為n-1。下述表達式

正确地對i進行了加1模n和減1模n的操作。表達式i = (i+1) % n可以工作,但i = ( i-1) % n無法工作,因為當i為0時,(i-1) % n可能是-1或n-1。程式員在(-1) % n傳回n-1的計算機上可以使用(i-1) %n,但如果依賴這種由具體實作定義的行為,那麼在将代碼移植到(-1) % n傳回-1的計算機上時,就可能遭遇到非常出人意料的行為。庫函數div(x,y)也無濟于事。它傳回一個結構,其quot和rem字段分别儲存x/y的商和餘數。在i為零時,div(i-1, n).rem總是-1。使用i = (i-1+n) % n是可以的,但僅當i-1+n不造成溢出時才行。

繼續閱讀