我试图了解更多关于 vtables 和 vpointers 的内部工作原理,所以我决定尝试使用一些技巧直接访问 vtable。我创建了两个类,Base
每个Derv
都有两个virtual
函数(Derv
覆盖那些Base
)。
class Base
{
int x;
int y;
public:
Base(int x_, int y_) : x(x_), y(y_) {}
virtual void foo() { cout << "Base::foo(): x = " << x << '\n'; }
virtual void bar() { cout << "Base::bar(): y = " << y << '\n'; }
};
class Derv: public Base
{
int x;
int y;
public:
Derv(int x_, int y_) : Base(x_, y_), x(x_), y(y_) {}
virtual void foo() { cout << "Derived::foo(): x = " << x << '\n'; }
virtual void bar() { cout << "Derived::bar(): y = " << y << '\n'; }
};
现在,编译器为每个类添加一个 vtable 指针,占用内存中的前 4 个字节(32 位)。我通过将对象的地址转换为 a 来访问该指针size_t*
,因为该指针指向另一个大小为 的指针sizeof(size_t)
。现在可以通过索引 vpointer 并将结果转换为适当类型的函数指针来访问虚函数。我将这些步骤封装在一个函数中:
template <typename T>
void call(T *ptr, size_t num)
{
typedef void (*FunPtr)();
size_t *vptr = *reinterpret_cast<size_t**>(ptr);
FunPtr fun = reinterpret_cast<FunPtr>(vptr[num]);
//setThisPtr(ptr); added later, see below!
fun();
}
当以这种方式调用其中一个成员函数时,例如call(new Base(1, 2), 0)
调用 Base::foo(),很难预测会发生什么,因为它们是在没有this
-pointer 的情况下调用的。我通过添加一个小模板化函数解决了这个问题,知道 g++ 将this
-pointer 存储在ecx
寄存器中(但这迫使我使用编译-m32
器标志进行编译):
template <typename T>
void setThisPtr(T *ptr)
{
asm ( mov %0, %%ecx;" :: "r" (ptr) );
}
取消注释setThisPtr(ptr)
上面代码段中的行现在使它成为一个工作程序:
int main()
{
Base* base = new Base(1, 2);
Base* derv = new Derv(3, 4);
call(base, 0); // "Base::foo(): x = 1"
call(base, 1); // "Base::bar(): y = 2"
call(derv, 0); // "Derv::foo(): x = 3"
call(derv, 1); // "Derv::bar(): y = 4"
}
我决定分享这个,因为在编写这个小程序的过程中,我对 vtables 的工作原理有了更多的了解,它可能有助于其他人更好地理解这些材料。但是我还有一些问题:
1. 编译 64 位二进制文件时使用哪个寄存器(gcc 4.x)来存储 this 指针?我尝试了这里记录的所有 64 位寄存器:http: //developers.sun.com/solaris/articles/asmregs.html
2. this-pointer 何时/如何设置?我怀疑编译器会通过一个对象在每个函数调用上设置 this 指针,就像我刚刚做的那样。这是多态性实际工作的方式吗?(通过先设置 this 指针,然后从 vtable 调用虚函数?)。