(十)模組化開發 -- 2. 在模組中維護內部狀態
技術標籤:C語言的科學和藝術
2. 在模組中維護內部狀態
當呼叫一個典型的函式時,會宣告一些區域性變數,但是當函式返回時,這些變數值就被丟棄了。
因為必須要記錄內部狀態,掃描器的實現是不允許其中的函式每次返回時都丟棄所有資訊的。
2.1 全域性變數
在函式內部宣告的變數叫區域性變數,區域性變數僅存在於一個棧幀中。當函式返回時,其棧幀中的變數就完全消失了。
- 全域性變數(global variable)
變數宣告也可以在函式定義之外出現,以這種方式宣告的變數稱為全域性變數。
例如,在程式碼片段
int g;
void MyProceure()
{
int i;
...
}
中, 變數g
i
是區域性變數。區域性變數
i
僅在函式MyProceure
內有效,而全域性變數g
可以在模組中隨後宣告的任何函式中使用。
可以使用變數的程式部分叫變數的作用域。
這樣,區域性變數的作用域是定義它的函式中,全域性變數的作用域則是原始檔中在它所出現後的其餘部分。
和區域性變數不同,全域性變數以某一種方式保持在記憶體中,在這種方式下它的值不受函式呼叫的影響。
全域性變數保持相同的值直到給它賦予新值為止。
2.2 使用全域性變數的危險性
缺點:使用全域性變數會使得程式碼很難讀懂。
例如,在查詢一個由於變數被錯誤賦值而導致的程式錯誤。
如果是個全域性變數,由於模組中的每個函式都能操作那個變數,那麼問題可能存在於原始檔的任何位置;
為了避免這樣的問題,在結構良好的程式中,一般很少使用全域性變數。
它們的主要優點在於,在函式呼叫之間可以維護它們的值。
2.3 保持變數的模組私有化
全域性變數在一個模組的任何地方都可見只是使用它們的問題之一。
除非明確地宣告,否則C編譯器假定其他模組也可以看到全域性變數。
這樣,當聲明瞭一個全域性變數後,可能改變其值的函式不只是侷限在一個模組中。該變數可能被整個程式的任何模組引用。
在一個結構良好的程式中,獨立的模組之間通過在模組間傳遞引數的函式呼叫來交換資料。
在大多情況中,最好確保每個全域性變數不會被一個以上的模組引用。
為了避免兩個模組引用同一個全域性變數的可能性,應該在宣告前用關鍵字static
來徹底避免這種危險。如:
static int cpos;
這個宣告定義cpos
為一個全域性整型變數,在所定義的模組裡的任何地方都可見。
然而,cpos
對於別的模組是無效的,因此它是當前模組私有的。
在本書中,所有的全域性變數都用static
來宣告。
2.4 初始化全域性變數
- 動態初始化(dynamic initialization)
使用一個初始化函式來給代表模組內部狀態的全域性變數賦值的方式稱為動態初始化。
其主要特點是,在程式執行時執行。
在C語言中,也可以在程式執行前給全域性變數賦初值。
- 靜態初始化(static initialization)
由於這種初始化在執行程式之前發生,因而稱為靜態初始化。
為了定義一個全域性變數的靜態初始化,要在宣告中的變數名之後加上一個等號,然後跟上初值,初值必須是一個常量。
例如,宣告
static int startingValue=1;
不僅宣告startingValue
為此模組的私有全域性變數,而且保證當程式開始執行時變數的內容是1。
在本書中,大多數維護內部狀態的介面使用動態初始化,幷包括一個函式來顯式執行它。
然而對於以下兩種情況,靜態初始化是更好的選擇:
(1) 變數值在程式整個生命週期內都是常量。
(2) 變數的初值只有少數幾個客戶想要改變。
對於第二種情況,舉例如下:
假設一個掃描器模組的客戶要求改變掃描器介面以便所有的包含字母的記號都以大寫形式返回。這樣,在呼叫
InitScanner("Hello there");
之後,使用者希望記號為"HELLO","“和"THERE”。
這個行為只對其中某一些客戶會有用。
如何滿足這個客戶的需要,同時不會讓其他客戶不滿意呢?
需要在從GetNextToken
返回之前呼叫函式ConvertToUpperCase
。
另一方面,僅當用戶有此要求時才這樣做。
為了追蹤客戶是否想要大寫記號,可以宣告如下所示的全域性布林變數:
static bool uppercaseFlag;
如果uppercaseFlag
為真,掃描器全部返回大寫記號;如果為假,則照原樣返回。
如何初始化uppercaseFlag
,如何設計介面來讓客戶改變這個標誌的值?
這些問題引出了一些介面設計的重要問題。
一種途徑是,使用動態初始化。
客戶通過傳遞額外的布林引數給設定uppercaseFlag
選項的InitScanner
來選擇行為方式。即,呼叫:
InitScanner("Hello there", TRUE);
InitScanner("Hello there", FALSE);
然而,該方法存在兩個嚴重缺點:
(1) 程式閱讀者很難知道在呼叫InitScanner
時TRUE和FALSE引數的含義。為理解這些引數的用途,任何客戶都不得不閱讀介面註釋。
(2) 新的設計改變了已有的介面而破壞了穩定性。如果掃描介面已經有客戶,那麼這些客戶
不得不修改程式。
避免以上兩種問題的更好的辦法是,擴充套件掃描器介面而非改變它:
所有老的函式都像以前那樣工作;
為了提供返回大寫記號的選項,可以增加一個新的函式ReturnUppercaseTokens
,它帶有一個布林值,用來設定uppercaseFlag
。
因此,呼叫
ReturnUppercaseTokens(TURE);
客戶可以選擇返回大寫記號的新的行為模式。
使用老的scanner.h
介面的現有程式中沒有一個程式會呼叫ReturnUppercaseTokens
。
為了保證它有恰當的值,需要使用靜態初始化:
static bool uppercaseFlag=FALSE;
- 預設值(default value)
除非客戶專門採取行動去改變,否則一直在程式裡使用的值稱為預設值。
在一個典型的模組中,指定客戶可以設定的選項的全域性變數通常被靜態初始化為其預設值。
客戶通過呼叫介面提供的函式來改變這些值。
2.5 私有函式
關鍵字static
除了用於全域性變數外,還可以用來指示某函式是某個特定模組的私有函式。
定義介面時,介面匯出的函式不是私有的。介面的要點就是讓這些函式可以在其他模組中呼叫。
在很多情況下,介面還包括一些只能在當前模組中呼叫的函式。
要指出某一函式是否被限制在一個特定的模組中,可以把關鍵字static
放在函式原型和其實現的前面。
這樣使得客戶無法呼叫這些函式,從而使介面與使用者間的抽象邊界更加堅固穩定。
宣告函式為static
在由幾個程式設計師參與開發的大型程式環境中也有好處。
在不同函式中,避免名字相互干擾,就可以使用static
關鍵字來保證他們使用的名字對於自己的模組的私有化。
如下的規則對於模組化開發來說是極好的指導:
參考
《C語言的科學和藝術》 —— 第10章 模組化開發