为什么要把数据"藏"起来?public、private、protected 三个关键字各管什么——用门禁卡的比喻彻底搞清楚。
想象你的银行账户。银行当然知道你账户里有多少钱,但它不会把账本直接放在柜台上让任何人随便翻看。你只能通过"存款"、"取款"、"查余额"这几个固定窗口来操作——这就是封装。
封装的核心思想:把数据藏起来,只对外提供受控的操作接口。 这样做有两个好处:
在 C++ 里,封装是靠三个访问控制关键字实现的:public、private、protected。
public 是大厅——所有人都能进;private 是保险库——只有本人能进;protected 是员工专区——本人和家属(子类)能进,其他人不行。| 关键字 | 类内部 | 子类(继承) | 外部代码 |
|---|---|---|---|
| public | ✅ 可以 | ✅ 可以 | ✅ 可以 |
| protected | ✅ 可以 | ✅ 可以 | ❌ 不行 |
| private | ✅ 可以 | ❌ 不行 | ❌ 不行 |
private 把数据藏起来,public 把操作接口暴露出去。protected 在学到继承(12.8)时自然就明白了。关键字后面跟一个冒号,之后的所有成员都属于这个权限级别,直到遇到下一个关键字为止。
| 1 | class BankAccount { |
| 2 | private: // 🔒 以下成员外部不可见 |
| 3 | string owner; |
| 4 | double balance; // 余额(外部不能直接改) |
| 5 | |
| 6 | public: // 🔓 以下成员对外公开 |
| 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 | |
| 21 | int main() { |
| 22 | BankAccount acc; |
| 23 | // acc.balance = 99999; ❌ 编译错误!private 不可直接访问 |
| 24 | acc.Deposit(1000); // ✅ 通过接口存钱 |
| 25 | acc.Withdraw(200); // ✅ 通过接口取钱 |
| 26 | cout << acc.GetBalance(); // 输出:800 |
| 27 | } |
Deposit 只允许存正数,Withdraw 会检查余额够不够——这些校验逻辑只需要写一次,放在类里。外部无论多少地方调用,都自动受到保护,不可能绕过去。balance 被 private 保护,外部任何代码都无法直接改写它。Deposit(),取款必须经过 Withdraw(),校验逻辑就在这两个函数里——漏洞从根本上消除了。
数据藏进 private 之后,如果外部代码需要读或改它,怎么办?答案是提供专门的"读取函数"和"写入函数",分别叫 getter 和 setter。
s.SetScore(95)
| 1 | class Student { |
| 2 | private: |
| 3 | string name; |
| 4 | int score; |
| 5 | |
| 6 | public: |
| 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 | |
| 20 | int main() { |
| 21 | Student s; |
| 22 | s.SetName("Alice"); |
| 23 | s.SetScore(95); // ✅ 正常写入 |
| 24 | s.SetScore(-10); // ⚠️ 不合法,被 setter 拦截,score 不变 |
| 25 | cout << s.GetScore(); // 输出:95 |
| 26 | } |
CSP-J / NOIP 的题目里,并不强制要求每个成员都用 private 封装,很多时候直接用 struct 全部公开也完全没问题。封装最大的价值在于大项目的维护和防错。
但有一种情况在竞赛中很常见,用封装会更优雅——带有合法性约束的数据结构,比如分数必须在 0~100、坐标不能越界等,用 setter 统一拦截比每次赋值都手写 if 判断要干净得多。
private,对外不可见。public,对外提供接口。