1. 為何需要 const
如果你剛開始學程式,const 看起來像多餘的嚴格:「我本來就承諾不會改變值 — 為何還要讓編譯器插手?」但在程式設計中,沒有檢查的承諾就像在商店說「我只是待一分鐘」:有時真的是一分鐘,有時你會在那裡找到人生意義和折扣。const 是把承諾變成規則的一種方式,編譯器會實際檢查它。
概念很簡單:如果某個變數從語意上不該改變,那就告訴語言。這樣編譯器會成為你的盟友:它會捕捉到意外的修改,且即便一週或 30 秒後回看程式碼,你也會更容易理解它。
什麼是 const
先用不帶魔術的說法。關鍵字 const 表示:初始化之後該值不得更改。不是「好像不能」,不是「不建議」,而是「編譯器會禁止」。
重要的細節是:const 並不表示「值在事前已知」,而是「在取得值之後它就不可變」。因此 const 很適合搭配使用者輸入的值:你可以從 std::cin 讀入一個數,再把它「凍結」到一個 const 變數中,避免在後續計算中不小心改掉它。
一個小例子 — 最誠實的示範:
#include <iostream>
int main() {
const int max_retries = 3;
std::cout << max_retries << '\n'; // 3
max_retries = 4; // 編譯錯誤
return 0;
}
這裡 max_retries 是一個「設定值」,如果它是設定值,那麼在程式中途突然改變通常不合常理。
const 需要初始化
接下來會有一個點讓很多人第一次從編譯器那裡聽到堅定的「不」。普通變數可以先宣告、後賦值(雖然這也要謹慎)。但 const 變數必須立即取得值,否則...我們要去「凍結」什麼?
因此這種寫法是 無效的:
#include <iostream>
int main() {
const int x; // 錯誤:const 未初始化
return 0;
}
而這樣是有效的:
#include <iostream>
int main() {
const int x{10};
std::cout << x << '\n'; // 10
return 0;
}
心理上這很有用:看到 const 時,你立刻知道該變數有一個合理的初始值。編譯器也不用猜測裡面裝的是什麼。
3. const 與初始化:=、()、{}
在前一講後會有個自然疑問:「const 只適用某一種初始化嗎?」不,const 不是一種單獨的「初始化形式」。它是個修飾符,意思是:初始化之後不要碰它。
我們用一個例子示範三種寫法:
#include <iostream>
int main() {
const int a = 10;
const int b(10);
const int c{10};
std::cout << a << ' ' << b << ' ' << c << '\n'; // 10 10 10
return 0;
}
為何常建議使用 {}?原因與不用 const 時相同:花括號通常能更好地捕捉那些因類型轉換造成的潛在問題。它也在視覺上提醒你:「我們是在建立一個值」,而不是「做個操作」。
4. const 與編譯期已知的值
能不能想出比 const 更「常量」的東西?可以,C++ 有一個更嚴格的概念 — 常量表達式(constant expressions):編譯器必須能在編譯階段事先計算出的值。這種值不只是不可變,而是在程式執行前就已知——在編譯期。
今天我們不深入 constexpr(那會在下一講),但先把差別釐清:
- const 回答的問題是:「在取得之後可以更改嗎?」
- constexpr 回答的問題是:「這個值必須在編譯期已知嗎?」
一個示例很好地去神秘化:使用者輸入一個數字,我們把它設為 const。編譯器事前不知道這個數,但在輸入後我們禁止它被改變。
#include <iostream>
int main() {
int x{};
std::cin >> x;
const int snapshot{x}; // 值的快照
std::cout << snapshot << '\n';
return 0;
}
5. 實務:在哪裡 const 特別有用
「快照」值的場景
現在一個非常實用的想法。有時變數本質上會變(例如你在累加一個總和),但你需要把某個狀態「固定」下來,之後當作事實來使用。這就像拍電表的照片:電表在轉,但照片不會改,靠照片去計費。
場景:使用者輸入基礎價格,你之後會多次用它做計算。如果把這個值放在普通變數,可能會不小心被覆寫(尤其當程式碼變多時)。因此我們做一個「快照」。
#include <iostream>
int main() {
double entered_price{};
std::cin >> entered_price;
const double base_price{entered_price}; // 固定
std::cout << base_price << '\n';
return 0;
}
這不是「強制性的風格」,而是一種工具。但當你開始寫超過 30 行的程式時,你會突然發現「不小心覆寫變數」不是假設,而是常態。
const 對抗「魔法數」
新手程式碼中最常見的問題之一是,那些「莫名其妙出現」的數字。今天的 0.2 是 VAT,明天是折扣,後天又變成係數,你就不記得它原本代表什麼。這類數字被稱為「魔法數」。
const 讓數字有了一個名字,並顯著提升可讀性。不需要立即引入複雜的架構思想:只要把係數抽成變數即可。
比較:
#include <iostream>
int main() {
double price{100.0};
double total = price + price * 0.2; // 0.2 是什麼?
std::cout << total << '\n'; // 120
return 0;
}
和:
#include <iostream>
int main() {
const double vat_rate{0.2};
double price{100.0};
double total = price + price * vat_rate;
std::cout << total << '\n'; // 120
return 0;
}
第二種寫法即使沒有註解也能看懂發生了什麼。如果要改稅率,只需改一處。
簡單示意:把事實與工作變數分開
寫程式時,語意上你的變數通常分兩類:會變的資料與應保持為事實的資料。const 幫你在程式碼中直接分離它們。這讓除錯更簡單:你知道哪些值絕對不該被意外覆寫。
我們畫個簡單的流程圖,接著會把它做成完整範例:
flowchart TD
A[輸入原始資料] --> B[用 const 固定事實]
B --> C[計算中間值]
C --> D[計算總計]
D --> E[輸出結果]
在真實程式中,「事實」通常是係數、限制、計算的初始參數;而「會變的」則是累加器、總和、計數器、當前值。
6. 實作範例:迷你收銀機
現在我們組一個小型的 console 程式,當作本章的練習應用。我們暫時不使用函式、容器等(那會在後面講),而是誠實地使用變數、初始化與 const。
情境:使用者輸入商品價格與折扣百分比。我們的 VAT 稅率是固定的。計算最終價格。
版本 1:天真(且脆弱)
先來一個簡單版本,技術上能運作,但容易被意外修改破壞:
#include <iostream>
int main() {
double price{};
double discount_percent{};
std::cin >> price >> discount_percent;
discount_percent = discount_percent / 100.0; // 重新使用變數
double total = price - price * discount_percent;
std::cout << total << '\n';
return 0;
}
問題不在於這樣做「不好」。問題是變數 discount_percent 的語意改變了:一開始是「百分比」,後來成為「比率」。幾天後你可能忘了目前到底處於哪個階段。
版本 2:分離事實並固定它們
我們把輸入值固定成 const,中間計算另用新變數:
#include <iostream>
int main() {
double price_input{};
double discount_input{};
std::cin >> price_input >> discount_input;
const double price{price_input};
const double discount_percent{discount_input};
const double discount_rate{discount_percent / 100.0};
double total{price - price * discount_rate};
std::cout << total << '\n';
return 0;
}
這裡可以明顯看出 discount_percent 不會改變,discount_rate 是個獨立概念,你不會把同一變數當成變色龍使用。
版本 3:把 VAT 當作 const
我們再加一個想法:VAT 率是固定的,不該變。
#include <iostream>
int main() {
const double vat_rate{0.2};
double price_input{};
double discount_input{};
std::cin >> price_input >> discount_input;
const double price{price_input};
const double discount_rate{discount_input / 100.0};
const double discounted{price - price * discount_rate};
const double total{discounted + discounted * vat_rate};
std::cout << total << '\n';
return 0;
}
注意效果:幾乎所有變數都變成 const,因為我們 不需要改變它們。可變的只有輸入緩衝(price_input, discount_input)— 嚴格來說,這兩者也可以不設為可變,只是這樣寫比較直觀。結果程式看起來像一連串公式,這是計算型問題一個很舒服的風格。
7. const 與未來的維護
有個簡單準則:如果變數會改變,讀者就得遍歷程式碼去找它在哪裡、為何改變。若變數是 const,讀者可以放心:「好,這是個事實,不用追蹤它被哪裡改了」。
當你在修 bug 時這尤其有用。常見的 bug 是:「為何總和變成負的?」最後發現是某處不小心改掉了某個係數或初始總和。const 把這類問題轉變為編譯期錯誤 — 讓你在程式還沒跑之前就發現問題。
額外好處:const 會養成紀律。它迫使你思考:「這是參數?設定?事實?還是工作變數?」隨著時間推移,你的程式碼會變得不那麼隨意。
8. 使用 const 的常見錯誤
錯誤 №1:宣告 const 卻不初始化。
這樣不行,因為 const 要「凍結」一個具體值。如果寫了 const int x;,不清楚我們在禁止改變什麼。正確做法是立即初始化:例如 const int x{10}; 或 const int x = 10;。
錯誤 №2:把 const 用在語意上應該會變的變數上。
有時新手為了「提高安全」把所有東西都標為 const,包括累加的總和或計數器。編譯器會報錯,而你會對此感到挫折。重點是區分:const 適合事實(限制、稅率、初始資料、快照),可變資料保留普通變數。
錯誤 №3:混淆 const 與「編譯器已知的值」。
const 是禁止在初始化後改變。值可能來自使用者輸入、執行時或時間函數,仍然可以是 const。而「必須在編譯期計算出來」是另一個概念,會在下一講討論(constexpr)。
錯誤 №4:在途中改變變數語意,而不建立新變數。
典型例子:一個變數先代表「百分比」,後來代表「比率」,再後來代表「折扣金額(歐元)」。這使程式脆弱。const 常常能揭發這種思維,促使你引入清晰的新名字:discount_percent、discount_rate、discount_value。
錯誤 №5:害怕使用 const,擔心「也許將來會改變」。
這是很人的習慣:保留「備用出口」。但在程式碼中,這種備用出口成為漏洞:明天你或別人真的會不小心改掉不該改的東西。如果語意上不應改變,就把它固定。如果以後確定需要改動,你可以有意識地在具體位置移除 const,並理解原因。
GO TO FULL VERSION