③ 面向对象程序设计 — 第 2 课
: public 父类,只有「是一种」才继承。protected 给自己和子类,类外进不来。假设要写 Dog 和 Cat 两个类,写完发现它们都有 name、age、都会 eat()——重复代码出现了。
把公共部分抽出来做成「爸爸类」,两个儿子共享它。这就是继承。它和组合的关系,先记住这张表:
| 说法 | 含义 | 例子 |
|---|---|---|
| 继承 Inheritance | is-a(是一种) | 狗是动物 |
| 组合 Composition | has-a(有一个) | 汽车有引擎 |
判断口诀只有一句:「是一种」用继承,「有一个」用组合。这一课剩下的内容,都是把这句口诀落到代码上。
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」。
protected —— 给子类留门三种权限在继承里的表现,一张表看完:
| 权限 | 类外 | 自己类内 | 子类 |
|---|---|---|---|
private | ❌ | ✅ | ❌ |
protected | ❌ | ✅ | ✅ |
public | ✅ | ✅ | ✅ |
如果 Animal 的 name 是 private,Dog 里 bark() 就读不到 name。所以「打算让子类用的数据」用 protected。
protected 只多开了「子类」这一扇门,类外照样访问不了。它的定位始终是「家族内部可见」。
Dog d("旺财");
// 执行顺序:
// 1. Animal("旺财") ← 先构造基类
// 2. Dog 自己的部分
所以 Dog 的构造函数必须把参数传给 Animal:: Animal(n)。析构顺序相反:先析构儿子,再析构爸爸。
为什么必须这个顺序:儿子身上有一部分是从爸爸那儿继承来的,爸爸没造好,儿子没地方放;拆的时候则要先拆儿子,再拆爸爸。
class Cat : public Animal {
public:
Cat(string n) : Animal(n) {}
void eat() { // 和基类同名,覆盖基类版本
cout << name << " 优雅地吃饭" << endl;
}
};
Dog 调 eat() 是「在吃饭」,Cat 调 eat() 是「优雅地吃饭」。同名覆盖,各用各的。
这里的「覆盖」是静态的:编译时看的是变量的类型。下一课会看到,想要「运行时看实际对象」,还得加 virtual。
class Engine {
public:
void start() { cout << "引擎启动" << endl; }
};
class Car {
Engine engine; // 组合:Car 内部有一个 Engine
public:
void start() { engine.start(); } // 转交给内部的 Engine
};
看代码就能体会差别:Car 想换引擎,改一个成员类型就行;如果让 Car 继承 Engine,汽车就「是一种引擎」了,这显然说不通。
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) —— 先父后子,和构造顺序一致。写反了也照样能编过,但读起来是在骗自己。
name 直接编译报错。打算给子类用的数据要用 protected。: Animal(n),而父类又没有无参构造,编译就过不去。class Car : public Engine 就成了「汽车是一种引擎」,继承来的接口全都不对味。Shape:protected string name + 构造 + getName()Circle:半径 r,area() 返回面积(3.14159 * r * r)Rectangle:宽高 w, h,area() 返回面积Book 类和 Library 类(内部 vector<Book> 组合),支持添加和打印所有书。
name 在基类里是 protected 吗?子类要读它,private 一定不行。: Shape(n)?area() 是各算各的,还是误用了父类的版本?getName() 的权限。Library 用的是 vector<Book> 成员,还是错误地继承了 Book?cout 有没有漏 endl,以及对象是不是建在了 main 里。class 子类 : public 基类,表示 is-a 关系。protected:自己 + 子类可访问,类外仍然不行。: Animal(n)。析构顺序相反。virtual 才管运行时)。eat(),如果写成 Animal* p = &dog; 再调 p->eat(),你会发现它走的还是 Animal 的版本。下一课用 virtual + override 把「跟着实际对象走」这件事做出来,并解释什么叫对象切片。