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++判断水仙花数并输出的相关资料,文中通过代码介绍的非常详细,需要的朋友可以... 以下是使用C++实现的相同逻辑代码:#include <IOStream>#include <vec

基于C++的UDP网络通信系统设计与实现详解

《基于C++的UDP网络通信系统设计与实现详解》在网络编程领域,UDP作为一种无连接的传输层协议,以其高效、低延迟的特性在实时性要求高的应用场景中占据重要地位,下面我们就来看看如何从零开始构建一个完整... 目录前言一、UDP服务器UdpServer.hpp1.1 基本框架设计1.2 初始化函数Init详解

C++ 右值引用(rvalue references)与移动语义(move semantics)深度解析

《C++右值引用(rvaluereferences)与移动语义(movesemantics)深度解析》文章主要介绍了C++右值引用和移动语义的设计动机、基本概念、实现方式以及在实际编程中的应用,... 目录一、右值引用(rvalue references)与移动语义(move semantics)设计动机1

Java线程池核心参数原理及使用指南

《Java线程池核心参数原理及使用指南》本文详细介绍了Java线程池的基本概念、核心类、核心参数、工作原理、常见类型以及最佳实践,通过理解每个参数的含义和工作原理,可以更好地配置线程池,提高系统性能,... 目录一、线程池概述1.1 什么是线程池1.2 线程池的优势二、线程池核心类三、ThreadPoolE

C++ move 的作用详解及陷阱最佳实践

《C++move的作用详解及陷阱最佳实践》文章详细介绍了C++中的`std::move`函数的作用,包括为什么需要它、它的本质、典型使用场景、以及一些常见陷阱和最佳实践,感兴趣的朋友跟随小编一起看... 目录C++ move 的作用详解一、一句话总结二、为什么需要 move?C++98/03 的痛点⚡C++

Spring Boot Interceptor的原理、配置、顺序控制及与Filter的关键区别对比分析

《SpringBootInterceptor的原理、配置、顺序控制及与Filter的关键区别对比分析》本文主要介绍了SpringBoot中的拦截器(Interceptor)及其与过滤器(Filt... 目录前言一、核心功能二、拦截器的实现2.1 定义自定义拦截器2.2 注册拦截器三、多拦截器的执行顺序四、过

详解C++ 存储二进制数据容器的几种方法

《详解C++存储二进制数据容器的几种方法》本文主要介绍了详解C++存储二进制数据容器,包括std::vector、std::array、std::string、std::bitset和std::ve... 目录1.std::vector<uint8_t>(最常用)特点:适用场景:示例:2.std::arra

C++构造函数中explicit详解

《C++构造函数中explicit详解》explicit关键字用于修饰单参数构造函数或可以看作单参数的构造函数,阻止编译器进行隐式类型转换或拷贝初始化,本文就来介绍explicit的使用,感兴趣的可以... 目录1. 什么是explicit2. 隐式转换的问题3.explicit的使用示例基本用法多参数构造

C++,C#,Rust,Go,Java,Python,JavaScript的性能对比全面讲解

《C++,C#,Rust,Go,Java,Python,JavaScript的性能对比全面讲解》:本文主要介绍C++,C#,Rust,Go,Java,Python,JavaScript性能对比全面... 目录编程语言性能对比、核心优势与最佳使用场景性能对比表格C++C#RustGoJavapythonjav

C++打印 vector的几种方法小结

《C++打印vector的几种方法小结》本文介绍了C++中遍历vector的几种方法,包括使用迭代器、auto关键字、typedef、计数器以及C++11引入的范围基础循环,具有一定的参考价值,感兴... 目录1. 使用迭代器2. 使用 auto (C++11) / typedef / type alias