Lesson 0302 · 继承与组合

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

三句话速览
  1. 子类 : public 父类,只有「是一种」才继承。
  2. protected 给自己和子类,类外进不来。
  3. 先父后子构造,析构反过来;能组合别继承。
上节课(0301):封装 = 数据 private + 方法 public + 入口校验。
这节课:OOP 第二个特性继承,以及和它形影不离的组合。

1. 重复代码从哪来

假设要写 Dog 和 Cat 两个类,写完发现它们都有 name、age、都会 eat()——重复代码出现了。

把公共部分抽出来做成「爸爸类」,两个儿子共享它。这就是继承。它和组合的关系,先记住这张表:

说法含义例子
继承 Inheritanceis-a(是一种)狗是动物
组合 Compositionhas-a(有一个)汽车有引擎

判断口诀只有一句:「是一种」用继承,「有一个」用组合。这一课剩下的内容,都是把这句口诀落到代码上。

2. 继承怎么写

class Animal {
protected:                  // protected:自己和子类能访问
    string name;

public:
    Animal(string n) : name(n) {}
    void eat() { cout << name << " 在吃饭" << endl; }
};

class Dog : public Animal {   // Dog 继承 Animal
public:
    Dog(string n) : Animal(n) {}   // 先初始化基类
    void bark() { cout << name << " 汪汪叫" << endl; }
};

int main() {
    Dog d("旺财");
    d.eat();     // 基类的方法,Dog 直接用
    d.bark();    // 自己的方法
    return 0;
}
语法:class Dog : public Animal —— 冒号后面写继承方式,public 表示「爸爸的 public 成员,儿子也保持 public」。

3. protected —— 给子类留门

三种权限在继承里的表现,一张表看完:

权限类外自己类内子类
private❌✅❌
protected❌✅✅
public✅✅✅

如果 Animal 的 name 是 private,Dog 里 bark() 就读不到 name。所以「打算让子类用的数据」用 protected。

别把它当 public 用:protected 只多开了「子类」这一扇门,类外照样访问不了。它的定位始终是「家族内部可见」。

4. 构造与析构的顺序

Dog d("旺财");
// 执行顺序:
// 1. Animal("旺财")    ← 先构造基类
// 2. Dog 自己的部分

所以 Dog 的构造函数必须把参数传给 Animal:: Animal(n)。析构顺序相反:先析构儿子,再析构爸爸。

为什么必须这个顺序:儿子身上有一部分是从爸爸那儿继承来的,爸爸没造好,儿子没地方放;拆的时候则要先拆儿子,再拆爸爸。

5. 方法重写 —— 儿子改写法

class Cat : public Animal {
public:
    Cat(string n) : Animal(n) {}
    void eat() {   // 和基类同名,覆盖基类版本
        cout << name << " 优雅地吃饭" << endl;
    }
};

Dog 调 eat() 是「在吃饭」,Cat 调 eat() 是「优雅地吃饭」。同名覆盖,各用各的。

这里的「覆盖」是静态的:编译时看的是变量的类型。下一课会看到,想要「运行时看实际对象」,还得加 virtual。

6. 组合 —— has-a

class Engine {
public:
    void start() { cout << "引擎启动" << endl; }
};

class Car {
    Engine engine;              // 组合:Car 内部有一个 Engine
public:
    void start() { engine.start(); }   // 转交给内部的 Engine
};
设计原则:能用组合就别用继承。继承是「强关系」(改爸爸影响所有儿子),组合更灵活。判断口诀:「是一种」用继承,「有一个」用组合。

看代码就能体会差别:Car 想换引擎,改一个成员类型就行;如果让 Car 继承 Engine,汽车就「是一种引擎」了,这显然说不通。

7. 完整示例 —— Person / Student

class Person {
protected:
    string name;
    int age;
public:
    Person(string n, int a) : name(n), age(a) {}
    void intro() { cout << "我是 " << name << "," << age << " 岁" << endl; }
};

class Student : public Person {
    int score;                 // Student 自己新增的数据
public:
    Student(string n, int a, int s) : Person(n, a), score(s) {}
    void study() { cout << name << " 在学习,分数 " << score << endl; }
};

int main() {
    Student s("小明", 18, 95);
    s.intro();    // 继承来的
    s.study();    // 自己的
    return 0;
}

注意 Student 的初始化列表:: Person(n, a), score(s) —— 先父后子,和构造顺序一致。写反了也照样能编过,但读起来是在骗自己。

8. 坑点清单

9. 小测

Q1. 写继承的语法是?
Q2. protected 成员谁能访问?
Q3. 创建派生类对象时,构造顺序是?
Q4.「汽车有引擎」应该用哪种关系?

10. 动手练习

题目:图形类

- 基类 Shape:protected string name + 构造 + getName()
- 派生类 Circle:半径 r,area() 返回面积(3.14159 * r * r)
- 派生类 Rectangle:宽高 w, h,area() 返回面积
- main 里各创建一个,输出名字和面积

进阶:写 Book 类和 Library 类(内部 vector<Book> 组合),支持添加和打印所有书。
卡住时的自查顺序
  1. name 在基类里是 protected 吗?子类要读它,private 一定不行。
  2. 两个派生类的构造函数,有没有写 : Shape(n)?
  3. area() 是各算各的,还是误用了父类的版本?
  4. main 里能不能只通过基类引用看到名字?看不到就先检查 getName() 的权限。
  5. 进阶题里,Library 用的是 vector<Book> 成员,还是错误地继承了 Book?
  6. 都对了还不输出:检查 cout 有没有漏 endl,以及对象是不是建在了 main 里。

本课小结

下一课预告
0303 多态与虚函数 —— 同样是 eat(),如果写成 Animal* p = &dog; 再调 p->eat(),你会发现它走的还是 Animal 的版本。下一课用 virtual + override 把「跟着实际对象走」这件事做出来,并解释什么叫对象切片。
本课第 5 节最后那句「静态覆盖」,下一课会正式拆开讲。

← 上一课:封装 · 课程对照表

视频 黑马第 80-105 集对应段 · 书籍 Primer Plus 第 13 章

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