希望大家没有被这么拗口的标题吓到:). 本文源于论坛上的这个问题:
struct base
{
void foo()
{
cout << "base::foo" << endl;
}
virtual void bar()
{
cout << "base::bar" << endl;
}
};
struct derived : base
{
virtual void bar()
{
cout << "derived::bar" << endl;
}
};
int __cdecl _tmain( int argc, _TCHAR* argv[] )
{
void (base::*pfn)() = &base::bar;
derived d;
d.base::bar(); // 1
(d.base::*pfn)(); // 2 想实现和上一行一样的输出, 但是编译失败
return 0;
}
很明显, 标 2 的那一行是想使用派生类对象通过成员函数指针调用基类虚函数, 以实现与标 1 的那行相同的输出, 但却无法编译通过. 这是个语法错误, 因为 ::
运算符的优先级高于 .
, 所以那一行会先计算 base::*pfn
, 然而 *pfn
并不是 base
的成员, 故有错误时很自然的. 那么是否可以通过修改那条语句来达到目的呢? 分析了成员函数指针的实现后, 我发现, 至少在 vc7.1 和 vc8 上, 这是不可能的. 由于 vc 的标准兼容性已经非常高,所以我怀疑 c++ 标准就不支持这种调用, 但没有证实.
在上面的例子中, pfn
指向的是虚函数 bar
, 但它也必须能指向普通成员函数 foo
. 当指向 foo
时, 它保存的就是 foo
的入口地址; 然而当指向 bar
时, 直接保存这个地址就不行了,因为对 base
和 derived
来说, 这个地址并不相同. vc 对此的解决方法是由编译器加入了一系列的内部函数 vcall
. 一个类中的每个虚函数都有一个唯一与之对应的 vcall
函数, 但在不同类之间, 这些 vcall
实际上是公用的. pfn
指向的就是这些 vcall
中的一个.
我们知道, 调用成员函数时要传递 this
指针. 一般情况下, 它是通过 ecx
寄存器传递的, 所以 vcall
的实现如下所示:
mov eax, dword ptr[ecx];
jmp dword ptr [eax+xx]
第一句中, 由于 ecx
是 this
指针, 而一般 vfptr
是类的第一个成员, 所以它是把 vfptr
, 也就是 vtable
的地址存到了 eax
中. 第二句里面的 xx
, 在 32 位计算机上, 是 4 的整数倍, 所以这一句的意思是: 跳转到 vtable
的第 xx/4
项所指的地址上, 这个地址就是最终要调用的函数的入口.
明白了虚成员函数指针的实现, 就可以看那种调用为什么不能实现了. 让我们用反证法, 如果它能实现, 为讨论方便就假设标 2 的那一句是正确的吧, 在进行调用时, 编译器首先要保证传递的是指向 d
的 this
指针, 然后还要保证 this
所指向的 vfptr
所指的是 base
的 vtable
. 编译器能做到吗? 当然能, 它可以在调用前偷偷修改 vfptr
使其指向 base
的 vtable
, 并在调用返回后再把它恢复过来, 但想想这样做在多线程环境中的后果吧. 所以编译器是不可能这样做的, 也就是说"使用派生类对象通过成员函数指针调用基类虚函数是不可能的".
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于