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

相关文章

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

ShardingProxy读写分离之原理、配置与实践过程

《ShardingProxy读写分离之原理、配置与实践过程》ShardingProxy是ApacheShardingSphere的数据库中间件,通过三层架构实现读写分离,解决高并发场景下数据库性能瓶... 目录一、ShardingProxy技术定位与读写分离核心价值1.1 技术定位1.2 读写分离核心价值二

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深入浅出Spring中的@Autowired自动注入的工作原理及实践应用

《深入浅出Spring中的@Autowired自动注入的工作原理及实践应用》在Spring框架的学习旅程中,@Autowired无疑是一个高频出现却又让初学者头疼的注解,它看似简单,却蕴含着Sprin... 目录深入浅出Spring中的@Autowired:自动注入的奥秘什么是依赖注入?@Autowired

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

Python中的filter() 函数的工作原理及应用技巧

《Python中的filter()函数的工作原理及应用技巧》Python的filter()函数用于筛选序列元素,返回迭代器,适合函数式编程,相比列表推导式,内存更优,尤其适用于大数据集,结合lamb... 目录前言一、基本概念基本语法二、使用方式1. 使用 lambda 函数2. 使用普通函数3. 使用 N

C++ vector越界问题的完整解决方案

《C++vector越界问题的完整解决方案》在C++开发中,std::vector作为最常用的动态数组容器,其便捷性与性能优势使其成为处理可变长度数据的首选,然而,数组越界访问始终是威胁程序稳定性的... 目录引言一、vector越界的底层原理与危害1.1 越界访问的本质原因1.2 越界访问的实际危害二、基

MyBatis-Plus 与 Spring Boot 集成原理实战示例

《MyBatis-Plus与SpringBoot集成原理实战示例》MyBatis-Plus通过自动配置与核心组件集成SpringBoot实现零配置,提供分页、逻辑删除等插件化功能,增强MyBa... 目录 一、MyBATis-Plus 简介 二、集成方式(Spring Boot)1. 引入依赖 三、核心机制