1. 為什麼要用 for,如果已經有 while
從新手的角度看程式設計,可能會想:「反正有 while,它可以重複 — 不就夠了嗎?」但做幾道題後你會注意到一個重複出現的模式:你會建立一個計數器、設定一個邊界、在主體中做實際工作,然後一定會增加計數器。就這樣一次又一次地重複 — 像一部可以濃縮成電影的連續劇。
for 正是為了讓這個模板看起來更直觀、更精簡而誕生:「這是起始、這是條件、這是步驟」。也就是說,for 並不是新的魔法,而是對非常典型的 while 做了一個更方便的包裝。
我們用例子比較:輸出從 1 到 5 的數字。
使用 while 的版本:
#include <iostream>
int main() {
int i = 1;
while (i <= 5) {
std::cout << i << '\n';
i = i + 1;
}
}
使用 for 的版本(相同功能,但更短、更「集中」):
#include <iostream>
int main() {
for (int i = 1; i <= 5; i = i + 1) {
std::cout << i << '\n';
}
}
注意感受:在 for 中,重複機制都在一行可見。腦子不會再問「在哪裡增加了 i?」— 因為步驟就在標頭裡面。
2. for 的語法:初始化、條件、步驟
當你第一次看到 for(某事; 某事; 某事),可能會想:這是 Wi‑Fi 密碼還是迴圈?其實一切都很合乎邏輯。for 的標頭由三部分組成,以分號分隔,每部分負責不同的角色:起始、檢查、推進。
通用形式:
for (初始化; 條件; 步驟) {
// 迴圈主體
}
各部分的含義如下。
初始化 在迴圈開始前只執行 一次。通常在此處建立並設定計數器的起始值,例如 int i = 0。
條件 在每次迭代前檢查(如同 while)。只要條件為真,就執行主體。
步驟 在每次迭代後執行。通常在這裡增加計數器,例如 i = i + 1。
下面是一個簡單的例子,「打招呼五次」,但讓人看得出我們是在「計算迭代次數」:
#include <iostream>
int main() {
for (int i = 1; i <= 5; i = i + 1) {
std::cout << "Hello #" << i << '\n';
}
}
看到 for 時,習慣用字面方式讀它會很有幫助:「從 i = 1 開始,只要 i <= 5,每次做 i = i + 1」。
對了,括號內的分號是必須的。這是新手常見的「錯誤 №1」,今天我們會用知識把它擊破(而不是用淚水)。
執行順序
for 並沒有什麼隱藏的魔法:它按照非常具體的順序執行。但由於寫得很緊湊,新手可能會搞混何時發生什麼。因此我們把 for 拆成步驟。這不是要求你每次都這樣思考,而是給你在疑惑時的一個清晰模型。
執行順序是:
- 執行初始化(一次)
- 檢查條件
- 如果條件為真 — 執行主體
- 執行步驟
- 回到檢查條件
可以把它想成一個簡單的流程圖:
flowchart TD
A[初始化] --> B{條件?}
B -- false --> E[退出迴圈]
B -- true --> C[迴圈主體]
C --> D[步驟]
D --> B
一個實用的技巧:如果你不確定迴圈會執行幾次,拿紙和筆(或筆記)手動演算前面 2–3 次迭代。這樣你會捕捉到大約 80% 的邊界錯誤。
例如,這段程式碼:
#include <iostream>
int main() {
for (int i = 0; i < 3; i = i + 1) {
std::cout << i << '\n';
}
}
會輸出:
- 0
- 1
- 2
為什麼不是 3? 因為條件 i < 3 在 i 等於 3 時不再成立,而條件是在執行主體之前檢查的。
如何把 for 轉換成 while
有時候 for 顯得太緊湊,尤其在學習初期。好消息是:你已經會用 while,所以可以在腦中(或直接在程式裡)把 for 展開成等價的 while。這會消除「魔法感」:事實上 for 只是把三個組件放在不同的地方。
等價關係如下:
for (init; cond; step) {
body
}
大致同於:
init;
while (cond) {
body
step;
}
我們用真實程式碼比較:把某個值增加 10 三次。
#include <iostream>
int main() {
int a = 0;
for (int i = 0; i < 3; i = i + 1) {
a = a + 10;
}
int b = 0;
int j = 0;
while (j < 3) {
b = b + 10;
j = j + 1;
}
std::cout << a << ' ' << b << '\n'; // 30 30
}
關於可讀性的一個細節:雖然 for 與 while 可能等價,但在實際程式中通常會選擇更能「講好故事」的那個。
如果重複可以自然地用計數器描述(1…n、0…n-1、精確 n 次)— 通常 for 更清晰。
如果重複更自然地用一個非計數器條件描述(例如「直到輸入的數字符合條件為止」)— 通常 while 更清晰。
3. 迴圈邊界與 off-by-one
程式設計師大致分為兩類:已經遇過 off-by-one 的,和還以為那是樂團名稱的。off-by-one 指的是迴圈執行次數比預期多或少一圈。這個錯誤看起來小,但後果可能很煩人:多了不必要的輸入、多了多餘的輸出、總和錯誤 — 你會盯著結果想「我明明都做對了……應該吧」。
這種錯誤通常源於對「包含」與「不包含」邊界的混淆。
比較兩個非常相似的迴圈:
for (int i = 0; i < 5; i = i + 1) { ... } // 0,1,2,3,4 (不包含 5)
for (int i = 0; i <= 5; i = i + 1) { ... } // 0,1,2,3,4,5 (包含 5)
為了不猜測,心裡記住兩種常見的「範圍風格」會很方便。
| 想要達成的事 | 最常見的風格 | 如何閱讀 |
|---|---|---|
| 「剛好 n 次」 | |
i 取值 0..n-1,迭代次數剛好是 n |
| 「從 1 到 n(包含)」 | |
i 取值 1..n,迭代次數剛好是 n |
心理上要注意:當你想「剛好 n 次」時,從 0 開始並使用 < n 很方便。這給了個漂亮的規則:迭代次數等於 n - 0,也就是 n。而當你想「從 1 到 n」時,自然就從 1 開始並用 <= n 結束。
示範:「計算從 1 到 n 的總和」。在這裡 n 應該被包含:
#include <iostream>
int main() {
int n = 0;
std::cin >> n;
int sum = 0;
for (int i = 1; i <= n; i = i + 1) {
sum = sum + i;
}
std::cout << sum << '\n';
}
如果不小心寫成 i < n,那總和會變成「從 1 到 n-1」——這是經典的 off-by-one。
另一個有用的習慣是把邊界念出來。例如,把 i < n 念作「i 嚴格小於 n,表示 n 不被包含」。而 i <= n 念作「i 小於或等於 n,表示 n 被包含」。聽起來簡單,但在腦子疲累、原地打一題「五分鐘搞定」卻拖了三小時的時候,這個方法很管用。
4. 迷你應用:StepTracker 與讀入 N 個值
為了讓 for 不只是「漂亮的語法」,我們把它用在一個小應用裡。設計 StepTracker —— 程式會詢問天數,然後逐日讀入步數,最後印出總和。
為什麼這是個適合 for 的例子:我們事先知道要重複多少次輸入——正好是 days 次。這是計數器的理想任務。
先從骨架開始:讀取天數,對每一天做迴圈。
#include <iostream>
int main() {
int days = 0;
std::cout << "How many days? ";
std::cin >> days;
for (int day = 1; day <= days; day = day + 1) {
std::cout << "Day " << day << ": ";
int steps = 0;
std::cin >> steps;
}
}
注意邊界:day 從 1 到 days(包含)。這對使用者來說可讀性好(「第 1 天」「第 2 天」……),但若把 < 與 <= 搞混,off-by-one 很容易發生。
總和與平均
現在加入累加總和——這是「累加器」模式的經典做法。
#include <iostream>
int main() {
int days = 0;
std::cout << "How many days? ";
std::cin >> days;
int total = 0;
for (int day = 1; day <= days; day = day + 1) {
int steps = 0;
std::cout << "Day " << day << ": ";
std::cin >> steps;
total = total + steps;
}
std::cout << "Total steps: " << total << '\n';
}
接著想要算平均。但目前我們只用 int,所以會算「整數平均」及餘數(這樣既誠實又有用,我們也已經會用 %)。
#include <iostream>
int main() {
int days = 0;
std::cout << "How many days? ";
std::cin >> days;
int total = 0;
for (int day = 1; day <= days; day = day + 1) {
int steps = 0;
std::cout << "Day " << day << ": ";
std::cin >> steps;
total = total + steps;
}
int avg = total / days;
int rem = total % days;
std::cout << "Average (integer): " << avg << '\n';
std::cout << "Remainder: " << rem << '\n';
}
這裡有個重要點:如果 days 為 0,會發生除以零(這不是編譯錯誤,而是執行時的可怕錯誤)。我們還沒學完整的輸入驗證,但會用基礎的 if 做簡單保護,這在當前學習階段是合適的。
做一個簡單的保險:
#include <iostream>
int main() {
int days = 0;
std::cout << "How many days? ";
std::cin >> days;
if (days <= 0) {
std::cout << "Days must be positive.\n";
return 0;
}
int total = 0;
for (int day = 1; day <= days; day = day + 1) {
int steps = 0;
std::cout << "Day " << day << ": ";
std::cin >> steps;
total = total + steps;
}
std::cout << "Total steps: " << total << '\n';
}
在沒有陣列的情況下求最小值和最大值
現在加入最小值與最大值。在沒有陣列的情況下我們無法「存所有天數」,但也沒必要:最小/最大值可以即時更新。這裡最常見的問題是如何初始化 minSteps。一個常見做法是使用「第一個值的旗標」。
#include <iostream>
int main() {
int days = 0;
std::cin >> days;
int total = 0;
int minSteps = 0;
int maxSteps = 0;
bool first = true;
for (int day = 1; day <= days; day = day + 1) {
int steps = 0;
std::cin >> steps;
total = total + steps;
if (first) {
minSteps = steps;
maxSteps = steps;
first = false;
} else {
if (steps < minSteps) minSteps = steps;
if (steps > maxSteps) maxSteps = steps;
}
}
std::cout << "Min: " << minSteps << ", max: " << maxSteps << '\n';
}
沒錯,這裡連續兩個 if 是正常的:在目前的學習階段這樣寫很合適。重點是邏輯不會欺騙你:第一個輸入設定了初始的 min/max,之後我們只做更新。
for 在這裡特別方便:所有關於重複次數的控制(「讀入剛好 days 個數字」)都在標頭裡,而在迴圈內我們專注於業務邏輯:總和、最小值、最大值。這正是大家喜歡 for 的原因:它把「樣板性」的東西從主體裡抽離出去。
5. 使用 for 時的典型錯誤
錯誤 №1:把 < 和 <= 搞混,導致多一圈或少一圈。
這是典型的 off-by-one。遇到這種情況時,養成把計數器的第一個和最後一個值寫下來的習慣會很有幫助:例如你需要 1,2,3,4,5 — 那麼起始是 1,條件是 <= 5。如果想「剛好 5 次」,比較方便的是用 i = 0; i < 5。
錯誤 №2:步驟不會導致結束。
有時條件寫得對,但忘了改變計數器(或改錯方向)。例如,for (int i = 0; i < n; i = i - 1) 幾乎可以保證在 n > 0 時發生無窮迴圈,因為 i 會遞減到負數,而條件 i < n 仍然成立。在執行迴圈前,問自己一個問題會有幫助:「我的迴圈中有什麼會改變,以至於條件最終會變成 false?」
錯誤 №3:不小心在 for 標頭後加了 ;。
像這樣寫 for (... ); { ... } 會把迴圈變成「空迴圈」:它自己跑完,然後大括號裡的區塊只執行一次。這種情況看起來程式碼像是對的,但行為很奇怪。對策是養成紀律:在教學範例中總是在 for 之後立即寫出帶有 { ... } 的主體,並注意不要多寫分號。
錯誤 №4:同時在步驟與主體中修改計數器。
例如,for (int i = 0; i < n; i = i + 1) { i = i + 1; } — 這會讓計數器每次跳兩格,導致「漏掉一半元素」。通常的約定是:要麼在 for 的步驟中改變計數器,要麼在主體中改變,但不要兩邊都改(在我們的學習階段,大多數情況下步驟放在標頭就足夠了)。
錯誤 №5:誤解計數器的作用域。
如果你寫了 for (int i = 0; ... ), 那麼 i 只存在於迴圈內。這通常是優點:你不會在後面的程式中不小心使用到舊的 i。但有時新手會驚訝地問:「為什麼編譯器在迴圈後面不知道 i?」——因為變數的生命週期在迴圈結束時結束,這是一種保護以避免意外錯誤。
GO TO FULL VERSION