1. 為什麼需要 public 和 private
如果你剛開始,會很自然地想:「我有資料——所以我把欄位設為公開,問題解決」。這種方式在專案很小且你把所有規則記在腦中時可行。但一旦程式變得稍長一些(或僅僅過了一週),就會出現典型的 bug:程式某處有人把欄位賦了「不對的值」,然後你得花半小時找是誰為什麼那樣做。
把你的物件想像成咖啡機。欄位是它的內部:壓力、溫度、時間。外部程式碼是那個說「幫我做杯濃縮咖啡」的人。如果我們讓使用者直接動手改變鍋爐壓力,他可能會做出濃縮咖啡……也可能在廚房造成小火山。在程式中「小火山」通常表現為「為什麼我的餘額變成負值?」或「為什麼一個標題為空的任務突然出現在列表中?」。
public 和 private 用於一個簡單但強大的技巧:將「可以對物件做什麼」與「內部如何實現」分離。外部程式碼透過清晰的操作(方法)與物件互動,而物件自己則不允許被弄壞。
區段 public: 與 private: — 真正的邊界,而非約定
當我們說「封裝」,這聽起來像是一種哲學。但在 C++ 裡,它也是相當實際的機制:編譯器會字面上禁止訪問 private。也就是說,這不是「團隊約定」,而是「地鐵閘門」:沒有票就過不去,即使你再急。
在 class(或 struct) 的宣告內可以寫 public: 與 private: 區段。所有位於 public: 之下的成員對外部可見;所有位於 private: 之下的成員僅對物件自身(即其方法)可見。區段的作用一直持續到下一個區段為止。
先看一個小例子,感受這個「閘門」:
#include <iostream>
class SecretBox {
private:
int code_ = 1234;
public:
int reveal_code() { return code_; }
};
int main() {
SecretBox b;
std::cout << b.reveal_code() << '\n'; // 1234
// std::cout << b.code_ << '\n'; // 錯誤:private
}
這裡重要的是:外部程式碼 不能 偶然或有意地讀取 code_,除非你允許。這不是「一種風格」,而是在語言層面的真正封鎖。
2. 最小介面與封裝的實踐
最小化公開介面的想法非常實用:對外只暴露真正需要的東西,其他都作為實作細節隱藏。這有三重好處。
第一個原因 — 安全性:公開的改變狀態的方式越少,越少出錯的可能性。
第二個原因 — 清晰性:當其他開發者(或幾週後的你)看到 public 部分時,他會理解這些是「正式的控制按鈕」。
第三個原因 — 自由度:如果某個東西放在 private,你可以任意重寫它,外部程式碼不會受影響(因為它本就不能直接接觸)。
把元素心理上分成兩類會很有幫助:
| 分類 | 這是什麼 | 範例 |
|---|---|---|
| 介面 (public) | 「可以做哪些操作」 | |
| 實作 (private) | 「如何儲存與檢查」 | |
這個表格很簡單,但能幫助自律:如果你無法解釋一個方法為什麼要被外部呼叫,那它可能不該放在 public。
私有欄位與可預測的初始狀態
新手的一個常見且令人頭痛的錯誤是:「建立物件後內部是垃圾值」。在現代 C++ 我們習慣在欄位宣告處直接給初始值。這在類別中同樣有效,能幫助物件在建立後立即「準備就緒」。
通常私有欄位會以下劃線 _ 作為後綴。這既不是魔法也不是編譯器要求,而是一種視覺習慣:這樣更容易看出哪裡是物件的內部、哪裡是函式參數。
現在開始設計我們的教學用主控台應用(暫名為 TaskTracker)——任務列表。以前我們可能用 struct Task { ... } 並把欄位設為公開。現在反過來:把欄位隱藏,只在外部保留有意義的操作。
#include <string>
class Task {
private:
int id_ = 0;
std::string title_ = "(empty)";
bool done_ = false;
public:
void set_id(int id) { id_ = id; }
};
這還不是完美設計(我們還沒檢查 id 的正確性),但重點已經清楚:欄位被隱藏,外部程式碼不能隨意碰觸 done_ 或 title_。
公開方法作為改變狀態的「唯一入口」
當我們封閉欄位時,合理的問題是:「那要怎麼操作物件?」答案是透過方法。重點是不走上「為了寫方法而寫方法」的歧路。
一個好的公開方法應該是有明確語意的操作,而不是「只是賦值」。即便內部真的是一行賦值,外部仍然想看到意圖。
例如,我們不會允許所有地方隨意改變 done_,而是提供一個公開指令「把任務標記為完成」:
class Task {
private:
bool done_ = false;
public:
void mark_done() { done_ = true; }
void mark_undone() { done_ = false; }
};
雖然內部只是一次賦值,但差別很大:外部程式碼現在不會寫 task.done_ = 123; 並把 bool 變成「把 123 塞進去」。外部程式碼表達的是意圖:「把它設為完成」。
私有輔助方法:避免檢查散落專案各處
隨著程式碼增長,另一個典型問題是檢查開始重複出現。在某處你檢查了「標題不為空」,在另一處忘了,在第三處又有不同的檢查(例如「移除空白後不為空」),這已經是不同的語意了。
解藥很簡單:把檢查提取到私有方法。重要的是它必須是 private,因為外部程式碼不應該依賴你的內部檢查邏輯。今天你覺得某種檢查是正確的,明天可能改變。外部最好只有一個穩定按鈕:rename(...) 回傳成功/失敗。
看一個小例子:
#include <string>
class Task {
private:
std::string title_ = "(empty)";
bool is_title_ok(const std::string& s) {
return !s.empty();
}
public:
bool rename(const std::string& new_title) {
if (!is_title_ok(new_title)) return false;
title_ = new_title;
return true;
}
};
注意到的架構效果:外部程式碼不需要記住「命名規則」。它只呼叫 rename(...) 並取得結果。所有規則集中在一處。
不要把多餘的東西暴露出去:如何避免把 private 變成虛設
有時會看到這種情況:欄位是私有的,但你提供了一個函式,通過它可以達到與公開欄位同樣的效果,只是路徑更長。這會給人一種「我們好像隱藏了欄位,但實際上沒隱藏」的感覺。
封裝洩漏的最明顯跡象是:你返回了一個可修改的引用到內部欄位。外部程式碼就可以繞過你的保護,任意修改內部狀態。
看下面的例子,想像一下會發生什麼(這是故意讓你感到緊張的例子):
#include <string>
class Task {
private:
std::string title_ = "read book";
public:
std::string& title_ref() { return title_; } // 危險
};
為什麼危險?因為外部程式碼現在可以做 task.title_ref().clear(); 或寫入違反你未來規則的內容。也就是說,你在 private 前面放了門,然後在旁邊鑿了個跟門一樣大的窗戶。
即便現在你沒有嚴格的規則,養成這樣的習慣很有益:「如果我把資料直接暴露出去,我幾乎總是會失去控制」。
類別內部的控制流示意
為了加深印象,把這個流程看成一張圖會很有幫助。外部程式碼不應直接動欄位;它應該呼叫公開方法,公開方法再使用私有欄位和私有輔助方法。
flowchart TD
A[外部程式碼:main / 函式] --> B[類的 public 方法]
B --> C{驗證輸入資料}
C -->|OK| D[修改 private 狀態]
C -->|不通過| E[拒絕:false / 不做任何改變]
D --> F[物件保持正確狀態]
這不是「為了 OOP 而 OOP」。這只是避免程式變成「某人在某處不知為何改了什麼」的一種方便方式。
4. 實作迷你範例:具有最小介面的 TaskTracker
組合一小段應用程式,準確展示本講座的主題:哪些是 public、哪些是 private,以及為什麼介面應該保持小而清晰。
首先是模型 Task。我們讓外部程式碼只能「給任務 id」、「重新命名」、「標記為完成」。目前方法沒有標註 const(這會在另一講討論),所以不做更多複雜化。
#include <string>
class Task {
private:
int id_ = 0;
std::string title_ = "(empty)";
bool done_ = false;
public:
void set_id(int id) { id_ = id; }
bool rename(const std::string& t) {
if (t.empty()) return false;
title_ = t;
return true;
}
void mark_done() { done_ = true; }
};
現在是「任務儲存庫」——一個在內部持有 std::vector<Task> 的類別。對外我們只提供兩個操作:新增任務與列印所有任務。注意:我們不把底層的 vector 直接暴露,否則任何人都能把任務拿出來並搞壞它。
#include <iostream>
#include <vector>
class TaskBook {
private:
std::vector<Task> tasks_;
public:
void add(Task t) { tasks_.push_back(t); }
void print_all() {
std::cout << "Tasks: " << tasks_.size() << '\n';
}
};
程式看起來簡單,但它正好說明了重點:tasks_ 被隱藏,外部程式無法做出「哎呀,我把所有東西刪了」的操作。
現在看使用這個 API 的 main。它完全不知道內部如何儲存資料;它只看到被允許的操作。
#include <iostream>
int main() {
Task t;
t.set_id(1);
t.rename("Buy milk");
t.mark_done();
TaskBook book;
book.add(t);
book.print_all(); // Tasks: 1
std::cout << "OK\n"; // OK
}
這個小應用的意義不是它已經完整可用,而是它示範了正確的界線:外部程式碼透過少數清晰的方法管理物件,而內部狀態則被封閉。
5. 設計 public/private 時常見錯誤
錯誤 1:把所有欄位都設為 public,把檢查放在「腦子裡」。
這是非常常見的開始:「我很小心,真的。」問題是注意力無法無限擴展。幾天後你會忘記某條規則,幾週後別人根本不知道你的約定。正因如此,需要 private,以確保規則總是被執行,否則編譯器根本不會讓你通過。
錯誤 2:把欄位藏起來,卻提供能做任何事的方法。
形式上有封裝,但實質上沒。最明顯的徵象是:你返回可變引用(T&)到內部欄位,或直接把容器原封不動地暴露。外部程式碼又能任意破壞你的限制。如果只是為了方便,最好新增一些有意義的小操作,而不是把內部直接暴露。
錯誤 3:為了「以防萬一」把 public 介面擴得很大。
新手常想:「也許將來會用到?」然後加了 15 個方法。但公開方法是一種承諾:外部程式會依賴它。以後你想重寫實作卻做不到,因為它已被廣泛使用。最好等真正需要時再新增方法,而不是永遠維護多餘的按鈕。
錯誤 4:把重要的正確性規則放在類別外部。
當規則散落在 main 與多個函式中時,會出現:某處檢查了、某處忘了。更可靠的做法是:所有狀態變更都通過類別的 public 方法,檢查放在內部。即使外部程式碼犯錯,物件也不會接受不正確的狀態。
錯誤 5:忘了 public:/private: 區段的作用範圍是「到下一個區段」。
有時會寫了 private:,然後在下面加方法,卻驚訝為什麼在 main 無法呼叫。或者反過來:想隱藏欄位,卻不小心把它放在 public: 之下。養成習慣:在類別開頭快速掃一遍區段,確認「按鈕」確實在 public,而「內部東西」在 private。
GO TO FULL VERSION