存取控制是 C++ 封裝機制的核心,決定「誰可以存取什麼」。C++ 提供三種基本存取指定符、三種繼承方式組合產生的存取調整規則、以及打破封裝邊界的友元機制。本文系統梳理完整規則體系,並給出現代 C++ 封裝設計最佳實踐。
| 指定符 | 類別內部 | 衍生類別 | 友元函式/類別 | 外部代碼 (物件/指標/參考) |
|---|---|---|---|---|
public | ✅ | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ✅ | ❌ |
private (預設) | ✅ | ❌ | ✅ | ❌ |
關鍵點:
friend 函式/類別獲得與成員函式同等存取權obj.public_member 合法,obj.private_member 編譯錯誤繼承方式決定基類成員在衍生類中的呈現存取層級:
| 基類成員原層級 | public 繼承 | protected 繼承 | private 繼承 |
|---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | 不可存取 (但存在於物件中) | 不可存取 | 不可存取 |
class Base {
public: int pub = 1;
protected: int pro = 2;
private: int pri = 3;
};
class PubDerived : public Base {
void test() { pub; pro; /* pri; // 錯誤 */ }
};
class ProDerived : protected Base {
void test() { pub; pro; /* pri; // 錯誤 */ } // pub/pro 變為 protected
};
class PriDerived : private Base {
void test() { pub; pro; /* pri; // 錯誤 */ } // pub/pro 變為 private
};
int main() {
PubDerived d1; d1.pub; /* d1.pro; d1.pri; // 錯誤 */
ProDerived d2; /* d2.pub; d2.pro; d2.pri; // 全部錯誤 */
PriDerived d3; /* d3.pub; d3.pro; d3.pri; // 全部錯誤 */
}
class Matrix {
std::vector data_;
int rows_, cols_;
public:
// 友元函式:非成員函式獲得私有存取
friend Matrix multiply(const Matrix& a, const Matrix& b);
// 友元類別:整個類別獲得私有存取
friend class MatrixFactory;
// C++11 樣板友元:特定實例化獲得存取
template friend class Serializer;
};
// 友元函式定義 (非成員)
Matrix multiply(const Matrix& a, const Matrix& b) {
// 直接存取 a.data_, a.rows_ 等私有成員
return Matrix(/* ... */);
}
operator<<, operator==) 需存取私有資料friend class Factory)friend class Serializer)#ifdef TEST)protected 介面、Pimpl 惯用法隱藏私有細節| 維度 | 定義 | 可存取成員 |
|---|---|---|
| 水平存取 | 外部代碼透過物件/指標/參考存取成員 | 僅 public |
| 垂直存取 | 衍生類別透過繼承存取基類成員 | public + protected |
本質:存取控制作用於「名稱查找」與「存取檢查」階段,不影響物件記憶體佈局。私有成員依然存在於衍生類物件中,只是名稱在衍生類作用域不可見。
class BankAccount {
// 實現細節完全隱藏
int64_t balance_cents_ = 0;
std::mutex mtx_;
std::vector history_;
public:
// 穩定介面
explicit BankAccount(int64_t initial = 0) : balance_cents_(initial) {}
// 命令:修改狀態
bool deposit(int64_t amount) { /* ... */ }
bool withdraw(int64_t amount) { /* ... */ }
// 查詢:不修改狀態
int64_t balance() const noexcept {
std::lock_guard lock(mtx_);
return balance_cents_;
}
// 只讀存取歷史 (避免拷貝大向量)
const std::vector& history() const noexcept { return history_; }
};
// 介面類別:純虛擬解構函式 + 純虛擬介面
class IRepository {
public:
virtual ~IRepository() = default;
virtual std::optional find_by_id(ID id) = 0;
virtual void save(const Entity& e) = 0;
};
// 實現類別:final 封鎖繼承 (除非設計為可擴展)
class SqlRepository final : public IRepository {
// 私有繼承內部實現細節 (如連線池、快取)
std::unique_ptr pool_;
std::unique_ptr cache_;
public:
// 實現介面...
};
// Widget.hpp (公開標頭檔,極小依賴)
class Widget {
struct Impl; // 前向宣告
std::unique_ptr pimpl_;
public:
Widget();
~Widget(); // 必須在 .cpp 定義 (Impl 完整型別可見)
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
void do_work();
};
// Widget.cpp (實現細節,變更不觸發使用者重編)
#include "Widget.hpp"
#include "HeavyInternal.hpp"
#include
struct Widget::Impl {
HeavyInternal engine_;
std::vector cache_;
};
Widget::Widget() : pimpl_(std::make_unique()) {}
Widget::~Widget() = default;
// 移動操作預設即可
void Widget::do_work() { pimpl_->engine_.process(pimpl_->cache_); }
// ❌ 貧血模型:資料結構偽裝成類別
struct User {
std::string name;
std::string email;
int age;
// 一堆 get/set...
};
// ✅ 豐富模型:封裝業務規則
class User {
std::string name_;
std::string email_;
int age_;
public:
User(std::string name, std::string email, int age)
: name_(std::move(name)), email_(std::move(email)), age_(age) {
validate();
}
void change_email(std::string new_email) {
if (!is_valid_email(new_email)) throw InvalidEmail{};
email_ = std::move(new_email);
}
void celebrate_birthday() { ++age_; }
// 僅暴露必要查詢
const std::string& name() const noexcept { return name_; }
bool is_adult() const noexcept { return age_ >= 18; }
};
| 誤區/陷阱 | 正確理解 |
|---|---|
| 「private 成員不被繼承」 | 私有成員存在於衍生類物件中,只是名稱不可見;大小/佈局受影響 |
| 「protected 允許衍生類物件存取基類 protected 成員」 | 衍生類成員函式可存取 this 指向物件的 protected 成員,不能存取其他基類物件的 protected 成員 |
| 「friend 破壞封裝,絕不使用」 | 運算子多載、工廠、序列化等場景適度使用 反而增強封裝 (避免暴露 getter/setter) |
| 「繼承方式影響物件大小」 | 繼承方式僅影響名稱查找與存取檢查,不影響記憶體佈局、虛函式表、大小 |
| 「struct 預設 public,class 預設 private,其他無差異」 | 完全正確。唯一語義差異。慣例:struct 用於純資料聚合 (POD-like),class 用於不變量封裝 |
C++ 存取控制系統雖規則繁多,核心邏輯清晰:
掌握這套體系,不僅能通過面試,更能設計出封裝嚴密、介面穩定、易於維護、二進位相容的 C++ 類別架構——這才是存取控制的工程價值。