『 C++类与对象 』多态之单继承与多继承的虚函数表

2023-11-25 04:15

本文主要是介绍『 C++类与对象 』多态之单继承与多继承的虚函数表,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 🫧 前言
    • 🫧 查看虚表
    • 🫧 单继承下的虚函数表
    • 🫧 多继承下的虚函数表


🫧 前言

多态是一种基于继承关系的语法,既然涉及到继承,而继承的方式有多种:

  • 单继承
  • 多继承
  • 棱形继承
  • 棱形虚拟继承
    不同的继承方式其虚表的形式也不同;
以下操作均为在CentOS7_x64机器上的操作

🫧 查看虚表

已知虚表为一个void (*)()的函数指针数组,除了以内存的方式查看虚表以外还可以使用函数调用的方式来查看虚表的真实情况;
其思路即为将该指针数组的指针打印并调用;
根据函数调用可以知道哪个指针是哪个函数;

typedef void(*VFPTR)();
void PrintVT( VFPTR vTable[] ,size_t n/*虚函数个数*/){cout<<"ptr: "<< vTable <<endl;for(size_t i = 0;i<n;++i){printf(" 第%u地址:0x%x,->",i,vTable[i]);VFPTR f=vTable[i];f();}cout<<endl;
}
//函数的参数为函数指针数组(虚表)的首地址;
//由于是自定义类型的前4/8个字节(在该平台下为8个字节)
//应使用对应的方式取到前8个字节;
//通过该首地址向后进行遍历;

🫧 单继承下的虚函数表

存在一个单继承关系:

class A{//基类public:virtual void Func1(){//虚函数cout<<"A::Func1()"<<endl;}virtual void Func2(){//虚函数cout<<"A::Func2()"<<endl;}int _a = 10;
};class B:public A{//派生类public:virtual void Func1(){//虚函数且完成重写cout<<"B::Func1()"<<endl;}virtual void Func3(){//虚函数cout<<"B::Func3()"<<endl;}int _b = 20;
};void test1(){//分别实例化出两个对象A aa;B bb;
}

使用GDB打印出实例化出的aabb的内容;

(gdb) display aa
1: aa = {_vptr.A = 0x400ad8 <vtable for A+16>, _a = 10}
(gdb) display bb
2: bb = {<A> = {_vptr.A = 0x400ab0 <vtable for B+16>, _a = 10}, _b = 20}

由于子类对象和父类对象种都存在一张虚表,所以对应的子类对象的虚函数存储于子类的虚表当中,父类对象的虚函数存储于父类的虚表当中;

其中该段所出现的结果中的_vptr.A = 0x400ad8_vptr.A = 0x400ab0即为虚表指针,该地址不是两个对象的地址,而是该对象地址中首地址所存储的内容;

可以使用&将两个对象的地址取出并使用x/x进行解析从而验证;

(gdb) p &aa 
$10 = (A *) 0x7fffffffe430	#aa对象的首地址
(gdb) x/x 0x7fffffffe430 
0x7fffffffe430:	0x00400ad8	#其首地址所存储的数据(gdb) p &bb
$11 = (B *) 0x7fffffffe420	#bb对象的首地址
(gdb) x/x 0x7fffffffe420
0x7fffffffe420:	0x00400ab0	#其首地址所存储的数据

其中上面的首地址所存储的数据即为一个指针,这个指针即为虚表(虚函数表)指针,也就是虚函数表的首地址位置;

在该示例中基类和派生类中各有两个虚函数,其中派生类的Func1()虚函数重写了基类的Func1()虚函数,所以在基类和派生类的虚表中都存在该函数,且该函数的地址不同;

  • A类虚表

    #	A类虚表
    (gdb)  p aa
    $12 = {_vptr.A = 0x400ad8 <vtable for A+16>, _a = 10}
    #----------------------------------
    (gdb) x/x 0x400ad8	
    0x400ad8 <_ZTV1A+16>:	0x00400924	#虚表首地址所存储的数据(A::Func1()函数的地址)
    (gdb) x/x 0x00400924
    0x400924 <A::Func1()>:	0xe5894855	#将地址解析后得到函数
    #----------------------------------
    (gdb) x/x 0x400ae0
    0x400ae0 <_ZTV1A+24>:	0x00400950	#虚表中第二个位置所存储的数据(由于是64位机器偏移量为8,A::Func2()函数的地址)
    (gdb) x/x 0x00400950
    0x400950 <A::Func2()>:	0xe5894855	#将地址解析后得到函数
    #----------------------------------
    
  • B类虚表
    B类虚表与之不同的是,B类作为派生类,而派生类的虚表可以看成是基类虚表的拷贝,且若发生重写的话虚表中的那个被重写的函数将会被重写的函数进行覆盖;

    (gdb) p bb
    #	B类虚表
    $14 = {<A> = {_vptr.A = 0x400ab0 <vtable for B+16>, _a = 10}, _b = 20}
    #----------------------------------
    (gdb) x/x 0x400ab0
    0x400ab0 <_ZTV1B+16>:	0x0040097c	#虚表首地址所存储的数据(B::Func1()函数的地址[已被重写所以地址不同])
    (gdb) x/x 0x0040097c
    0x40097c <B::Func1()>:	0xe5894855	#将地址解析后得到函数
    #----------------------------------
    (gdb) x/x 0x400ab8
    0x400ab8 <_ZTV1B+24>:	0x00400950	#虚表中第二个位置所存储的数据(由于是64位机器偏移量为8,A::Func2()函数的地址[派生类的虚函数表可以看成是基类函数表的拷贝])
    (gdb) x/x 0x00400950
    0x400950 <A::Func2()>:	0xe5894855	#将地址解析后得到函数
    #----------------------------------
    (gdb) x/x 0x400ac0
    0x400ac0 <_ZTV1B+32>:	0x004009a8	#虚表中第三个位置所存储的数据(由于是64位机器偏移量为8,B::Func3()函数的地址[这里存放的是B类中自身的函数])
    (gdb) x/x 0x004009a8
    0x4009a8 <B::Func3()>:	0xe5894855	#将地址解析后得到函数

在这里插入图片描述

使用函数查看:

typedef void(*VFPTR)();void PrintVT( VFPTR vTable[] ,size_t n/*虚函数个数*/){cout<<"ptr: "<< vTable <<endl;for(size_t i = 0;i<n;++i){printf(" 第%u地址:0x%x,->",i,vTable[i]);VFPTR f=vTable[i];f();}cout<<endl;
}void test1(){A aa;B bb;PrintVT(*(VFPTR**)&aa,2);PrintVT(*(VFPTR**)&bb,3);
}

结果为 (重新编译过所以导致最终结果不同,但结论相同):

ptr: 0x400c60第0地址:0x400a94,->A::Func1()第1地址:0x400ac0,->A::Func2()ptr: 0x400c38第0地址:0x400aec,->B::Func1()第1地址:0x400ac0,->A::Func2()第2地址:0x400b18,->B::Func3()

🫧 多继承下的虚函数表

多继承下的虚函数表较于单继承来说会更加的复杂;
复杂的原因在于多继承为多个基类继承给一个派生类,那么假设两个基类都有同名虚函数,且派生类重写了这个虚函数应该如何判断?

class A{public:virtual void Func1(){cout<<"A::Func1()"<<endl;}virtual void Func2(){cout<<"A::Func2()"<<endl;}
};class B{public:virtual void Func1(){cout<<"B::Func1()"<<endl;}virtual void Func2(){cout<<"B::Func2()"<<endl;}
};class C : public A,public B{public:virtual void Func1(){cout<<"C::Func1()"<<endl;}virtual void Func3(){cout<<"C::Func3()"<<endl;}
};void test2(){C cc;
}

存在以上的继承关系;

使用GDB调试该程序并打印cc的内容;

p cc
$9 = {<A> = {_vptr.A = 0x400cc0 <vtable for C+16>}, <B> = {_vptr.B = 0x400ce8 <vtable for C+56>}, <No data fields>}

由第一点可以知道,派生类的虚表可以看作是基类虚表的拷贝,那么在该程序中由于存在两个基类(多继承),所以应当也有两个虚表;

那么在这个继承关系中,派生类自身所增加的虚函数处于哪个虚表?

实际上在多继承关系中,派生类自身所增加的虚函数都在第一个虚表中,且第一张虚表不仅只存在派生类自身的虚函数,还有一个较为关键的数据;

  • 第一张虚表
    #-------64位机器偏移量为8---------
    #	C::Func1()	被重写
    (gdb) x/x 0x400cc0
    0x400cc0 <_ZTV1C+16>:	0x00400b56
    (gdb) x/x 0x00400b56
    0x400b56 <C::Func1()>:	0xe5894855
    #-------------------------------
    #	A::Func2() 
    (gdb) x/x 0x400cc8
    0x400cc8 <_ZTV1C+24>:	0x00400ad2
    (gdb) x/x 0x00400ad2
    0x400ad2 <A::Func2()>:	0xe5894855
    #-------------------------------
    #	C::Func3()	派生类自身
    (gdb) x/x 0x400cd0 
    0x400cd0 <_ZTV1C+32>:	0x00400b88
    (gdb) x/x 0x00400b88
    0x400b88 <C::Func3()>:	0xe5894855
    #-------------------------------
    (gdb) x/x 0x400cd8
    0x400cd8 <_ZTV1C+40>:	0xfffffff8 #关键数据
    #-------------------------------
    

从该结果可以观察到,派生类自身的虚函数位于第一张虚表当中;
且在最后一个位置存在一个0xfffffff8的数据;


  • 第二张虚表
    #-------------------------------
    #	所存数据并不为虚函数
    (gdb) x/x 0x400ce8
    0x400ce8 <_ZTV1C+56>:	0x00400b81
    (gdb) x/x 0x00400b81
    0x400b81 <_ZThn8_N1C5Func1Ev>:	0x08ef8348
    (gdb) x/x 0x08ef8348
    0x8ef8348:	Cannot access memory at address 0x8ef8348
    #-------------------------------
    #	B类中未重写的虚函数
    (gdb) x/x 0x400cf0
    0x400cf0 <_ZTV1C+64>:	0x00400b2a
    (gdb) x/x 0x00400b2a
    0x400b2a <B::Func2()>:	0xe5894855
    #-------------------------------
    #	NULL空
    (gdb) x/x 0x400cf8
    0x400cf8 <_ZTV1B>:	0x00000000
    #-------------------------------
    
    从该虚表中能看到第二张虚表的第一个位置所存储的数据并不是函数指针;
    在这里就可以提到对应的0xfffffff8数据;
    已知0xffffffff的值为-1,对应的0xfffffff8即为-8;
    这里的值其实是一个偏移量,这个偏移量:

    当走到该处时将该处的偏移量-8,即得到该处函数所在的位置;
    根据这个点进行验证;
    此时已经知道了位置为0x400ce8,且该位置所存储的数据为0x00400b81;

    (gdb) x/x 0x400ce8
    0x400ce8 <_ZTV1C+56>:	0x00400b81
    (gdb) x/x 0x00400b81-8
    0x400b79 <C::Func1()+35>:	0xfffcb2e8
    
    从这里就已经看出,这里通过了偏移量间接的找到了对应的函数;
    当编译器在处理这段代码时,将根据偏移量做出一些处理,使得最终能够通过该偏移量找到对应的函数;

结论为:若是出现多继承,其中两个基类都存在同名的虚函数且在派生类中对该虚函数已经完成了重写的条件时,其虚表构造为如下图:

在这里插入图片描述


这篇关于『 C++类与对象 』多态之单继承与多继承的虚函数表的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

【C++ Primer Plus习题】13.4

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

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

hdu1171(母函数或多重背包)

题意:把物品分成两份,使得价值最接近 可以用背包,或者是母函数来解,母函数(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v) 其中指数为价值,每一项的数目为(该物品数+1)个 代码如下: #include<iostream>#include<algorithm>

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现

c++的初始化列表与const成员

初始化列表与const成员 const成员 使用const修饰的类、结构、联合的成员变量,在类对象创建完成前一定要初始化。 不能在构造函数中初始化const成员,因为执行构造函数时,类对象已经创建完成,只有类对象创建完成才能调用成员函数,构造函数虽然特殊但也是成员函数。 在定义const成员时进行初始化,该语法只有在C11语法标准下才支持。 初始化列表 在构造函数小括号后面,主要用于给