前兩堂學會了讓程式走不同的路(條件判斷)和重複做事(迴圈),能寫的程式其實已經不少了。但有些事還是很難做,例如算十個人、三十個人的平均。這份講義整理 Vol. 14 直播的內容,主角只有一個:陣列。上一堂的內容有點多,所以這一堂只專心講一個概念,而且改成先從程式碼開始,看到問題再回到簡報講細節。沒看過前兩堂的話,建議先讀 #1、#2 的講義。
這是 2021 年 8 月的內容。工具一樣用 Dev-C++,換成其他 IDE 或線上編譯器也可以,這裡教的語法跟工具沒有關係。
整場的順序:
- 複習:輸入與加法、
if、for迴圈 - 遇到問題:十個數字的平均,複製貼上好痛苦
- 陣列登場:一次跟記憶體要好多塊空間,搭配
#define - 陣列的細節:宣告、初始化、
sizeof、陣列名稱與記憶體位置、複製陣列 - 二維陣列
- 今天最難的例子:兩數交換與選擇排序法
- 沒有編譯器時:用 ideone 線上執行
開始寫之前,一樣從骨架開始:到範例程式碼複製基本語法,把中間的 printf 刪掉再寫。骨架不用背,複製就好。
一、複習:前兩堂學了什麼
輸入兩個整數,印出總和
第一堂的概念:先跟記憶體要兩塊可以存整數的地方(int a, b;),用 scanf 輸入,再用 printf 印出結果。輸入前先印一行提示,使用者才知道要輸入什麼,程式比較「人性化」:
#include <stdio.h>
int main(){
int a, b;
printf("輸入第一個整數:");
scanf("%d", &a);
printf("輸入第二個整數:");
scanf("%d", &b);
printf("總和:%d\n", a + b);
return 0;
}
輸入 3 和 2,印出「總和:5」。整數的格式代表字是 %d;scanf 裡為什麼要加 &,第一堂講過。
除法,以及除以 0
把加法改成除法 a / b。兩個整數相除只會留下整數部分,所以 5 除以 2 會得到 2,想要小數就要用 double。
更麻煩的是除以 0:數學上沒辦法算,程式遇到整數除以 0 會直接掛掉,什麼都沒印,而且連 return 0; 都沒執行到,結束時回傳的值會是一個亂七八糟的數字。既然是不能算的狀況,就要先用 if 把它擋下來:
#include <stdio.h>
int main(){
int a, b;
printf("輸入第一個整數:");
scanf("%d", &a);
printf("輸入第二個整數:");
scanf("%d", &b);
if (b == 0) {
printf("第二個數字不能是 0 喔\n");
} else {
printf("相除的結果:%d\n", a / b);
}
return 0;
}
輸入 5 和 2 印出「相除的結果:2」;輸入 3 和 0 印出「第二個數字不能是 0 喔」。程式不再只是一行一行往下跑,而是有些指令會執行、有些不會。
用 for 迴圈從 a 加到 b
if 讓某些指令執行零次,迴圈則讓某些指令執行很多次。假設第一個數字比較小,要算 a + (a+1) + … + b:
#include <stdio.h>
int main(){
int a, b, i, sum = 0;
printf("輸入第一個整數:");
scanf("%d", &a);
printf("輸入第二個整數:");
scanf("%d", &b);
for (i = a; i <= b; i++) {
sum += i;
}
printf("總和:%d\n", sum);
return 0;
}
i是控制迴圈的變數,從a開始。- 中間的條件是「什麼時候要繼續」,不是什麼時候跳出去:
i還小於等於b就繼續。 - 每做完一次就
i++。 sum += i就是sum = sum + i的簡寫。
輸入 1 和 10,印出「總和:55」。
二、問題來了:十個數字的平均
兩個數字的平均
先從簡單的開始:兩個數字的平均就是加起來再除以個數 2。想要有小數,就把 int 換成 double,格式代表字也要從 %d 換成 %lf:
#include <stdio.h>
int main(){
double a, b;
printf("輸入第一個小數:");
scanf("%lf", &a);
printf("輸入第二個小數:");
scanf("%lf", &b);
printf("平均:%lf\n", (a + b) / 2.0);
return 0;
}
輸入 3 和 5,印出「平均:4.000000」。
程式碼裡直接寫的數字,沒有小數點會被當成整數,有小數點才會被當成小數。想做小數運算時,數字盡量寫成 2.0 這種帶點的形式。
十個數字:複製貼上地獄
那十個數字呢?很直覺的做法就是宣告 a 到 j 十個變數,再把輸入複製十次:
#include <stdio.h>
int main(){
double a, b, c, d, e,
f, g, h, i, j;
printf("輸入第 1 個小數:"); scanf("%lf", &a);
printf("輸入第 2 個小數:"); scanf("%lf", &b);
printf("輸入第 3 個小數:"); scanf("%lf", &c);
printf("輸入第 4 個小數:"); scanf("%lf", &d);
printf("輸入第 5 個小數:"); scanf("%lf", &e);
printf("輸入第 6 個小數:"); scanf("%lf", &f);
printf("輸入第 7 個小數:"); scanf("%lf", &g);
printf("輸入第 8 個小數:"); scanf("%lf", &h);
printf("輸入第 9 個小數:"); scanf("%lf", &i);
printf("輸入第 10 個小數:"); scanf("%lf", &j);
printf("平均:%lf\n", (a + b + c + d + e
+ f + g + h + i + j) / 10.0);
return 0;
}
輸入 1 到 10,平均是 5.500000,答案是對的。但寫的過程中一直在複製、一直在改,直播時就是寫到一半才發現「這是小數不是整數」,結果十個地方都要跟著改,而且一定會漏改。要算 30 個數字的話,英文字母只有 26 個,還得命名成 aa、ab 或 a1、a2……
這裡要養成一個警覺:當你開始複製貼上某段程式碼,就表示你在做重複的事。 程式就是要叫電腦做事的,重複的事為什麼還要自己寫?
用迴圈試試看
重複的事,上一堂學過可以交給迴圈。提示的部分很容易,「輸入第幾個小數」就是經典的從 1 數到 10:
#include <stdio.h>
int main(){
int i;
for (i = 1; i <= 10; i++) {
printf("輸入第 %d 個小數:\n", i);
}
return 0;
}
會印出「輸入第 1 個小數:」到「輸入第 10 個小數:」。
迴圈變數叫 i 只是慣例,沒有規定一定要這樣命名,但大家看到 i 就知道它是拿來控制迴圈的。(同一個範圍裡已經有變數叫 i 的話,就不能再宣告一個 i,名字會衝突。)
問題在 scanf:要存進哪個變數?就算把變數改名成 a1 到 a10,也不能寫 ai 讓 C 自己把 i 換成數字。ai 到底是一個叫 ai 的變數,還是 a 後面接 i 的值?寫程式的人分不出來,電腦也分不出來。C 沒有這種「把變數的值拼進變數名稱」的功能,現代的程式語言也很少這樣做。
我們要的是:十個可以個別使用的變數,而且可以用迴圈的控制變數去選要用哪一個。這就是陣列。
三、陣列登場
一次跟記憶體要十塊空間
陣列就是一次跟記憶體要一大塊空間,存放相同型態的資料:
double a[10];
double:一樣是要小數的空間。a:這一整大塊的名字,也就是陣列的名稱。[10]:中括號告訴電腦「我不是只要一塊,我要 10 塊」。
使用的時候,用 a[編號] 選其中一塊,每一塊都可以當成一個普通的 double 變數來用。編號從 0 開始:a[0] 是第一塊、a[1] 是第二塊……a[9] 是第十塊,沒有 a[10]。
重點是陣列名稱跟編號分開了,中括號裡可以放整數,當然也可以放迴圈的 i:
#include <stdio.h>
int main(){
double a[10];
int i;
for (i = 0; i < 10; i++) {
printf("輸入第 %d 個小數:", i + 1);
scanf("%lf", &a[i]);
}
printf("第一個值是 %lf\n", a[0]);
printf("第五個值是 %lf\n", a[4]);
return 0;
}
輸入 1 到 10,印出「第一個值是 1.000000」、「第五個值是 5.000000」;怕是巧合的話改輸入 2、4、6……20,就會印出 2.000000 和 10.000000。第五個值的編號是 4,因為從 0 開始數:0、1、2、3、4。
迴圈的寫法有幾個小地方:
- 編號是 0 到 9,所以迴圈也從 0 開始。
- 條件寫
i <= 9也對,但看到 9 容易以為只有 9 個。習慣寫成i < 10,數字就跟個數一樣。 - 提示文字從「第 0 個」開始很奇怪,所以印的時候用
i + 1。給人看的從 1 開始,陣列的編號從 0 開始,要分清楚。
這 10 塊在記憶體裡是連續的,這一整塊就叫一個小數的陣列。
用迴圈算平均
既然每一格都能用 i 拿到,加總也交給迴圈:
#include <stdio.h>
#define N 10
int main(){
double a[N];
double sum = 0;
int i;
for (i = 0; i < N; i++) {
printf("輸入第 %d 個小數:", i + 1);
scanf("%lf", &a[i]);
}
for (i = 0; i < N; i++) {
sum += a[i];
}
printf("平均是 %lf\n", sum / N);
return 0;
}
輸入 1 到 10,印出「平均是 5.500000」。
直播踩到的錯:加總一開始寫成 sum += i,結果算出 4.5。這樣加的是編號 0 + 1 + … + 9,不是陣列裡存的值。要加的是 a[i],第 i 格裡的值。
第一個迴圈裡的 i 跑完之後,可以在第二個迴圈再拿來用,不用另外宣告。
用 #define N 統一個數
上面的程式第一版到處都寫著 10:陣列大小、兩個迴圈的條件、最後除的個數。它們的意思都一樣,就是「個數」。想改成 30 個,又要改好幾個地方,又可能漏改。
所以在最上面加一行:
#define N 10
然後把所有代表個數的 10 都寫成 N。#define 的作用很單純,像搜尋與取代:在編譯之前,把程式裡每一個剛好是 N 的符號(前後沒有接其他字,所以 NUM 裡的 N 不算)都換成 10。
以後只要改這一行就好。把 10 改成 5,執行時就只會要求輸入 5 個數字;輸入 1 到 5,印出「平均是 3.000000」。
四、一維陣列的宣告和使用
回到簡報,整理一下陣列的規則。只有一個編號的陣列叫一維陣列,等一下會看到二維的。
宣告
型態 陣列名稱[N];
- 跟記憶體要 N 塊空間,編號是 0 到 N-1,例如 N 是 10 就是 0 到 9。
- 每一塊都是同一種型態,用
int宣告就只能存整數,用double就存小數。 - 宣告
int a[3];的話,能用的只有a[0]、a[1]、a[2],a[3]不存在。 - N 要寫整數常數,例如直接寫數字,或是
#define出來的符號(它只是複製貼上,程式執行中不會變)。
為什麼不能放變數?編譯器不知道那個變數執行時會是多少,萬一是 -10 呢?跟記憶體要負 10 塊空間是什麼意思?所以入門階段就記得大小寫常數。
補充:C99 之後的 C 其實允許用變數當大小(可變長度陣列),gcc 也接受,但這種陣列不能初始化,C++ 標準也不支援。剛開始學就照上面的規則寫常數就好。
中括號的兩種意思
同樣是中括號,在兩個地方意思不一樣:
- 宣告時(
double a[10];):要跟記憶體要幾塊空間。 - 使用時(
a[4]):要拿陣列的第幾號那一格。
C 裡面這種「宣告和使用時意思不同」的符號還有好幾個,例如第一堂最後看過的 *(米字號),之後會越來越常遇到。
初始化
宣告陣列時可以順便給每一格初始值,用大括號依序填:
#include <stdio.h>
int main(){
int a[5] = {1, 2, 3, 4, 5}; // 五格都給值
int b[] = {1, 2, 3}; // 不寫大小,依初始值決定是 3 格
int c[5] = {1}; // 不夠的部分補 0
int d[5] = {0}; // 全部都是 0
int i;
for (i = 0; i < 5; i++) {
printf("%d ", a[i]);
}
printf("\n");
for (i = 0; i < 3; i++) {
printf("%d ", b[i]);
}
printf("\n");
for (i = 0; i < 5; i++) {
printf("%d ", c[i]);
}
printf("\n");
for (i = 0; i < 5; i++) {
printf("%d ", d[i]);
}
printf("\n");
return 0;
}
1 2 3 4 5
1 2 3
1 0 0 0 0
0 0 0 0 0
有初始化的話,中括號裡可以不寫大小,編譯器會數後面給了幾個值。以後從 10 個改成 20 個,只要改初始值就好,不用再去改數字。
但不寫大小又不初始化就一定錯,電腦不知道要給你幾塊空間:
int a[]; // 錯誤:不知道要幾塊初始值不夠的話,剩下的格子會補 0。所以
{1}是第一格 1、其他都是 0;{0}就是全部都 0。
那完全不初始化時裡面是什麼?跟普通變數一樣,是拿到那塊記憶體時本來就在裡面的值,不一定是 0。直播時印出來剛好都是 0,那只是剛好沒事。所以建議不管怎樣都初始化,寫 = {0} 就能讓全部從 0 開始。
一開始就有值的陣列
如果資料本來就知道,不需要使用者輸入,直接寫在初始值裡就好。例如自己要算一組成績的平均,不用特地做輸入介面:
#include <stdio.h>
#define N 10
int main(){
double a[N] = {80, 75, 92, 60, 58, 100, 66, 71, 89, 94};
double sum = 0;
int i;
for (i = 0; i < N; i++) {
sum += a[i];
}
printf("平均是 %lf\n", sum / N);
return 0;
}
印出「平均是 78.500000」。想算別組數字,改初始值就好。
要注意:錯過初始化之後,就不能一次指定整個陣列了。千萬不要這樣寫:
int a[5];
a = {1, 2, 3, 4, 5}; // 錯誤:C 沒有這種寫法
宣告完之後只能一格一格給值。很多現代程式語言可以直接換掉整個陣列的值,但 C 沒有這麼方便,這也是很多人學陣列卡住的原因。不過有其他方法可以「複製」陣列,後面會講。
五、sizeof:東西佔多少記憶體
接下來要講陣列的複製,在那之前需要一些記憶體的概念。這部分比較進階,第一次聽不懂也沒關係。
sizeof 可以算出一個東西佔多少記憶體(單位是 byte)。括號裡可以放型態,也可以放變數:
#include <stdio.h>
int main(){
int a = 10;
int b[5] = {1, 2, 3, 4, 5};
double c = 1.5;
double d[5] = {1.2, 2.3, 3.4, 4.5, 5.6};
printf("int : %d\n", (int)sizeof(int));
printf("a : %d\n", (int)sizeof(a));
printf("b : %d\n", (int)sizeof(b));
printf("double : %d\n", (int)sizeof(double));
printf("c : %d\n", (int)sizeof(c));
printf("d : %d\n", (int)sizeof(d));
return 0;
}
int : 4
a : 4
b : 20
double : 8
c : 8
d : 40
int是 32 位元,也就是 4 個 byte。a是整數變數,不管裡面存什麼值都是 4 個 byte。b是 5 個整數的陣列,4 × 5 = 20 個 byte。double是 64 位元,也就是 8 個 byte;d有 5 格,所以是 40。sizeof算出來的是整數,就算算的是double,也不是用%lf印。
小修正:直播說
sizeof是「函式」,其實它是 C 的運算子,只是寫起來很像函式。它的結果是一種叫size_t的無號整數,正式的格式代表字是%zu;舊版 Dev-C++ 不一定支援%zu,所以上面先轉成int再用%d印,在哪裡都能正確顯示。
int 有幾個 byte 其實要看機器和編譯器。現在大部分的電腦都是 4 個 byte,但比較古老或特殊的機器上,int 可能只有 16 位元(2 個 byte)。(直播一開始講成「16 個 byte」,馬上改口成 16 位元,16 位元才對。C 標準只規定 int 至少要 16 位元。)想知道某台機器上的 int 多大,就用 sizeof(int) 問那台機器上的編譯器。
六、陣列名稱其實是記憶體位置
用 b[0]、b[1] 一格一格拿值,前面已經做過了。那不加中括號,直接印 b 會是什麼?
會印出一個看不懂的數字。第一堂最後看過類似的東西:那是記憶體位置,也就是這個陣列存在記憶體的哪裡。
既然是位置,可以拿來加減嗎?把 b + 1、d + 1 也印出來看看:
#include <stdio.h>
int main(){
int b[5] = {1, 2, 3, 4, 5};
double d[5] = {1.2, 2.3, 3.4, 4.5, 5.6};
printf("b[0] = %d, b[1] = %d\n", b[0], b[1]);
printf("b = %p\n", (void *)b);
printf("b + 1 = %p\n", (void *)(b + 1));
printf("d = %p\n", (void *)d);
printf("d + 1 = %p\n", (void *)(d + 1));
printf("-----我是分隔線-----\n");
printf("*b = %d, *(b + 1) = %d\n", *b, *(b + 1));
printf("*d = %lf, *(d + 1) = %lf\n", *d, *(d + 1));
return 0;
}
其中一次執行的結果(位置每台電腦、每次執行都不一樣,看最後一兩位的差距就好):
b[0] = 1, b[1] = 2
b = 0x7ffe9cc235b0
b + 1 = 0x7ffe9cc235b4
d = 0x7ffe9cc235d0
d + 1 = 0x7ffe9cc235d8
-----我是分隔線-----
*b = 1, *(b + 1) = 2
*d = 1.200000, *(d + 1) = 2.300000
直播是用
%d把位置印成十進位(例如 6487536),比較好算,但這不是正確寫法:印記憶體位置要用%p(顯示成 16 進位,b4 - b0就是 4),而且 64 位元電腦的位置可能比int存得下的還大。
奇怪的事出現了:b + 1 不是加 1,是加 4;d + 1 是加 8。
原因是 b 不只是一個數字,它知道自己是「整數陣列的位置」。對它加 1 的意思是「往後跳一格」,一格整數是 4 個 byte,所以位置加 4;d 是浮點數陣列,一格是 8 個 byte,所以加 8。
再來,第一堂最後學過 *(米字號)運算:可以把記憶體位置上存的值拿出來。對 b 和 b + 1 做 *,拿到的就是 1 和 2,跟 b[0]、b[1] 一模一樣。
所以:
b[0]就是*(b + 0):從陣列開頭跳 0 格,拿出那裡的值。b[1]就是*(b + 1):從陣列開頭跳 1 格,拿出那裡的值。b[2]就是*(b + 2):跳 2 格,拿到第三格。
中括號其實是一個簡寫:從陣列的位置往後跳幾格,然後拿出那格的值。
這也解釋了為什麼陣列從 0 開始編號:陣列名稱代表的位置就是第一格存的地方,要第一格當然不用跳,就是跳 0 格。
很多人學到指標(pointer)會很頭痛,但記憶體位置的概念其實早就出現了:scanf 要加 & 是因為它,陣列也是它,只是被藏起來了。現在聽起來好像有點被騙、又好像很合理的話,沒關係,之後真的遇到問題時再回來看就好。
七、複製陣列
不能直接用等號
要把陣列 b 的五個值複製到陣列 e,最直覺的寫法是這樣,但這是錯的:
int b[5] = {1, 2, 3, 4, 5};
int e[5];
e = b; // 錯誤:陣列不能這樣指定
編譯器會直接說不行。照剛才的概念,b 和 e 代表的都是記憶體位置,而不是整個陣列的值,所以 e = b 不會被當成「把 b 的值都丟給 e」,而是「把 b 的位置存進 e」。但用陣列宣告出來的名字,代表的位置是固定的,不能改,所以這行過不了。
方法一:for 迴圈一格一格複製
最簡單、一定可以的做法:
#include <stdio.h>
int main(){
int b[5] = {1, 2, 3, 4, 5};
int e[5];
int i;
for (i = 0; i < 5; i++) {
e[i] = b[i];
}
printf("結果:\n");
for (i = 0; i < 5; i++) {
printf("e[%d] = %d\n", i, e[i]);
}
return 0;
}
結果:
e[0] = 1
e[1] = 2
e[2] = 3
e[3] = 4
e[4] = 5
方法二:memcpy 直接複製記憶體
memcpy 是 memory copy,「複製記憶體」的意思,放在 string.h 裡,要先 #include <string.h>。它需要三個東西:
memcpy(複製到哪裡, 從哪裡開始複製, 複製多少 byte);
前兩個放的都是記憶體位置,剛好陣列名稱就是位置:
#include <stdio.h>
#include <string.h>
int main(){
int b[5] = {1, 2, 3, 4, 5};
int e[5] = {0};
int i;
memcpy(e, b, sizeof(b)); // 也可以寫 sizeof(int) * 5
printf("結果:\n");
for (i = 0; i < 5; i++) {
printf("e[%d] = %d\n", i, e[i]);
}
return 0;
}
輸出跟方法一一樣,e 裡面是 1 到 5。大小可以寫 sizeof(b)(整個 b),也可以寫 sizeof(int) * 5(5 個整數的大小)。
for 迴圈一次只能搬一格,memcpy 是整塊記憶體直接搬,通常比較快。
懂了位置的概念,可以只複製一部分
既然 b + 1 是「往後跳一格的位置」,就可以從中間開始複製:
#include <stdio.h>
#include <string.h>
int main(){
int b[5] = {1, 2, 3, 4, 5};
int e[5] = {0};
int f[5] = {0};
int i;
// 從 b[1] 開始,複製 4 個 int 到 e[0] 開始的位置
memcpy(e, b + 1, sizeof(int) * 4);
// 從 b[2] 開始複製 3 個到 f[0],再從 b[1] 開始複製 2 個到 f[3]
memcpy(f, b + 2, sizeof(int) * 3);
memcpy(f + 3, b + 1, sizeof(int) * 2);
for (i = 0; i < 5; i++) {
printf("%d ", e[i]);
}
printf("\n");
for (i = 0; i < 5; i++) {
printf("%d ", f[i]);
}
printf("\n");
return 0;
}
2 3 4 5 0
3 4 5 2 3
e:b[1]到b[4](2、3、4、5)複製到e[0]到e[3];e[4]沒有被複製到,因為有初始化成 0,所以是 0。直播第一次沒有初始化,e[4]就出現一個完全不相關的數字。f:先把b[2]到b[4](3、4、5)放到f[0]到f[2],再把b[1]、b[2](2、3)放到f[3]、f[4],所以第二次的目標要寫f + 3。(直播時一度寫錯目標位置,馬上改成從第 4 格,也就是編號 3 開始。)
這樣玩很有彈性,但也很危險:算錯格數就會蓋到不該動的記憶體。平常幾乎都是整個複製,知道自己在做什麼比較重要。覺得這些記憶體的東西太複雜的話,用 for 迴圈就好,它可以處理所有情況。
直接操作記憶體在現代的程式語言裡被視為很危險的事,例如 C# 要用指標就得標上 unsafe,等於跟它說「我知道不安全,但我就是要寫」。反過來說,這正是 C 的特色:其他語言當作不安全的記憶體操作,在 C 裡是很基本的東西。
八、二維陣列
兩個編號
陣列也可以有兩個編號,寫兩組中括號,各自說那個維度要多大,有點像座標的 X 和 Y:
int a[2][3];
這樣會有 2 × 3 = 6 格:
[0] |
[1] |
[2] |
|
|---|---|---|---|
a[0] |
a[0][0] |
a[0][1] |
a[0][2] |
a[1] |
a[1][0] |
a[1][1] |
a[1][2] |
記憶體是一整條的,所以實際上還是排成一直線:先放完 a[0] 這一排(a[0][0]、a[0][1]、a[0][2]),再放 a[1] 這一排。也就是最後面的編號變得最快。
聊天室說可以想成向量和矩陣,沒錯。那既然是 6 格,直接寫 a[6] 不行嗎?也可以,在記憶體裡一樣是 6 格,只是要自己算「第幾排第幾個」是第幾格。寫成二維讓程式幫你算,處理座標、表格(例如 Excel 的每一格)這種本來就有兩個編號的資料會方便很多。
初始化
int a[2][3] 可以想成兩個一維陣列,每個裡面有三格,所以初始化時一排用一組大括號。因為記憶體本來就是一整條,裡面的大括號也可以不寫,就照順序填;不夠的一樣補 0:
#include <stdio.h>
int main(){
int a[2][3] = {{1, 2, 3}, {4, 5, 6}};
int b[2][3] = {1, 2, 3, 4};
int i, j;
for (j = 0; j < 2; j++) {
for (i = 0; i < 3; i++) {
printf("%d ", a[j][i]);
}
printf("\n");
}
printf("-----\n");
for (j = 0; j < 2; j++) {
for (i = 0; i < 3; i++) {
printf("%d ", b[j][i]);
}
printf("\n");
}
return 0;
}
1 2 3
4 5 6
-----
1 2 3
4 0 0
(b 那種不分排的寫法,編譯器開了較多警告時會提醒你「最好加上大括號」,寫成分排的比較清楚。)
使用時一樣要寫出是哪一格,才能當成一個變數來用,不能整個陣列一起用。
巡覽每一格:巢狀迴圈
一維陣列用一個迴圈走過每一格,二維就要用兩層迴圈,又回到上一堂最難的巢狀迴圈。下面外層的 j 控制前面的編號,內層的 i 控制後面的編號,把一個 3 × 3 陣列每一格的編號印出來:
#include <stdio.h>
int main(){
int i, j;
for (j = 0; j < 3; j++) {
for (i = 0; i < 3; i++) {
printf("%d%d ", j, i);
}
printf("\n");
}
return 0;
}
00 01 02
10 11 12
20 21 22
j 是 0 時,內層讓 i 跑 0、1、2,印出 00、01、02,換行;接著 j 變 1,內層再從 0 跑到 2 一次,印出 10、11、12;j 變 2 時同理。
演練:兩班各三位同學的成績
多一個維度,就可以多一種分類。例如一個維度代表「第幾班」,另一個代表「這班的第幾位同學」,就像表格的 row 和 column 代表兩種不同的屬性:
#include <stdio.h>
#define CLASS 2
#define STUDENT 3
int main(){
int score[CLASS][STUDENT];
int c, s, sum;
for (c = 0; c < CLASS; c++) {
for (s = 0; s < STUDENT; s++) {
printf("輸入第 %d 班第 %d 位同學的成績:", c + 1, s + 1);
scanf("%d", &score[c][s]);
}
}
for (c = 0; c < CLASS; c++) {
sum = 0;
for (s = 0; s < STUDENT; s++) {
sum += score[c][s];
}
printf("第 %d 班平均:%lf\n", c + 1, sum / (double)STUDENT);
}
return 0;
}
score[0][...] 是第一班,score[1][...] 是第二班。輸入 60、70、80 和 90、85、95,會印出「第 1 班平均:70.000000」、「第 2 班平均:90.000000」。sum 是整數,除以 (double)STUDENT 才會用小數算。
直播沒有細講平均的部分,概念跟一維一樣,只是多一層。
三維、四維?
可以繼續往上加維度,但越高維越難想像資料怎麼存放。實際上大部分時候用的都是一維陣列,二維偶爾用到,更高維很少見。先把一維搞懂最重要。
九、今天最難的例子:排序
最後一個例子:輸入 5 個數字,由小到大排好再印出來。這已經有點演算法的味道了,需要一點邏輯去想。
聊天室說現成的工具都有排序功能可以直接用,沒錯,實務上就用別人寫好的。但自己寫過一次,才知道排序在做什麼。排序的方法有很多,有快有慢,這裡先教比較基本的一種。
先學會兩數交換
排序會一直需要「交換兩個變數的值」。假設 a 是 3、b 是 5,想交換成 5 和 3。直覺寫法是把 b 給 a、再把 a 給 b:
#include <stdio.h>
int main(){
int a = 3, b = 5;
a = b; // a 變成 5,原本的 3 被蓋掉了
b = a; // b 拿到的也是 5
printf("%d %d\n", a, b);
return 0;
}
結果印出 5 5。第一行把 a 原本的 3 蓋掉了,第二行再怎麼給都只剩 5。
就像整理房間時想把衣櫃和床的位置對調:不能直接把床搬到衣櫃的位置,因為衣櫃還在那裡。要先把衣櫃移到別的地方,床搬過去,再把衣櫃搬過來。程式也一樣,需要一個暫時存放的地方,習慣叫 temp:
#include <stdio.h>
int main(){
int a = 3, b = 5;
int temp;
temp = b; // 先把 b 放到暫存的地方:a=3, b=5, temp=5
b = a; // b 可以放心蓋掉了: a=3, b=3, temp=5
a = temp; // 把暫存的值拿回來: a=5, b=3
printf("%d %d\n", a, b);
return 0;
}
印出 5 3,交換成功。
選擇排序法
選擇排序法的想法很簡單:從還沒排好的數字裡,選出最小的,放到最前面的空位。
- 排第 0 個位置:從全部裡找最小的,放到第 0 個。
- 排第 1 個位置:從剩下的裡找最小的(也就是第二小),放到第 1 個。
- 以此類推。
寫成程式時,用一個迴圈的 i 決定現在要排第幾個位置,i 從 0 跑到 4。因為是一個一個往後排,排到第 i 個時,前面的 0 到 i-1 都已經排好了。例如 i 是 2 時,第 0、1 個已經是最小的兩個,所以只要做兩件事:
- 從第
i個開始往後看(2、3、4 這三個),找出最小的人的編號。 - 把最小的人跟第
i個人交換。
這樣第 i 個就排好了,接著排下一個。
找最小的人時,要有一個變數記住「目前最小的人的編號」。陣列的編號英文叫 index(索引),所以取名 minIndex。一開始先假設第 i 個最小,再用第二層迴圈的 j 往後一個一個比,看到更小的就更新:
#include <stdio.h>
int main(){
int a[5];
int i, j, minIndex, temp;
for (i = 0; i < 5; i++) {
scanf("%d", &a[i]);
}
for (i = 0; i < 5; i++) {
// 第一步:從第 i 個開始往後看,找出最小的人的編號
minIndex = i;
for (j = i; j < 5; j++) {
if (a[j] < a[minIndex]) {
minIndex = j;
}
}
// 第二步:把最小的人跟第 i 個人交換
temp = a[i];
a[i] = a[minIndex];
a[minIndex] = temp;
}
for (i = 0; i < 5; i++) {
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
輸入 5 4 1 8 2,印出 1 2 4 5 8 。
這裡又是巢狀迴圈,而且內層的起點跟著外層的 i 變。看不懂的話,拿 5 4 1 8 2 一步一步代進去,把每一輪的 i、j、minIndex 和陣列內容寫下來,會比較清楚。
跟網路上的寫法比較
直播最後去維基百科查了選擇排序法,那裡的寫法有兩個小差別:
#include <stdio.h>
int main(){
int a[5];
int i, j, minIndex, temp;
for (i = 0; i < 5; i++) {
scanf("%d", &a[i]);
}
for (i = 0; i < 5 - 1; i++) {
minIndex = i;
for (j = i + 1; j < 5; j++) {
if (a[j] < a[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) {
temp = a[i];
a[i] = a[minIndex];
a[minIndex] = temp;
}
}
for (i = 0; i < 5; i++) {
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
- 內層從
i + 1開始:已經先假設第i個最小了,再拿它跟自己比沒有意義。 - 交換前先檢查
minIndex != i:最小的就是自己的話,自己跟自己交換也沒有意義。 - 外層只跑到
5 - 1:前四個排好時,最後一個自然就是最大的。(這點直播沒提,結果一樣。)
結果完全一樣,輸入 5 4 1 8 2 一樣印出 1 2 4 5 8 。網路上的資料通常還會分析這種排序要比較幾次、交換幾次,以及其他更快的排序法,有興趣可以去看看,但有些比較複雜,不一定現在看得懂。
十、沒有編譯器?用 ideone 線上跑
如果手邊的電腦沒有裝 Dev-C++ 或任何 C 的編譯器,可以用直播示範的線上編譯器 ideone。名字的意思是一個 IDE(整合開發環境)就能跑幾乎所有程式語言。
- 語言選 C,它給的範本跟我們的骨架差不多。
- 範本裡是
int main(void),void表示「空的」,也就是不需要傳東西進來。跟int main()意思差不多,寫不寫都可以。 - 把程式貼上去。程式有
scanf的話,把要輸入的東西填在下面的 stdin(standard input,也就是stdio.h的那個 standard input)欄位,例如排序程式填5 4 1 8 2。 - 按右下角的 Run,結果會出現在 stdout(standard output)。
好處是輸入和輸出分開顯示。在黑色視窗裡,你打的字和 printf 印的字會混在一起,在這裡就分得很清楚。
小複習
- 開始複製貼上程式碼時要有警覺:重複的事交給電腦。
- 陣列一次跟記憶體要好多塊相同型態的空間;編號從 0 開始到 N-1,搭配
for (i = 0; i < N; i++)使用。 - 宣告時的中括號是「要幾塊」,使用時是「第幾號」;宣告的大小要寫常數,可以用
#define。 - 初始化用大括號,不夠的補 0;有初始化可以不寫大小;宣告完就不能再整個指定。
sizeof算出佔幾個 byte:int通常是 4,double是 8,陣列是一格大小乘上格數。- 陣列名稱代表記憶體位置,
b + 1是跳一格;b[i]就是*(b + i)的簡寫,所以編號從 0 開始。 - 陣列不能用
=複製,用 for 迴圈一格一格複製,或用memcpy。 - 二維陣列有兩個編號,用巢狀迴圈走過每一格。
- 交換兩個變數要有暫存的地方;選擇排序法就是一直「選出剩下最小的,跟目前位置交換」。
練習
今天的內容,最重要的是第三節:陣列怎麼宣告、怎麼搭配迴圈使用。陣列名稱代表什麼、怎麼複製、二維陣列比較進階,先有印象就好,哪天真的遇到問題再回來翻。
- 找出低於平均的同學:算出平均後,再用一個
for一個一個看哪些人比平均低,把他們印出來。 - 倒過來印:輸入
1 2 3 4 5,印出5 4 3 2 1。可以全部存完再倒著印,也可以有其他做法,自己想想看。 - 把今天的例子(十個數字的平均、選擇排序法)先看懂,再遮起來自己打一遍。骨架和宣告忘了可以複製,重點是你能不能把這些概念拼起來。
寫好了可以截圖發到 Twitter,加上 tag #悠太作業;有問題可以在聊天室、Discord 或棉花糖問。
下一堂預告
下一堂要講怎麼處理文字(字元與字串)。文字其實也是陣列的延伸,所以今天至少要先搞懂陣列的基本用法。