C++虚函数和虚函数表原理

article/2025/10/4 4:12:23

虚函数的地址存放于虚函数表之中。运行期多态就是通过虚函数和虚函数表实现的。

类的对象内部会有指向类内部的虚表地址的指针。通过这个指针调用虚函数。

虚函数的调用会被编译器转换为对虚函数表的访问:

ptr->f();   //ptr代表this指针,f是虚函数
*(ptr->vptr[1])(ptr);

上述代码中,ptr代表一个this指针,ptr指向的vptr是类内部的虚表指针。这个虚表指针会被放在类的最前方(VS2017),1就是虚函数指针在虚函数表中的索引值。在这个索引值表示的虚表的槽中存放的就是f()的地址。

虚表指针的名字也会被编译器更改,所以在多继承的情况下,类的内部可能存在多个虚表指针。通过不同的名字被编译器标识。

虚函数表中可能还存在其他的内容,如用于RTTI的type_info类型。或者直接将虚基类的指针存放在虚表中。

压制多态可以通过域操作符进行。

class A1
{
public:virtual void f() { cout << "A1::f" << endl; }
};
class C : public A1
{
public:virtual void f() { cout << "C::f" << endl; }
};
c.A1::f();  //A1::f
c.f();  //C::f

单继承

这种情况下,派生类中仅有一个虚函数表。这个虚函数表和基类的虚函数表不是一个表(无论派生类有没有重写基类的虚函数),但是如果派生类没有重写基类的虚函数的话,基类和派生类的虚函数表指向的函数地址都是相同的。

class A1
{
public:A1(int _a1 = 1) : a1(_a1) { }virtual void f() { cout << "A1::f" << endl; }virtual void g() { cout << "A1::g" << endl; }virtual void h() { cout << "A1::h" << endl; }~A1() {}
private:int a1;
};
class C : public A1
{
public:C(int _a1 = 1, int _c = 4) :A1(_a1), c(_c) { }//virtual void f() { cout << "C::f" << endl; }//virtual void g() { cout << "C::g" << endl; }//virtual void h() { cout << "C::h" << endl; }
private:int c;
};

类C没有重写A的虚函数,所以虚函数表内部的情况如下:

这里写图片描述

可以看出,两个类的__vfptr的值不同,但是每个槽内部的函数地址都是相同的。

这里写图片描述

如果类C中重写了A类中的函数:

class C : public A1
{
public:C(int _a1 = 1, int _c = 4) :A1(_a1), c(_c) { }virtual void f() { cout << "C::f" << endl; }virtual void g() { cout << "C::g" << endl; }virtual void h() { cout << "C::h" << endl; }
private:int c;
};

这里写图片描述

那么就会覆盖A类的虚函数,重写一部分就会覆盖一部分,重写全部就会覆盖全部。

这里写图片描述

如果C中重新写了一些别的虚函数,那么这些虚函数将排在父类的后面,这里编译器无法显示,可以通过打印虚表来进行。

打印的过程比较简单,通过访问类C的前8字节(64位编译器)找到虚函数表,再一次遍历虚函数表即可。虚函数表最后一项用的是0,代表虚函数表结束。

C c;
long long *p = (long long *)(*(long long*)&c);
typedef void(*FUNC)();        //重定义函数指针,指向函数的指针
void PrintVTable(long long* vTable)  //打印虚函数表
{if (vTable == NULL){return;}cout << "vtbl:" << vTable << endl;int  i = 0;for (; vTable[i] != 0; ++i){printf("function : %d :0X%x->", i, vTable[i]);FUNC f = (FUNC)vTable[i];f();         //访问虚函数}cout << endl;
}

通过这样的打印可以得知C的虚函数表为:

vtbl:00007FF6CD2CBE68
function : 0 :0Xcd2c115e->A1::f
function : 1 :0Xcd2c146a->A1::g
function : 2 :0Xcd2c1113->A1::h

vtbl:00007FF6CD2CBEA8
function : 0 :0Xcd2c115e->A1::f
function : 1 :0Xcd2c146a->A1::g
function : 2 :0Xcd2c1113->A1::h
function : 3 :0Xcd2c1023->C::f
function : 4 :0Xcd2c132a->C::g
function : 5 :0Xcd2c11d1->C::h

具体的图解为:

这里写图片描述

多继承

多继承情况下,派生类中有多个虚函数表,虚函数的排列方式和继承的顺序一致。派生类重写函数将会覆盖所有虚函数表的同名内容,派生类自定义新的虚函数将会在第一个类的虚函数表的后面进行扩充

class A1
{
public:A1(int _a1 = 1) : a1(_a1) { }virtual void f() { cout << "A1::f" << endl; }virtual void g() { cout << "A1::g" << endl; }virtual void h() { cout << "A1::h" << endl; }~A1() {}
private:int a1;
};
class A2
{
public:A2(int _a2 = 2) : a2(_a2) { }virtual void f() { cout << "A2::f" << endl; }virtual void g() { cout << "A2::g" << endl; }virtual void h() { cout << "A2::h" << endl; }~A2() {}
private:int a2;
};
class A3
{
public:A3(int _a3 = 3) : a3(_a3) { }virtual void f() { cout << "A3::f" << endl; }virtual void g() { cout << "A3::g" << endl; }virtual void h() { cout << "A3::h" << endl; }~A3() {}
private:int a3;
};class B : public A1, public A2, public A3
{
public:B(int _a1 = 1, int _a2 = 2, int _a3 = 3, int _b = 4) :A1(_a1), A2(_a2), A3(_a3), b(_b) { }virtual void f1(){ cout << "B::f" << endl; }virtual void g1(){ cout << "B::g" << endl; }virtual void h1(){ cout << "B::h" << endl; }
private:int b;
};

这里通过编译器的部分可以看出来,未被重写的虚函数指针将和基类指向同一个位置,一旦被重写,函数指针就指向新的位置。

这里写图片描述

在B类中,函数指针指向的位置不变:

这里写图片描述

而这时候B类中第一个虚函数表已经增加了新的项,从打印结果可知。

vtbl:00007FF7DD62BF38
function : 0 :0Xdd621177->A1::f
function : 1 :0Xdd621497->A1::g
function : 2 :0Xdd621127->A1::h

vtbl:00007FF7DD62BF78
function : 0 :0Xdd6212df->A2::f
function : 1 :0Xdd62105f->A2::g
function : 2 :0Xdd6213fc->A2::h

vtbl:00007FF7DD62BFB8
function : 0 :0Xdd621032->A3::f
function : 1 :0Xdd62129e->A3::g
function : 2 :0Xdd621221->A3::h

vtbl:00007FF7DD62BFF8
function : 0 :0Xdd621177->A1::f
function : 1 :0Xdd621497->A1::g
function : 2 :0Xdd621127->A1::h
function : 3 :0Xdd62144c->B::f
function : 4 :0Xdd621019->B::g
function : 5 :0Xdd62133e->B::h

而如果B类重写了函数,那么打印结果将是:

vtbl:00007FF720C8BF38
function : 0 :0X20c8117c->A1::f
function : 1 :0X20c814b5->A1::g
function : 2 :0X20c8112c->A1::h

vtbl:00007FF720C8BF78
function : 0 :0X20c812f8->A2::f
function : 1 :0X20c8105a->A2::g
function : 2 :0X20c8141a->A2::h

vtbl:00007FF720C8BFB8
function : 0 :0X20c8102d->A3::f
function : 1 :0X20c812b2->A3::g
function : 2 :0X20c81230->A3::h

vtbl:00007FF720C8BFF8
function : 0 :0X20c814ab->B::f
function : 1 :0X20c81370->B::g
function : 2 :0X20c81393->B::h

并且此时B类的信息为:

这里写图片描述

从编译器给出的信息我们可以看到在第二个虚函数表中有adjustor{16}的字样,这就是A类的大小,也就是说,这就是告诉编译器,需要进行16字节的偏移(thunk技术)。这就引出了接下来的一个问题:

B类用不同的基类指针指向的时候,运行的是不同的基类中的虚函数(这就是多态的表现),这里可以知道,当A2类指针指向B的时候,虚函数指针是自动跳到B类中A2类所在的地方的,这个跳转是怎么进行的呢?

首先在编译期,就可以知道一个指针需要偏移多少个字节:

A2 *p = new B;

编译器会将这个代码改为:

B *tmp = new B;
A2 *p = tmp ? tmp + sizoef(A1) : 0;

经过这样的调整A1,A2,A3都会指向正确的类的位置。但是这样还不够。

由上面的编译器信息图我们可以知道,当B类重写了函数之后,A2,A3的虚函数表所指对象已经不再是简单的函数指针了,而是一个thunk对象。这就是C++的thunk技术。

所谓的thunk就是一段汇编代码,这段汇编代码可以以适当的偏移值来调整this指针以跳到对应的虚函数中去,并调用这个函数,也就是说当使用A1的指针指向B的对象,不需要发生偏移,而使用A2的指针指向B则需要进行偏移sizeof(A1)个字节。并跳转到A1中的函数来执行。这就是通过thunk的jmp指令跳转到这个函数。

所以具体的虚函数表中的情况如下:

  1. 如果两个基类中的虚函数名字不同,派生类只重写了第二个基类的虚函数,则不会产生thunk用以跳转。
  2. 如果基类中虚函数名字相同,派生类如果重写,将会一次性重写两个基类的虚函数,这时候第二个基类的虚函数表中存放的就是thunk对象,当指针指向此处的时候,会自动跳转到A类的对应虚函数(已经被B重写)执行。
  3. 第一个基类的虚函数被重写与否都不会产生thunk对象,因为这个类是被别的基类指针跳转的目标,而这个类的指针施行多态的时候是不会发生跳转的。
  4. 派生类的重新定义的虚函数将会排在第一个虚函数表内部A1虚函数的后面,但是当A2调用这个函数的时候,会通过thunk技术跳转回第一个类的虚函数表以执行相对应的虚函数。
  5. 除了第一个基类的虚析构函数,其他基类的析构函数都是thunk对象。

综上所述,thunk对象用于所有基类都被派生类重写后,调用虚函数将跳到最开始的基类部分。或者派生类中定义的虚函数也会跳转到第一个基类的虚函数表中。而仅出现在后面的基类的虚函数表中的虚函数,无论被重写与否都不会产生thunk对象。因为这里不会在第一个基类中由对应的虚函数指针。


http://chatgpt.dhexx.cn/article/fIrbhSqe.shtml

相关文章

虚函数 虚函数表

虚函数是面向对象编程函数的一种特定形态&#xff0c;是C用于实现多态的一种有效机制。C的多态可以分为静态多态和动态多态。函数重载和运算符重载实现的多态属于静态多态&#xff0c;而通过虚函数可以实现动态多态。实现函数的动态联编其本质核心则是虚表指针与虚函数表。 虚…

虚函数表和虚表指针

1&#xff0c;虚函数的含义 用virtual声明类的成员函数称之为虚函数 2&#xff0c;作用 用于实现多态 存在继承关系&#xff0c;子类继承父类子类重写了父类的virtual function子类以父类的指针或者引用的身份出现 3&#xff0c;虚函数的实现原理 其中的关键就是两点&#xf…

C++ 虚函数和虚函数表

一、虚函数 1.虚函数的概念 1.虚函数就是在基类中被关键字 virtual 说明&#xff0c;并在派生类中重新定义的函数。 2.虚函数的作用是允许在派生类中重新定义与基类同名的函数&#xff0c;并且可以通过基类指针或引用来访问基类和派生类中的同名函数。 3.虚函数的定义是在基类…

C++虚函数表剖析

关键词&#xff1a;虚函数&#xff0c;虚表&#xff0c;虚表指针&#xff0c;动态绑定&#xff0c;多态 一、概述 为了实现 C 的多态&#xff0c;C 使用了一种动态绑定的技术。这个技术的核心是虚函数表&#xff08;下文简称虚表&#xff09;。本文介绍虚函数表是如何实现动态…

C++ 虚函数表解析

C 虚函数表解析 陈皓 http://blog.csdn.net/haoel 前言 C中的虚函数的作用主要是实现了多态的机制。关于多态&#xff0c;简而言之就是用父类型别的指针指向其子类的实例&#xff0c;然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”&#x…

虚函数表详解

关键词&#xff1a;虚函数&#xff0c;虚表&#xff0c;虚表指针&#xff0c;动态绑定&#xff0c;多态 一、概述 为了实现C的多态&#xff0c;C使用了一种动态绑定的技术。这个技术的核心是虚函数表&#xff08;下文简称虚表&#xff09;。本文介绍虚函数表是如何实现动态绑定…

虚函数及虚函数表

虚函数及虚函数表 各个类对象共享类的虚函数表&#xff0c;每个类对象有个虚函数指针vptr&#xff0c;虚函数指针vptr指向虚函数表&#xff08;对于只有一个虚函数表的情况&#xff09;。 虚函数 简单的说&#xff0c;每一个含有虚函数&#xff08;无论是其本身的&#xff0…

简述虚函数表

前段时间我在博客中简单地说了下C的虚函数&#xff0c;所谓虚函数&#xff0c;就是C实现多态性的方法。那么编译器是如何识别虚函数的呢&#xff1f;据百度百科描述&#xff0c;C并未规定用何种方法实现虚函数&#xff0c;但是大部分编译器厂商都选择使用虚函数表这种方法&…

虚函数表的问题

虚函数表&#xff1a; 多态是由虚函数实现的&#xff0c;而虚函数主要是通过虚函数表&#xff08;V-Table&#xff09;来实现的。 如果一个类中包含虚函数&#xff08;virtual修饰的函数&#xff09;&#xff0c;那么这个类就会包含一张虚函数表&#xff0c;虚函数表存储的每…

虚函数表详解及其应用场景

目录 概述1. 虚函数表概述2. 虚函数表的实现原理2.1. 虚函数的声明和定义2.2. 虚函数表的创建和初始化2.3. 虚函数调用的过程 3. 虚函数表的应用场景3.1. 多态性3.2. 基类指针和引用的使用3.3. 动态绑定3.4. 接口定义 结论 概述 在面向对象编程中&#xff0c;虚函数表&#xf…

C++ 面试必问:深入理解虚函数表

点击蓝字 关注我们 深入理解C 虚函数表 C中的虚函数的作用主要是实现了多态的机制。关于多态&#xff0c;简而言之就是用父类型别的指针指向其子类的实例&#xff0c;然后通过父类的指针调用实际子类的成员函数。 Derive d; Base1 *b1 &d; Base2 *b2 &d; Base3 *b3 …

C++ 虚函数表

C类在内存中的存储方式 C 内存分为 5 个区域&#xff1a; 堆 heap &#xff1a;由 new 分配的内存块&#xff0c;其释放编译器不去管&#xff0c;由程序员自己控制。如果程序员没有释放掉&#xff0c;在程序结束时系统会自动回收。涉及的问题&#xff1a;“缓冲区溢出”、“内…

ThinkPHP3.2.2获取数据列getField()优化

getField()是一个常用方法&#xff0c;我习惯用来获取带key的数组&#xff0c;方便数据整合。 使用第1个参数&#xff0c;传入一个字段名&#xff0c;获取某一个数据值&#xff0c;返回满足条件的数据表中的该字段的第一行的值&#xff1a; $id M("User")->getFi…

java class getfield_java.lang.Class.getField()方法实例

全屏 java.lang.Class.getField() 返回一个Field对象&#xff0c;它反映此Class对象所表示的类或接口的指定公共成员字段。 name参数是一个字符串&#xff0c;指定所需字段的简单名称。 声明 以下是java.lang.Class.getField()方法的声明public Field getField(String name) th…

java getfield_Java 反射:通过 getField() 设置公共全局变量

Java 通过 getField() 操作公共全局变量 以前写 JavaWeb 项目启动初始化系统配置全局变量的代码&#xff0c;都是 variable Properties.getProperty(name) 这样一行一行代码的设置&#xff0c;变量少还好说&#xff0c;变量一多真的很磨叽。所以一直想通过 循环 简化代码&…

getField和getDeclaredField的区别

这两个方法都是用于获取字段 1.getField 只能获取public的&#xff0c;包括从父类继承来的字段。 2.getDeclaredField 可以获取本类所有的字段&#xff0c;包括private的&#xff0c;但是不能获取继承来的字段。 (注&#xff1a; 这里只能获取到private的字段&#xff0c;但并不…

vscode插件开发总结

一、关于vscode插件 相信大家对vscode应该都不陌生&#xff0c;VSCode是微软出的一款轻量级代码编辑器&#xff0c;免费而且功能强大&#xff0c;以功能强大、提示友好、不错的性能和颜值俘获了大量开发者的青睐&#xff0c;对JavaScript和NodeJS的支持非常好&#xff0c;自带…

2021-前端-VsCode插件

此乃吾习前端&#xff0c;VsCode之插件&#xff0c;个人所装&#xff0c;喜着自拿&#xff0c;不足之处还望海涵&#xff0c;多加批评。 1.Auto Close Tag——自动闭合尾部的标签 2.Atuo Rename Tag——修改 html 标签 自动帮你完成头部和尾部闭合标签的同步修改 3.Bracket…

关于VSCode插件的安装位置

VSCode的插件地址修改_上善若泪-CSDN博客_vscode插件位置文章目录1 data文件夹2 使用--extensions-dir命令3 使用mklink命令vscode编辑器强大的地方是可以使用各种各样的插件&#xff0c;但是插件默认的地方是在:C盘&#xff0c;让一些强迫症可能会受不了&#xff0c;非要迁移到…

vscode 插件-常用插件

VSCode常用插件(安装步骤同汉化) 1、*Auto Close Tag (自动闭合HTML/XML标签) 2、*Auto Rename Tag (自动帮你完成尾部闭合标签的同步修改&#xff0c;不过有些bug) 3、*Prettier(Prettier 是目前 Web 开发中最受欢迎的代码格式化程序) 安装Prettier -Code formatter这个插件…