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 計數器當作展示機制或「建立次數」的度量,而不是嚴格的存活數量會計。
GO TO FULL VERSION