🔷 第十章 类和对象
📄 一句话概括

类由两部分组成:成员变量(数据,通常藏在 private 里)和成员函数(方法,通常开在 public 里)。前者是"这个对象有什么",后者是"这个对象能干什么"。衡量一个类写得好不好,就一条:外界能不能在不碰数据的情况下,把事情办完。

📝 本节和前面几节的关系

10.1 讲了"为什么要造类型",本节把类的内部构造拆开看清楚:哪些东西算成员、它们各扮演什么角色。这一节会反复用到 9.3.2 的 UML 类图三格框——类图里画的"属性 / 方法",就是这里的成员变量和成员函数,一一对应。另外,本节开始正式使用 private / public 这两个访问修饰符。

一、类的两个组成部分

部分 另一个名字 回答什么问题 通常放在 UML 类图里画在
成员变量 属性 / 数据成员 / field 这个对象有什么 private 第二格(属性格)
成员函数 方法 / 成员方法 / method 这个对象能干什么 public 第三格(方法格)

用一句话记住它们的关系:

成员变量是"家底",成员函数是"窗口"。

所有对家底的读写,都应该从窗口进。窗口开得越少越好,但要开得够用。

二、访问修饰符:三档权限

类里面用三个标签来划分地盘:

标签 谁能访问 UML 符号 用在哪
private(私有) 只有自己(本类的成员函数) - 成员变量几乎总是放这里
public(公有) 谁都能 + 成员函数(对外接口)放这里
protected(保护) 自己 + 子类 # 留给继承用(本章几乎用不到,见到不慌)

三个必须知道的细节:

  1. 标签可以写多次。private: 写一片成员、public: 再写一片、回头又写 private:——都合法。
  2. 标签管到"下一个标签"为止,不是管一行。
  3. 不写标签时看类关键字:class 默认 private、struct 默认 public(10.1 讲过)。
class Demo {
    int a;              // 没写标签 → class 默认 private
private:
    int b;              // private
public:
    int c;              // public
protected:
    int d;              // protected
    int e;              // 延续 protected
};

三、成员函数里可以直接用成员变量(不用带对象名)

这是类和普通函数最直观的差别:

class Circle {
private:
    double r;                        // 成员变量

public:
    void setR(double v) {
        if (v > 0) r = v;            // 直接写 r,不用写 "哪个圆的 r"
    }
    double area() {
        return 3.14159 * r * r;      // 也是直接写 r
    }
};

为什么可以不用带对象名? 因为成员函数一定是"某个对象"在调用它——调用时那个对象自动成了"当前对象"。10.4.4 会告诉你这个"当前对象"是怎么被记住的(this 指针),现在只需要知道:在成员函数里,成员变量就像自家抽屉,伸手就拿。

用起来:

Circle c1, c2;        // 两个圆,各有一份自己的 r
c1.setR(2);
c2.setR(3);
cout << c1.area() << endl;   // 12.56636  ← c1 的 r
cout << c2.area() << endl;   // 28.27431  ← c2 的 r

每个对象都有一份自己独立的成员变量——这一点和结构体完全一致(8.1.2)。类只描述"有哪些成员",每个对象各自存一份值。

四、一个好类的三条标准

不是"能跑就行"。对照下面三条自查:

标准 好 坏
数据私有 成员变量都在 private 成员变量裸露在 public
接口有意义 withdraw(100)、deposit(50) setBalance(x)、getBalance() 一把梭
职责单一 Student 只关心学生的数据与行为 Student 又会打印、又会存文件、还会发网络请求

第三条尤其重要:一个类如果"什么都会",改任何一处都可能碰它——9.2.2 说的"改动被锁在对象内部"就废了。职责越单一,改动的影响面越小。

五、动手 Demo:写一个"会自己守规矩"的圆

#include <iostream>
using namespace std;

class Circle {
private:
    double r;                                   // 只有一个成员变量

public:
    void setR(double v) {                       // 唯一的入口,带校验
        if (v > 0) {
            r = v;
        } else {
            cout << "半径必须大于 0,忽略 " << v << endl;
        }
    }

    double area()      { return 3.14159 * r * r; }
    double perimeter() { return 2 * 3.14159 * r; }

    void describe() {
        cout << "半径 = " << r
             << ",面积 = " << area()
             << ",周长 = " << perimeter() << endl;
    }
};

int main() {
    Circle c;

    c.setR(5);            // ✅ 正常
    c.describe();

    c.setR(-3);           // ❌ 被拦下来
    c.describe();         // 半径还是 5,数据没被弄坏

    // c.r = 100;         // ← 去掉注释:编译错误(private)
    return 0;
}

看到什么算成功:

半径 = 5,面积 = 78.5398,周长 = 31.4159
半径必须大于 0,忽略 -3
半径 = 5,面积 = 78.5398,周长 = 31.4159

读结果:非法值被拦,第二行打印的还是 5。这就是"类自己守规矩"——不需要调用方处处小心,规则写一次,谁都绕不过去。

30 秒实验:把 double r; 从 private 挪到 public,再在 main 里写 c.r = -3; c.describe();。你会发现程序照样跑,还高高兴兴地算出了负半径的"面积"——没人报错,但结果是错的。这就是 9.2.1 那个"不报错的错"。把 private 亲手去掉一次,你对封装的理解会牢一大截。

六、常见误区

  1. "成员变量必须全 private"——绝大多数情况是,但纯数据类(DTO/配置项)全公开也没问题。判据不是教条,而是:这些数据需不需要被保护、有没有规则要守。需要守规矩(分数范围、余额不能为负)就必须私有;纯粹是个数据袋子,公开反而省事。
  2. "private 成员子类也看不到吧?"——看不到。子类只能访问 protected 和 public 的(private 连子类都挡)。这就是为什么有的成员要写成 protected。
  3. "读取数据的函数必须有"——不一定。能不给就不给——每多一个 getX(),就多一扇门。先问:调用方真的需要这个值,还是只需要一个结果? 要结果就给他个算好的方法(area()),而不是把原料(r)递出去。
  4. "一个类里方法越多越强"——恰恰相反。方法越多,说明它管的闲事越多,职责越不单一。写类时的好习惯是:先想它"不该管什么",划清边界再动手。
  5. "类必须先定义完才能用"——同一文件里是的;跨文件靠"声明"(10.1.3 讲头文件的作用)。现在就记住这个词::: 是"这个函数属于谁"的标记。
自测三问(点开对答案) 1. 类的两个组成部分分别是什么?各回答什么问题?——**成员变量**(这个对象有什么,通常私有)和**成员函数**(这个对象能干什么,通常公有)。 2. 三个访问修饰符各管到哪儿?——`private` 只有本类能访问;`protected` 本类 + 子类;`public` 谁都能。标签**管到下一个标签为止**,可以写多次;不写时看类关键字(`class` → `private`,`struct` → `public`)。 3. 判断一个类"封装得好不好",看哪一条?——**看外界能不能在不直接碰数据的情况下把事情办完**。如果外面必须拿到原始数据才能干活,说明**接口没设计好**(门开错位置了)。

相关笔记