C++ 抽象类、纯虚函数与多态性:核心概念与生动解析

1. 抽象类与纯虚函数:制定“通用行动纲领”

想象一下,你是一个项目经理,要制定一个“通用行动纲领”,让所有团队成员(派生类)都必须完成某个任务,但具体怎么完成,由每个团队成员自己决定。

  • 纯虚函数 (Pure Virtual Function):
    • 它就像项目经理在“行动纲领”里写下的一个“待办事项”virtual void 完成任务() = 0;
    • = 0 的意思是:我只列出这个任务,具体怎么做(函数体),你们自己去想办法!它没有具体的实现。
  • 抽象类 (Abstract Class):
    • 如果一个“项目经理”(类)的“行动纲领”里有至少一个这样的“待办事项”(纯虚函数),那这个“项目经理”就是个“抽象项目经理”(抽象类)。
    • 特点:你不能直接“雇佣”一个“抽象项目经理”(不能创建抽象类的对象),因为他还有没具体落实的任务。
    • 作用:抽象项目经理存在的意义,就是让他的“下属团队”(派生类)去继承他,然后把那些“待办事项”都具体化(实现纯虚函数)。这样,所有团队成员就都得完成这个任务了!

生动示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 抽象的“动物”类:它规定了所有动物都得“叫”
class Animal {
public:
// 纯虚函数:规定了“叫”这个动作,但具体怎么叫(狗汪汪,猫喵喵),由子类决定
virtual void makeSound() = 0;

// 虚析构函数:后面会讲,很重要哦!
virtual ~Animal() {}
};

// “狗”类:继承了 Animal,并实现了“叫”
class Dog : public Animal {
public:
void makeSound() override { // override 后面讲
std::cout << "汪汪汪!" << std::endl;
}
};

// “猫”类:继承了 Animal,并实现了“叫”
class Cat : public Animal {
public:
void makeSound() override {
std::cout << "喵喵喵!" << std::endl;
}
};

2. override 关键字:我的专属“任务完成报告”!

  • 当你看到 void makeSound() override { 这样的代码时,override 就像一个“任务完成报告”上的特殊标记
  • 作用:它告诉编译器和读代码的人:“看,我这个 makeSound() 函数,是特意去重写(覆盖)了基类(Animal)里那个 makeSound() 虚函数!”
  • 好处
    • 防错:如果你不小心把 makeSound() 写成了 makeSoundd(),或者参数写错了,编译器会立刻报错,提醒你“基类里没有这个虚函数让你重写啊!”。这能帮你避免很多低级错误。
    • 清晰:一眼就能看出这个函数是重写的,代码意图更明确。
  • 建议:虽然不写 override 也能跑,但为了代码更健壮、更易读,强烈建议你每次重写虚函数时都加上它!

3. 内存管理:栈和堆,你的“工作台”和“仓库”

想象一下,你的电脑内存就是个大空间,里面有不同的区域。

  • (Stack):

    • 就像你的“临时工作台”:空间有限,放的东西都是临时的。
    • 特点:你处理短期任务(函数执行)时,用的草稿纸(局部变量)就放在这儿。任务完成(函数结束),草稿纸就自动扔了(内存自动释放)。
    • 优点:速度快,不用你操心。
    • 缺点:空间小,东西不能久放。
  • (Heap):

    • 就像你的“大型仓库”:空间大,可以放很多东西,但需要你手动管理。
    • 特点:你想长期保存的东西(动态创建的对象),就得放这儿。你得自己去“申请”仓库空间(new),用完了还得自己去“清理”(delete),不然仓库就会堆满垃圾(内存泄漏)。
    • 优点:空间大,东西可以放很久。
    • 缺点:速度相对慢,需要你手动管理,容易忘记清理(内存泄漏)。
  • new 操作符:就是去仓库“申请”一块空间,并把东西放进去。

  • delete 操作符:就是去仓库“清理”你之前申请的空间。

所以,Animal* myPet = new Dog(); 这句话的意思就是:在你的“大型仓库”(堆)里,养了一只“狗”,然后用一个“动物”类型的“标签”(指针 myPet)贴在它身上。

4. 指针与多态性基础:一个“通用遥控器”,控制多种动物!

  • 指针:就像一个“标签”,它不存储具体的东西,只存储东西的“地址”(在哪儿)。
  • **向上转型 (Upcasting)**:
    • C++ 有个很酷的规定:一个“动物”类型的标签,可以贴在“狗”身上,也可以贴在“猫”身上,只要“狗”和“猫”都是“动物”的子类就行。
    • 示例:Animal* myPet = new Dog();
    • 为什么可以? 因为“狗”一种“动物”。
    • 好处:这样你就可以用一个统一的“标签”(Animal*)来操作各种不同的动物,而不用管它具体是狗还是猫。这就是多态性的魅力!

5. 静态绑定与动态绑定:什么时候决定“谁来执行任务”?

想象一下,你有一个“通用遥控器”(指针),可以控制不同的动物。

  • 静态绑定 (Static Binding) / 编译时多态:

    • 非虚函数:如果遥控器上的按钮(函数)不是“智能”的(非虚函数),那么在你买遥控器(编译)的时候,就已经决定了按这个按钮会控制哪种动物。
    • 问题:如果你用“动物”的遥控器去控制一只“狗”,但遥控器上的“吃饭”按钮不是智能的,它可能只会发出“动物”默认的“吃饭”指令,而不会发出“狗”特有的“吃饭”指令。
    • 析构函数:如果析构函数(对象销毁时自动调用的函数)不是虚的,那么当你用“动物”的遥控器去“销毁”一只“狗”时,它只会执行“动物”的销毁步骤,而不会执行“狗”特有的销毁步骤,这可能导致“狗”内部的一些资源(内存)没被清理干净,造成内存泄漏
  • 动态绑定 (Dynamic Binding) / 运行时多态:

    • 虚函数:如果遥控器上的按钮是“智能”的(虚函数),那么在你按下按钮(运行时)的时候,遥控器会根据它实际控制的是“狗”还是“猫”,来发出对应的“叫”指令。
    • 好处:这就是多态的精髓!用一个统一的接口(基类指针),可以实现不同的行为。

6. 虚析构函数的重要性:安全“送走”动物!

  • 作用:当你的“动物”遥控器(基类指针)指向一只“狗”(派生类对象),并且你想“送走”这只“狗”(delete 对象)时,如果“动物”的析构函数是虚的,那么它会确保先调用“狗”的“送走”步骤,再调用“动物”的“送走”步骤。
  • 目的:防止内存泄漏!确保所有派生类特有的资源都能被正确清理。
  • 调用顺序:从最具体的动物(最底层派生类)开始“送走”,一步步往上“送走”到最抽象的动物(基类)。

7. 虚函数(非析构函数)的调用顺序:只听最具体的指令!

  • 当你的“动物”遥控器(基类指针)指向一只“狗”(派生类对象),并按下“智能”按钮(调用虚函数 makeSound())时,它只会执行“狗”特有的“汪汪汪”指令。
  • 不会先执行“动物”的“叫”,再执行“狗”的“叫”。它直接跳到最具体的那个“叫”指令去执行。

我的学习总结

虚函数只触发最底层的,虚析构函数从底向顶依次触发。