C++ 多态 虚表原理

2024-01-09 20:38
文章标签 c++ 原理 多态 虚表

本文主要是介绍C++ 多态 虚表原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

C++ 多态 虚表的原理

前言

C++有三大特性,封装、继承、多态。我们今天要说到的虚表就是实现虚函数调用机制的,而虚函数又是实现多态的基础。虚函数的函数指针存储于虚表中,一个类调用虚函数其实就是访问它的虚表。
在这里我说一下多态的理解,多态其实就是父类类型的指针指向其子类的实例,这时通过父类指针调用子类的方法就可以实现不改变代码的情况下调用不同的方法,通过实例化不同的子类,父类就具有不同的形态。
我们一步步的来分析C++的虚表,首先看C++对象的内存布局。假设现在有这么个类A:

class A {public:A(int a = 0, int b = 0) :A_a(a), A_b(b){}void A1() {}private:int A_a;int A_b;
};

我们来看看类A的内存布局(IDE用的是VS2015):
在这里插入图片描述
我们可以看到类A的内存布局中只存在A_a和A_b。
现在有一个类B继承类A,

class B : public A
{
public:B(int a = 0, int b = 0, int c = 0) :A(a, b), B_a(c){}void B1() {}private:int B_a;
};

看一下类B的内存布局:
在这里插入图片描述
在这儿说明一下,在调用类B的构造之前会先调用类A的构造函数,也就是先构造父类,再构造子类,所以类B的内部布局如上图,类A的成员变量加上类B的成员变量。

以上说的是类没有虚函数的情况,现在我们给类加上虚函数,类A:

class A {public:A(int a = 0, int b = 0) :A_a(a), A_b(b){}virtual void A1() { std::cout << "A::A1" << std::endl; }virtual void A2() { std::cout << "A::A2" << std::endl; }private:int A_a;int A_b;
};

此时看一下类A的内存布局:
在这里插入图片描述
我们可以看到是类A的内存布局中多出了一个指针,由于类A中有虚函数,所以会创建一个虚函数表,而这个指针指向的就是类A的虚函数表,且虚函数表指针的位置在对象首位置(大多数编译器都是讲其放在对象首位置)。
我们给继承类A的B类也加上虚函数,它重写类A的A1方法,并且加上自己的虚函数B1方法,类B:

class B : public A
{
public:B(int a = 0, int b = 0, int c = 0) :A(a, b), B_a(c){}virtual void A1() { std::cout << "B::A1" << std::endl; }virtual void B1() { std::cout << "B::B1" << std::endl; }private:int B_a;
};

此时我们来看类B的内存布局:
在这里插入图片描述
若是父类有虚函数,则子类会继承父类的虚函数表。子类重写了父类的虚函数,会在虚函数表里替换虚函数指针地址,虚函数表内存储的虚函数指针地址顺序是先父类的虚函数指针地址,再子类的虚函数指针地址。这里类B会继承类A的虚表,它重写了父类的A1函数。类A和类B的内存布局对照如下:
在这里插入图片描述通过上图我们可以很直观的看出类A与类B的内存布局对比,由于这里编译器不能显示类B写的其它的虚函数地址,我们通过打印虚函数表来看。代码如下:

typedef void(*pfun)();   //定义函数指针
int main()
{B *b = new B(1, 2, 3);pfun fun = NULL; //函数指针变量,用于循环迭代虚函数表for (int i = 0; i < 3; i++){fun = (pfun)*((long*)*(long*)b+i);fun();}std::cout << *((long*)*(long*)b + 3) << std::endl;   //输出虚函数表的结束符return 0;
}

打印类B的虚函数表如下:
在这里插入图片描述

多重继承

多重继承就是至少有三层继承关系,如这里的B继承A,C继承B,这里写一个类C继承类B

class C : public B
{
public:C(int a = 0, int b = 0, int c = 0, int d = 0) :B(a, b, c), C_a(d){}virtual void A2() { std::cout << "C::A2" << std::endl; }virtual void B1() { std::cout << "C::B1" << std::endl; }virtual void C1() { std::cout << "C::C1" << std::endl; }private:int C_a;
};

这里的类C的内部布局如图所示:
在这里插入图片描述
多重继承和单继承是类似的,就是在父类的基础上增加成员变量和更新虚函数表。

多继承

多继承下,子类所继承的父类不止一个,子类中会存在多个虚函数表,重写虚函数,则会更新对应的虚函数表,若是增加了新的虚函数,则在第一个虚函数表后增加虚函数地址。
代码如下:

class A {public:A(int a = 0, int b = 0) :A_a(a), A_b(b){}virtual void A1() { std::cout << "A::A1" << std::endl; }virtual void A2() { std::cout << "A::A2" << std::endl; }private:int A_a;int A_b;
};class B
{
public:B(int a = 0) :B_a(a){}virtual void B1() { std::cout << "B::B1" << std::endl; }virtual void B2() { std::cout << "B::B2" << std::endl; }private:int B_a;
};class C : public A, public B
{
public:C(int a = 0, int b = 0, int c = 0, int d = 0) :A(a, b), B(c), C_a(d){}virtual void A1() { std::cout << "C::A1" << std::endl; }virtual void B1() { std::cout << "C::B1" << std::endl; }virtual void C1() { std::cout << "C::C1" << std::endl; }private:int C_a;
};

在这里类C继承类A与类B,类C分别重写了类A与类C的虚函数,我们来看看编译器的显示:
在这里插入图片描述
从编译器可以看出,类C继承了类A与类B的虚函数表,并且在内存中的顺序是按照继承顺序来的,类C重写的虚函数分别替换了两个父类的虚函数。类C自己新定义的虚函数C1()则是在继承的第一张虚函数表后添加。类C的具体内存布局如下:
在这里插入图片描述
在我看来,理解内部的原理能使我们解决有时候认为的“无法理解”的问题。虽然这是一条不容易的路,但每走一步都会有收获。

这篇关于C++ 多态 虚表原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/588365

相关文章

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

C++中实现调试日志输出

《C++中实现调试日志输出》在C++编程中,调试日志对于定位问题和优化代码至关重要,本文将介绍几种常用的调试日志输出方法,并教你如何在日志中添加时间戳,希望对大家有所帮助... 目录1. 使用 #ifdef _DEBUG 宏2. 加入时间戳:精确到毫秒3.Windows 和 MFC 中的调试日志方法MFC

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

SpringCloud配置动态更新原理解析

《SpringCloud配置动态更新原理解析》在微服务架构的浩瀚星海中,服务配置的动态更新如同魔法一般,能够让应用在不重启的情况下,实时响应配置的变更,SpringCloud作为微服务架构中的佼佼者,... 目录一、SpringBoot、Cloud配置的读取二、SpringCloud配置动态刷新三、更新@R

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

在 VSCode 中配置 C++ 开发环境的详细教程

《在VSCode中配置C++开发环境的详细教程》本文详细介绍了如何在VisualStudioCode(VSCode)中配置C++开发环境,包括安装必要的工具、配置编译器、设置调试环境等步骤,通... 目录如何在 VSCode 中配置 C++ 开发环境:详细教程1. 什么是 VSCode?2. 安装 VSCo

C++11的函数包装器std::function使用示例

《C++11的函数包装器std::function使用示例》C++11引入的std::function是最常用的函数包装器,它可以存储任何可调用对象并提供统一的调用接口,以下是关于函数包装器的详细讲解... 目录一、std::function 的基本用法1. 基本语法二、如何使用 std::function

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于