【C++快速上手】八、C++虚函数的vptr与vtable学习笔记
【摘要】
一、基础理论
为了实现虚函数,C ++使用一种称为虚拟表的特殊形式的后期绑定。该虚拟表是用于解决在动态/后期绑定方式的函数调用函数的查找表。虚拟表有时会使用其他名称,例如“vtable”,“虚函数表”,...
一、基础理论
为了实现虚函数,C ++使用一种称为虚拟表的特殊形式的后期绑定。该虚拟表是用于解决在动态/后期绑定方式的函数调用函数的查找表。虚拟表有时会使用其他名称,例如“vtable
”,“虚函数表”,“虚方法表”或“调度表”。
虚拟表实际上非常简单,虽然用文字描述有点复杂。
- 首先,每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表(
vtable
)。该表只是编译器在编译时设置的静态数组。虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的派生函数。 - 其次,编译器还会添加一个隐藏指向基类的指针,我们称之为
vptr
。vptr
在创建类实例时自动设置,以便指向该类的虚拟表。与this
指针不同,this
指针实际上是编译器用来解析自引用的函数参数,vptr
是一个真正的指针。
因此,它使每个类对象的分配大一个指针的大小。这也意味着vptr
由派生类继承,这很重要。
二、实现与内部结构
下面来看自动操纵vptr
来获取地址与调用虚函数!开始看代码之前,为了方便理解,这里给出调用图:
#include <iostream>
#include <stdio.h>
using namespace std;
/**
* @brief 函数指针
*/
typedef void (*Fun)();
/**
* @brief 基类
*/
class Base
{
public:
Base(){};
virtual void fun1()
{
cout << "Base::fun1()" << endl;
}
virtual void fun2()
{
cout << "Base::fun2()" << endl;
}
virtual void fun3(){}
~Base(){};
};
/**
* @brief 派生类
*/
class Derived: public Base
{
public:
Derived(){};
void fun1()
{
cout << "Derived::fun1()" << endl;
}
void fun2()
{
cout << "Derived::fun2()" << endl;
}
~Derived(){};
};
/**
* @brief 获取vptr地址与func地址,vptr指向的是一块内存,这块内存存放的是虚函数地址,这块内存就是我们所说的虚函数表
*
* @param obj
* @param offset
*
* @return
*/
Fun getAddr(void* obj,unsigned int offset)
{
cout<<"======================="<<endl;
void* vptr_addr = (void *)*(unsigned long *)obj; //64位操作系统,占8字节,通过*(unsigned long *)obj取出前8字节,即vptr指针
printf("vptr_addr:%p\n",vptr_addr);
/**
* @brief 通过vptr指针访问虚函数表,因为虚函数表中每个元素(虚函数指针)在64位编译器下是8个字节,因此通过*(unsigned long *)vptr_addr取出前8字节,
* 后面加上偏移量就是每个函数的地址!
*/
void* func_addr = (void *)*((unsigned long *)vptr_addr+offset);
printf("func_addr:%p\n",func_addr);
return (Fun)func_addr;
}
int main(void)
{
Base ptr; // 基类实例
Derived d; // 派生类实例
Base *pt = new Derived(); // 基类指针指向派生类实例
Base &pp = ptr; // 基类引用指向基类实例
Base &p = d; // 基类引用指向派生类实例
cout<<"基类对象直接调用"<<endl;
ptr.fun1();
cout<<"基类对象调用基类实例"<<endl;
pp.fun1();
cout<<"基类指针指向派生类实例并调用虚函数"<<endl;
pt->fun1();
cout<<"基类引用指向派生类实例并调用虚函数"<<endl;
p.fun1();
// 手动查找vptr 和 vtable
Fun f1 = getAddr(pt, 0);
(*f1)();
Fun f2 = getAddr(pt, 1);
(*f2)();
delete pt;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
输出结果如下:
基类对象直接调用
Base::fun1()
基类对象调用基类实例
Base::fun1()
基类指针指向派生类实例并调用虚函数
Derived::fun1()
基类引用指向派生类实例并调用虚函数
Derived::fun1()
=======================
vptr_addr:00000000004099e0
func_addr:0000000000407e10
Derived::fun1()
=======================
vptr_addr:00000000004099e0
func_addr:0000000000000000
Derived::fun2()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
运行程序有如下警告(两个):
warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]
- 1
定位代码问题在:
...
void* vptr_addr = (void *)*(unsigned long *)obj;
...
void* func_addr = (void *)*((unsigned long *)vptr_addr+offset);
...
- 1
- 2
- 3
- 4
- 5
打印少东西:
- win10下,最后输出的时候可能不会打印
Derived::fun2()
原因何在?
总结
C++的动态多态性是通过虚函数来实现的。简单的说,通过virtual
函数,指向子类的基类指针可以调用子类的函数。例如,上述通过基类指针指向派生类实例,并调用虚函数,将上述代码简化为:
Base *pt = new Derived(); // 基类指针指向派生类实例
cout<<"基类指针指向派生类实例并调用虚函数"<<endl;
pt->fun1();
- 1
- 2
- 3
其过程为:
- ①、首先,程序识别出
fun1()
是个虚函数; - ②、其次,程序使用
pt->vptr
来获取Derived
的虚拟表; - ③、最后,它查找
Derived
虚拟表中调用哪个版本的fun1()
。这里就可以发现调用的是Derived::fun1()
。因此pt->fun1()
被解析为Derived::fun1()
!
参考
- 深入浅出C++虚函数的vptr与vtable
- typedef函数指针用法
- 函数指针与指针函数
- C++ 虚函数表解析—陈皓改进版
- 【C++快速上手】十九、typedef函数指针 与 assert学习笔记
文章来源: recclay.blog.csdn.net,作者:ReCclay,版权归原作者所有,如需转载,请联系作者。
原文链接:recclay.blog.csdn.net/article/details/108056396
【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)