CodeGym /課程 /C++ SELF /static‑類別成員:共用設定與 id 生成

static‑類別成員:共用設定與 id 生成

C++ SELF
等級 49, 課堂 1
開放

1. 什麼是 static 類別成員

當開始撰寫類別時,很容易走到「一切都是物件欄位」的極端。某個時刻你會發現一個奇怪的情況:你有 10 個物件,每一個都保存著相同的「預設值」、相同的設定或相同的參考資料。這就好像每個樓梯間都有自己的一套物理法則——雖然可行,但通常是多餘的。

static 成員允許描述屬於類別「作為一個概念」而非某個具體實例的資料與函式。在實務上常見的用法包括「共用設定」、「發放過的 id 計數器」、「狀態到文字的對照表」或「全域上限」。而且,這也是避免全域變數的一種整潔做法,不會把專案弄得「哪裡都可存取」。

非靜態欄位與 static 欄位:誰擁有什麼

簡單說來,非靜態欄位是每個物件的一部分。相對地,static 欄位是類別的一部分,它在整個程式中只有一個實例(更精確地說,是在該類別所處的「世界」中只有一個)。

想像我們有一個迷你應用「任務清單(Task)」。每個任務有標題和「已完成」旗標,這些屬於具體任務的資料。而「下一個 id」則不是針對個別任務,而是針對整個任務系統。

簡要示意(很具象徵性,請勿苛求):

flowchart LR
    subgraph Program["程式"]
      subgraph ClassTask["類別 Task"]
        NextId["static next_id_(全域唯一)"]
      end

      T1["task1: title_, done_, id_"]
      T2["task2: title_, done_, id_"]
      T3["task3: title_, done_, id_"]

      NextId --> T1
      NextId --> T2
      NextId --> T3
    end

以及一張簡短的對照表以加深印象:

屬性 非靜態欄位 (title_, done_) static 欄位 (next_id_)
實例數量 每個物件各一個 整個類別只有一個
是否可在沒有物件時存取 否 是
典型含意 具體實體的狀態 共用設定 / 共用資源 / 類別級的全域變數

2. static 方法:沒有具體物件的函式

資料講完了,接著是行為。static 方法是可以在沒有物件情況下呼叫的方法。它沒有「隱含參數 this」,因此不知道要操作哪個物件。

這很合理:如果方法的意義不是針對某個任務,而是針對「應用設定」或「產生 id」,那麼不必在物件上呼叫它。

小範例:為任務應用做一個設定類別。

#include <iostream>

class AppConfig {
    static bool show_done_;
public:
    static void set_show_done(bool value) { show_done_ = value; }
    static bool show_done() { return show_done_; }
};

bool AppConfig::show_done_ = true;

int main() {
    std::cout << AppConfig::show_done() << '\n'; // 1
    AppConfig::set_show_done(false);
    std::cout << AppConfig::show_done() << '\n'; // 0
}

重要的感受是:我們沒有建立 AppConfig config; 這樣的實例,而這是正常的。類別被當成一個「容器」來保存共用設定。

現在來個理解檢查:為何 static 方法不能直接輸出某個任務的 title_?因為它沒有那個任務;沒有任何東西可供它參照。

3. static 欄位住在哪裡,為何需定義

這裡通常會遇到與連結器(linker)的首次真正接觸。編譯器可能沒問題,但連結器會抱怨:「你宣告了 static 欄位,但它實際在記憶體哪裡?」

新手規則如下:

如果你在類別內寫了 static 欄位,那只是做了宣告。要讓它真正在程式中存在,必須在某個 .cpp 檔案(或如教學範例在同一檔案下方)定義一次。

看範例:

#include <iostream>

class Counter {
    static int total_;
public:
    static void inc() { ++total_; }
    static int total() { return total_; }
};

// 定義 static 欄位(沒有這行會導致連結錯誤)
int Counter::total_ = 0;

int main() {
    Counter::inc();
    std::cout << Counter::total() << '\n'; // 1
}

如果忘了那一行 int Counter::total_ = 0;, 編譯器有時會通過,但在連結階段會出現「undefined reference」或「unresolved external symbol」之類的錯誤(錯誤敘述取決於使用的編譯器或 IDE)。換句話說:系統知道有這個變數,但找不到它的實體。

關於現代 C++ 的備註

有時候你會在程式中看到:

inline static int total_ = 0;

這表示宣告同時也是定義(在現代標準中如此)。

但在教學階段更建議先掌握基本且清楚的方式:「在類別中宣告 — 在其他地方單獨定義」。這方法在所有環境下都可用,且能幫助理解發生了什麼事。

4. 情境:應用的共用設定

對於 static 來說,一個很自然的場景是:有個會影響整個程式行為的共用設定。例如:是否顯示已完成的任務,或列印時標題的最大長度(避免把終端機變成長篇大論)。

在我們的教學小應用「TaskList」(假想的任務清單)中,我們先加入兩項設定:

  • 顯示已完成的任務或將其隱藏;
  • 列印時標題的最大長度(若超過就截斷)。

透過一個單獨的類別使用 static 來實作這些設定。

#include <string>

class AppConfig {
    static bool show_done_;
    static std::size_t max_title_len_;
public:
    static bool show_done() { return show_done_; }
    static void set_show_done(bool v) { show_done_ = v; }

    static std::size_t max_title_len() { return max_title_len_; }
    static void set_max_title_len(std::size_t v) { max_title_len_ = v; }
};

bool AppConfig::show_done_ = true;
std::size_t AppConfig::max_title_len_ = 20;

這裡我們使用了 std::size_t(在容器與 size() 的情境中已經見過),它是一個很適合表示「長度」的型別。

注意風格:透過方法存取資料。雖然可以把 static 欄位設為 public,但那樣很容易演變成「任何人都可以隨意改變」,然後你到處尋找為何 max_title_len_ 被設成 999999;。

5. 情境:為物件產生 id

第二個經典用途是:每個新物件都應該獲得一個唯一的識別碼。而這個唯一性不是「在物件內部唯一」,而是「在整個程式中唯一」。

我們來實作一個 Task 類別,其包含:

  • id_ — 該任務的個別編號(一般欄位);
  • next_id_ — 共用計數器,決定要發給下一個的 id(static 欄位)。

類別骨架

#include <string>

class Task {
    int id_;
    std::string title_;
    bool done_ = false;

    static int next_id_;
public:
    explicit Task(std::string title);
    int id() const { return id_; }
    const std::string& title() const { return title_; }
};

目前我們只宣告了 static 的 int next_id_;。接下來很重要的是要進行定義。

建構子與 next_id_ 的定義

#include <utility>

int Task::next_id_ = 1;

Task::Task(std::string title)
    : id_(next_id_++)
    , title_(std::move(title))
{}

可以清楚看到:建立物件時,我們取用目前的 next_id_,放進 id_,然後把 next_id_ 增加。

此外,next_id_++ 的語意是「先使用舊值,之後再增加」。看到這種寫法時常有人覺得像魔法,這很正常——只要記住語意就好。

在 main 中檢查

#include <iostream>

int main() {
    Task a{"Read C++ book"};
    Task b{"Write C++ code"};

    std::cout << a.id() << '\n'; // 1
    std::cout << b.id() << '\n'; // 2
}

重要的是:我們並沒有手動指定 id。在物件建立時它會自動產生。這有點像寄物櫃的號碼:你不會與寄物員爭論,接受遊戲規則即可。

6. static 計數器:示範與侷限

很容易產生的想法是:我們可以統計有多少個 Task 物件。直覺上會想到用一個 static int count_。

此處要提醒一點:已建立的物件次數 與 當前存活的物件數 是不同概念。另外還有複製(copy)會產生新物件、暫時物件等情況,這會讓你突然意識到「存活物件計數」並非一件簡單的小玩具,而是一項小型會計工作。

不過若僅做為示範,可以用 static 去記錄「建立了多少次任務」。

#include <iostream>

class TaskFactoryStats {
    static int created_;
public:
    static void on_created() { ++created_; }
    static int created() { return created_; }
};

int TaskFactoryStats::created_ = 0;

int main() {
    TaskFactoryStats::on_created();
    TaskFactoryStats::on_created();

    std::cout << TaskFactoryStats::created() << '\n'; // 2
}

在這裡我們誠實地命名:這不是「現在有多少物件存活」,而是「我們記錄了多少次建立」。這對統計或日誌是有用的,但不要把它當成萬物真理。

7. 範例:根據 static 設定列印任務

現在想看看設定如何實際影響程式行為。寫一個很小的函式,用於截短任務標題,根據 AppConfig::max_title_len() 來處理。

先寫成獨立函式(暫不複雜化),以體會「設定影響所有使用它的地方」這個概念。

#include <iostream>
#include <string>

std::string shorten(std::string s, std::size_t max_len) {
    if (s.size() <= max_len) return s;
    return s.substr(0, max_len) + "...";
}

int main() {
    std::cout << shorten("Very very long task title", 10) << '\n'; // Very very ...
}

接著把想法串接起來:max_len 可以從 AppConfig 取得。此時 static 欄位就變成真實工具:一次更改設定,所有讀取該設定的地方行為都會改變。

8. 使用 static 成員時常見的錯誤

錯誤 1:忘記定義 static 欄位(導致連結錯誤)。
這是最常見的情況:在類別內寫了 static int next_id_;,卻沒有那一行 int Task::next_id_ = 1;。結果程式可能能通過編譯,但在連結時會看到「undefined reference / unresolved external symbol」。解法簡單:每個 static 欄位需要且只能有一個定義。

錯誤 2:在 static 方法中嘗試以「直接方式」存取非靜態欄位。
有時會想在 static 方法裡寫 std::cout << title_;,但 title_ 是物件欄位,static 方法不知道要操作哪一個物件。如果需要處理特定任務,應該把該物件作為參數傳入,或使用非靜態方法在物件上呼叫。

錯誤 3:把本該屬於個別實例的狀態做成 static。
例如任務的「狀態」或「標題」在語意上應屬於特定任務,而非所有任務共用。若把它們設為 static,所有任務會意外共享同一個標題,這會導致非常奇怪的情況,就像桌面上的「新資料夾 (37)」一堆相同名稱的資料夾。

錯誤 4:把 static 當成「方便的全域變數」而缺乏紀律。
static 設定很容易變成隱藏的全域狀態:某處改了設定,其他地方的行為就變了,然後你花很久找原因。因此良好風格是用 getter/setter 包裝 static 資料、給予清晰名稱,並限定在確實需要共用時使用。

錯誤 5:把 static 計數器當成精確的存活物件數。
即便今天在建構子中增加計數器,明天可能有人開始複製物件(有時是無意的),統計就會變得不準確。在教學階段比較務實的做法是:把 static 計數器當作展示機制或「建立次數」的度量,而不是嚴格的存活數量會計。

1
任務
C++ SELF, 等級 49, 課堂 1
上鎖
總長度
總長度
1
任務
C++ SELF, 等級 49, 課堂 1
上鎖
自動識別碼
自動識別碼
1
任務
C++ SELF, 等級 49, 課堂 1
上鎖
購物匯率
購物匯率
1
任務
C++ SELF, 等級 49, 課堂 1
上鎖
玩家統計
玩家統計
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION