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

相关文章

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

从入门到精通C++11 <chrono> 库特性

《从入门到精通C++11<chrono>库特性》chrono库是C++11中一个非常强大和实用的库,它为时间处理提供了丰富的功能和类型安全的接口,通过本文的介绍,我们了解了chrono库的基本概念... 目录一、引言1.1 为什么需要<chrono>库1.2<chrono>库的基本概念二、时间段(Durat

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Visual Studio 2022 编译C++20代码的图文步骤

《VisualStudio2022编译C++20代码的图文步骤》在VisualStudio中启用C++20import功能,需设置语言标准为ISOC++20,开启扫描源查找模块依赖及实验性标... 默认创建Visual Studio桌面控制台项目代码包含C++20的import方法。右键项目的属性:

MySQL中的表连接原理分析

《MySQL中的表连接原理分析》:本文主要介绍MySQL中的表连接原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、背景2、环境3、表连接原理【1】驱动表和被驱动表【2】内连接【3】外连接【4编程】嵌套循环连接【5】join buffer4、总结1、背景

c++中的set容器介绍及操作大全

《c++中的set容器介绍及操作大全》:本文主要介绍c++中的set容器介绍及操作大全,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录​​一、核心特性​​️ ​​二、基本操作​​​​1. 初始化与赋值​​​​2. 增删查操作​​​​3. 遍历方

解析C++11 static_assert及与Boost库的关联从入门到精通

《解析C++11static_assert及与Boost库的关联从入门到精通》static_assert是C++中强大的编译时验证工具,它能够在编译阶段拦截不符合预期的类型或值,增强代码的健壮性,通... 目录一、背景知识:传统断言方法的局限性1.1 assert宏1.2 #error指令1.3 第三方解决

C++11委托构造函数和继承构造函数的实现

《C++11委托构造函数和继承构造函数的实现》C++引入了委托构造函数和继承构造函数这两个重要的特性,本文主要介绍了C++11委托构造函数和继承构造函数的实现,具有一定的参考价值,感兴趣的可以了解一下... 目录引言一、委托构造函数1.1 委托构造函数的定义与作用1.2 委托构造函数的语法1.3 委托构造函

C++11作用域枚举(Scoped Enums)的实现示例

《C++11作用域枚举(ScopedEnums)的实现示例》枚举类型是一种非常实用的工具,C++11标准引入了作用域枚举,也称为强类型枚举,本文主要介绍了C++11作用域枚举(ScopedEnums... 目录一、引言二、传统枚举类型的局限性2.1 命名空间污染2.2 整型提升问题2.3 类型转换问题三、C

深度解析Spring AOP @Aspect 原理、实战与最佳实践教程

《深度解析SpringAOP@Aspect原理、实战与最佳实践教程》文章系统讲解了SpringAOP核心概念、实现方式及原理,涵盖横切关注点分离、代理机制(JDK/CGLIB)、切入点类型、性能... 目录1. @ASPect 核心概念1.1 AOP 编程范式1.2 @Aspect 关键特性2. 完整代码实