大家晚安。還記得上次提到了如何使用陣列或結構(甚至兩者搭配起來)來宣告大量的變數,把它們分別編號化或模擬成一個物件。
然而宣告歸宣告,如果只憑靠先前豬腳分享的這些技巧,那仍然應付不了大量處理的需求,自然就無法發揮電腦的優勢了,不是嗎?
所以,今天就來談談程式的精髓「迴圈」吧。當我們在開發程式的時候,總是會遇上需要重複執行類似行為的時候,例如我們想要求 2 的 0 次方到 30 次方的值(並且顯示出來),那我們可以這麼做:
int result = 1;
printf("2 的 0 次方為 %d\n", result);
result *= 2;
printf("2 的 1 次方為 %d\n", result);
result *= 2;
//(這段中間我就省略了,因為都是重複的動作)
printf("2 的 29 次方為 %d\n", result);
result *= 2;
printf("2 的 30 次方為 %d\n", result);
算一算,包含宣告變數,單就這樣從 0 到 30 次方的計算,這樣加起來就會需要用到總共 62 行的程式碼,那不是很麻煩嗎?這時候我們有個新的做法:
int result = 1;
for (int i=0; i<=30; i++) {
printf("2 的 %d 次方為 %d\n", i, result);
result *= 2;
}
然後把它拿來執行:
天啊豬腳,這真是太神奇了!我們居然只用了 5 行程式碼就可以做到剛才花了 62 行程式碼才能做到的事情。
沒有錯,上述舉例的東西就是所謂的迴圈了。在 C 語言當中,我們有三種迴圈的形式可以使用:
1. for 迴圈
2. while 迴圈
3. do-while 迴圈
剛才的例子,相信大家一看就可以猜得到是上列的 for 迴圈了。
一般來說,我們把 for 迴圈的設定(也就是小括號那段)依序分成三個部分:
1. 初始行為
2. 執行條件
3. 單次迴圈結束行為
回過頭來看剛剛的迴圈:
for (int i=0; i<=30; i++) { ... }
括號當中的意思依序是:
1. 先宣告一個變數 i(並且設定初始值為 0)
2. 設定迴圈的執行條件:「只要 i 小於或等於 30,那我就執行大括號裡面的行為」
3. 每做完一次大括號裡的行為以後,所需要做的事情
看起來好像很複雜,但其實我們可以把 1 和 2 擺在一起看,簡單來說就是令 i = 0 到 30,而其中驅動 i 往上加的,就是 i++ 這個東西了。
先前我們曾經說過,i++ 可以用來代替 i += 1 這件事情,它「同時」也可以代表 i 這個數,也就是說它具備了數字的本質,也具備了「行為」。當然在這邊你可以把 i++ 改為 i += 1,但大部分的人習慣上還是用 i++,畢竟方便又漂亮。
你還可以用「++i」來做到一樣的事,但它同時表示的是「i+1」這個數而不是「i」,不信你用 int i = 7; 和 printf("%d", ++i); 試試看就知道了。
順帶補充一點,將 int i=0 寫在 for 迴圈裡的行為,可能會因為開發環境的設定不同而無法成功編譯,遇到這種情況時我們可以改成:
int i;
for (i=0; i<=30; i++) { ... }
如果有遇到編譯問題的時候還請自己更改。我個人習慣是把宣告變數(int i=0)寫在 for 迴圈的括號裡,因此以下我仍然會把宣告變數跟 for 迴圈寫在一起。
咳,回正題,也就是說整個迴圈的流程是這樣:
1. 宣告變數 i,並且令它為 0
2. 判斷 i 是否小於或等於 30,如果是的話就執行 { ... } 裡面的東西
3. 令 i 往上加一
4. 重複 2 跟 3 的動作,直到條件不符合就停
你也可以把它倒過來做:
int result = 1073741824;
for (int i=30; i>=0; i--) {
printf("2 的 %d 次方為 %d\n", i, result);
result /= 2;
}
這樣的話,就會從 30 次方開始往下輸出,最後 i 會被扣到 -1 而令迴圈的循環結束。
當然,如果單純想要計算次方的話,可以不需要像上例這麼麻煩得自己用變數儲存結果,如果我們在開頭使用 #include <math.h> 將 C 語言裡面自帶的數學相關函式標頭檔嵌進來,就可以把上面整段改成:
for (int i=30; i>=0; i--) {
printf("2 的 %d 次方為 %d\n", i, (int)pow(2, i));
}
這樣只需要三行就可以解決了。可以注意到這裡比較特別的是 pow(2, i):在 C 語言的 math.h 裡面,定義了 pow(x, y) 函數,表示 x 的 y 次方。然而,math.h 將這個函數的計算結果定義為 double 資料型態,所以如果你想要直接把它塞進平時給 int 用的 %d,就會顯示不出東西(除非你換成 %lf,但會有小數點),也因為如此,我們才會需要在這前面加上 (int) 來把結果轉換成 int。
當然,for 迴圈沒有規定每次都只能 +1,如果你需要的話,也可以跳著做,這就看你如何去操控這個迴圈計數器(也就是 i,你想把它取別的名字也可以):
for (int i=0; i<=30; i+=2) {
printf("2 的 %d 次方為 %d\n", i, (int)pow(2, i));
}
結果就會是這樣:
總之呢,for 迴圈可以很直觀地設定起點跟終點(而且這個 i 變數的使用範圍會被限定在該迴圈裡,不會跟外面的同名變數衝突),還能設定方向(增或減)和速度(每次增減數量),是三種迴圈裡面最基本也最泛用的。
另外一種則是「while」迴圈,它沒有硬性規定必須使用像上面 for 迴圈所用到的計數器,而是單純用「條件」來判斷。例如今天有個情境:「假設豬腳的存款有 18,000 元,銀行每個月都會給予 0.08% 的利息(複利),那麼豬腳要經過幾個月,存款總額才會達到 19,000 元呢?」
我們這樣做:
int monthCount = 0;
double money = 18000;
while (money <= 19000) {
money *= 1.0008;
monthCount++;
}
printf("經過了 %d 個月, 豬腳的存款終於來到了 %lf", monthCount, money);
首先把初始值設定好(也就是豬腳原有的存款,以及用來儲存「經過了幾個月」的計數器),接著就直接開始執行迴圈:
1. 只要豬腳的存款還沒達到 19,000
2. 把它下個月的存款算出來,儲存結果
3. 因為已經過了一個月,所以把計數器往上 +1
重複上面三個步驟,直到存款達 19,000 為止,這樣我們就可以知道豬腳在每月複利計息 0.8% 的情況下,需要花 68 個月才能夠從 18,000 變成 19,000 了。
實務上,while 迴圈通常都是用來達成「由外部條件來決定的持續行為」,又或者是用來做「持續不停的行為」,例如你想做個「每秒跳一次的計時器」,就可以這麼做:
#include <stdio.h>
#include <windows.h>
int main(void) {
while (1) {
Sleep(1000); // 請注意這個是 windows.h 裡面才有的東西
printf("滴答\n");
}
}
這裡就要談到條件式的問題了:為什麼這裡要填 1 呢?
實際上,我們平常在 C 語言裡面做 if (...) {...}、while (...) {...}、for (...) {...} 的時候,當我們要做條件判斷(例如上面銀行利息的例子當中「money <= 19000」這個地方),這個填寫條件的位置,最後都是會計算出一個「結果」的。
在電腦眼中,這麼複雜的東西它們做不來,所以會逐步把式子化簡,然後才變成它們可以直接處理的東西。例如,在 67 個月過去以後,在電腦眼中,這個 while 迴圈的條件式就會依序這樣變化:
1. while (money <= 19000)
(讀取 money 變數儲存的資料)
2. while (18990.717921 <= 19000)
(因為 18990.717921 <= 19000 成立,所以是 1)
3. while (1)
4. 因為 while 裡面的結果是 1,所以執行 {...} 裡面的東西
到了第 68 個月的利息結算完以後,則是:
1. while (money <= 19000)
(讀取 money 變數儲存的資料)
2. while (19005.910495 <= 19000)
(因為 19005.910495 <= 19000 不成立,所以是 0)
3. while (0)
4. 因為 while 裡面的結果是 0,所以不執行 {...} 裡面的東西,跳出迴圈
所以,在上面的計時器中,可以用常數 1 來讓迴圈強制重複執行,直到你把程式關閉為止。
畢竟,1 永遠是 1,就跟你交不到女朋友的命運一樣,永遠改變不了。
這種逐步簡化的概念,正是所謂的「布林代數」(boolean)。所以我們時常說「電腦的眼中只有 1 和 0」,這個道理不只在先前「用電腦來操控資料吧」篇提到過,判斷條件也是如此。
在 for/while 迴圈裡面,如果我們想要有計畫地跳過某些特定的數字,可以使用 continue 的句法,跳過該次的迴圈:
for (int i=0; i<=20; i++) {
if (i%3 == 0) { // 如果 i 除以 3 得到的餘數是 0 的話
continue;
}
printf("%d\n", i);
}
這樣做的話,當 i 是 3 的倍數而執行 continue 的時候,就會自動結束這一輪的迴圈,也就是略過 for 迴圈裡面、continue 以下的所有工作:
另外一種情況是,你想要完全結束整個迴圈,就使用 break,這樣就會直接跳到迴圈外面了:
for (int i=1; i<=20; i++) {
if (i%7 == 0){
break;
}
printf("%d\n", i);
}
多數情況下,我們可以用 for 迴圈和 while 迴圈就達到大部分重複性質的工作,這兩種迴圈是最主要的方式。
最後要談的一種,則是 do-while 迴圈。它的作用跟 while 差不多,但它是「先執行過一次,之後才檢查條件決定是否要繼續」,也就是一定會執行「至少一次」迴圈:
int i = 0;
do {
printf("%d\n", i);
i++;
} while (i <= 5);
在上面的例子當中,只要 i 還沒達到 5 就再繼續做,這大概就是「去面試了一個工作先做做看,如果覺得不爽再辭職」的概念。
學會使用 for、while 迴圈以後,當然就有了更深一層的問題:「如果我想要的是二維的循環呢?」
比如說,你想要列出九九乘法表的每個結果,那怎麼辦?你單用一個 for 迴圈,當然不是做不到,但這前提必須建立在你早就知道答案:
for (int i=1; i<=81; i++){
if (i%9 != 0){
printf("%d*%d=%d\t", i/9+1, i%9, (i/9+1)*(i%9));
}else{
printf("%d*%d=%d\t", i/9, 9, (i/9)*9);
printf("\n");
}
}
——如果早就知道答案了,那我還要寫程式幹嘛?這不是本末倒置嗎?
總之呢,別擔心!寫程式就像組積木一樣,迴圈都是可以組合起來的。我們可以在迴圈裡面使用迴圈:
for (int i=1; i<=9; i++) {
for (int j=1; j<=9; j++){
printf("%d*%d=%d\t", i, j, i*j);
}
printf("\n");
}
我們用更簡單的方法也達到了一樣的目的。
原理非常簡單,我們只是依序做了這樣的事:
1. 在 i 為 1 的情況下,依序執行 j = 1 到 j = 9 的工作
2. 換行(輸出換行符號 \n)
3. 在 i 為 2 的情況下,依序執行 j = 1 到 j = 9 的工作
4. 換行(輸出換行符號 \n)
5. 在 i 為 3 的情況下,依序執行 j = 1 到 j = 9 的工作
6. 換行(輸出換行符號 \n)
……
15. 在 i 為 8 的情況下,依序執行 j = 1 到 j = 9 的工作
16. 換行(輸出換行符號 \n)
17. 在 i 為 9 的情況下,依序執行 j = 1 到 j = 9 的工作
18. 換行(輸出換行符號 \n)
也就是說,j 每執行完 9 輪,i 才會往上 +1,直到 i 的迴圈結束為止。
你當然也可以利用 i 來當作內層迴圈起點的參考值:
char str[256] = "ABCDE";
for (int i=0; i<=4; i++) {
for (int j=i+1; j<=4; j++) {
printf("%c%c, ", str[i], str[j]);
}
printf("\n");
}
像上面的例子,就變成了典型的排列組合之中的「五選二組合」。
總之呢,我們使用迴圈、條件判斷等有大括號的語法時,「縮排」的習慣就顯得重要了。只要你利用 space(或是 tab)把這些程式碼的從屬關係歸類好,這樣寫 code、修改 code 的時候,一看就可以知道原來 j 的 for 迴圈是在 i 的 for 迴圈裡面,輕易地看出每次執行完 9 輪 j 的迴圈以後,都會執行一次換行。
不信你可以試一下沒有好好縮排的程式碼會變得多難看:
#include <stdio.h>
int main(void) {
for (int i=1; i<=9; i++) {
for (int j=1; j<=9; j++){
printf("%d*%d=%d\t", i, j, i*j);
}
printf("\n");
} }
有些程式語言,並不會強制使用縮排(例如我們現在使用的 C 語言),有的就會(例如 Python),因為 Python 語言是不使用大括號來表示從屬關係,而是直接用縮排來決定,所以只要你 Python 沒有縮排好,就可能造成程式邏輯整個錯誤。
即使 C 語言沒有這樣的規定,我們仍然要養成寫 code 的時候一面寫、一面調整縮排的習慣,這樣當你以後回來修改自己的 code 時,才不會覺得懷疑人生(特別是程式碼很複雜的時候)。
這樣的用法,我們通常叫做「巢狀迴圈」,也就是迴圈裡面又放了一個迴圈。不過,這種情況所造成的執行次數畢竟是以級數成長,所以要是你明明只是想解決一個小問題,你卻一連使用了好幾層迴圈(例如 for(m) { for(n) { for(p) { for(q) { ... }}}}),那你最好還是想個辦法優化你的程式邏輯,因為這樣的執行效率一定會很差。
如果跟前篇提到的陣列搭配,更可以在迴圈裡面利用 array[i] 來一次控制整個 array 裡面的所有數,例如我想把陣列裡面每個數都乘以 2,我可以這麼做:
int array[4] = {123, 456, 789, 444};
printf("目前的 array 內容:\n");
for(int i=0; i<4; i++){
printf("%d: %d\n", i, array[i]);
}
for(int i=0; i<4; i++){
array[i]*=2;
}
printf("乘以二之後的 array 內容:\n");
for(int i=0; i<4; i++){
printf("%d: %d\n", i, array[i]);
}
迴圈就是程式語言的精髓了。學會使用迴圈,你就掌握了電腦重複處理大量資料的方法。這玩意大概就是這樣了!我們下回見~