1.基础理论
为了实现虚函数,C ++使用一种称为虚拟表的特殊形式的后期绑定。该虚拟表是用于解决在动态/后期绑定方式的函数调用函数的查找表。虚拟表有时会使用其他名称,例如“vtable”,“虚函数表”,“虚方法表”或“调度表”。
虚拟表实际上非常简单,虽然用文字描述有点复杂。首先,每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表。该表只是编译器在编译时设置的静态数组。虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的派生函数。
其次,编译器还会添加一个隐藏指向基类的指针,我们称之为vptr。vptr在创建类实例时自动设置,以便指向该类的虚拟表。与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针。
因此,它使每个类对象的分配大一个指针的大小。这也意味着vptr由派生类继承,这很重要。
2.调用图
3. 代码展示:
#include <iostream>
#include <stdio.h>
using namespace std;
/**
*@ 函数指针
*/
typedef void(*Fun)();
/**
@基类
*/
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 << "DerivedClass::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指针访问virtual table,因为虚表中每个元素(虚函数指针)在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;
d.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;
}
4. 结果展示与分析
其过程为:首先程序识别出fun1()是个虚函数,其次程序使用pt->vptr来获取Derived的虚拟表。第三,它查找Derived虚拟表中调用哪个版本的fun1()。这里就可以发现调用的是Derived::fun1()。因此pt->fun1()被解析为Derived::fun1()!