C++ 多态详解:从概念到实现原理----《Hello C++ Wrold!》(14)--(C/C++)

C++ 多态详解:从概念到实现原理----《Hello C++ Wrold!》(14)--(C/C++)

文章目录

前言

多态是面向对象编程的三大核心特性(封装、继承、多态)之一,它使得同一接口可以呈现出不同的行为,极大地提升了代码的灵活性和可扩展性。在 C++ 中,多态的实现与虚函数、虚表等机制紧密相关,其底层逻辑涉及编译期与运行期的不同处理方式。
本文将系统梳理 C++ 多态的概念、实现条件、核心机制(虚函数与虚表),并深入解析多态在继承场景下的表现,同时结合典型问题与示例代码,帮助读者全面理解多态的本质与应用。无论是基础的虚函数重写,还是复杂的多继承虚表结构,本文都将逐一剖析,为开发者在实际编程中合理运用多态提供清晰指引。

多态的概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

为了更方便和灵活的实现多种形态的调用

多态的定义和实现

虚函数

概念:被virtual修饰的类成员函数称为虚函数(和前面的虚继承区分)

虚函数的重写(覆盖)

概念:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写或者覆盖了基类的虚函数。

省流:虚函数+三同
虚构函数重写的两个例外情况:

1.协变:

此时基类与派生类虚函数返回值类型可以不同,但是返回值必须是父子关系的指针和引用

2.派生类重写虚函数可以不加virtual(但是建议加上)
总问题: 析构函数可以是虚函数吗?为什么需要是虚函数?

析构函数加virtual,是不是虚函数重写?
是,因为类析构函数都被处理成destructor这个统一的名字

为什么要这么处理呢?

因为要让他们构成重写

那为什么要让他们构成重写呢?

因为下面的场景

多态的构成条件

1.必须通过基类的指针或者引用调用虚函数(注意是基类!!!)

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
注意:多态调用看的是指向的对象,普通的调用看的是当前的类型 eg: class Person{ public: virtual void text(){} }; class Student : public Person{ public: virtual void text(){}//--a }; void func(const Person& p) { p.text(); } main函数里面func(Student());是调用的a的 
问题:

1.为什么必须要是父类的指针或引用,而不是父类对象或者子类的指针或引用

(编译器把这几种行为ban了的原因)

原因:

1.不能是父类对象的原因:

不会拷贝子类的虚表和其他特有的,所以这个父类对象根本不知道子类的存在(指针和引用就可以避开这一点)

编译器选择不拷贝子类的虚表指针的原因:

害怕别人不知道父类对象虚表中是父类的还是子类的

2.不能是子类指针或引用:

怕去访问到父类中没有的成员
引申:

1.子类虚表的构建:

子类继承父类时,会先复制一份父类的虚表。如果子类没有重写父类的虚函数,那么虚表中对应函数指针就指向父类虚函数实现;若子类重写了某个虚函数,就会用子类自己的虚函数地址覆盖虚表中从父类继承来的对应函数指针。

2.子类赋值给父类对象切片,不会拷贝虚表,父类还是会要自己的虚表

override 和 final(C++11提出)

final

作用:1.修饰虚函数,表示该虚函数不能再被重写

2.使用final关键字修饰类,直接禁止任何类继承它
用法:eg:virtual void text() final {}(前有无virtual不重要哈) 引申:一个有final一个无final也能构成重载和隐藏 

override

作用:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Person{ public: virtual void text(){} }; class Student :public Person { public: virtual void text() override {} }; 

重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述

抽象类

概念:

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

作用:强制要求派生类重写虚函数,另外抽象类体现出了接口继承的关系

接口继承和实现继承

普通函数的继承是一种实现继承,继承的是函数的实现。

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。

多态的原理

虚函数表(也叫做虚表)

包含虚函数的类会有虚函数表指针,虚函数表指针指向的是虚函数表的地址

虚函数表里面存了虚函数的指针

注意:同一个类的所有实例对象共享同一个虚函数表
比较特殊的是:VS编译器的虚表指向的地址后面会有0作为结束(可以用内存窗口看)

比如:
但是在进行增量编译之后,可能这个0就没了,这时候需要清理一下解决方案或者重新生成解决方案才行

引申:虚表的打印

虚表本质上是函数指针数组
typedef void(*FUNC_PTR) (); //这里就是将 一个void(*)()的函数指针类型取别名为FUNC_PTR // 打印函数指针数组的方法 void PrintVFT(FUNC_PTR* table) { for (size_t i = 0; table[i] != nullptr; i++) { printf("[%d]:%p->", i, table[i]); FUNC_PTR f = table[i]; f(); } printf("\n"); } int main() { Student st; int vft2 = *((int*)&st); //这个强转之后就一次++指++(int)个字节的东西了;而且这个32位上正好一个指针4个字节,正好读完 //注意:Linux是64位的!!! PrintVFT((FUNC_PTR*)vft2); //发现隐式类型转换会报错,就改成强转了 return 0; } 
注意:成员变量的变化会导致虚表的打印出错–因为可能会影响到内存布局
虚表和虚基表都是在编译阶段生成的

对象实例化之后,才会与虚表有联系(通过虚表指针)

多态的原理

核心的实现机制就是虚函数表和虚指针

满足多态的话,子类的虚指针指向的虚表中的虚函数就会覆盖父类的虚函数的地址,然后调用的就是子类的虚函数了

静态多态和动态多态

静态多态,又叫静态绑定,前期绑定(早绑定),在程序编译期间就确定了程序的行为

比如:函数重载

动态多态又称为动态绑定,后期绑定(晚绑定),是在程序运行期间才确定调用什么函数的

也就是继承+虚函数重写实现的多态

在默认情况下,多态一般指的是动态多态

多继承中的虚函数表

class Base1 { public: virtual void func1() { cout << "Base1::func1" << endl; } virtual void func2() { cout << "Base1::func2" << endl; } private: int b1; }; class Base2 { public: virtual void func1() { cout << "Base2::func1" << endl; } virtual void func2() { cout << "Base2::func2" << endl; } private: int b2; }; class Derive : public Base1, public Base2 { public: virtual void func1() {cout << "Derive::func1" << endl;} virtual void func3() {cout << "Derive::func3" << endl;} private: int d1; }; int main() { Derive d; cout << sizeof(d) << endl; //X86环境下,这个占20个字节,组成:两个基类(都是一个虚表指针加一个成员变量)加一个成员变量 Base1* ptr1 = &d; ptr1->func1(); Base2* ptr2 = &d; ptr2->func1();//通过修正this指针,来让this指针指向派生类的头 return 0; } 问题:为什么重写func1,Base1和Base2的虚表中func1的地址不一样? Base2中func1的地址不一样是为了jmp去修正this指针的位置 
注意:

1.多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中(其实是末尾)

作业部分

设计不想被继承类,如何设计? 方法1:基类构造函数私有 (C++98) 方法2:基类加一个final (C++11) 方法1:eg: class A { public: static A CreateObj()//这个static不能去掉,不然就不能通过域名去调用了 { return A(); } private: A() {} };//当然,用析构函数这么搞也行哈 int main() { A::CreateObj(); return 0; } 方法2: class A final {} 
 这里常考一道笔试题:sizeof(Base)是多少?(X86环境下的话) 答案:8个字节//不是一个字节,也不是四个字节 要注意的是:类里面还有一个虚函数表指针(_vfptr) class Base { public: virtual void Func1() { cout << "Func1()" << endl; } private: char _b = 1; }; int main() { cout << sizeof(Base) << endl; return 0; } 
class A { public: virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; } virtual void test() { func(); } }; class B : public A { public: void func(int val = 0) { std::cout << "B->" << val << std::endl; } }; //三同里面的形参相同只用形参的类型相同就行,缺省参数和名字可以不同(但要有名) int main(int argc, char* argv[])//相当于int main() { B* p = new B; p->test(); return 0; } 结果:输出B->1 引申:如果把test()放在了B里面的话,就应该输出B->0了 因为此时this->func()的this不是父类指针,不构成多态 
派生类那里不用加virtual的原因:

本质上只重写了实现
面试常考题:

1.什么是多态?–静态多态和动态多态都要答

2.inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是

inline,因为虚函数要放到虚表中去。

3.静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数

的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。(语法也会强制检查这个,会报错)

4.构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表

阶段才初始化的。

5.对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针

对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函

数表中去查找。

6.虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况

下存在代码段(常量区)的。

Read more

airsim无人机自动避障路径规划自动跟踪实验辅导

airsim无人机自动避障路径规划自动跟踪实验辅导

计算机人工智sci/ei会议/ccf/核心,擅长机器学习,深度学习,神经网络,语义分割等计算机视觉,精通大小lun文润色修改,代码复现,创新点改进等等。文末有方式 2025-2026最容易出顶会/毕业论文的热门方向之一:   基于AirSim的无人机深度强化学习路径规划——你真的“卷”对了吗? 如果你现在还在做传统A*、RRT、DWA、人工势场、或者纯深度学习的端到端避障…… 那很抱歉,2025年底~2026年审稿人和答辩老师已经开始审美疲劳了。 真正让审稿人眼睛一亮、让毕业答辩现场鸦雀无声的关键词组合,现在大概长这样: AirSim + 深度强化学习 + 无人机 + 路径规划 + Sim-to-Real + 视觉/激光融合 + 端到端 + 稀疏奖励 下面这几个组合,几乎是目前最容易做出“看上去就很前沿”的实验结果的赛道(尤其适合发中文核心、EI、SCI三区~二区,以及部分顶会workshop): 1.DQN/DDPG/

By Ne0inhk

【无人机避障算法核心技术】:揭秘五种主流算法原理与实战应用场景

第一章:无人机避障算法概述 无人机避障算法是实现自主飞行的核心技术之一,其目标是在复杂环境中实时感知障碍物,并规划安全路径以避免碰撞。随着传感器技术和计算能力的提升,避障系统已从简单的距离检测发展为融合多源信息的智能决策体系。 避障系统的基本组成 典型的无人机避障系统包含以下关键模块: * 感知模块:利用激光雷达、超声波、立体视觉或RGB-D相机获取环境数据 * 数据处理模块:对原始传感器数据进行滤波、特征提取和障碍物识别 * 决策与规划模块:基于环境模型生成避障轨迹,常用算法包括A*、Dijkstra、RRT和动态窗口法(DWA) 常见避障算法对比 算法优点缺点适用场景A*路径最优,搜索效率高高维空间计算开销大静态环境全局规划DWA实时性强,适合动态避障局部最优风险室内低速飞行RRT*渐进最优,适应复杂空间收敛速度慢三维未知环境 基于深度学习的避障方法示例 近年来,端到端神经网络被用于直接从图像生成控制指令。以下是一个简化的行为克隆模型推理代码片段: import torch import torchvision.transforms as tran

By Ne0inhk
医疗连续体机器人模块化控制界面设计与Python库应用研究(下)

医疗连续体机器人模块化控制界面设计与Python库应用研究(下)

软件环境部署 系统软件架构以实时性与兼容性为核心设计目标,具体配置如下表所示: 类别配置详情操作系统Ubuntu 20.04 LTS,集成RT_PREEMPT实时内核补丁(调度延迟<1 ms)开发环境Python 3.8核心库组件PyQt5 5.15.4(图形界面)、OpenCV 4.5.5(图像处理)、NumPy 1.21.6(数值计算) 该环境支持模块化控制界面开发与传感器数据的实时融合处理,为连续体机器人的逆运动学求解(如FB CCD算法测试)提供稳定运行基础[16]。 手眼协调校准 为实现视觉引导的精确控制,需完成相机与机器人基坐标系的空间映射校准,具体流程如下: 1. 标识点布置:在机器人末端及各段首尾、中间位置共固定7个反光标识点,构建臂型跟踪特征集[29]; 2. 数据采集:采用NOKOV度量光学动作捕捉系统(8台相机,

By Ne0inhk

简单易学的分离式部署小米智能家居Miloco方法

一、安装环境 * Windows用户:安装WSL2以及Docker * macOS/Linux用户:安装Docker 此处不再赘述,网上随便找个教程即可。特别地,对于Windows用户来说,你需要将 WSL2 的网络模式设置为 Mirrored。 二、使用Docker部署Miloco后端 以下均为bash命令。请Windows用户进入WSL2 / Linux、macOS用户进入终端操作: mkdir miloco cd milico vi docker-compose.yml 以下是compose的内容(不会使用vi的同学可以傻瓜式操作:先按i,再使用粘贴功能,然后按冒号,输入wq然后回车,记得关闭输入法): services:backend:container_name: miloco-backend image: ghcr.nju.edu.cn/xiaomi/miloco-backend:latest network_mode:

By Ne0inhk