跳轉到內容

C 程式設計/程式流程控制

來自華夏公益教科書,自由的教科書,開放的世界

很少有程式完全遵循一條控制路徑,並且每個指令都明確宣告。為了有效地程式設計,有必要了解如何根據使用者輸入或其他條件改變程式執行的步驟,如何用幾行程式碼多次執行某些步驟,以及如何使程式看起來能夠表現出基本的邏輯能力。C 中被稱為條件語句和迴圈的構造提供了這種能力。

從現在開始,有必要了解“塊”通常指的是什麼。塊是一組關聯的程式碼語句,旨在作為一個單元執行。在 C 中,程式碼塊的開始用 {(左花括號)表示,程式碼塊的結束用 } 表示。程式碼塊後面不需要分號。程式碼塊可以為空,例如 {}。程式碼塊也可以巢狀;即程式碼塊可以存在於更大的程式碼塊中。

條件語句

[編輯 | 編輯原始碼]

很可能在 C 語言中沒有一個有意義的程式,其中計算機沒有表現出基本的決策能力。實際上,可以認為,在任何有意義的人類活動中,都必然會發生某種決策,無論是本能的還是其他形式的。例如,當駕駛汽車並接近交通訊號燈時,人們不會想,“我將繼續穿過十字路口”。相反,人們會想,“如果訊號燈是紅色,我將停車,如果訊號燈是綠色,我將行駛,如果訊號燈是黃色,只有在我距離十字路口一定距離並且以一定速度行駛時,我才會行駛。”這些過程可以使用條件語句在 C 中模擬。

條件語句是一個指令,指示計算機僅在滿足特定條件時才執行特定的程式碼塊或更改特定資料。最常見的條件語句是 If-Else 語句,條件表示式和 Switch-Case 語句通常用作更簡短的方法。

在瞭解條件語句之前,有必要先了解 C 如何表達邏輯關係。C 將邏輯視為算術。值 0(零)表示假,而 _**所有其他值**_ 表示真。如果你選擇某個特定值來表示真,然後將值與它進行比較,遲早你的程式碼會在你假設的值(通常是 1)被證明是錯誤時失敗。不熟悉 C 語言的人編寫的程式碼通常可以透過使用 #define 來定義一個“TRUE”值來識別。 [1]

因為邏輯在 C 中是算術,所以算術運算子和邏輯運算子是相同的。然而,有一些運算子通常與邏輯相關聯。

關係和等價表示式

[編輯 | 編輯原始碼]
a < b
如果 _**a**_ 小於 _**b**_,則為 1,否則為 0。
a > b
如果 _**a**_ 大於 _**b**_,則為 1,否則為 0。
a <= b
如果 _**a**_ 小於或等於 _**b**_,則為 1,否則為 0。
a >= b
如果 _**a**_ 大於或等於 _**b**_,則為 1,否則為 0。
a == b
如果 _**a**_ 等於 _**b**_,則為 1,否則為 0。
a != b
如果 _**a**_ 不等於 _**b**_,則為 1,否則為 0。

新手程式設計師需要注意的是,“等於”運算子是 ==,而不是 =。這是導致許多編碼錯誤的原因,並且通常是一個難以查詢的錯誤,因為表示式 (a = b) 將 _**a**_ 設定為等於 _**b**_,然後計算為 _**b**_;但通常意圖的表示式 (a == b) 檢查 _**a**_ 是否等於 _**b**_。需要指出的是,如果你將 = 與 == 混淆,你的錯誤通常不會被編譯器提醒。語句,例如 if (c = 20) {} 被語言視為完全有效,但將始終將 20 分配給 _**c**_ 並計算為真。避免這種錯誤(在許多情況下,並非所有情況下)的一個簡單技巧是將常量放在前面。這將導致編譯器發出錯誤,如果 == 被誤寫成 =。

請注意,C 沒有像許多其他語言那樣具有專門的布林型別。0 表示假,其他任何值都表示真。因此,以下兩者是等價的

 if (foo()) {
   // do something
 }

 if (foo() != 0) {
   // do something
 }

通常使用 #define TRUE 1#define FALSE 0 來解決沒有布林型別的問題。這是不好的做法,因為它做了不成立的假設。最好是表明你實際上期望從函式呼叫中得到的結果,因為有許多不同的方法來指示錯誤條件,具體取決於情況。

 if (strstr("foo", bar) >= 0) {
   // bar contains "foo"
 }

這裡,strstr 返回找到子字串 foo 的索引,如果沒有找到,則返回 -1。注意:這將始終使用上一段中提到的 TRUE 定義失敗。如果省略了 >= 0,它也不會產生預期結果。

還需要注意的是,關係表示式計算的方式與數學文字中的計算方式不同。也就是說,表示式 myMin < value < myMax 的計算方式可能與你想象的有所不同。在數學上,這將測試 _**value**_ 是否在 _**myMin**_ 和 _**myMax**_ 之間。但在 C 中,首先將 _**value**_ 與 _**myMin**_ 進行比較。這將產生 0 或 1。正是這個值將與 myMax 進行比較。示例

 int value = 20;
 /* ... */
 if (0 < value < 10) { // don't do this! it always evaluates to "true"!
   /* do some stuff */
 }

因為 _**value**_ 大於 0,所以第一個比較產生值為 1。現在將 1 與小於 10 進行比較,這是真的,所以 if 語句中的語句將被執行。這可能不是程式設計師所期望的。正確的程式碼應該是

 int value = 20;
 /* ... */
 if (0 < value && value < 10) {   // the && means "and"
   /* do some stuff */
 }

邏輯表示式

[編輯 | 編輯原始碼]
a || b
當 _**a**_ 或 _**b**_ 為真(或兩者都為真)時,結果為 1,否則結果為 0。
a && b
當 _**a**_ 和 _**b**_ 都為真時,結果為 1,否則結果為 0。
!a
當 _**a**_ 為真時,結果為 0;當 _**a**_ 為 0 時,結果為 1。

以下是一個更大型的邏輯表示式的示例。在語句中

  e = ((a && b) || (c > d));

如果 a 和 b 不為零,或者 c 大於 d,則 e 設定為 1。在所有其他情況下,e 設定為 0。

C 使用邏輯表示式的短路求值。也就是說,一旦它能夠確定邏輯表示式的真值,它就不再進行進一步的求值。這在以下情況下通常很有用

int myArray[12];
....
if (i < 12 && myArray[i] > 3) { 
....

在程式碼片段中,首先將 i 與 12 進行比較。如果它計算為 0(假),則 _**i**_ 將超出作為 _**myArray**_ 的索引的範圍。在這種情況下,程式永遠不會嘗試訪問 _**myArray[i]**_,因為表示式的真值已知為假。因此,我們不需要擔心這裡試圖訪問超出範圍的陣列元素,如果已經知道 i 大於或等於零。對於包含或 || 運算子的表示式,也會發生類似的事情。

while (doThis() || doThat()) ...

如果 doThis() 返回非零(真)值,則永遠不會呼叫 doThat()。

If-Else 語句

[編輯 | 編輯原始碼]

If-Else 提供了一種方法來指示計算機僅在滿足特定條件時才執行程式碼塊。If-Else 構造的語法是

 if (/* condition goes here */) {
   /* if the condition is non-zero (true), this code will execute */
 } else {
   /* if the condition is 0 (false), this code will execute */
 }

如果緊隨 _**if**_ 之後的括號中的條件計算為非零(真),則執行第一個程式碼塊;否則,執行第二個程式碼塊。

_**else**_ 和後面的程式碼塊是完全可選的。如果不需要在條件不滿足時執行程式碼,則將其省略。

此外,請記住,_**if**_ 可以直接跟在 _**else**_ 語句之後。雖然這在某些情況下可能有用,但以這種方式將兩個或三個以上的 if-elses 連線起來被認為是不好的程式設計習慣。我們可以使用後面介紹的 Switch-Case 構造來解決這個問題。

需要指出兩個其他的一般語法注意事項,您在其他控制結構中也會看到它們:首先,請注意在ifelse之後沒有分號。可以有,但塊(用 { 和 } 包含的程式碼)代替了它。其次,如果您只打算執行一個語句作為ifelse的結果,則不需要花括號。但是,許多程式設計師認為在這種情況下插入花括號是一種良好的編碼習慣。

以下程式碼將變數 c 設定為兩個變數 a 和 b 中較大的那個,如果 a 和 b 相等,則設定為 0。

 if (a > b) {
   c = a;
 } else if (b > a) {
   c = b;
 } else {
   c = 0;
 }

考慮這個問題:為什麼不能只忘記else並將程式碼寫成

 if (a > b) {
   c = a;
 }
 
 if (a < b) {
   c = b;
 }
 
 if (a == b) {
   c = 0;
 }

對此有幾個答案。最重要的是,如果您的條件不互斥,則可能執行兩個情況,而不是隻有一個。如果程式碼不同,並且 a 或 b 的值在某個塊中以某種方式發生了變化(例如:您在比較之後將 a 和 b 中較小的那個重置為 0)?您最終可能會呼叫多個if語句,這不是您的本意。此外,評估if條件需要處理器時間。如果您使用else來處理這些情況,在上面的情況下假設 (a > b) 不為零(為真),則程式可以避免評估其他if語句的開銷。最重要的是,通常最好為所有條件將不評估為非零(真)的情況插入else子句。

條件表示式

[edit | edit source]

條件表示式是在比 If-Else 更簡潔的方式下有條件地設定值的一種方法。語法是

(/* logical expression goes here */) ? (/* if non-zero (true) */) : (/* if 0 (false) */)

邏輯表示式將被評估。如果它不為零(為真),則整個條件表示式將評估為放置在 ? 和 : 之間的表示式,否則,它將評估為 : 之後的表示式。因此,上面的示例(稍微更改其功能,以便在 a 和 b 相等時將 c 設定為 b)變為

c = (a > b) ? a : b;

條件表示式有時可以闡明程式碼的意圖。通常應該避免巢狀條件運算子。最好僅在 a 和 b 的表示式很簡單時使用條件表示式。此外,與初學者普遍的看法相反,條件表示式不會使程式碼更快。儘管假設較少的程式碼行會導致更快的執行時間很誘人,但沒有這樣的相關性。

Switch-Case 語句

[edit | edit source]

假設您編寫了一個程式,其中使用者輸入一個 1-5 的數字(對應於學生成績,A(表示為 1)-D(4)和 F(5)),將其儲存在變數grade中,程式透過將相關的字母等級列印到螢幕上進行響應。如果使用 If-Else 實現,程式碼將類似於以下內容

 if (grade == 1) {
   printf("A\n");
 } else if (grade == 2) {
   printf("B\n");
 } else if /* etc. etc. */

有一長串 if-else-if-else-if-else 可能會讓人頭疼,無論是對程式設計師還是對閱讀程式碼的任何人來說都是如此。幸運的是,有一個解決方案:Switch-Case 結構,其基本語法是

 switch (/* integer or enum goes here */) {
 case /* potential value of the aforementioned int or enum */:
   /* code */
 case /* a different potential value */:
   /* different code */
 /* insert additional cases as needed */
 default: 
   /* more code */
 }

Switch-Case 結構接受一個變數(通常是 int 或 enum),放置在switch之後,並將其與case關鍵字後面的值進行比較。如果變數等於case之後指定的值,則該結構將“啟用”或開始執行case語句之後的程式碼。一旦該結構“啟用”,將不會進一步評估case

Switch-Case 在語法上很“奇怪”,因為與case關聯的程式碼不需要花括號。

非常重要:通常,每個 case 的最後一條語句都是 break 語句。這會導致程式執行跳到 switch 語句的結束括號後面的語句,這是通常希望發生的情況。但是,如果省略了 break 語句,程式執行將繼續進行下一個 case 的第一行(如果有)。這稱為穿透。當程式設計師希望採取此操作時,應在語句塊的末尾放置一個註釋,表明希望穿透。否則,維護程式碼的另一位程式設計師可能會認為省略“break”是一個錯誤,並且無意中“糾正”了這個問題。這是一個例子

 switch (someVariable) {
 case 1:
   printf("This code handles case 1\n");
   break;
 case 2:
   printf("This prints when someVariable is 2, along with...\n");
   /* FALL THROUGH */
 case 3:
   printf("This prints when someVariable is either 2 or 3.\n" );
   break;
 }

如果指定了default case,則如果其他 case 都不匹配,則將執行關聯的語句。default case 是可選的。以下是一個與上面的 if-else if 語句序列對應的 switch 語句。

回到我們上面的例子。以下是它作為 Switch-Case 的樣子

 switch (grade) {
 case 1:
   printf("A\n");
   break;
 case 2:
   printf("B\n");
   break;
 case 3:
   printf("C\n");
   break;
 case 4:
   printf("D\n");
   break;
 default:
   printf("F\n");
   break;
 }

一組要執行的語句可以與變數的多個值一起分組,如下面的示例所示。(這裡不需要穿透註釋,因為預期行為是顯而易見的)

 switch (something) {
 case 2:
 case 3:
 case 4:
   /* some statements to execute for 2, 3 or 4 */
   break;
 case 1:
 default:
   /* some statements to execute for 1 or other than 2,3,and 4 */
   break;
 }

當與使用者定義的enum資料型別結合使用時,Switch-Case 結構特別有用。一些編譯器能夠警告未處理的列舉值,這可能有助於避免錯誤。

迴圈

[edit | edit source]

在計算機程式設計中,通常需要執行某個操作一定次數,或者直到滿足某個條件為止。簡單地多次鍵入某個語句或語句組是不切實際且繁瑣的,更不用說這種方法過於死板且不直觀,無法保證在發生特定事件時停止。作為一個現實世界的類比,有人問一家餐廳的洗碗工他整晚都做了什麼。他會回答,“我整晚都在洗碗”。他不太可能回答,“我洗了一個盤子,然後又洗了一個盤子,然後又洗了一個盤子……”。使計算機能夠執行某些重複任務的結構稱為迴圈。

While 迴圈

[edit | edit source]

while 迴圈是最基本的迴圈型別。只要條件不為零(為真),它就會執行。例如,如果您嘗試以下操作,程式將看起來像鎖定,您將不得不手動關閉程式。條件永遠不會變為真的迴圈退出情況稱為無限迴圈。

 int a = 1;
 while (42) {
   a = a * 2;
 }

以下是另一個 while 迴圈的示例。它打印出小於 100 的所有 2 的冪。

 int a = 1;
 while (a < 100) {
   printf("a is %d \n", a);
   a = a * 2;
 }

所有迴圈的流程也可以透過breakcontinue語句來控制。break 語句將立即退出封閉迴圈。continue 語句將跳過塊的剩餘部分,並再次從控制條件語句開始。例如

 int a = 1;
 while (42) { // loops until the break statement in the loop is executed
   printf("a is %d ", a);
   a = a * 2;
   if (a > 100) {
     break;
   } else if (a == 64) {
     continue;  // Immediately restarts at while, skips next step
   }
   printf("a is not 64\n");
 }

在此示例中,計算機照常列印 a 的值,並列印一條通知,說明 a 不等於 64(除非它被 continue 語句跳過)。

與上面的 If 類似,如果程式碼只包含一個語句,則可以省略與 While 迴圈關聯的程式碼塊的花括號,例如

 int a = 1;
 while (a < 100)
   a = a * 2;

這隻會增加 a,直到 a 不小於 100。

當計算機到達 while 迴圈的末尾時,它始終返回到迴圈頂部的 while 語句,在那裡它重新評估控制條件。如果該條件在那一刻為“真”(即使它在迴圈內部的幾個語句中暫時為 0),那麼計算機將再次開始執行迴圈內部的語句;否則計算機將退出迴圈。計算機不會在執行 while 迴圈期間“持續檢查”while 迴圈的控制條件。它只在每次到達迴圈頂部的while時“檢視”控制條件。

非常重要的是要注意,一旦 While 迴圈的控制條件變為 0(假),迴圈將不會終止,直到程式碼塊完成並且重新評估條件的時間到來。如果您需要在達到某個條件時立即終止 While 迴圈,請考慮使用break

一個常見的習慣用法是寫

 int i = 5;
 while (i--) {
   printf("java and c# can't do this\n");
 }

這將在 while 迴圈中執行程式碼 5 次,i 的值從 4 降至 0(在迴圈內)。方便的是,這些是訪問包含 5 個元素的陣列中每個專案的所需值。

For 迴圈

[edit | edit source]

For 迴圈通常看起來像這樣

for (initialization; test; increment) {
  /* code */
}

初始化語句只執行一次 - 在第一次評估測試條件之前。通常,它用於將初始值分配給某個變數,儘管這不是嚴格必要的。初始化語句也可以用於宣告和初始化迴圈中使用的變數。

每次在for迴圈中的程式碼執行之前,都會評估測試表示式。如果此表示式在檢查時評估為 0(假)(即表示式不為真),則不會(重新)進入迴圈,執行將繼續正常進行,從緊接 FOR 迴圈之後的程式碼開始。如果表示式不為零(為真),則將執行花括號內的程式碼。

在每次迭代迴圈之後,都會執行遞增語句。這通常用於遞增迴圈索引以進行迴圈,在初始化表示式中初始化並在測試表達式中測試的變數。在執行完該語句之後,控制將返回到迴圈的頂部,在那裡測試操作發生。如果在for迴圈中執行了continue語句,則遞增語句將是下一個執行的語句。

for 語句的每個部分都是可選的,可以省略。由於 for 語句的自由形式,可以用它做一些相當花哨的事情。通常,for 迴圈用於迴圈遍歷陣列中的專案,每次處理一個專案。

 int myArray[12];
 int ix;
 for (ix = 0; ix < 12; ix++) {
   myArray[ix] = 5 * ix + 3;
 }

上面的 for 迴圈初始化 myArray 的 12 個元素中的每一個。迴圈索引可以從任何值開始。在以下情況下,它從 1 開始。

 for (ix = 1; ix <= 10; ix++) {
   printf("%d ", ix);
 }

它將列印

1 2 3 4 5 6 7 8 9 10 

您最常使用從 0 開始的迴圈索引,因為陣列從零開始索引,但有時您也會使用其他值來初始化迴圈索引。

遞增操作可以執行其他操作,例如遞減。因此這種型別的迴圈很常見。

 for (i = 5; i > 0; i--) {
   printf("%d ", i);
 }

它會產生

5 4 3 2 1 

以下是一個測試條件只是一個變數的例子。如果該變數的值為 0 或 NULL,則迴圈退出,否則執行迴圈體中的語句。

 for (t = list_head; t; t = NextItem(t)) {
   /* body of loop */
 }

WHILE 迴圈可以用來執行與 FOR 迴圈相同的操作,但是 FOR 迴圈是執行一組固定次數的重複的更簡潔的方式,因為所有必要的資訊都包含在一個單行語句中。

FOR 迴圈也可以不指定任何條件,例如

 for (;;) {
   /* block of statements */
 }

這被稱為無限迴圈,因為它會一直迴圈,除非在 for 迴圈的語句中有一個 break 語句。空測試條件實際上被評估為真。

在 for 迴圈中使用逗號運算子執行多個語句也很常見。

 int i, j, n = 10;
 for (i = 0, j = 0; i <= n; i++, j += 2) {
   printf("i = %d , j = %d \n", i, j);
 }


在設計或重構條件部分時,應格外小心,特別是使用 < 或 <=,是否應該對起點和終點進行 1 的修正,以及字首和字尾表示法的情況。(在一條 100 碼的步行街上,每 10 碼有一棵樹,總共有 11 棵樹。)

 int i, n = 10;
 for (i = 0; i < n; i++)
   printf("%d ", i); // processed n times => 0 1 2 3 ... (n-1)
 printf("\n");
 for (i = 0; i <= n; i++)
   printf("%d ", i); // processed (n+1) times => 0 1 2 3 ... n 
 printf("\n");
 for (i = n; i--;)
   printf("%d ", i); // processed n times => (n-1) ...3 2 1 0 
 printf("\n");
 for (i = n; --i;)
   printf("%d ", i); // processed (n-1) times => (n-1) ...4 3 2 1 
 printf("\n");

Do-While 迴圈

[編輯 | 編輯原始碼]

DO-WHILE 迴圈是後檢查 while 迴圈,這意味著它在每次執行後檢查條件。因此,即使條件為零(false),它也會至少執行一次。它遵循以下形式

 do {
   /* do stuff */
 } while (condition);

請注意終止分號。這是正確語法所必需的。由於這也是一種 while 迴圈,因此breakcontinue 語句在迴圈中按預期工作。continue 語句會導致跳轉到條件測試,而break 語句會退出迴圈。

值得注意的是,Do-While 和 While 在功能上幾乎相同,但有一個重要的區別:Do-While 迴圈始終保證至少執行一次,而 While 迴圈如果其條件在第一次評估時為 0(false),則根本不會執行。

最後一點:goto

[編輯 | 編輯原始碼]

goto 是一種非常簡單和傳統的控制機制。它是一個語句,用於立即無條件地跳轉到另一行程式碼。要使用 goto,您必須在程式中的某個位置放置一個標籤。標籤由一個名稱後跟一個冒號 (:) 組成,並且位於單獨的一行中。然後,您可以在程式中的所需位置鍵入“goto label;”。然後,程式碼將從label 開始繼續執行。這看起來像

 MyLabel:
   /* some code */
   goto MyLabel;

goto 啟用的控制流轉移能力非常強大,以至於除了簡單的 if 之外,所有其他控制結構都可以使用 goto 代替。在這裡,我們可以讓“S”和“T”代表任何任意的語句

 if (''cond'') {
   S;
 } else {
   T;
 }
 /* ... */

相同的語句可以用兩個 goto 和兩個標籤來完成

 if (''cond'') goto Label1;
   T;
   goto Label2;
 Label1:
   S;
 Label2:
   /* ... */

這裡,第一個 goto 有條件地取決於“cond”的值。第二個 goto 是無條件的。我們可以對迴圈執行相同的轉換

 while (''cond1'') {
   S;
   if (''cond2'')
     break;
   T;
 }
 /* ... */

可以寫成

 Start:
   if (!''cond1'') goto End;
   S;
   if (''cond2'') goto End;
   T;
   goto Start;
 End:
   /* ... */

正如這些情況所示,通常程式正在執行的操作的結構通常可以不使用 goto 來表達。不加控制地使用 goto 會建立不可讀、不可維護的程式碼,而更慣用的替代方案(例如 if-elses 或 for 迴圈)可以更好地表達您的結構。從理論上講,goto 結構永遠不需要使用,但有些情況下它可以提高可讀性、避免程式碼重複或使控制變數變得不必要。您應該先考慮掌握慣用解決方案,只有在必要時才使用 goto。請記住,許多(如果不是大多數)C 風格的指南嚴格禁止使用goto,唯一常見的例外是以下示例。

goto 的一個用途是跳出深度巢狀的迴圈。由於break 無法工作(它只能跳出一個迴圈),因此可以使用goto 完全跳出迴圈。跳出深度巢狀的迴圈而無需使用 goto 始終是可能的,但通常涉及建立和測試額外的變數,這可能會使生成的程式碼的可讀性遠低於使用goto 的程式碼。使用goto 可以輕鬆地以有序的方式撤消操作,通常是為了避免未能釋放已分配的記憶體。

另一個公認的用途是建立狀態機。這雖然是一個相當高階的主題,但並不常見。

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int years;

	printf("Enter your age in years : ");
	fflush(stdout);
	errno = 0;
	if (scanf("%d", &years) != 1 || errno)
		return EXIT_FAILURE;
	printf("Your age in days is %d\n", years * 365);
	return 0;
}

參考資料

[編輯 | 編輯原始碼]


華夏公益教科書