CodeGym /課程 /C++ SELF /const 與不可變性契約

const 與不可變性契約

C++ SELF
等級 7, 課堂 2
開放

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,並理解原因。

1
任務
C++ SELF, 等級 7, 課堂 2
上鎖
數值快照
數值快照
1
任務
C++ SELF, 等級 7, 課堂 2
上鎖
含稅價格
含稅價格
1
任務
C++ SELF, 等級 7, 課堂 2
上鎖
快遞員速度
快遞員速度
1
任務
C++ SELF, 等級 7, 課堂 2
上鎖
收銀單掃描器
收銀單掃描器
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION