🔷 第九章 面向对象编程基础
📄 一句话概括

面向对象的三大特点:封装(把数据藏起来,只留门——解决"乱改")、继承(子承父业,只写多出来的部分——解决"重复")、多态(同一句话,不同的对象做出不同的反应——解决"一堆 if-else")。它们是三个独立的能力,可以只用其中一个。三个都围绕同一个目标:让程序在长大和变动的时候,改动范围尽量小。

📝 本节和前面几节的关系

9.2.1 讲面向过程的软肋,9.2.2 讲对象怎么组织程序,本节把面向对象"最拿手的三个本领"一次交代清楚。三个特点里,封装其实在 9.2.2 已经露过面(private);继承会用到第八章的结构体嵌套(8.1.4)作为直观对照;多态会用到第七章的函数指针(7.6.2)——你已经学过的工具,正好能拼出多态的雏形。这一节是本章最"概念密集"的一节,读的时候把每个特点都翻译成一句大白话。

一、封装:把数据藏起来,只留门

一句话:数据是私房钱,只能通过"我允许的动作"来动。

生活类比——自动售货机:

售货机 对象
里面 货道、余量、找零盒(private) 属性(数据)
外面 按钮、投币口(public) 方法(接口)
好处 你按"可乐",不需要知道货道怎么转 你调 withdraw(),不需要知道余额怎么减
保护 隔着玻璃,伸手不进去拿 编译期就挡住直接访问

封装带来三件具体的好事:

  1. 防乱改:acc.balance = -9999; 这类"从外面捅进去"的写法直接编译不过。
  2. 留退路:内部实现随时可换(把数组换成链表、把线性查找换成哈希),只要对外的按钮不变,调用方一行都不用改。
  3. 能自检:所有修改都必须经过方法,于是"取款不能超过余额"这类规则有了唯一的落点,不可能被绕过。

封装的本质不是"藏",是"约定入口"。 藏起来是手段,"只能从门进"才是目的——因为门只有一扇,规则就守得住。

用已学的知识对照:第八章的 struct 是没有门的柜子(8.4.2 里 p->score = 0 想改就改);class 给柜子装了门和锁,同样是想改分数,得调用 setScore()——而 setScore() 里可以写"分数必须在 0~100 之间"。

二、继承:子承父业,只写多出来的部分

一句话:已有的东西不重写,新东西只写"和它不一样的地方"。

生活类比——户口本上的亲属关系:儿子不必重新学一遍"怎么走路、怎么吃饭",他天生会(继承),只需要学自己额外的部分(上学、工作)。

代码里的画面(用第八章结构体嵌套先感受一下"包含"的写法):

struct Person {                 // 基础:凡"人"都有姓名和年龄
    char name[20];
    int  age;
};

struct Student {                // 学生"是一种"人
    Person base;                // 把"人"的部分包含进来(8.1.4 嵌套)
    char   school[30];          // 学生多出来的部分
    double score;
};

用起来很别扭:

Student s;
strcpy(s.base.name, "张三");    // 每访问一个"人的属性"都要先写 .base

继承(下一章的语法)解决的就是这个别扭:

class Student : public Person {   // 读作"学生继承自人"
    // Person 里的 name / age 自动就有了,不用重写
    char   school[30];            // 只写多出来的
    double score;
};

Student s;
strcpy(s.name, "张三");           // 直接用,不再需要 .base

继承帮我们省掉三样东西:

省掉什么 说明
重复的属性 name、age 只写一次,所有"人"共用
重复的方法 比如"自我介绍"写在 Person 里,学生、老师都会了
重复的修改 以后要给"人"加"生日"字段,所有子类自动都获得

两条必须记住的行话:

继承的暗礁:层次一旦超过两三层,改一个父类可能震动一大片子类。所以有句口头禅:"优先用组合(包含),能不用继承就不用"(下一章会展开)。

三、多态:同一句话,不同的反应

一句话:同一个调用(同一句话),不同对象各自做出符合自己身份的反应。

生活类比——同一句"把作业交上来":

谁听见了 反应
语文老师 交作文本
数学老师 交练习册
体育老师 交……(没作业,跑两圈)

动作的名字一样("交作业"),做的具体事各自不同。 这正是"多态"(poly = 多,morph = 形态)。

为什么它值钱?因为它能消灭一大串 if-else。没有多态的写法通常长这样:

void makeSound(int kind) {
    if (kind == 1)      cout << "汪汪" << endl;    // 狗
    else if (kind == 2) cout << "喵喵" << endl;    // 猫
    else if (kind == 3) cout << "哞哞" << endl;    // 牛
    // 每加一种动物,都要回来改这个函数 ⚠️
}

有多态之后:

Animal *p = ...;      // 手上拿的可能是狗、猫、牛
p->speak();           // 同一句话;各自发出自己的声音
// 加一种新动物:写一个新的类就行,这个函数一行都不用改 ✅

多态的价值就是"对扩展开放,对修改关闭":加新种类不用改老代码。9.2.1 里"改动扩散"的病,这里被治住了。

四、三个特点各自治什么病(一张表收束)

特点 大白话 治什么病 靠什么机制
封装 数据藏起来,只留门 数据被乱改 private + 公开方法
继承 子承父业,只写不一样的 代码重复 class 子 : public 父
多态 同一句话,各家有各家的做法 一堆 if-else 且越加越长 虚函数(下一章之后学)

它们的关系:封装是地基(没有封装,继承和多态都失去了"保护内部实现"的意义);继承是复用的手段;多态是"灵活调用"的手段——而且多态通常要站在继承的肩膀上("不同的对象"大多数时候就是"同一个父类的不同子类")。

五、动手 Demo:用函数指针拼出"多态"的雏形

class 还没学,但用第七章的函数指针(7.6.2),你已经能做出"同一句话、不同做法"的效果:

#include <iostream>
using namespace std;

// 三种"交作业"的具体做法(各自的反应)
void chineseHw() { cout << "交上作文本" << endl; }
void mathHw()    { cout << "交上练习册" << endl; }
void peHw()      { cout << "体育没作业,跑两圈" << endl; }

// 一个"统一的调用入口":谁传进来,就执行谁的做法
void collectHomework(void (*doHw)()) {    // ← 形参是函数指针(7.6.2)
    cout << "老师说:把作业交上来 → ";
    doHw();                                // 同一句调用,行为由传入的函数决定
}

int main() {
    void (*teachers[3])() = { chineseHw, mathHw, peHw };   // 三根"做法指针"

    for (int i = 0; i < 3; i++)
        collectHomework(teachers[i]);      // 同一个函数,三种反应

    // 要加一位"音乐老师"?只需写个 musicHw(),塞进数组
    // collectHomework 这个函数一个字都不用改 ✅
    return 0;
}

看到什么算成功:

老师说:把作业交上来 → 交上作文本
老师说:把作业交上来 → 交上练习册
老师说:把作业交上来 → 体育没作业,跑两圈

读结果:collectHomework 里只有一句 doHw();,它永远不知道会执行哪个函数——这就是"同一句话、不同反应"。而且加一位老师不用改 collectHomework——这就是多态买到的"加新东西不动老代码"(这段代码同时还是 7.6.2 那个"函数表"的实战用法)。

30 秒实验:加一个 void musicHw() { cout << "交上乐谱" << endl; },把数组改成 void (*teachers[4])() = { chineseHw, mathHw, peHw, musicHw };、循环上限改成 4。你会发现真正"改"的只有数据和数组长度,collectHomework 一个字没动。 下一章的虚函数,就是让编译器自动帮你做这件"看着对象是谁、就调它的做法"的事——不用你再手动摆函数指针数组。

六、常见误区

  1. "三大特点必须一起用"——不是。只用封装也完全合法、且非常常见(很多类就是"数据 + 方法",不继承、不多态)。三者是各管一摊病的独立工具。
  2. "继承越多越厉害"——反过来。继承层次越深,父类一改,子类全震。"是一种"才用继承,"有一个"要用成员变量(包含)。 很多设计原则都主张优先用组合而不是继承。
  3. "多态就是函数重载"——不是一回事。第六章的重载(6.5)是"同名函数、参数表不同",编译期就定下来调用哪一个;多态是"同一句调用、运行期才决定执行哪份实现"(靠虚函数)。前者是静态的,后者是动态的。
  4. "封装就是加个 private 完事"——加锁容易,难的是"门开在哪、开几扇"。把所有字段配一对 getX/setX 等于门开得跟没有墙一样。好的封装是"只暴露有意义的行为",而不是"暴露每个字段"。
  5. "多态和虚函数是一回事"——虚函数是 C++ 实现多态的主要机制,但"多态"是个更宽的概念(同一接口、多种实现)。本节用函数指针做出的效果也是多态(虽然不算 C++ 语境里最标准的那个)。
自测三问(点开对答案) 1. 封装解决什么问题?它的本质是什么?——解决**数据被外部乱改**、规则容易被绕过的病。本质是**"约定入口"**:数据藏起来只是手段,**让所有修改必须走规定的门**才是目的,这样规则才有唯一的落点。 2. 判断该不该用继承,唯一的标准是什么?——**是不是"是一种"(is-a)关系**。"学生是一种人"→可以继承;"学生有一个书包"→要用**成员变量包含**,不能用继承。 3. 多态和函数重载有什么区别?——**重载**是编译期根据参数表决定调用哪个同名函数(静态);**多态**是运行期根据对象的实际类型决定执行哪份实现(动态,靠虚函数)。多态的好处是**加新类型不用改老代码**。

相关笔记