6. goto語句和標號

分支、循環都講完了,現在只剩下最後一種影響控制流程的語句了,就是goto語句,實現無條件跳轉。我們知道break只能跳出最內層的循環,如果在一個嵌套循環中遇到某個錯誤條件需要立即跳出最外層循環做出錯處理,就可以用goto語句,例如:

for (...)
	for (...) {
		...
		if (出現錯誤條件)
			goto error;
	}
error:
	出錯處理;

這裡的error:叫做標號(Label),任何語句前面都可以加若干個標號,每個標號的命名也要遵循標識符的命名規則。

goto語句過于強大了,從程序中的任何地方都可以無條件跳轉到任何其它地方,只要在那個地方定義一個標號就行,唯一的限制是goto只能跳轉到同一個函數中的某個標號處,而不能跳到別的函數中[11]濫用goto語句會使程序的控制流程非常複雜,可讀性很差。著名的計算機科學家Edsger W. Dijkstra最早指出編程語言中goto語句的危害,提倡取消goto語句。goto語句不是必須存在的,顯然可以用別的辦法替代,比如上面的代碼段可以改寫為:

int cond = 0; /* bool variable indicating error condition */
for (...) {
	for (...) {
		...
		if (出現錯誤條件) {
			cond = 1;
			break;
		}
	}
	if (cond)
		break;
}
if (cond)
	出錯處理;

通常goto語句只用於這種場合,一個函數中任何地方出現了錯誤條件都可以立即跳轉到函數末尾做出錯處理(例如釋放先前分配的資源、恢復先前改動過的全局變數等),處理完之後函數返回。比較用goto和不用goto的兩種寫法,用goto語句還是方便很多。但是除此之外,在任何其它場合都不要輕易考慮使用goto語句。有些編程語言(如C++)中有異常(Exception)處理的語法,可以代替gotosetjmp/longjmp的這種用法。

回想一下,我們在第 4 節 “switch語句”學過casedefault後面也要跟冒號(:號,Colon),事實上它們是兩種特殊的標號。和標號有關的語法規則如下:

語句 → 標識符: 語句
語句 → case 常量表達式: 語句
語句 → default: 語句

反覆應用這些語法規則進行組合可以在一條語句前面添加多個標號,例如在例 4.2 “缺break的switch語句”的代碼中,有些語句前面有多個case標號。現在我們再看switch語句的格式:

switch (控製表達式) {
case 常量表達式: 語句列表
case 常量表達式: 語句列表
...
default: 語句列表
}

{}裡面是一組語句列表,其中每個分支的第一條語句帶有casedefault標號,從語法上來說,switch的語句塊和其它分支、循環結構的語句塊沒有本質區別:

語句 → switch (控製表達式) 語句
語句 → { 語句列表 }

有興趣的讀者可以在網上查找有關Duff's Device的資料,Duff's Device是一段很有意思的代碼,正是利用“switch的語句塊和循環結構的語句塊沒有本質區別”這一點實現了一個巧妙的代碼優化。



[11] C標準庫函數setjmplongjmp配合起來可以實現函數間的跳轉,但只能從被調用的函數跳回到它的直接或間接調用者(同時從棧空間彈出一個或多個棧幀),而不能從一個函數跳轉到另一個和它毫不相干的函數中。setjmp/longjmp函數主要也是用於出錯處理,比如函數A調用函數B,函數B調用函數C,如果在C中出現某個錯誤條件,使得函數BC繼續執行下去都沒有意義了,可以利用setjmp/longjmp機制快速返回到函數A做出錯處理,本書不詳細介紹這種機制,有興趣的讀者可參考[APUE2e]