Lesson 0301 · 封装:把细节藏起来

③ 面向对象程序设计 — 第 1 课

三句话速览
  1. 数据藏进 private,只留 public 方法。
  2. 改数据一律走 setter,检查就写在那里面。
  3. 只读函数加 const,同名参数用 this->。
上节课(0015):C++ 语法总复习,class、构造函数、析构函数都已经写过一遍。
这节课:进入面向对象三大特性——封装、继承、多态。今天先学第一个:封装。

1. 三大特性先看一眼

面向对象说的三件事,先用一张表建立印象:

特性一句话生活例子
封装 Encapsulation数据和操作打包,细节藏起来自动售货机:投币按按钮,内部结构看不到
继承 Inheritance子类复用父类猫和狗都是动物
多态 Polymorphism同一句话,不同对象做不同的事同样喊“叫”,猫喵、狗汪

这一课只管第一行。判断有没有做到封装,看一个标准:外面能不能绕过你的检查,直接把数据改坏。

2. 访问权限 —— 把门锁起来

封装的第一步是控制访问。C++ 用三个关键字决定「谁能进来」:

class Student {
private:            // 私有:只有类内函数能碰
    string name;
    int score;

public:             // 公有:外面可以调用
    Student(string n, int s) : name(n), score(s) {}
};
关键字谁能访问什么时候用
private只有类内成员函数数据成员(默认!)
public谁都可以接口:构造函数、方法
protected自己 + 子类留给继承用(下一课)
记忆:struct 默认 public,class 默认 private。写 class 时数据成员习惯放 private。

3. 为什么要藏数据 —— getter / setter

藏起来不是为了神秘,是为了让所有修改都过一个关口。

如果不藏:s.score = -100; 也能写进去,明显不合理。藏起来后,所有修改必须经过 setScore,可以在里面做检查:

class Student {
    string name;
    int score;                    // private:外面改不了

public:
    Student(string n, int s) : name(n) {
        setScore(s);              // 构造时也走检查
    }

    void setScore(int s) {        // 唯一修改入口
        if (s < 0)   s = 0;       // 太低了,拉回 0
        if (s > 100) s = 100;     // 太高了,拉回 100
        score = s;
    }

    int getScore() const { return score; }   // 只读,不能改
};

这就是封装的精髓:数据 private + 方法 public + 入口校验。以后数据出问题,只需要查一个地方。

4. this 指针 —— 「我自己」

参数名和成员名一样时,光写 score = score 就是自己赋自己。用 this-> 区分:

void setScore(int score) {
    this->score = score;   // 左边是成员变量,右边是参数
}

this 是指向当前对象的指针,this->score 就是「我这个对象的 score」。

5. const 成员函数 —— 保证不改

int getScore() const { return score; }   // const 在括号后面

意思是:这个函数保证不修改对象。好处:const 对象也只能调用 const 方法,编译器帮你检查。养成习惯:只读函数都加 const。

6. 完整示例 —— 学生成绩类

把访问权限、构造函数、const 方法拼在一起,就是一个能用的类:

#include <iostream>
#include <string>
using namespace std;

class Student {
    string name;
    int scores[3];                 // 三科成绩

public:
    Student(string n, int a, int b, int c) : name(n) {
        scores[0] = a;
        scores[1] = b;
        scores[2] = c;
    }

    string getName() const { return name; }
    int total() const { return scores[0] + scores[1] + scores[2]; }
    double avg() const { return total() / 3.0; }
};

int main() {
    Student s("Alice", 95, 87, 92);
    cout << s.getName() << " " << s.total() << " " << s.avg() << endl;
    return 0;
}

三个方法都是 const,因为它们只读不写。外面拿不到 scores,只能通过 total()、avg() 要结果——这就是「数据藏起来,接口露出去」。

7. 坑点清单

8. 小测

Q1. class 的成员默认访问权限是什么?
Q2. private 数据成员谁能访问?
Q3. getter 为什么常写成 const 成员函数?
Q4. this->score = score 中,左边的 this->score 是什么?

9. 动手练习

题目:银行账户 BankAccount

- private 一个 double balance(余额)
- 构造函数:传入初始余额
- deposit(double m):存款,余额增加
- withdraw(double m):取款,余额不够时返回 false 且不扣钱
- getBalance() const:返回余额
- 进阶:把余额存进 money.txt,下次启动读回来(用上 0014 的文件读写)

main 里测试:存 100 → 取 50 → 取 200(应该失败)→ 打印余额

写完按这个顺序自查:

卡住时的自查顺序
  1. balance 是不是 private?如果是 public,先把门关上再谈别的。
  2. 构造函数有没有走校验入口,还是直接给 balance 赋了值?
  3. withdraw 里,判断余额够不够是不是在扣钱之前?
  4. 余额不够时返回了 false,并且真的没有改动 balance?
  5. getBalance 后面有没有 const?
  6. main 里三步打印的余额,和手算的一不一样?

本课小结

下一课预告
0302 继承与组合 —— 两个类有大量重复代码时,把公共部分抽成「父类」;再用一张表分清 is-a(继承) 和 has-a(组合),以及子类怎么拿到父类的 protected 数据、构造为什么总是先父后子。
本课的 private 到了下一课会有一个专门的开门口令:protected。

← 上一课:C++ 进阶总复习 · 课程对照表

视频 黑马第 80-92 集(复习+深入) · 书籍 Primer Plus 第 10 章

在线 CodeFather 编程导航 — C++ 教程(补充视频参考)