C++ 多态终极完整版:从虚函数到 vtable、对象切片、插件框架设计、面试题库全覆盖

目录

封装继承多态

一、前言之为何学习多态

二、多态是什么

三、多态实现的三大条件

四、完整代码示例

五、逐行深度解析

1)基类中 virtual void Speak() = 0

2)子类重写(override)基类的虚函数

3)main() 中创建子类对象

4)sayHello(&c):发生向上转型

5)多态调用点(核心)

(1)运行时检查 → p 的 vptr

(2)查它的 vtable

(3)跳转执行子类实现

六、必须用指针或引用而不能按值传递?

1)按值传递:

2)对象切片(Object Slicing):

3)指针或引用方式:

七、虚函数表(vtable)到底长什么样?

1)Chinese :

2)American:

3)调用 p->Speak() 时:

八、多态设计建议

九、多态优势总结

十、虚函数表(vtable)+ 虚表指针(vptr)完整内存图

1)父类 Person 中的成员

(1)编译器生成

(2)对象内部结构

2)子类 Chinese 内存结构

(1)对象内部:

(2)vtable(Chinese):

3)多态调用过程可视化

(1)调用:

(2)执行流程:

(3) C++ 多态本质

十一、复杂多态案例 ① —— 策略模式实现“不同导航算法”

1)场景:

2)代码含义:

3)抽象接口:class Controller

(1)class Controller { ... };

(2)virtual void computeControl() = 0;

(3)virtual ~Controller() {}

4)各种控制算法子类

(1)PurePursuit

(2)Stanley

5) Navigator:持有一个“控制策略”

(1)Controller* ctrl;(成员变量)

(2)构造函数:Navigator(Controller* c) : ctrl(c) {}

(3)void run() { ctrl->computeControl(); }

6)main():如何切换算法?

(1)MPC m;

(2)Navigator nav(&m);

(3)nav.run();

(4)纯跟踪版本

7)工程实践中的优化

十二、C++ 多态八股文面试题(含答案)

(1)什么是多态?

(2)多态的触发条件?

(3)为什么按值传递不能多态?

(4)什么是虚函数表(vtable)?

(5)虚表指针(vptr)是什么?

(6)什么是动态绑定?

(7)为什么基类要写虚析构函数?

(8)override / final 的作用?

十三、全文总结


封装继承多态

在之前文章中已经详细介绍了面向对象编程的三大特性:封装、继承和多态。下面是具体链接:

面向对象编程(OOP)三大特性全解析:封装 / 继承 / 多态(含现实类比与 C++ 代码)-优快云博客https://blog.youkuaiyun.com/m0_58954356/article/details/154478398?spm=1001.2014.3001.5502有很多小伙伴私信,封装和继承很好了解,多态怎么都学不明白。博主在此写一篇关于最近学习多态的一些自己的理解和认知,如有错误请批评指正!(我将继续用之前不同国家人说不同语言的例子循序渐进到最近的一些项目展开)


一、前言之为何学习多态

在 C++ 面向对象三大特性(封装、继承、多态)中,多态是最核心也最容易迷糊的一个。

多态解决的问题是:

同一个接口,不同对象可以给出不同的实现。

在机器人、游戏引擎、框架设计、驱动封装、策略模式等场景中,多态都是必不可少的机制。


二、多态是什么

多态 = 一个接口,多种实现。

当你用基类指针或引用指向不同的子类对象时,调用同一个方法可以产生不同的行为,这就是多态。

例如,“Person” 都会 “Speak”,但不同国家的人说不同语言:

  • Chinese → “你好!”

  • American → “Hello!”

  • Russian → “Привет!”


三、多态实现的三大条件

实现多态必须满足一下三个条件:

条件含义
1. 基类函数必须是 virtual启用动态绑定
2. 子类必须重写父类虚函数提供不同实现
3. 必须通过父类指针引用调用否则无法触发多态

四、完整代码示例

#include <iostream>
using namespace std;

class Person {
public:
    virtual void Speak() = 0;  // ★ 纯虚函数:实现多态的关键
};

class Chinese : public Person {
public:
    void Speak() { cout << "你好!" << endl; }
};

class American : public Person {
public:
    void Speak() { cout << "Hello!" << endl; }
};

class Russian : public Person {
public:
    void Speak() { cout << "Привет!" << endl; }
};

void sayHello(Person* p) {       // ★ 统一接口,面向抽象
    p->Speak();                  // ★ 多态调用点
}

int main() {
    Chinese c;
    American a;
    Russian r;

    sayHello(&c);
    sayHello(&a);
    sayHello(&r);
}

运行结果:

你好!
Hello!
Привет!

五、逐行深度解析

1)基类中 virtual void Speak() = 0

含义:

  • virtual:开启多态“动态绑定”

  • =0纯虚函数 → Person 变成“抽象类”

  • Person 不能实例化,只能作为接口

这一行是多态得以存在的关键。没有 virtual,就没有多态


2)子类重写(override)基类的虚函数

例如:

void Speak() { cout << "你好!" << endl; }

作用:

  • 为自己提供独特实现

  • 虚函数表(vtable)中写入自己的函数入口

每个子类都会生成自己的 vtable(虚函数表)


3)main() 中创建子类对象

Chinese c;
American a;
Russian r;

每个创建的子类对象的内部都产生一个隐藏指针:

对象内部:
[vptr] → 指向各自的 vtable

4)sayHello(&c):发生向上转型

sayHello(&c);
  • Chinese* 隐式转换为 Person*

  • 这是合法的,因为 Chinese 继承了 Person

此时形参 p:

静态类型:Person*
动态类型:Chinese*

这里就为多态埋下伏笔:“静态看父类,动态看子类”。


5)多态调用点(核心)

p->Speak();

这里发生的动作如下:

(1)运行时检查 → p 的 vptr

判断对象的实际类型是 Chinese / American / Russian

(2)查它的 vtable

vtable 内第一项就是 Speak 的入口

(3)跳转执行子类实现

例如:

  • Chinese → Chinese::Speak

  • American → American::Speak

 这就是动态绑定(Dynamic Dispatch)。


六、必须用指针或引用而不能按值传递?

1)按值传递:

void sayHello(Person p) { p.Speak(); } // ❌

2)对象切片(Object Slicing)

会出现对象切片(Object Slicing)

  • 子类对象传给父类按值时,会被“切掉”子类部分

  • 变成一个纯粹的 Person

  • 不再带有 vptr → 虚函数表变为父类的

  • 多态完全消失

3)指针或引用方式:

所以必须用:

 Person* p 或  Person& p


七、虚函数表(vtable)到底长什么样?

1)Chinese :

Chinese 对象:
 ┌───────────────┐
 │   vptr ───────┼─→ [ Chinese::Speak ]
 └───────────────┘

2)American:

American 对象:
 ┌───────────────┐
 │   vptr ───────┼─→ [ American::Speak ]
 └───────────────┘

3)调用 p->Speak() 时:

→ 找 vptr
→ 找到不同 vtable
→ 跳转到对应的 Speak


八、多态设计建议

建议原因
基类析构函数加 virtual否则 delete Person* 会导致内存泄漏
子类重写写 override编译器自动检查函数签名
不允许子类再重写函数 → 用 final如:void Speak() override final;
抽象类只负责接口,不负责实现遵循面向抽象编程

九、多态优势总结

优势说明
扩展性好新增子类不需要修改 sayHello
解耦面向父类编程,不依赖具体实现
代码复用一个接口,多种实现

十、虚函数表(vtable)+ 虚表指针(vptr)完整内存图

1)父类 Person 中的成员

class Person {
public:
    virtual void Speak() = 0;
};

(1)编译器生成

编译器会自动为 Person 生成:

  • 一个 虚函数表 vtable

  • 表中存放:Person::Speak(纯虚:指向 0 或特殊入口)

(2)对象内部结构

抽象类无法实例化,但结构如下:

Person对象
 ┌───────────────┐
 │   vptr         │──→ vtable(Person)
 └───────────────┘

vtable(Person):

vtable(Person):
 ┌──────────────────────┐
 │  Person::Speak(=0)   │  
 └──────────────────────┘

2)子类 Chinese 内存结构

class Chinese : public Person {
public:
    void Speak() { cout << "你好!"; }
};

(1)对象内部:

Chinese 对象内存
 ┌───────────────┐
 │ vptr ----------┼──→ vtable(Chinese)
 └───────────────┘

(2)vtable(Chinese):

vtable(Chinese):
 ┌─────────────────────────────┐
 │ &Chinese::Speak             │
 └─────────────────────────────┘

3)多态调用过程可视化

(1)调用:

Person* p = new Chinese();
p->Speak();

(2)执行流程:

p ---指向----> Chinese 对象
                 │
                 └→ vptr → vtable(Chinese)
                            │
                            └→ Chinese::Speak()

(3) C++ 多态本质

编译期不知道要调哪个函数(静态看 Person)
但运行时能根据 vptr 找到真正的实现(动态看 Chinese)。


十一、多态案例  —— 策略模式实现“不同导航算法”

1)场景:

同一辆 AGV 可以采用不同路径跟踪算法

如:Pure Pursuit、Stanley、LQR、MPC

2)代码含义:

这段代码在做两件事:

  1. 用多态 + 抽象接口 把“控制算法”抽象成一个统一接口 Controller

  2. 用工厂模式 把“创建哪个控制器对象”的逻辑统一集中管理

这样:

  • 导航器 Navigator 只知道有个 Controller,不关心具体是 MPC 还是 Stanley

  • 想换控制算法,只用换一个子类对象或改工厂的传参

3)抽象接口:class Controller

class Controller { 
public: 
    virtual void computeControl() = 0;
    virtual ~Controller() {} 
};

逐句解释:

(1)class Controller { ... };

  • 定义一个“控制器接口”

  • 约定所有控制算法都必须提供同样的“对外功能”:computeControl()

(2)virtual void computeControl() = 0;

  • virtual:虚函数 → 支持多态(运行时根据实际对象类型来调用)

  • = 0:纯虚函数 → 这个类变成抽象类,不能直接实例化

任何继承 Controller 的类 必须实现自己的 computeControl(),否则也会变成抽象类。


(3)virtual ~Controller() {}

  • 虚析构函数

  • 作用:当你通过 Controller* 删除子类对象时,可以正确调用子类 析构函数防止内存泄漏

Controller* c = new MPC();
delete c;  // 会先调用 MPC::~MPC(),再调 Controller::~Controller()

规范写法
凡是有虚函数的基类,析构函数几乎都应该是虚的。


4)各种控制算法子类

(1)PurePursuit

class PurePursuit : public Controller {
public:
    void computeControl() override {
        cout << "使用 Pure Pursuit 控制" << endl;
    }
};
  • : public Controller:公有继承 → PurePursuit 是一种 Controller

  • void computeControl() override

    • 覆盖(重写)父类的 computeControl()

    • override 告诉编译器:“我就是要重写父类虚函数,帮我检查签名是否一致”

输出语句只是示意:真实工程中我们会写轨迹跟踪控制律,比如计算转向角、速度等。

(2)Stanley

class Stanley : public Controller {
public:
    void computeControl() override {
        cout << "使用 Stanley 控制" << endl;
    }
};

同样结构,只是内部实现不同。
理解为:同一个接口,不同控制策略


5) Navigator:持有一个“控制策略”

class Navigator {
public:
    Navigator(Controller* c) : ctrl(c) {}
    void run() {
        ctrl->computeControl();   // ★ 多态调用
    }
private:
    Controller* ctrl;
};

逐行解释:

(1)Controller* ctrl;(成员变量)

  • Navigator 里面持有一个指向 Controller 的指针

  • 这里不关心具体是哪种 Controller(MPC / LQR / …)

  • 这就是面向抽象编程:只依赖接口,不依赖具体实现


(2)构造函数:Navigator(Controller* c) : ctrl(c) {}

  • 使用构造函数初始化列表把传进来的 Controller* 保存在成员变量 ctrl

  • 也就是常说的依赖注入(Dependency Injection)

    • 算法对象在外面创建

    • 导航器只拿来用,不负责“选哪一种算法”


(3)void run() { ctrl->computeControl(); }

  • 调用的是 基类指针 Controller* 上的虚函数

  • 因为前面 computeControl() 是 virtual,所以这里会发生多态

    • 如果 ctrl 指向 MPC → 调用的是 MPC::computeControl()

    • 如果 ctrl 指向 Stanley → 调用的是 Stanley::computeControl()

这一行就是整个系统多态的核心调用点。


6)main():如何切换算法?

int main() {
    MPC m;
    Navigator nav(&m);   // 注入 MPC 算法
    nav.run();           // 输出:使用 MPC 控制
}

逐行看:

(1)MPC m;

  • 在栈上创建一个 MPC 控制器对象

(2)Navigator nav(&m);

  • &m(即 MPC*)传给构造函数 Navigator(Controller* c)

  • 这里发生向上转型(Upcasting)

    • 子类指针 MPC* 自动转换为父类指针 Controller*

  • 以后 Navigator 内部就通过 Controller* 接口来使用这个具体的 MPC 算法

(3)nav.run();

  • 内部是:ctrl->computeControl();

  • ctrl 实际上指向的是 MPC 对象

  • 因为是虚函数动态绑定到 MPC::computeControl()

  • 所以输出:使用 MPC 控制

(4)纯跟踪版本

PurePursuit pp;
Navigator nav(&pp);
nav.run();   // → 使用 Pure Pursuit 控制

同一份 Navigator 代码,完全不用改任何一行,控制算法已经换了。
这就是多态 + 策略模式带来的扩展性。


7)工程实践中的优化

  1. std::unique_ptr<Controller> 管理指针,避免手动 delete

  2. 工厂返回智能指针:

  3. enum / 配置结构体替代裸字符串


十二、C++ 多态八股文面试题(含答案)

(1)什么是多态?

一个接口,多种实现。
通过基类指针或引用指向子类对象,调用虚函数时根据对象实际类型执行不同版本。


(2)多态的触发条件?

  • 父类函数必须加 virtual

  • 子类必须 override

  • 必须通过父类指针/引用调用


(3)为什么按值传递不能多态?

因为会发生 对象切片(object slicing)
子类对象按值转换为父类对象,子类部分被切掉,vptr 被替换成父类,无法多态。


(4)什么是虚函数表(vtable)?

一个函数指针数组,存放类的虚函数入口
每个具有虚函数的类都有一张 vtable。


(5)虚表指针(vptr)是什么?

每个对象内部都有一个隐藏指针 vptr → 指向该对象所属类的 vtable。
调用虚函数时根据 vptr 动态跳转执行。


(6)什么是动态绑定

函数执行的版本在运行时决定,而不是编译期。


(7)为什么基类要写虚析构函数

为了通过 delete base_ptr 正确调用子类析构,避免内存泄漏。


(8)override / final 的作用?

  • override:编译器检查正确重写。

  • final:阻止进一步重写。


十三、全文总结

多态 = virtual + override + 父类指针/引用 + 动态绑定(vtable)
是 C++ 最核心的面向对象能力,

多态让系统更灵活、更解耦、更容易扩展、更面向接口而不是实现。

没有 virtual → 不叫多态
按值传递 → 不叫多态
直接用子类调用 → 不叫多态

真正的多态:

Person* p = new Chinese(); 
p->Speak(); // 调子类实现
代码下载地址: https://pan.quark.cn/s/35e46f7e83fb 关于 Build Status Lines of code 这是一个参考 PotPlayer 的界面使用 Java 以及图形界面框架 JavaFX 使用 MCV 图形界面与业务逻辑分离的开发模式, 所开发的个人视频播放器项目, 开发这个项目旨在于学习图形界面框架 JavaFX 实现了具有和 PotPlayer相同 的简洁界面和流畅的操作逻辑。 Note: PotPlayer 是 KMPlayer 的原制作者姜龙喜先生(韩国)进入 Daum 公司后的 新一代网络播放器, PotPlayer的优势在于强大的内置解码器以及支持各类的 视频格式, 而且是免费下载提供使用的。 目前版本: 2020/10/28 v1.0.0 [x] 支持打开文件自动播放 [x] 支持查看播放记录 [x] 支持屏幕边沿窗口自动吸附 [x] 支持双击视频来播放和暂停 [x] 支持左键点击窗口任意位置来拖到窗口 [x] 支持左键双击播放窗口打开文件 [x] 支持根据视频尺寸自动调整窗口大小 [x] 支持根据播放文件类型调整窗口模式 [x] 支持根据视频尺寸自动调整窗口显示位置防止超出屏幕 [x] 支持记录上一次访问的文件路径 [x] 支持播放记录文件读写 已实现样式 未播放效果: 播放效果: 运行环境 本项目使用 NetBeans 配合 JDK 开发, NetBeans8.0 以及 JDK8.0 以上版本的均可以运行。 亦可使用其他集成开发环境, 例如 Eclipse, IntelliJ IDEA 配合使用 JDK8.0 以上版本均可构建此项目。 NetBeans download Eclipse downlo...
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值