首页 > 编程语言 >C++ 虚函数表解析

C++ 虚函数表解析

时间:2024-09-13 22:35:13浏览次数:10  
标签:虚表 函数 int void C++ vptr 解析 指针

一、何为多态

多态(polymorphism)指为不同数据类型的实体提供统一的接口,或使用单一的符号来表示多个不同的类型。比如我们熟悉的函数重载、模板技术,都属于多态。无论是模板还是函数重载,都是静态绑定的。也就是说,究竟该调用哪个重载函数或者说调用哪个模板类的实例化,在编译期就是确认的。虚函数也是多态的一种,它是运行时的多态。

下面的代码演示了通过虚函数实现的多态:

 1 #include<iostream>
 2 using namespace std;
 3 class Base 
 4 { 
 5 public: 
 6     virtual void f()
 7     {
 8         cout<<"Base::f()"<<endl;
 9     }
10 };
11 class Derived: public Base 
12 {
13 public:
14     void f()
15     {
16         cout<<"Derived::f()"<<endl;
17     }
18 };
19 int main() {
20     Derived x;
21     Base* p = &x;
22     p->f(); //输出Derived::f()。
23     Base &b = x;
24     b.f();  //输出Derived::f()。
25     Base b1 = x;
26     b1.f();//输出Base::f(),值语义,不能表现出多态
27     return 0;
28 }

 

运行结果:

[root@VM-16-4-opencloudos vtable]# ./main 
Derived::f()
Derived::f()
Base::f()

 

用法是:基类的指针或引用,用不同的子类赋值时,就表现不同的行为。而值语义是不能表现出多态的。

实现的机制是因编译器而异,但基本上使用虚函数表来实现的,这个后面再介绍。

这会我想谈的是:为什么说虚函数是运行时的多态,基类的指针指向的类型需要在运行期间才能确定?

其实单看上面的代码,也不需要在运行时才知道p->f()调用的是Devied中的函数呀,代码里已经明确了Base的指针p就是指向子类Derived的,我直接看代码都知道了,难道编译器是傻的吗,还要等到运行期时才去通过虚函数表找到p->f()实际调用的是Derived中的函数?其实不是的,对于编译期能确定调用目标的虚函数,最终生成的代码并不会傻乎乎的去查虚表,编译器会执行一些优化,进行静态绑定。具体的讨论可以参考下面这个回答:虚函数一定是运行期才绑定么?

那为什么都说是运行期绑定的?

其实上面这份代码看不出来,可以看下面这份:

 1 #include<iostream>
 2 using namespace std;
 3 class Base 
 4 { 
 5 public: 
 6     virtual void f()
 7     {
 8         cout<<"Base::f()"<<endl;
 9     }
10 };
11 class Derived: public Base 
12 {
13 public:
14     void f()
15     {
16         cout<<"Derived::f()"<<endl;
17     }
18 };
19 int main() {
20     char k = getchar();
21     Base *p = NULL;
22     if(k == 'a') {
23         p = new Base();
24     } else {
25         p = new Derived();
26     }
27     p->f(); //输出Base::f() or Derived::f() ?
28     return 0;
29 }

在编译期间无法分析出这个指针究竟指向什么类型的对象,只有等到程序运行时,用户从键盘输入字符之后才能确定。此时只能是通过运行期动态绑定了。

之所以要在运行期动态绑定来实现的原因就是运行期外部 IO。参考知乎的回答:为什么C++实现多态必须要虚函数表?

搞清楚了为什么需要运行期动态绑定了,那下面就来说说怎么实现的吧。

二、虚函数表

我们来看以下的代码。类 A 包含虚函数vfunc1vfunc2,由于类 A 包含虚函数,故类 A 拥有一个虚表。

1 class A {
2 public:
3     virtual void vfunc1();
4     virtual void vfunc2();
5     void func1();
6     void func2();
7 private:
8     int m_data1, m_data2;
9 };

类 A 的虚表如图 1 所示。

图 1:对象它的虚表示意图

虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。

虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。

虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。

为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。

为什么弄懂虚表的内存布局,我找了很多资料,认为这两个知乎的回答我能理解清楚:

一个回答是:单继承下的虚函数表的内存布局

另一个回答是我不太明白的地方:对象在其起始地址处存放了虚表指针(vptr),vptr指向虚表,虚表中存储了实际的函数地址。原来我以为虚表中存储的只有虚函数的地址,但其实不是的,vptr指向的并不是虚表的表头,而是直接指向虚函数的位置。实际上虚表中虚函数的位置之前,还有两个槽位,每个槽位占8个字节,这篇回答说明了这两个槽位的作用:多继承下的虚函数表的内存布局

另外之前我有个疑惑:派生类中会有几个vptr呢?比如B继承A,然后C继承了B,C类中会有几个vptr呢?是有3个吗?(A、B、C各一个),还是只有1个?

答案是:在单链继承中,每一个派生类型都包含了其基类型的数据以及虚函数,这些虚函数可以按照继承顺序,依次排列在同一张虚表之中,因此只需要一个虚指针即可。并且由于每一个派生类都包含它的直接基类,且没有第二个直接基类,因此其数据在内存中也是线性排布的,这意味着实际类型与它所有的基类型都有着相同的起始地址。

而对于多继承而言,假设类型C同时继承了两个独立的基类A和B,比如:

 1 struct A
 2 {
 3     int ax;
 4     virtual void f0() {}
 5 };
 6 
 7 struct B
 8 {
 9     int bx;
10     virtual void f1() {}
11 };
12 
13 struct C : public A, public B
14 {
15     int cx;
16     void f0() override {}
17     void f1() override {}
18 };

与单链继承不同,由于AB完全独立,它们的虚函数没有顺序关系,即f0f1有着相同对虚表起始位置的偏移量,不可以顺序排布。 并且AB中的成员变量也是无关的,因此基类间也不具有包含关系。这使得ABC中必须要处于两个不相交的区域中,同时需要有两个虚指针分别对它们虚函数进行索引。 其内存布局如下所示:

                                               C Vtable (7 entities)
                                                +--------------------+
struct C                                        | offset_to_top (0)  |
object                                          +--------------------+
    0 - struct A (primary base)                 |     RTTI for C     |
    0 -   vptr_A -----------------------------> +--------------------+       
    8 -   int ax                                |       C::f0()      |
   16 - struct B                                +--------------------+
   16 -   vptr_B ----------------------+        |       C::f1()      |
   24 -   int bx                       |        +--------------------+
   28 - int cx                         |        | offset_to_top (-16)|
sizeof(C): 32    align: 8              |        +--------------------+
                                       |        |     RTTI for C     |
                                       +------> +--------------------+
                                                |    Thunk C::f1()   |
                                                +--------------------+

在上面的内存布局中,C将A作为主基类,也就是将它虚函数“并入”A的虚表之中,并将A的vptr作为C的内存起始地址。

上面的内存布局中,offset_to_top(-16)用于确保如果将类C实例的地址赋给类B的指针p,调用p->f0()时,能找到对应的虚函数,其内部会将指针偏移offset_to_top个字节,找到类C的虚表指针(vptr_A),然后找到对应的虚函数来调用。至于为什么是16个字节,是因为vptr本身占8个字节,另外还有int ax; 虽然int是4字节的,但因为内存对齐,所以总共占16个字节。

 

假设类型C同时继承了两个独立的基类AB

C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置, 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

下面的程序用于验证包含虚函数的类对象内存的首地址是vptr,并演示如何通过vptr找到虚函数表并访问虚函数:

#include <iostream>
using namespace std;

class Base {
     public:
            virtual void f() { cout << "Base::f" << endl; }
            virtual void g() { cout << "Base::g" << endl; }
            virtual void h() { cout << "Base::h" << endl; }

};
typedef void(*Fun)(void);
int main(int argc, char *argv[])
{
    Base b;
    Fun pFun = NULL;
    cout << "虚函数表地址:" << (int*)(&b) << endl;  //vptr
    cout << "虚函数表 — 函数指针数组的首地址:" << (int*)*(int*)(&b) << endl;
    // Invoke the first virtual function
    pFun = (Fun)*((int*)*(int*)(&b)+0);  // Base::f()
    pFun();
    return 0;
}

运行结果:

[root@VM-16-4-opencloudos vtable]# ./main 
虚函数表地址:0x7ffdb2560880
虚函数表 — 第一个函数地址:0x400b78
Base::f

&b取到了虚函数表的地址(vptr),*(int*)(&b)是对虚函数表地址的解引用,得到的是虚函数表。虚函数表中存放了一个函数指针数组,即数组中的每个元素都是一个函数指针,指向每一个虚函数。直接访问(int*)*(int*)(&b)得到的是函数指针数组的首地址。对其进行解引用,即*(int*)*(int*)(&b),则可以得到函数指针数组的首地址指向的元素,该元素是一个函数指针,因为虚函数按照其声明顺序放于虚函数表中,所以*(int*)*(int*)(&b)对应的是Base::f()函数的地址。

 

标签:虚表,函数,int,void,C++,vptr,解析,指针
From: https://www.cnblogs.com/codingmengmeng/p/18411080

相关文章

  • lesson04-设计初始化bss段、读写寄存器值的汇编函数
    在内核启动时需要将bss段的所有数据清0,这里就需要memzero函数。.globalmemzero;全局可见memzero:strxzr,[x0],#8subsx1,x1,#8b.gtmemzeroret内核启动时需要经常读写soc内部寄存器的值,这里就需要用到对应的函数put32和get32。.globalput32......
  • Javaweb之SpringBootWeb案例本地存储的详细解析
     2.2本地存储前面我们已分析了文件上传功能前端和后端的基础代码实现,文件上传时在服务端会产生一个临时文件,请求响应完成之后,这个临时文件被自动删除,并没有进行保存。下面呢,我们就需要完成将上传的文件保存在服务器的本地磁盘上。代码实现:在服务器本地磁盘上创建images目录,用来存......
  • Python XML 解析
    什么是XML?Python对XML的解析1.SAX(simpleAPIforXML)2.DOM(DocumentObjectModel)3.ElementTree(元素树)python使用SAX解析xmlContentHandler类方法介绍make_parser方法parser方法parseString方法Python解析XML实例使用xml.dom解析xml什么是XML?XML......
  • C++ 继承同名Impl简单测试构造顺序析构顺序
    /*****classBase*****/classBase{public:Base();virtual~Base();protected:structImpl;Impl*m_Impl;};structBase::Impl{intnum{100};};Base::Base(){m_Impl=newImpl;std::cout<<"Base构造!"......
  • Hash Table 哈希表工作原理介绍及C/C++/Python实现
    HashTable哈希表工作原理介绍及C/C++/Python实现哈希表(HashTable),也称为散列表,是一种通过哈希函数将键(Key)映射到表中一个位置以便快速访问记录的数据结构。它提供了非常高效的数据检索、插入和删除操作。哈希表的基本原理是使用一个哈希函数将输入(通常是字符串)转换为一个......
  • C++入门基础知识65——【关于C++ 数据封装】
    成长路上不孤单......
  • C++入门基础知识66——【关于C++ 接口(抽象类)】
    成长路上不孤单......
  • 多目标优化算法求解36个多目标测试函数(ZDT1、ZDT2、ZDT3、ZDT4、ZDT6、DTLZ1-DTLZ9、W
    36个多目标测试函数(ZDT1、ZDT2、ZDT3、ZDT4、ZDT6、DTLZ1-DTLZ9、WFG1-WFG9、UF1-UF10、LSMOP1-LSMOP3)是专门为了测试和比较不同多目标优化算法的性能而设计的。下面是每个函数集的简要介绍:ZDT(Zitzler-Deb-Thiele)函数集:ZDT系列是一组经典的多目标优化测试函数,由EckartZit......
  • SPI协议看这一篇就够了!(图文+代码+解析+仿真)
    目录SPI协议简介SPI工作原理实验需求模块图时序图小结SPI协议简介1.高速传输,SPI作为三大低速总线(UART、IIc、SPI)之一,其传输速度是这个个中最快的一个。它是一种高速、全双工、同步串行通信总线。所谓高速,指的是传输速度,最高能达到几十M/s,具体速度取决于硬件实现和时钟......
  • 深入解析 MyBatis:从理论到项目实例
    深入解析MyBatis:从理论到项目实例目录MyBatis概述MyBatis项目结构及作用核心概念详解分页功能的实现与深入剖析动态SQL缓存机制详解与Spring集成常见问题与深入分析完整项目示例总结1.MyBatis概述MyBatis是一个轻量级的持久层框架,使用SQL查询语句来访问数据......