跳轉到內容

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(false),則 **i** 將會超出 **myArray** 的索引範圍。在這種情況下,程式永遠不會嘗試訪問 **myArray[i]**,因為表示式的真假已經確定為假。因此,如果已經知道 i 大於或等於零,我們無需擔心嘗試訪問超出範圍的陣列元素。類似的事情也會發生在使用或 || 運算子的表示式中。

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

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

If-Else 語句

[edit | edit source]

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 之後的括號中的條件計算結果為非零(true),則第一個程式碼塊將執行;否則,第二個程式碼塊將執行。

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) 為非零(true),則程式將免於評估額外的 if 語句。最重要的是,通常最好為所有情況下插入 else 子句,在這些情況下,條件將不會計算為非零(true)。

條件表示式

[edit | edit source]

條件表示式是透過比 If-Else 更簡短的方式有條件地設定值的一種方法。語法如下:

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

邏輯表示式將被評估。如果它是非零(true),則整個條件表示式將計算為 ? 和 : 之間的表示式,否則它將計算為 : 之後的表示式。因此,上面的示例(稍作修改,使 c 在 a 和 b 相等時設定為 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 後面指定的 value,則該結構將“啟用”,或者開始執行 case 語句後面的程式碼。一旦該結構“啟用”,就不會再評估 case

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

非常重要:通常,每個 case 的最後一個語句都是 break 語句。這會導致程式執行跳轉到 switch 語句的結束括號後面的語句,這通常是人們希望發生的。但是,如果省略 break 語句,則程式執行將繼續執行下一個 case 的第一行(如果有)。這稱為 fall-through。當程式設計師需要此操作時,應該在語句塊的末尾放置一個註釋,說明需要 fall through。否則,維護程式碼的另一個程式設計師可能會認為省略 '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;
 }

一組要執行的語句可以與多個變數值分組,如下面的示例所示。(fall-through 註釋在這裡不需要,因為預期的行為很明顯)

 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;
 }

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

迴圈

[edit | edit source]

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

While 迴圈

[edit | edit source]

While 迴圈是最基本的迴圈型別。它將一直執行,直到條件為非零(true)。例如,如果您嘗試以下操作,程式將似乎鎖死,您需要手動關閉程式。條件永遠不會滿足退出迴圈的條件的情況稱為無限迴圈。

 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;
 }

所有迴圈的流程也可以由 **break** 和 **continue** 語句控制。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 語句處,在那裡它會重新評估控制條件。如果該條件在那一刻為“true”(即使它在迴圈內部的幾條語句中暫時為 0),則計算機將再次開始執行迴圈內部的語句;否則計算機將退出迴圈。計算機在執行 While 迴圈期間不會“持續檢查”控制條件。它只在每次到達迴圈頂部的 while 時“檢視”一下控制條件。

請務必注意,一旦 While 迴圈的控制條件變為 0(false),迴圈將不會終止,直到程式碼塊執行完畢並且需要重新評估條件。如果您需要在達到某個條件時立即終止 While 迴圈,請考慮使用 **break**。

一個常見的習慣用法是編寫:

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

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

For 迴圈

[編輯 | 編輯原始碼]

For 迴圈通常看起來像這樣

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

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

每次在 for 迴圈中的程式碼執行之前,都會評估測試表示式。 如果該表示式在檢查時評估為 0(false)(即表示式不為真),則不會(重新)進入迴圈,並且執行將繼續正常地在 FOR 迴圈之後的程式碼處繼續。 如果表示式為非零(true),則執行迴圈大括號內的程式碼。

在每次迴圈迭代之後,都會執行增量語句。 這通常用於增加迴圈索引,即在初始化表示式中初始化並在測試表達式中測試的變數。 在此語句執行之後,控制權將返回到迴圈頂部,測試操作將在那裡發生。 如果在 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 語句。 空測試條件實際上評估為 true。

在 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 而不是 if 來編寫。 在這裡,我們可以讓“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 的情況下表達出來。 當更具慣用性的替代方案(例如 if-else 或 for 迴圈)可以更好地表達您的結構時,不加約束地使用 goto 會建立難以閱讀、難以維護的程式碼。 從理論上講,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;
}

參考資料

[編輯 | 編輯原始碼]


華夏公益教科書