探究多态的原理与实现:虚函数表、动态绑定与抽象类

文章目录

  • 一、多态概念
  • 二、多态实现(具体)
    • 2.1 虚函数
    • 2.2 虚函数 重写
    • 2.3 override关键字 与 final关键字
    • 2.4 重载、重写(覆盖)、重定义(隐藏)的区别
  • 三、抽象类
    • 3.1 概念
    • 3.2 实现继承 && 接口继承
  • 四、多态原理
    • 4.1 虚函数表
    • 4.2 原理解释
    • 4.3 动态绑定 && 静态绑定
  • 五、单继承与多继承关系的虚函数表
    • 5.1 单继承中的虚函数表
    • 5.2 多继承中的虚函数表
    • 5.3 菱形继承 && 虚拟继承

一、多态概念

多态性(polymorphism)是面向对象编程的一个重要概念,它允许基类的指针或引用在运行时可以指向不同的派生类对象,并根据实际对象的类型调用相应的成员函数。

简单来说,多态不同继承关系的类对象,调用同一函数,产生了不同的行为

同时,必须满足以下构成条件才称为多态:

  1. 存在继承关系

  2. 使用基类的指针或引用调用函数

通过将派生类对象的地址赋值给基类指针或引用,可以在运行时根据实际对象的类型来动态调用适当的函数。这样才能实现多态性,使得同名函数在不同的派生类对象中产生不同的行为。

  1. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

图例:

在这里插入图片描述


二、多态实现(具体)

2.1 虚函数

定义:

class Base {
public:
    virtual void func() { // 使用 virtual 关键字声明虚函数
        // 函数实现
    }
};

2.2 虚函数 重写

虚函数的重写是指 派生类中重新实现(覆盖)基类中已经声明为虚拟的函数

一般重写函数时可以加上override关键字

代码举例:

// 基类
class Base
{
public:
	virtual void func() 
	{
		std::cout << "Base::func()" << std::endl;
	}
};

// 派生类
class Derived : public Base 
{
public:
	// void func() override{} 
	void func()  // 省略了override关键字
	{
		std::cout << "Derived::func()" << std::endl;
	}
};

int main() 
{
	// 使用基类指针指向派生类对象
	Base* basePtr = new Derived();

	// 动态调用虚函数,根据实际对象类型选择不同的实现
	basePtr->func(); // 输出: Derived::func() 

	delete basePtr;

	return 0;
}

其中,虚函数的重写有两种特殊情况:

  1. 协变: 指的是派生类重写基类虚函数时,与基类虚函数返回值类型不同

但是 返回类型是 基类虚函数返回类型的子类型

代码举例:

class A{}; // 基类
class B : public A {}; // 派生类

// 基类Person
class Person {
public:
	virtual A* f() {return new A;}
};

// 派生类Student
class Student : public Person {
public:
	virtual B* f() {return new B;}
};
  1. 析构函数重写

如果 基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写

(当编译器编译类的时候,它会自动为析构函数生成一个唯一的名称,以便在程序中正确地调用析构函数,所以重写条件成立)

代码举例:

class Base {
public:
    virtual ~Base() {
        cout << "Base的析构函数" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived的析构函数" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;
    return 0;
}

输出结果:

Derived的析构函数
Base的析构函数

2.3 override关键字 与 final关键字

override 关键字

介绍虚函数重写时我们提到:
一般函数重写时我们加上override关键字,其作用在于:

  1. 提示编译器,override 关键字在函数声明中使用,可以让编译器知道这是一个重写的虚函数,并在编译期对函数进行检查,如果没有发现重写,编译器会报错
  2. 易读性和易维护性
  3. 预防错误:如果派生类中的函数签名与基类虚函数的签名不匹配,并且没有使用 override 关键字,那么编译器可能会将该函数视为新的虚函数,而不是重写。

代码举例:

class Base {
public:
    virtual void foo() {
        std::cout << "Base 的 foo 函数" << std::endl;
    }
};

class Derived : public Base {
public:
    void foo() override {
        std::cout << "Derived 的 foo 函数" << std::endl;
    }
};

final 关键字

final关键字用于在C++中声明不可继承的类、禁止重写的虚函数或禁止Lambda函数进行捕获。这里我们介绍其在虚函数中的用法:

在虚函数的声明中使用final关键字可以 阻止派生类对该虚函数进行重写

代码举例:

class Base {
public:
    virtual void foo() final {
        // 函数定义
    }
};

class Derived : public Base {
public:
    // 下面的代码会引发编译错误,因为Derived试图重写被标记为final的foo函数
    void foo() override {
        // 函数定义
    }
};

2.4 重载、重写(覆盖)、重定义(隐藏)的区别

在这里插入图片描述

对于重写,派生类可以不必使用 virtual 关键字
因为在派生类中重写一个虚函数时,它会自动成为虚函数,无需再次使用 virtual 关键字进行声明。


三、抽象类

3.1 概念

在虚函数的后面写上 =0 ,这个函数称为 纯虚函数

包含纯虚函数的类叫做抽象类(也叫接口类)

  • 抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
  • 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
// 抽象类
class AbstractClass {
public:
	// 纯虚函数,没有实现
	virtual void pureVirtualFunc() = 0;

	// 普通成员函数,有实现
	void commonFunc() {
		std::cout << "这是一个普通的成员函数" << std::endl;
	}
};

// 派生类
class ConcreteClass : public AbstractClass {
public:
	void pureVirtualFunc() override {
		std::cout << "派生类实现了纯虚函数" << std::endl;
	}
};

3.2 实现继承 && 接口继承

  1. 实现继承 指的是从父类继承所有的实现代码和数据成员到子类中,使子类能够重用父类的代码和数据。子类可以覆盖或扩展从父类继承来的成员,进一步定制自己的行为。

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

  1. 接口继承 则是与实现无关的继承方式,它只继承从父类继承来的接口规范(方法签名),而没有提供任何实现。子类必须实现所有从父类继承来的接口,并可以根据需要添加新的接口。

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。


四、多态原理

4.1 虚函数表

虚函数表 是C++中实现多态性的一种机制。它是一张用于存储虚函数地址的表格,每个包含虚函数的类都会有一个对应的虚函数表。

  • 当一个类声明了虚函数时,编译器会在该类的对象中添加一个指向虚函数表的指针,通常被称为 虚函数指针(vptr)

虚函数表是一个静态的数据结构,在程序运行时被创建,并且与类的对象无关。

虚函数表中保存了虚函数的地址,它是一个由指针组成的数组,每个指针都指向对应虚函数的实际代码。子类继承了父类的虚函数表,并可以通过修改虚函数表中的指针来实现对虚函数的覆盖或扩展。这样,在通过基类指针或引用调用虚函数时,实际执行的是根据对象类型确定的子类中的虚函数。


我们看下面的代码,试问sizeof(Base)是多少

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};
  • 在上面的例子中:Base 是一个基类,它包含一个虚函数 Func1() 和一个私有成员变量 _b。而虚函数和非虚函数对于类的大小没有影响。

  • C++中,sizeof(Base) 的结果是编译器在编译时计算出来的类型的大小(以字节为单位),故为4。

为什么虚函数和非虚函数对于类的大小没有影响?
因为 C++ 实现了一种叫做虚表(vtable)的机制,用来支持虚函数的动态派发。

在这里插入图片描述

/// 在之前代码的基础上,加上虚函数Func2(),非虚函数Func3()
/// 只有虚函数Func1()被重写

// 基类
class Base
{
public:
	virtual void Func1() // Func1(),被派生类重写
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};

// 派生类
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;

	return 0;
}

运行代码,观察下面的监视窗口:
在这里插入图片描述
得出以下结论:

  1. 基类b对象派生类d对象 虚表是不一样的,由于Func1完成了重写,所以d的虚表中存的是重写的 Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  2. 由于Func2继承下来后是虚函数,则放进虚表,Func3由于不是虚函数,被继承下来但不会放进虚表。
  3. 虚函数表本质 是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

关于虚表生成:

派生类的虚表生成:

  1. 先将基类中的虚表内容拷贝一份到派生类虚表中。
  2. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。
  3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

虚函数,虚表的存放位置:

  • 我们知道:虚表存的是虚函数指针,不是虚函数虚函数和普通函数一样的,都是存在于代码段,只是虚函数的指针存到了虚表中
  • 而类对象中存的不是虚表,存的是虚表指针。那么虚表存在哪,根据上面监视窗口所示,在vs下是存在代码段的

4.2 原理解释

C++中,当一个类声明了虚函数时,编译器会在该类的对象中添加一个指向虚函数表的指针,通常被称为虚函数指针(vptr)。虚函数表是一个静态的数据结构,包含了所有虚函数的地址。子类继承了父类的虚函数表,并可以通过修改虚函数表中的指针来实现对虚函数的覆盖或扩展。这样,在通过基类指针或引用调用虚函数时,实际执行的是根据对象类型确定的子类中的虚函数。

由于C++中的引用和指针都支持动态绑定,因此可以通过基类引用或指针来实现多态。当程序通过基类指针或引用调用虚函数时,实际执行的是根据动态类型确定的子类中的虚函数。这意味着同样的代码对不同类型的对象的行为也是不同的。

4.3 动态绑定 && 静态绑定

上文提到了 绑定的概念:C++中,绑定(Binding)指的是将函数调用与函数实现关联起来的过程

静态绑定(Static Binding)和动态绑定(Dynamic Binding)是两种不同的绑定方式。

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。


五、单继承与多继承关系的虚函数表

5.1 单继承中的虚函数表

// 基类
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; } // 被派生类 重写
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
// 派生类
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};

对于上面的代码,当我们进行调试,会发现监视窗口中看不到,因为监视窗口隐藏了这两个函数,我们利用下面的代码打印出虚表函数来查看信息。

在这里插入图片描述

typedef void(*VFPTR) (); // 声明函数指针
void PrintVTable(VFPTR vTable[])
{
	// 依次取虚表里的虚函数指针(vfptr)地址,并打印
	// 通过调用可以看出存的函数
	cout << "虚表地址-> " << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf("第%d个虚函数地址: 0x%x,-> ", i, vTable[i]);
		VFPTR fun = vTable[i];
		fun(); // 执行虚函数
	}
	cout << endl;
}

int main()
{
	Base b;
	Derive d;

	// 使用指针类型转换和取地址运算符& 获取 b、d 对象的虚函数表指针
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb); // 打印虚函数表存储的虚函数指针地址

	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);

	return 0;
}

执行上面代码后,有以下结果:

在这里插入图片描述

5.2 多继承中的虚函数表

下面的代码,演示了多继承的虚函数表结构:

#include <iostream>
using namespace std;

// 基类A
class A {
public:
    virtual void func1() { cout << "A::func1" << endl; }
    virtual void func2() { cout << "A::func2" << endl; }
};

// 基类B
class B {
public:
    virtual void func3() { cout << "B::func3" << endl; }
    virtual void func4() { cout << "B::func4" << endl; }
};

// 派生类C,多继承自A和B
class C : public A, public B {
public:
    virtual void func5() { cout << "C::func5" << endl; }
    virtual void func6() { cout << "C::func6" << endl; }
};

typedef void(*VFPTR) ();

void PrintVTable(VFPTR vTable[])
{
    cout << "虚表地址-> " << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
        cout << "第" << i << "个虚函数地址: " << vTable[i] << ",-> ";
        VFPTR fun = vTable[i];
        fun(); // 执行虚函数
    }
    cout << endl;
}

int main()
{
    C c;

    // 使用指针类型转换和取地址运算符& 获取 c 对象的虚函数表指针
    VFPTR* vTableC = (VFPTR*)(*(int*)&c);
    PrintVTable(vTableC);

    return 0;
}

5.3 菱形继承 && 虚拟继承

菱形继承与虚拟继承

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/153773.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

通过使用阿里云服务器,搭建Java程序的运行环境

&#x1f4da;&#x1f4da; &#x1f3c5;我是默&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; ​​ &#x1f31f;在这里&#xff0c;我要推荐给大家我的专栏《Linux》。&#x1f3af;&#x1f3af; &#x1f680;无论你是编程小白&#xff0c;还是有一…

ACM练习——第四天

ok&#xff0c;今天又是三节课的一天。 刚刚吃完饭的我一刻也不敢耽误&#xff0c;直接开干&#xff01;&#xff01;&#xff01; 这些题和力扣上的&#xff0c;为了练习数据结构的题是不一样的。 直接上嘴脸 题目 第一次放学 (nowcoder.com)来自牛客网 思路 先看他的输入…

8086与8088

一、8086与8088概述 8088/8086都是16位微处理器&#xff0c;内部运算器和寄存器都是16位的&#xff0c;同样具有20位地址线8088/8086都是由执行单元(EU)和总线接口部件(BIU)两大部分构成指令系统和寻址能力都相同&#xff0c;两种CPU是兼容的8088被称作准十六位的、是紧继8086…

【文件上传】upload-labs 通关

环境准备 靶场upload-labs-env-win-0.1环境windows 靶场描述 靶场攻略 Pass-01 1.上传一句话木马 发现有类型限制 2.上传图片木马 抓包 3.修改后缀 将png修改为php. 成功上传&#xff0c; 4.查找文件地址 5.中国蚁剑连接 6.成功 7.源码分析 function checkFile() {var fi…

深入理解锁

目录 常用锁策略 1.乐观锁 VS 悲观锁 2.轻量级锁 VS 重量级锁 3.自旋锁 VS 挂起等待锁 4.互斥锁 VS 读写锁 5.公平锁 VS 非公平锁 6.可重入锁 VS 可重入锁 CAS ABA问题 Synchronized原理 1. 锁升级/锁膨胀 2.锁消除 3.锁粗化 常用锁策略 1.乐观锁 VS 悲观锁 站在…

自建es数据迁移阿里云方案

一、ElasticSearch数据迁移方法介绍 https://help.aliyun.com/document_detail/170095.html?spma2c4g.26937906.0.0.429240c9ymiXGm 可以通过Logstash、reindex和OSS等多种方式完成阿里云Elasticsearch间数据迁移、Elasticsearch数据迁移至Openstore存储中、自建Elasticsear…

教你轻松保存视频号里的视频到相册

在今天的数字化社会&#xff0c;人们在各种社交平台上分享视频已经成为一种习惯。而在短视频平台上&#xff0c;用户可以轻松地上传、分享和观看各种内容丰富的视频。然而&#xff0c;很多用户在观看完善了的视频后&#xff0c;希望将喜欢的视频保存到手机相册中&#xff0c;以…

yyds!这个写文案神器爱了爱了

每次写文案都绞尽脑汁&#xff0c;不知道怎么写&#xff0c;想了很久&#xff0c;好不容易写完了&#xff0c;数据真的很差&#xff0c; 心累啊&#xff0c;家人们&#xff01;&#xff01; 只要输入你想写的标题&#xff0c;马上就能得到一篇&#xff0c;不满意就重写&#…

力扣-路径总和问题

路径总和 --简单 112. 路径总和 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有节点值相加等于目标和 targetSum 。如果存在&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 f…

MATLAB中zticks函数用法

目录 语法 说明 示例 指定 z 轴刻度值和标签 指定非均匀 z 轴刻度值 以 2 为增量递增 z 轴刻度值 将 z 轴刻度值设置回默认值 指定特定坐标区的 z 轴刻度值 删除 z 轴刻度线 zticks函数的功能是设置或查询 z 轴刻度值。 语法 zticks(ticks) zt zticks zticks(auto)…

猫罐头哪个牌子好性价比高?5款良心性价比的猫罐头推荐给新手养猫!

养猫新手很容易陷入疯狂购买的模式&#xff0c;但有些品牌真的不能乱买&#xff01;现在的市场环境不太好&#xff0c;我们需要学会控制自己的消费欲望&#xff0c;把钱花在刀刃上&#xff01;宠物市场真的很内卷&#xff0c;很多品牌都在比拼产品的数据和营养成分。很多铲屎官…

qt Rectangle 使用Gradient设置渐变方向 制作渐变进度条

1.Gradient方向可查看官网 Gradient.Horizontala horizontal gradient Gradient.Verticala vertical gradient ​​ ProgressBar {id: batteryvalue: 0.5width: 150height: 20anchors.centerIn: parentbackground: Rectangle {implicitWidth: battery.widthimplicitHeight:…

python+appium自动化测试如何控制App的启动和退出

由于本人使用的是Android设备做自动化测试&#xff0c;所以以下内容均基于Android系统做出的整理 一、启动app 启动app需要设置Capability参数&#xff0c;而Capability参数放在Desired Capalibity中&#xff0c;Desired Capalibity告诉Appium想要的自动化平台和应用程序&…

AI自动直播软件,ai无人直播工具2.0支持多平台矩阵直播一键同步直播脚本内容【直播脚本+使用技术教程】

AI实景直播软件简介&#xff1a; 支持一台手机自动直播&#xff0c;支持语音和文字同时回复&#xff0c;商品自动弹窗&#xff0c;支持抖音、快手、视频号、美团平台直播&#xff0c;支持矩阵直播&#xff0c;一键同步直播脚本内容。 设备需求&#xff1a; 安卓手机&#xf…

python连接redis库

在自动化过程中&#xff0c;如果需要动态获取某个数据时&#xff0c;需要连接redis数据库。下面来详细介绍下如何操作。 redis这个库是python自带的&#xff0c;直接import导入即可,如下; import redis 1. redis 地址和端口&#xff0c;端口一般都是默认的6379,只需要换下地…

VS2017的redis客户端实现

VS2017下Redis服务器源码地址 https://download.csdn.net/download/qq_23350817/88541316 VS2017下Redis客户端源码地址(hiredis已完成windows下编译)&#xff1a; https://download.csdn.net/download/qq_23350817/88541242 C代码实现&#xff1a; #include <stdio.h>…

【Maven教程】(十一):使用 Maven 构建 Web应用 —— 使用 jetty-maven-plugin 进行测试、使用 Cargo 实现自动化部署~

Maven 使用 Maven 构建 Web应用 1️⃣ Web 项目的目录结构2️⃣ account-service2.1 account-service的 POM2.2 account-service 的主代码 3️⃣ account-web3.1 account-web 的POM3.2 account-web 的主代码 4️⃣ 使用 jetty-maven-plugin 进行测试5️⃣ 使用 Cargo 实现自动…

【文件上传】01ctfer 文件上传获取flag

1.1漏洞描述 漏洞名称01ctfer 文件上传漏洞类型文件上传漏洞等级⭐⭐⭐漏洞环境docker攻击方式 1.2漏洞等级 高危 1.3影响版本 暂无 1.4漏洞复现 1.4.1.基础环境 靶场docker工具BurpSuite 1.4.2.环境搭建 1.创建docker-compose.yml文件 version: 3.2 services: upload: …

Banana Pi BPI-W3之RK3588安装Qt+opencv+采集摄像头画面.

场景&#xff1a;在Banana Pi BPI-W3 RK3588上做qt开发工作RK3588安装Qtopencv采集摄像头画面 2. 环境介绍 硬件环境&#xff1a; Banana Pi BPI-W3RK3588开发板、MIPI-CSI摄像头( ArmSoM官方配件 )软件版本&#xff1a; OS&#xff1a;ArmSoM-W3 Debian11 QT&#xff1a;QT5…

Compose学习之绘制速度表盘

内心想法XX compose已经发布好久了&#xff0c;还没有用过compose写过UI&#xff0c;之前只是在官网上了解过&#xff0c;看着这可组合函数嵌套&#xff0c;我就脑袋大&#xff0c;更Flutter一个德行&#xff0c;我的内心是抵触的&#xff0c;还是觉得用XML写香&#xff0c;抱…