🐶

继承与多态

小狗不用重学吃喝——白拿"动物套餐";喊一声"说话",狗汪汪、猫喵喵。

💡 打个比方

所有动物都会吃、会睡、有名字。如果给狗、猫、鹦鹉各写一个类,这些共同能力就要抄三遍——太浪费!继承:先造一个"动物"基类把公共部分写好,狗和猫用一句 class Dog : public Animal 白拿这些能力,只补自己特有的(狗会汪、猫会喵)。多态:饲养员对任何宠物都只喊一句"说话!"——狗回"汪汪"、猫回"喵",同一句指令,不同行为,由对象的"真实身份"决定。

术语:基类/父类(被继承的一方,公共部分);子类/派生类(继承的一方,class Dog : public Animal);方法重写 override(子类同名方法覆盖父类版本);重载(同类中同名不同参数,另一回事);virtual(虚函数:运行时按真实对象选版本);多态(同一接口,不同表现)。

🖼️ 大图看明白

左边:继承树——白拿公共能力 Animal(基类) name / eat() / sleep() 公共"动物套餐" Dog(子类) 白拿:name/eat/sleep 特有:bark() "汪汪" Cat(子类) 白拿:name/eat/sleep 特有:meow() "喵" 公共部分写一遍,子类白拿——不重复劳动 右边:多态——同一句"说话!" Animal* pets[] = { 狗, 猫, 狗 } 饲养员统一喊:speak()! 🐶 狗 "汪汪!" 🐱 猫 "喵~" 🐶 狗 "汪汪!" 指针类型都是 Animal*(同一件"工装"), virtual 让它按"真实身份"执行各自版本 同一接口,不同行为 = 多态

🧩 子任务 1:继承——一句话白拿公共能力

🎯 小目标:看懂 class Dog : public Animal,知道子类白拿了什么、要自己补什么。
// 基类:所有动物共有的能力,只写一遍
class Animal {
public:
    string name;
    Animal(string n) { name = n; }          // 基类构造器
    void eat()  { cout << name << " 在吃" << endl; }
    void sleep() { cout << name << " 在睡" << endl; }
};

// 子类:: public Animal → 白拿 name / eat() / sleep()
class Dog : public Animal {
public:
    Dog(string n) : Animal(n) {}            // 必须调用基类构造器填 name
    void bark() { cout << name << ": 汪汪!" << endl; }   // 特有
};

三条要点:

✅ 小检查:① Dog 对象能用哪些方法?② 如果 Dog 的构造器不写 : Animal(n),name 会是什么状态?③ 想再加一个 Parrot(鹦鹉)类,eat/sleep 要重新写吗?
看看答案
① eat()、sleep()、bark()(前两个白拿,最后一个特有)。 ② 基类构造器没被调用,name 是未初始化的空/随机值——输出乱码。 ③ 不用:class Parrot : public Animal 直接白拿,只写特有部分(比如 squawk())——继承就是"不重复劳动"。

🧩 子任务 2:方法重写——子类换掉父类的版本

🎯 小目标:理解"重写"(同名方法子类优先),并分清它和"重载"的区别。

基类给所有动物一个默认的"说话":发出"???"。但狗、猫都有自己的叫声——子类同名写一个方法,就把父类的版本"换掉"了,这叫方法重写(override,覆盖):

// 基类:默认行为
class Animal {
public:
    string name;
    Animal(string n) { name = n; }
    string speak() { return "???"; }        // 默认:说不清
};

class Dog : public Animal {
public:
    Dog(string n) : Animal(n) {}
    string speak() { return "汪汪"; }        // 重写:同名,狗换掉父类版本
};

class Cat : public Animal {
public:
    Cat(string n) : Animal(n) {}
    string speak() { return "喵"; }          // 重写:猫换掉父类版本
};

注意:重写不是新增功能,而是换版本——子类对象调 speak() 时,执行的是子类自己的。

顺便分清它的"同名不同姓"表亲——重载(overload):

// 这是"重载",不是重写:同一个类里,参数不同
void show() {}              // 无参数版本
void show(int x) {}         // 带参数版本——两个共存,按实参选择
✅ 小检查:① Dog d("小黄"); cout << d.speak(); 输出什么?② 若 Dog 里再写一个 string speak(int n)(返回"汪 n 次"),它和父类的 speak() 是"重写"还是"重载"关系?③ 一句话:重写看什么、重载看什么?
看看答案
① 输出 汪汪(子类版本覆盖了父类的"???")。 ② speak(int n) 与父类 speak() 参数不同、不是覆盖关系——这是 Dog 里多出的一个重载版本;而 Dog 的无参 speak() 对父类才是重写。 ③ 重写看"父子 + 同名同参"(换版本);重载看"同类 + 同名不同参"(加版本)。

🧩 子任务 3:virtual——按"真实身份"执行

🎯 小目标:搞懂为什么重写还不够,必须加 virtual 才能让"基类指针"触发子类的版本。

先看一个坑。假设 pets 数组存的是 Animal*(基类指针):

// 没加 virtual 时:
Dog d("小黄");
Animal* p = &d;              // 基类指针指向狗
cout << p->speak() << endl;   // 输出 ??? —— 执行的是 Animal 的版本!

为什么?没加 virtual 时,编译器按"指针的类型"(Animal)在编译期就选好了版本——它只看指针穿的是"Animal 工装",不管里面装的其实是狗。这叫静态绑定。

给基类方法加上 virtual 后,改成运行时看对象的真实身份再选版本——这叫动态绑定,也就是多态:

// 加了 virtual:
class Animal {
public:
    virtual string speak() { return "???"; }   // virtual:听真实身份
};
class Dog : public Animal {
public:
    string speak() override { return "汪汪"; } // override 是"我确实想重写"的声明
};

Dog d("小黄");
Animal* p = &d;
cout << p->speak() << endl;   // 输出 汪汪 —— 运行时发现是 Dog,执行 Dog 版

比喻:指针是工装。没 virtual = "看工装办事"(你穿 Animal 工装就按 Animal 对待);有 virtual = "看人下菜"(不管你穿什么工装,你是狗就汪汪)。

✅ 小检查:Animal* q = new Cat("小美"); cout << q->speak() << endl; delete q;——有 virtual 时输出什么?没 virtual 呢?delete 为什么安全?(提示:基类析构函数也应 virtual,本课记结论即可)
看看答案
有 virtual:输出 喵(运行时认出 Cat)。没 virtual:输出 ???(按 Animal* 的静态绑定)。 delete q 安全是因为 Animal 的析构函数也标了 virtual——通过基类指针删子类对象时,会"拆到根"(先调子类析构再调基类析构),不会"拆一半"泄漏。考试记结论:用基类指针 delete 子类对象,基类析构必须 virtual。
易错:virtual 加在基类方法上(一次声明,所有子类继承这个"可重写"属性);子类里用 override 关键字提醒自己"我确实在重写",写错签名编译器会报错。

🧩 子任务 4:小综合——宠物店的统一指挥

🎯 小目标:用"基类指针数组"统一管理不同宠物,体会多态的威力。

场景:宠物店来了 3 只宠物(狗、猫、狗)。饲养员只会喊一句 show()("报上名来 + 叫一声"),不想分别写"如果是狗就……如果是猫就……"。用多态,一个循环搞定:

✅ 小检查:程序对 3 只宠物依次调用 show(),输出应该是什么?如果明天来了只鹦鹉(新加 Parrot 类,重写 speak 返回"嘎!"),for 循环那几行要改吗?
看看答案
输出(每只"名字 + 叫声"):
  • 小黄:汪汪
  • 小美:喵
  • 小黑:汪汪
来了鹦鹉:把数组里某项换成 new Parrot("小绿") 即可,for 循环 0 行不用改——它只会喊 show(),鹦鹉的"嘎!"由 Parrot 自己的 speak() 负责。这就是多态的美:新类型自动接入旧系统(专业话:对扩展开放、对修改关闭)。

💻 完整代码:宠物店

// 输出:小黄: 汪汪 / 小美: 喵 / 小黑: 汪汪
#include <iostream>
#include <string>
using namespace std;

class Animal {
public:
    string name;
    Animal(string n) { name = n; }
    virtual string speak() { return "???"; }
    void show() { cout << name << ": " << speak() << endl; }
    virtual ~Animal() {}          // 虚析构:基类指针 delete 才安全
};

class Dog : public Animal {
public:
    Dog(string n) : Animal(n) {}
    string speak() override { return "汪汪"; }
};

class Cat : public Animal {
public:
    Cat(string n) : Animal(n) {}
    string speak() override { return "喵"; }
};

int main() {
    Animal* pets[3] = {
        new Dog("小黄"),
        new Cat("小美"),
        new Dog("小黑")
    };
    for (int i = 0; i < 3; i++) {
        pets[i]->show();      // 统一指令,各叫各的
        delete pets[i];       // 逐个释放
    }
    return 0;
}

✋ 你来试试

第 1 题:class Dog : public Animal 里的 public 是什么意思?Dog "白拿"了 Animal 的哪些东西?
public 表示公有继承:基类 public 的成员在子类里保持 public(还有 protected/private 两种继承方式,本课记 public 即可)。Dog 白拿了 name 属性、eat()、sleep()、speak() 的默认版本,只需补自己特有的 bark() 或重写 speak()。
第 2 题:重写(override)和重载(overload)各用一句话区分。
重写:父子类之间同名同参数,子类版本覆盖父类(换版本)。重载:同一个类里同名不同参数,多个版本共存(加版本)。
第 3 题:Animal* p = new Dog("小黄"); cout << p->speak() << endl; delete p;——speak 有 virtual 时输出什么?没有 virtual 呢?delete p 为什么安全?
有 virtual:输出 汪汪(运行时按真实身份 Dog 选版本);没有:输出 ???(编译期按指针类型 Animal 静态绑定)。delete p 安全是因为基类析构函数标了 virtual——通过基类指针删除子类对象时会完整"拆到根"。
第 4 题(加餐):宠物店的 for 循环为什么"来新宠物不用改"?如果 show() 里写满了 if (是狗)…else if (是猫)…,还会这样吗?
因为 show() 里调 speak() 是多态调用:每只宠物自带自己的 speak() 版本,循环只发统一指令。若 show() 里写满 if/else 判断类型,来新宠物就必须回去改 show() 加一个分支——多态把"改旧代码"变成了"加新类",更稳、更省。
🎯 一句话记住:继承 = 子类 : public 基类 白拿公共能力(构造器要先调基类);重写 = 同名换版本;virtual = 看真实身份办事——基类指针指向谁就执行谁的版本,这就是多态。