Download presentation
Presentation is loading. Please wait.
Published byVeronika Hermawan Modified 6年之前
4
本章中將會更詳細地考慮有關重複的概念,並且會 介紹for和do…while等兩種用來控制重複的敘述 式。
也將會介紹switch多重選擇敘述式。 我們會討論直接和迅速離開某種控制敘述式的 break敘述式,以及用來跳過重複敘述式本體剩餘 部份的continue敘述式。 本章會討論用來組合控制條件的邏輯運算子,最後 則會對第三、四章介紹的結構化程式設計加以整理。
5
迴圈是指當某一個迴圈繼續條件式 (loop-continuation condition) 為真時,電腦會重複執行的一群指令。
我們已經討論過以下這兩種重複的方法: 計數器控制的重複結構 警示訊號控制的重複結構 因為計數器控制的重複在迴圈開始執行之前就已經知道重複 的次數,所以有時候將它稱為明確重複 (definite repetition)。 而警示值控制的重複有時稱為非明確重複 (indefinite repetition),因為我們事先並不知道迴圈需重複執行多少次。
6
在計數器控制的重複當中,必須使用控制變數 (control variable) 來計算重複的次數。
每次這群指令執行之後,就會遞增控制變數 (通常 是1)。 當控制變數的值顯示已經執行了正確的重複次數時, 就會結束此迴圈,電腦會繼續執行重複敘述式之後 的敘述式。
7
在下列兩種情形時,必須用警示值來控制重複的動 作:
不能事先知道重複的次數,以及 此種迴圈會包含一個每次迴圈執行時都會讀取資料的敘述 式。 警示值表示「資料結束」。 警示值會在所有正式的資料項都輸入之後,才會輸 入到程式當中。 警示值必須與其他正式的資料項不同。
8
計數器控制的重複需要有: 控制變數 (或迴圈計數器) 的名稱 控制變數的初始值 (initial value)
每一次迴圈執行的時候控制變數的遞增 (或遞減) 量 判斷控制變數是否是終止值 (final value) 的條件 (也就是 檢查迴圈是否會繼續)。
9
考慮圖4.1列出的簡單程式,該程式會印出從1到 10的整數。 其中定義
int counter = 1; /* initialization */ 為控制變數 (counter) 命名(name),將它定義 為整數,為它保留記憶體空間,並且將它的初始值 (initial value) 指定為1。 這個定義並不是可執行的敘述式。
12
定義counter,以及指定counter初值的動作也 可以用以下的敘述式來完成
int counter; counter = 1; 宣告是不可執行的,而指定動作是可執行的。 這兩種方式都可以用來為變數指定初始值。 敘述式 ++counter; /* increment */ 在每次迴圈執行時讓迴圈計數器遞增 (increment) 的值是1。
13
在while敘述式中,迴圈持續執行的條件式 (loop- continuation condition) 會測試控制變數的值是 否小於等於10 (也就是會讓控制條件為真的最後一 個值)。
當控制變數超過10時 (例如:counter變成11), 這個迴圈就會結束。
14
你可以將圖4.1中的counter的初始值設為0,並且 會將while敘述式改寫如下,使得程式更為簡潔
while ( ++counter <= 10 ) printf( "%d\n", counter ); 因為遞增直接在while運算式中動作,並且在測試 控制條件之前完成,所以程式碼可以節省敘述式的 數量。 因為while現在只剩下一個敘述式,所以程式碼可 省略while本體原來所需的大括號, 但有些程式設計師卻認為這樣的程式碼不夠清楚明 瞭而且容易出錯。
19
for重複敘述式會自動處理計數器控制式重複的所 有細節。
輸出結果見圖4.2。
21
當for敘述式開始執行時,控制變數counter的值 會初始化為1。
因為counter的初始值1能滿足這個條件,所以 printf敘述式(第13列)會印出counter的值,也 就是印出1。 接著控制變數counter因運算式counter++而遞 增1,然後迴圈會再度測試迴圈持續執行的條件。
22
因為現在控制變數的值是2,也未超過終止值,因 此程式會再次執行printf敘述式。
這個程序將一直持續下去,直到控制變數counter 遞增到它的終止值11為止-這會使得測試迴圈持續 執行的條件變成失敗,並且結束重複動作。 接下來,程式會執行for敘述式之後的第一個敘述 式(在此例中為程式末端的return敘述式)。 圖4.3仔細檢視圖4.2中的for敘述式。
23
注意到for敘述式「作了每件事」-for敘述式指 定了含有控制變數的計數器控制重複結構所需要的 每個項目。
25
請注意圖4.2使用迴圈繼續條件counter <= 10。
這是一種常見的邏輯錯誤,稱為誤差為一的錯誤 (off-by-one error)。
28
在大部份的情況下,for敘述式可以用以下等效的 while敘述式來表示:
for ( expression1; expression2; expression3 ) statement 其中的expression1為迴圈的控制變數指定初始 值,expression2是迴圈的繼續條件,而 expression3則會遞增控制變數。 在大部份的情況下,for敘述式可以用以下等效的 while敘述式來表示: expression1; while ( expression2 ) { statement expression3; }
29
不過這個規則有例外狀況,我們將會在第4.9節討 論。
通常expression1和expression3是由逗號分隔 的一連串運算式。 逗號在這裡是當成逗號運算子 (comma operators),它確保這一連的運算式會由左到右執 行運算。 這一連串由逗號所分隔的運算式,其型別和數值是 由逗號分隔的一連串運算式中最右邊的運算式來決 定。
30
逗號運算子最常用在for敘述式中。 它的主要功用是讓程式設計師能夠使用多重的初始 值指定和(或)多重的遞增運算式。 例如在同一個for敘述式裡,可能會有兩個控制變 數必須指定初始值和遞增。
32
for敘述式裡的三個運算式都是可有可無的。
如果我們省略了expression2的話,則C會認為控 制條件永遠為真,因而建立一個無窮迴圈。 如果控制變數已在程式其他位置設定好了初始值, 則我們可以省略expression1。 如果遞增動作在迴圈本體內的敘述式當中執行,或 是不需要遞增動作,則expression3便可省略。 for敘述式當中的遞增表示式就像是位在for迴圈 本體結尾的一行C獨立敘述式。
33
許多C程式設計師較偏好使用counter++,因為遞增 動作是在迴圈本體執行後才進行的。所以用後置遞增格 式似乎較為自然。
因此下列各運算式 counter = counter + 1 counter += 1 ++counter counter++ 對for敘述式的遞增部分來說是一樣的。 許多C程式設計師較偏好使用counter++,因為遞增 動作是在迴圈本體執行後才進行的。所以用後置遞增格 式似乎較為自然。 因為這裡的前置遞增或後置遞增並沒有出現在大的運算 式中,這兩種遞增格式的效果是相同的。 最後提醒一點,for敘述式中的兩個分號是不能省略的。
36
初始值指定,迴圈繼續條件,和遞增的部分都可以 包含算術運算式。例如,假設x=2, y=10,底下的 敘述式
for ( j = x; j <= 4 * x * y; j += y / x ) 和以下的敘述式是相等的。 for ( j = 2; j <= 80; j += 5 ) 遞增量可以是負的 (在這種情況下是為遞減,亦即 迴圈是往下計數的)。 如果迴圈繼續條件一開始為偽,則迴圈的本體部分 將不會執行。
37
控制變數經常會由迴圈本體列印或使用,但並非一 定要這麼做。我們也常只用控制變數來控制重複的 次數,而在迴圈的本體內並不會使用它。
for敘述式的流程圖十分類似while敘述式。例如, 圖4.4展示了下列敘述式的流程圖 for ( counter = 1; counter <= 10; counter++ ) printf( "%d", counter ); 這張流程圖清楚地表示初始值指定動作只進行一次, 以及遞增發生在本體敘述式執行之後。
40
下列的範例展示了用來改變for敘述式內控制變數 的幾種方法。
將控制變數從1遞增到100 (遞增量為1) 。 for ( i = 1; i <= 100; i++ ) 將控制變數從100遞增到100,遞增量為-1(也就是遞減 量為1)。 for ( i = 100; i >= 1; i-- ) 將控制變數從7遞增到77,每次遞增7。 for ( i = 7; i <= 77; i += 7 ) 將控制變數從20變到2,每次遞增-2。 for ( i = 20; i >= 2; i -= 2 ) 將控制變數按照下列數列進行改變: 2, 5, 8, 11, 14, 17. for ( j = 2; j <= 17; j += 3 ) 將控制變數按照下列數列進行改變: 44, 33, 22, 11, 0. for ( j = 44; j >= 0; j -= 11 )
41
接下來兩個簡單的例子將討論for敘述式的簡單應 用。
43
請注意,圖4.5中,for敘述式的本體,事實上可以 經由逗號運算子的使用合併到for標頭的最右邊部 分,如下:
for ( number = 2; number <= 100; sum += number, number += 2 ) ; /* empty statement */ 而初始值指定sum=0也可合併到for的初始值指定 部分。
46
這個問題會用一個迴圈來計算每年結算時的本利和。 答案如圖4.6所示。
考慮以下的問題敘述: 某人將$1000元存在一個年利率為5%的存款帳戶裡。假 設利息採複利計算,請計算並印出十年中每年結算時,這 個帳戶內的錢是多少。請利用下列的公式來計算: a = p(1 + r)n 其中 p是原存款數(也就是本金) r是年利率 n是年數 a是第n年結算時的本利和 這個問題會用一個迴圈來計算每年結算時的本利和。 答案如圖4.6所示。
49
此for敘述式會執行迴圈的本體10次,並且將控制 變數由1遞增到10。
雖然C並沒有次方運算子,不過我們可以用標準函 式庫中的函式pow來做這件事。 函式pow(x, y)會計算出x之y次方的值。 它的兩個引數的型別都是double,其傳回值的型 別也是double。 double型別是種類似float的浮點數型別,不過 double可以表示的值以及精確度要比float大很 多。
50
請注意,當我們使用類似pow這樣的數學函式時, 應該將前置檔math.h(第4列)含括進來。
事實上如果沒有include進math.h,這個程式可 能會遇到些問題,連結器可能沒有辦法找到pow函 式。 pow函式需要兩個double型別的引數,但year是 一個整數。 math.h這個檔案裡含有一些資訊,用來告訴編譯 器在呼叫pow函式之前,要先將year的值轉換為一 個暫時性的double值。
51
這個資訊是包含在pow的函式原型 (prototype) 當 中。
我們將在第五章進一步介紹函式的原型, 我們也會在第五章提供pow函式以及其它數學函式 的總整理。 請注意到在此程式中,我們將變數amount, principal和rate宣告為double型別。 這麼做是為了簡單化,因為我們處理金額可能會帶 有小數。
53
讓我們簡單解釋一下,使用float或double表示 金錢時會發生什麼問題。
兩個存在電腦裡的float金錢數可能是14.234(以 %.2f列印時印出14.23)和18.673(以%.2f列印 時印出18.67)。 當這兩個值相加時會得到32.907,此值以%.2f列 印會印出32.91。
54
但很明顯的,14.23+18.67應該是32.90才對!現 在你已經得到了警惕!
因此列印時將會出現 但很明顯的, 應該是32.90才對!現 在你已經得到了警惕! 在程式中我們使用轉換指定詞%21.2f來列印變數 amount的值。 轉換指定詞中的21表示將要列印的欄位寬度(field width)。
55
亦即欄位寬度為21,在列印時將佔用21個列印單 位。
2則表示此數的精確度(即小數點後有幾位)。 如果顯示字元的數量小於欄寬,則這個值會自動地 在欄位內靠右對齊(right justified)。 用在對齊一些具有相同精確度的浮點數時會很有用 (會自動對齊小數點)。
56
若我們想要靠左對齊(left justify)的話,只要在%和 欄位寬度之間加一個負號(-)即可。
負號也可以用來控制整數(%-6d)和字串(如%-8s)的 靠左對齊。
57
而有時候演算法可能需要進行一連串的判斷,來檢驗某一變 數或運算式是否為數個常數整數中的一個,並針對不同情況 採取不同的動作。
這就稱為多重選擇。 C提供了switch多重選擇敘述式來處理這類型的判斷。 switch敘述式包含了一連串的case標籤(每個case標籤對 應到一組可執行的敘述式),以及一個可有可無的default case。 圖4.7的程式使用了switch來計算在某次考試中,學生們的 成績屬於各個等級(譯注:A, B, C, D和F)的個數。
63
在這個程式中,使用者輸入全班學生的成績等級。 在while敘述式的標頭裡 (第19列)
while ( ( grade = getchar() ) != EOF ) 括號中的( grade = getchar () )會先執行。 getchar函式 (來自<stdio.h>) 從鍵盤讀進一個 字元,並將此字元存放到整數變數grade當中。 字元通常是存到char型別的變數中。 不過C有一項重要的功能,便是可以將字元存成任 何的整數資料型別,因為在電腦裡字元是表示成一 個位元組的整數。
64
因此我們可視需要將字元當成一個整數或是一個字 元。 例如,以下的敘述式: 使用了轉換指定詞%c和%d印出字元a和它的整數值。 其結果為:
printf( "The character (%c) has the value %d.\n", 'a', 'a' ); 使用了轉換指定詞%c和%d印出字元a和它的整數值。 其結果為: The character (a) has the value 97. 整數97是字元在電腦中的數字表示方式。
65
目前大多數的電腦都使用ASCII (美國標準資訊交換 碼) 字元集,而在此字元集中,97便代表了小寫字 母'a'。
附錄B列出了ASCII字元及其十進制數值。 字元可以在scanf裡用轉換指定詞%c讀進來。 整個指定敘述式也具有值, 就是指定給等號左邊之變數的值。 因此grade = getchar()這個指定運算式的值, 就是由getchar()傳回並且指定給變數grade的 字元。
66
指定敘述式具有值這項事實,對我們將數個變數設 為同一個值是很有用的。 例如, 此敘述式會先執行c = 0 (因為=運算子是由右往左 結合)。
a = b = c = 0; 此敘述式會先執行c = 0 (因為=運算子是由右往左 結合)。 然後變數b會設定成c = 0的值(為0)。 然後變數a會設定成b =(c = 0) 的值(也是0)。 在這個程式當中,指定式grade = getchar()的 值會拿來與EOF(代表end of file)比較。
67
亦即我們是以EOF當成警示值(EOF通常為-1)。
用者根據系統的指定鍵入某種按鍵組合,來表示 「end of file」,亦即表示"我已經沒有別的資料要 輸入了"。EOF是定義在<stdio.h>前置檔裡的一 個整數常數 (在第六章我們將討論到如何定義符號 常數) 。 如果指定給grade的值等於EOF,則程式便會停止 輸入資料。 在此程式中選用整數來表示字元,因為EOF是個整 數值 (通常為-1)。
70
在Linux/UNIX系統中,EOF的輸入是在鍵盤上鍵入 下列的按鍵
<Ctrl> d 單獨在一行。 這個符號<Ctrl> d的意思是按下Enter鍵,然後再 同時按下ctrl和d鍵。 而在其他系統,如微軟公司的Windows裡,EOF的 輸入是鍵入 <Ctrl> z 在視窗系統中,你可能也需要按下Enter鍵。 使用者從鍵盤輸入成績的等級。
71
當你按下Enter鍵後,輸入的字元便由getchar函 式讀取 (一次只讀一個字元)。
如果所輸入的字元不等於EOF,程式便進入了 switch敘述式 (第22列) 。 在關鍵字switch之後是用括號包起來的變數名稱 grade。 這稱為控制運算式 (controlling expression)。 這個運算式的值會拿來與每一個case標籤 (case labels) 比較。 假設使用者輸入了字母C做為成績, 則C便會自動與switch中的每一個case比較。
72
如果找到一個符合要求的(case 'C':),則此 case的敘述式就會執行。
在字母C的case當中,cCount會遞增1(第36行), 然後switch敘述式將因break敘述式而馬上結束。 break敘述式會使程式從switch敘述式之後的第 一個敘述式繼續執行。 我們必須要使用break敘述式,否則switch當中 的case敘述式們會一起執行。 我們在switch敘述式中若沒有使用break敘述式 的話,則剩餘case們中的敘述式將都會執行。
73
(這項功能幾乎沒什麼用,雖然它很適合用來撰寫重複 歌曲The Twelve Days of Christmas !)
如果找不到符合的case,將會執行default case,它會印出錯誤 訊息。 每一個case可以有一個或一個以上的動作。 switch敘述式不同於其他的敘述式,它並不需為某一case裡的數個 動作加上大括號。 圖4.8所示為一般的switch多重選擇敘述式(每一個case均使用 break)的流程圖。 此流程圖清楚指出,在每個case之後的break敘述式會使程式的控 制立即離開switch敘述式。
79
在圖4.7的switch敘述式裡,下面幾行 會使程式跳過新行、tab和空白字元。 一次只讀一個字元會引起一些困擾。
case '\n': /* ignore newlines, */ case '\t': /* tabs, */ case ' ': /* and spaces in input */ break; /* exit switch */ 會使程式跳過新行、tab和空白字元。 一次只讀一個字元會引起一些困擾。
80
為了命令程式讀進輸入的字元,我們必須按鍵盤上 的Enter鍵,將字元送給電腦,
這會使得newline字元放在我們所希望處理的字元 之後。 通常我們必須特別處理新行字元,使程式能正確運 作。 在switch敘述式中加入上述的case之後,便可避 免每次newline、tab或空白輸入時,由default case列印的錯誤訊息。
83
請注意當數個case標籤列在一起時 (如圖4.7的 case ‘D’: case ‘d’: ),只是表示他們都會 進行同樣的一組動作。
當我們在使用switch敘述式時,請記住它只能檢 驗常數整數的運算式 (constant integral expression,即任何字元常數和整數常數所組合成 的運算式,其值也是個常數)。 字元常數的表示方式是將字元放在單引號中,如'A'。
84
字元必須被放在單引號中,才被視為是字元常數。 被放在雙引號中的字元會被視為字串。
而整數常數則是一般的整數值。 在本例中示範了字元常數。 請不要忘記字元實際上是一個小的整數值。
85
整數型別的注意事項 具可攜性的語言 (如C),其資料型別的大小必須具有彈性。 不同的應用程式可能需要不同大小的整數。
每一種型別的範圍,取決於電腦的硬體。 除了int和char型別之外,C還提供了short (short int的簡寫) 和long (long int的簡寫) 型別。 C規定short整數值的最小範圍為-32768到+32767。 對大多數的整數運算而言,long整數便已經夠用了。
86
整數型別的注意事項 C標準規定long整數值的最小範圍為-2147483648到 +2147483647,
而int的範圍至少要跟short的範圍相同,而最多不可超 過long的範圍。 至於char型別則可以用來表示-128到+127之間的整數, 或者電腦字元集中的任何一個字元。
87
do…while重複敘述式十分類似while敘述式。
因此迴圈本體至少會執行一次。 當do…while終止時,程式由while子句之後的第 一個敘述式繼續執行。
88
如果迴圈的本體只有一個敘述式的話,do…while 敘述式裡便不一定要用大括號。
不過我們通常還是會加上大括號,以避免while和 do…while敘述式造成混淆。 例如, while ( condition ) 通常認為是while敘述式的標頭。
89
而本體只有一個敘述式的do...while敘述式,若 不用大括號將此敘述式包起來的話,將會如下所示
do statement while ( condition ); 這兩者很容易令人混淆。 最後一行的while( condition );-可能會讓讀者 誤以為是一個含有空敘述式的while敘述式。 因此,只有一個敘述式的do…while通常寫成如下 的格式,以避免混淆:
92
圖4.9的程式利用一個do…while敘述式來印出從1 到10的整數。
請注意控制變數counter在迴圈繼續條件的檢驗時 進行前置遞增。 也請注意我們用大括號,將只有一個敘述式的 do...while本體包起來。
94
圖4.10的do…while流程圖清楚地指出,迴圈繼續 條件在動作執行一次之前是不會檢驗的。
96
break和continue敘述式可以用來改變程式的控 制流程。
當break敘述式在while, for, do...while或 switch敘述式內執行時,會使得程式馬上離開那 個敘述式。 程式接著由敘述式之後的第一個敘述式繼續執行。 break敘述式通常用來早點跳離迴圈,或跳過 switch敘述式中剩下的部分(見圖4.7)。
97
圖4.11示範了break敘述式用在for重複敘述式裡 的情形。
在此程式中,當if敘述式偵測到x變成5的時候, 便執行break。 此舉會終止for敘述式,程式接著執行的是for之 後的printf。 迴圈總共完整地執行了4次。
100
當continue敘述式在while,for或do…while敘 述式中執行時,敘述式本體內尚未執行的敘述式會 跳過,而直接執行下一次的迴圈動作。
在while和do…while敘述式中,迴圈繼續條件會 在continue執行之後馬上檢驗。 而在for敘述式中,則會先執行遞增運算式,然後 再檢驗迴圈繼續條件。
101
稍早前我們曾說過,在大多數的情形之下,可以用 while敘述式來改寫for敘述式。
不過當while敘述式內的遞增運算式寫在 continue敘述式之後時,這項規則可就行不通了。 在這種情況下,遞增不會在迴圈繼續條件檢驗之前 被執行,此時while的行為和for便不相同了。 圖4.12的程式在for敘述式中使用了一個 continue敘述式,來跳過敘述式中的printf敘 述式,然後繼續下一次的迴圈動作。
107
C提供了邏輯運算子 (logical operator),可以將數 個簡單條件式組合成一個複雜的條件式。
邏輯運算子包括了&& (邏輯AND) ,|| (邏輯OR) , 和 ! (邏輯NOT,也稱為邏輯否定)。 我們將來看看每一個運算子的例子。 如果我們希望在兩種條件都為真的情況下,才執行 某項動作。
108
條件式 gender == 1 (例如用來判斷某人是否為 女性) 和條件式 age >= 65 (例如用來判斷某人是否為年 長者)。
則我們可以用邏輯運算子&&,如下所示: if ( gender == 1 && age >= 65 ) ++seniorFemales; 這個if敘述式包含了兩個簡單的條件。 條件式 gender == 1 (例如用來判斷某人是否為 女性) 和條件式 age >= 65 (例如用來判斷某人是否為年 長者)。 這兩個條件會先執行,因為==和>=的優先順序均 比&&為高。
109
這個條件只有在兩個簡單條件都為真時才是真。 最後,如果此複合條件確實為真的話,便將 seniorFemales的值遞增1。
接下來if敘述式會考慮下面的複合條件 gender == 1 && age >= 65 這個條件只有在兩個簡單條件都為真時才是真。 最後,如果此複合條件確實為真的話,便將 seniorFemales的值遞增1。 如果簡單條件有一個 (或兩個都是) 為偽的話,程式 便會跳過遞增的動作,然後從if之後的第一個敘述 式繼續執行。 圖4.13的表列出了 && 運算子的行為特性。
110
表中列出了運算式1和運算式2之零(false)與非零 (true)值的四種可能組合。
這種表通常稱為真值表 (truth table)。 C會將所有的運算式(含有關係運算子,相等運算 子,和/或邏輯運算子)計算成0或1。 雖然C將真值設為1,不過只要是不為零的值都可接 受當成真。
112
現在讓我們來看看 ||(邏輯OR)運算子。 如果我們希望在任一個或兩種條件皆為真的情況下, 才執行某項動作,
我們可以運用 || 運算子,如下列的程式片段所示: if ( semesterAverage >= 90 || finalExam >= 90 ) printf( "Student grade is A\n" );: 此敘述式也包含了兩個簡單條件。 條件semesterAverage >= 90用來決定學生是 不是因一整個學期的學習成果可以得到"A"。
113
而條件finalExam >=90 則用來決定學生是不是 因期末考的表現出色而得到"A"。 接下來if敘述式會考慮下面的複合條件
semesterAverage >= 90 || finalExam >= 90 如果這兩個簡單條件中任一個(或兩個都是)為真 的話,就將這個學生的成績評定為 A。 只有在兩個條件皆為偽(0)時才不會印出這項訊息。 圖4.14列出了邏輯OR運算子(||)的真值表。
115
一個含有&&或||的運算式會一直執行,直到真或偽 成立為止。 因此,下列條件
&&運算子的運算優先順序比||高。 這兩個運算子均由左至右結合。 一個含有&&或||的運算式會一直執行,直到真或偽 成立為止。 因此,下列條件 gender == 1 && age >= 65 的執行在發現gender不等於1時停止 (此時整個條 件便為偽) ,而當gender等於1時繼續下去 (如果 age >= 65也為真的話,整個條件就為真) 。 這種計算邏輯AND和邏輯OR運算式的方式稱為 「捷徑計算」 (short-circuit evaluation)。.
117
(邏輯否定) 運算子,讓程式設計師將條件的意義朝相反 方向解釋。
它不像&&和||運算子結合了兩個條件式 (因此是二元運 算子),!運算子只有一個條件當成運算元 (因此是個一 元運算子)。 當我們想讓程式在某個條件為偽時執行某項動作,便可 在此條件之前加一個邏輯否定運算子,如底下的程式片 段: if ( !( grade == sentinelValue ) ) printf( "The next grade is %f\n", grade ); 包圍條件grade == sentinelValue的括號是必須的, 因為邏輯否定運算子的運算優先順序比相等運算子高。 圖4.15所列為邏輯否定運算子的真值表。
119
在大多數情況下,程式設計師可改用更恰當的關係 運算子,來避免使用邏輯否定運算子。 例如,上述的程式片段可改寫如下:
if ( grade != sentinelValue ) printf( "The next grade is %f\n", grade ); 圖4.16列出了到目前為止,我們所介紹過之運算子 的運算優先順序和結合性。 優先權順序是以表格的上方逐次往下遞減。
121
不管是有沒有經驗的C程式設計師,都很容易犯一 種錯誤。我們覺得有必要用一個章節來討論這種錯 誤。
這種錯誤就是:不小心地混用== (相等) 運算子和 = (指定) 運算子。 這種混用為什麼這麼危險呢?原因在於它通常不會 造成編譯錯誤。 含有這種錯誤的程式通常可正確地編譯,但程式執 行的結果卻可能會因執行時的邏輯錯誤而不正確。
122
引起這個問題的原因是來自於C的兩項特性。
如果值是零,便當做偽。如果值不是零,則當做真。 第二,C的指定動作會產生值,此值便是指定給指 定運算子左邊之變數的值。
123
but we accidentally write
舉例來說,假設我們想要寫的是 if ( payCode == 4 ) printf( "You get a bonus!" ); but we accidentally write if ( payCode = 4 ) printf( "You get a bonus!" ); 第一個if敘述式會在payCode等於4的時候,正確 地印出"You get a bonus!"這項訊息。 第一個if敘述式會在payCode等於4的時候,正確 地印出"You get a bonus!"這項訊息。第二個 if敘述式是一道有錯的敘述式,它會執行if條件 裡的指定運算式。
124
這個運算式是個簡單的指定動作,其值為常數4。
因為任何非零的數值都會解釋為「真」,因此if敘 述式的條件將永遠為真,不管那個人的payCode是 多少,都會拿到獎金!。
126
程式設計師在撰寫條件式時,通常會將變數名稱寫 在左邊,而將常數寫在右邊,如x == 7。
編譯器會認為這是一個語法錯誤,因為只有變數名 稱才能放在指定運算子的左邊。 至少這樣子可避免因不小心所造成的執行時邏輯錯 誤。
127
變數名稱認為是個lvalues ("left values", 即左邊數 值),因為他們可放在指定運算子的左邊。
常數認為是個rvalues ("right values",即右邊數 值),因為他們只能放在指定運算子的右邊。 lvalues也可以用做rvalues,但反過來則不行。
129
假設原先你想要把一個值設定給變數,如下列的簡 單敘述式:
另外一種情況是將=誤寫成==。 假設原先你想要把一個值設定給變數,如下列的簡 單敘述式: x = 1; 卻不小心地寫成 x == 1; 同樣的,這也不會是個語法錯誤。 編譯器只會把它當成是一個比較運算式。
130
如果x等於1的話,則此條件為真並且運算式會傳回 值1。
很不幸的,我們並沒有什麼法寶可以幫助你解決這 個問題。
132
圖4.17是第三和第四章所討論之控制敘述式的總整 理。
圖中的小圓形代表每一控制敘述式的單一入口和單 一出口。 任意地連接個別的流程圖符號可能會導致非結構化 的程式。 因此,程式設計的風格應以組合流程圖符號來構成 限定的一些控制敘述式,並以兩種方式正確地組合 控制敘述式來建構出結構化程式。
133
簡而言之,僅使用單一入口/單一出口的控制敘述 式,也就是說,每個控制敘述式只有一個方式能夠 進入也只有一個方式能夠離開。
循序地連接控制敘述式以形成結構化程式是很簡單 的-某一控制敘述式的出口點直接連到另一控制敘 述式的入口點,亦即在程式中一個接一個地放置控 制敘述式;這種方式稱為「控制敘述式的堆疊」。 此外我們還可以將控制敘述式連接成巢狀的敘述式。
134
圖4.18列出了正確建構結構化程式的規則。 這些規則假設矩形的流程圖符號可以用來表示任何 的動作,包括輸入/輸出在內。 圖4.19為最簡單的流程圖。 運用了圖4.18的規則一定能夠讓我們得到一張整齊 的,區塊狀的結構化流程圖。 例如,將規則2重複地運用到最簡單的流程圖 (圖 4.19) 身上,將可得到一張含有許多循序排列之矩 形的結構化流程圖 (圖4.20)。 請注意規則2產生了控制敘述式的堆疊,因此讓我 們稱之為堆疊規則 (stacking rule)。
135
規則3稱為巢狀規則 (nesting rule)。
對最簡單的流程圖重複運用規則3的話,將可得到 一張含有整齊巢狀控制敘述式的流程圖。 舉例來說,在圖4.21中,最簡單的流程圖的矩形首 先換成一個雙重選擇if…else敘述式。 然後再將規則3運用到雙重選擇敘述式的兩個矩形 身上,以雙重選擇敘述式換掉了每一個矩形。 圖中用虛線包起來的雙重選擇敘述式代表矩形替換 掉的部分。
136
圖4.18的規則4可產生更大,更複雜且更深的巢狀 敘述式。
運用此規則所畫出的流程圖可包含任何可能的結構 化流程圖,亦即包含了所有可能的結構化程式。 由於我們希望消除goto敘述式,因此這些建構用的 區塊彼此不會有重疊的現象發生。 結構化方法最漂亮的地方在於它只使用數種簡單的 單一入口/單一出口的零件,而且只能以兩種方式 來組合這些零件。
137
圖4.22所示為運用規則2所得到的堆疊式區塊,和 運用規則3所得到的巢式區塊。
另外,此圖也顯示了不會出現在結構化流程圖中的 重疊式區塊(因為結構化方法去除了goto敘述 式)。
144
如果我們能遵守圖4.18的規則的話,便不會產生如圖4.23 所示的非結構化流程圖。
如果你無法確定某個流程圖是否為結構化的,則你可以用圖 4.18的規則試著反向化簡此流程圖,看它最後能不能化簡成 最簡單的流程圖。 如果可以的話,表示此流程圖是一個結構化流程圖,否則便 不是。 結構化程式設計促成了程式的簡單化。 Bohm和Jacopini的研究告訴我們,所有的程式均可由三種 控制結構 (control structure) 寫成: 循序結構 (sequence structure) 選擇結構(selection structure) 重複結構(repetition structure)
147
其中循序是很自然的一種。 選擇可以用下列三種方式來製作:
if敘述式 (單一選擇) if...else敘述式 (雙重選擇) switch 敘述式 (多重選擇) 事實上,我們可以很直觀地證明,只需簡單的if敘 述式便能提供任何形式的選擇--任何以if…else 敘述式和switch敘述式所表示的事物都能用一個 或多個if敘述式來取代。
148
事實上while敘述式已足以提供任何形式的重複。 任何以do...while和for敘述式所表示的事物,均 能用while敘述式來取代。
重複是以下列三種方式中的一個來製作: while 敘述式 do…while 敘述式 for 敘述式 事實上while敘述式已足以提供任何形式的重複。 任何以do...while和for敘述式所表示的事物,均 能用while敘述式來取代。
149
綜合以上所述,我們可得到以下的結論。C程式中 所需要的任何形式的控制,都可以由下列三種控制 格式來達成:
sequence if statement (selection) while statement (repetition) 而且這些控制敘述式只能以兩種方式組合--堆疊 式和巢狀。 因此,結構化程式設計確實促進了簡單化。
150
在第三和第四章裡,我們討論了如何用含有動作和 判斷的控制敘述式來組成程式。
第五章我們將介紹另一種稱為函式 (function) 的程 式結構化單元。 我們將學習如何將控制敘述式組合成函式,以及如 何將函式組合成更大的程式。 此外,我們也將討論如何利用函式來提升軟體的再 使用性。
Similar presentations