Fantacity

Stand Alone Complex

C++虚函数的实现探究——vptr和vtable

C++区别的C的一大特性就是C++是面向对象的,面向对象有着三大特性:继承性,封装性和多态性。其中C++的动态多态性是通过虚函数来实现的。简单的说,通过virtual函数,指向子类的基类指针可以调用子类的函数。

下面先来看一段代码:

#include <iostream>
using namespace std;
class BaseClass
{
public:
BaseClass() = default;
virtual void fun()
{
cout << "call fun in BaseClass" << endl;
}
virtual void fun2()
{
cout << "call fun2 in BaseClass" << endl;
}
virtual ~BaseClass() = default;
};
class DerivedClass: public BaseClass
{
public:
DerivedClass() = default;
virtual void fun()
{
cout << "call fun in DerivedClass" << endl;
}
virtual void fun2()
{
cout << "call fun2 in DerivedClass" << endl;
}
virtual ~DerivedClass() = default;
};
void *getp(void *p)
{
return (void*)*(unsigned long *)p; // 64位系统
}
typedef void (*fun)();
fun getfun(void* obj, unsigned int off)
{
void *vptr = getp(obj);
unsigned char *p = (unsigned char *)vptr;
p += sizeof(void*) * off;
return (fun)getp(p);
}
int main(void)
{
// 多态性
BaseClass *b = new DerivedClass();
b->fun();
delete b;
return 0;
}

运行结果:
BaseClass::fun()
DerivedClass::fun()

程序仅通过基类的指针,却调用了子类的函数。其实编译器在编译的时候使用了延迟绑定技术(late binding)。 通过virtual关键字创建虚函数能引发晚捆绑,编译器在幕后完成了实现晚捆绑的必要机制。它对每个包含虚函数的类创建一个表(称为VTABLE),用于放置虚函数的地址。在每个包含虚函数的类中,编译器秘密地放置了一个称之为vpointer(缩写为VPTR)的指针,指向这个对象的VTABLE。所以无论这个对象包含一个或是多少虚函数,编译器都只放置一个VPTR即可。VPTR由编译器在构造函数中秘密地插入的代码来完成初始化,指向相应的VTABLE,这样对象就“知道”自己是什么类型了。 VPTR都在对象的相同位置,常常是对象的开头。这样,编译器可以容易地找到对象的VTABLE并获取函数体的地址。如下图所示:
vptr and vtable
我们可以做一个实验,如果定义一个只有一个虚函数的空类,那么sizeof这个类的大小应该是一个VPTR的大小,即4(32位系统)或8(64位系统),而如果定义一个个类,这个类里面有2个虚函数,那么sizeof这个类的大小是多少呢?也是4或8,因为它只保存了一个VPTR,这个VPTR指向的VTABLE才保存了真正虚函数地址。代码如下:

#include <iostream>
using namespace std;
class A
{
virtual void fun()
{
cout << "A::fun()" << endl;
}
};
class B
{
virtual void fun()
{
cout << "B::fun()" << endl;
}
virtual void fun1()
{
cout << "B::fun1()" << endl;
}
};
int main(void)
{
cout << "sizeof A: " << sizeof(A) << endl;
cout << "sizeof B: " << sizeof(B) << endl;
return 0;
}

运行结果:
sizeof A: 8
sizeof B: 8

既然知道了以上原理,其实我们可以自己编写代码来通过查找vtable获取虚函数的实际地址。首先我们需要获取vptr这个指针,因为vptr通常都在对象的开头,假设现在我们有一个BaseClass的对象指针p,那么在64位系统上我们通过(void*)*(unsigned long *)p即可获得vptr指向的vtable的首地址。而vtable其实就是一个指针数组,按照虚函数在类中被声明的顺序排列。因此,如果我们需要获取虚函数表的第2个函数,只要把指针后移1个指针的长度就可以了。至于返回的指针,可以存放在typedef void (*fun)();中,代码如下:

#include <iostream>
using namespace std;
class BaseClass
{
public:
BaseClass() = default;
virtual void fun()
{
cout << "BaseClass::fun()" << endl;
}
virtual void fun2()
{
cout << "BaseClass::fun2()" << endl;
}
virtual ~BaseClass() = default;
};
class DerivedClass: public BaseClass
{
public:
DerivedClass() = default;
virtual void fun()
{
cout << "DerivedClass::fun()" << endl;
}
virtual void fun2()
{
cout << "DerivedClass::fun2()" << endl;
}
virtual ~DerivedClass() = default;
};
void *getp(void *p)
{
return (void*)*(unsigned long *)p; // 64位系统
}
typedef void (*fun)();
fun getfun(void* obj, unsigned int off)
{
void *vptr = getp(obj);
unsigned char *p = (unsigned char *)vptr;
p += sizeof(void*) * off;
return (fun)getp(p);
}
int main(void)
{
// 多态性
BaseClass a;
BaseClass *b = new DerivedClass();
// 值得一提的是,这里用引用也是可以实现多态性的,
// 详见深度探索c++对象模型一书
BaseClass &c = a;
a.fun();
b->fun();
c.fun();
// 手动查找vptr 和 vtable
fun f1 = getfun(b, 0);
fun f2 = getfun(b, 1);
(*f1)();
(*f2)();
delete b;
return 0;
}

输出如下:
BaseClass::fun()
DerivedClass::fun()
DerivedClass::fun()
DerivedClass::fun()
DerivedClass::fun2()
可见,我们成功通过虚函数表的方式,执行了虚函数,也知道了C++中的动态性原来是这样实现的。