← 目录 / 第十二章 · 类与面向对象 / 12.3 封装与访问控制

12.3 封装与访问控制

为什么要把数据"藏"起来?publicprivateprotected 三个关键字各管什么——用门禁卡的比喻彻底搞清楚。

本页目录
12.3.1 封装是什么

想象你的银行账户。银行当然知道你账户里有多少钱,但它不会把账本直接放在柜台上让任何人随便翻看。你只能通过"存款"、"取款"、"查余额"这几个固定窗口来操作——这就是封装。

封装的核心思想:把数据藏起来,只对外提供受控的操作接口。 这样做有两个好处:

保护数据安全 🛡️
外部代码无法直接修改内部数据,必须经过类提供的方法。可以在方法里加校验逻辑,防止非法值写入。
隐藏实现细节 📦
使用者不需要知道内部怎么实现的,只关心"能做什么"。内部实现改变了,外部代码也不需要跟着改。

在 C++ 里,封装是靠三个访问控制关键字实现的:publicprivateprotected

12.3.2 三个访问控制关键字
🔓
public
公开的。任何地方都能访问——类内部、子类、外部代码全都可以。
谁能访问✅ 所有人
🔒
private
私有的。只有这个类自己的成员函数才能访问,子类和外部代码都不行。
谁能访问✅ 仅本类内部
🏠
protected
受保护的。本类和子类能访问,外部代码不行。主要在继承时用到。
谁能访问✅ 本类 + 子类
💡
用门禁卡来记:
想象一栋大楼,public 是大厅——所有人都能进;private 是保险库——只有本人能进;protected 是员工专区——本人和家属(子类)能进,其他人不行。
关键字类内部子类(继承)外部代码
public ✅ 可以✅ 可以✅ 可以
protected ✅ 可以✅ 可以❌ 不行
private ✅ 可以❌ 不行❌ 不行
📌
现阶段记住两个就够了:private 把数据藏起来,public 把操作接口暴露出去。protected 在学到继承(12.8)时自然就明白了。
12.3.3 代码里怎么写

关键字后面跟一个冒号,之后的所有成员都属于这个权限级别,直到遇到下一个关键字为止。

C++ · 银行账户示例
1class BankAccount {
2private: // 🔒 以下成员外部不可见
3 string owner;
4 double balance; // 余额(外部不能直接改)
5
6public: // 🔓 以下成员对外公开
7 void Deposit(double amount) {
8 if (amount > 0) // ✅ 只允许存正数
9 balance += amount;
10 }
11 bool Withdraw(double amount) {
12 if (amount > 0 && amount <= balance) {
13 balance -= amount;
14 return true;
15 }
16 return false; // 余额不足,取款失败
17 }
18 double GetBalance() { return balance; }
19};
20
21int main() {
22 BankAccount acc;
23 // acc.balance = 99999; ❌ 编译错误!private 不可直接访问
24 acc.Deposit(1000); // ✅ 通过接口存钱
25 acc.Withdraw(200); // ✅ 通过接口取钱
26 cout << acc.GetBalance(); // 输出:800
27}
🎯
注意第 8、12 行:Deposit 只允许存正数,Withdraw 会检查余额够不够——这些校验逻辑只需要写一次,放在类里。外部无论多少地方调用,都自动受到保护,不可能绕过去。
BankAccount private 🔒 owner balance 外部无法直接读写 必须通过接口操作 public 🔓 Deposit() Withdraw() GetBalance() 内部可访问 🧑 外部代码 ✅ 可调用 ❌ 直接访问 balance?拒绝 外部只能调用 public 方法;balance 的读写必须经过 Deposit / Withdraw / GetBalance
balanceprivate 保护,外部任何代码都无法直接改写它。
存款必须经过 Deposit(),取款必须经过 Withdraw(),校验逻辑就在这两个函数里——漏洞从根本上消除了。
12.3.4 getter 与 setter

数据藏进 private 之后,如果外部代码需要读或改它,怎么办?答案是提供专门的"读取函数"和"写入函数",分别叫 gettersetter

外部代码
s.SetScore(95)
调用者
SetScore(val)
校验后写入
setter(写入)
score
private 成员
被保护的数据
GetScore()
直接返回值
getter(读取)
C++ · getter 与 setter
1class Student {
2private:
3 string name;
4 int score;
5
6public:
7 // getter:只读,不改数据
8 string GetName() { return name; }
9 int GetScore() { return score; }
10
11 // setter:写入前先校验
12 void SetName(string n) {
13 if (!n.empty()) name = n; // 不允许空名字
14 }
15 void SetScore(int s) {
16 if (s >= 0 && s <= 100) score = s; // 只允许 0~100
17 }
18};
19
20int main() {
21 Student s;
22 s.SetName("Alice");
23 s.SetScore(95); // ✅ 正常写入
24 s.SetScore(-10); // ⚠️ 不合法,被 setter 拦截,score 不变
25 cout << s.GetScore(); // 输出:95
26}
💡
只需要 getter 不需要 setter 时,数据就变成了"只读"——外部能查到值,但永远无法修改,这比任何注释都更有约束力。
12.3.5 竞赛中怎么用

CSP-J / NOIP 的题目里,并不强制要求每个成员都用 private 封装,很多时候直接用 struct 全部公开也完全没问题。封装最大的价值在于大项目的维护和防错

但有一种情况在竞赛中很常见,用封装会更优雅——带有合法性约束的数据结构,比如分数必须在 0~100、坐标不能越界等,用 setter 统一拦截比每次赋值都手写 if 判断要干净得多。

📖
本节小结:封装的三句话
① 把数据private,对外不可见。
② 把操作public,对外提供接口。
③ 在接口里加校验逻辑,拦截非法数据。