我知道这里有很多关于vtables的问题,但我仍然有点困惑。
只有在我们有一个指向基类的指针来解析派生类的哪个虚函数来调用时才使用vtable吗?
在下面的示例中,在案例1中,vtable是在运行时使用的,即使Tiger对象不是在堆/免费存储上动态创建的吗?
在第2种情况下,使用vtable,即使编译器在编译时知道我们指向Tiger对象。
案例3怎么样?
提前致谢。
#include <iostream>
using namespace std;
class Animal // base class
{
public:
virtual void makeNoise() {cout<<" "<<endl;}
};
class Tiger: public Animal
{
public:
void makeNoise() {cout<<"Tiger Noise"<<endl;}
};
class Elephant: public Animal
{
public:
void makeNoise() {cout<<"Elephant Noise"<<endl;}
};
int main()
{
//case 1
Tiger t1;
Animal* aptr = &t1;
aptr->makeNoise(); // vtables used?
//case 2
Tiger t2;
Tiger* tptr = &t2; //vtables used ?
tptr->makeNoise();
//case 3
Elephant e1; //vtables used ?
e1.makeNoise();
}
答案 0 :(得分:4)
特定编译器是使用虚函数表还是完全不同的机制来实现动态虚函数调度,取决于编译器的内部实现。如果您想获得特定编译器行为的答案,请参阅该编译器的文档和/或源代码。
C ++语言本身定义了虚函数调用必须如何工作,并将其留给编译器来实现。
标准要求的是根据调用函数的对象的动态类型,将虚函数的调用分派给最终的覆盖器。在您的代码中,动态类型t1
和t2
为Tiger
,动态类型e1
为Elephant
。
是的,大多数(如果不是全部)编译器使用虚函数表来实现虚函数调用。是的,任何体面的编译器都应该最大限度地尝试在编译时解析动态调度(如果能够这样做),并在可能的情况下用直接调用替换virtual-table-usage(这是编译器的实现质量问题) )。
您的示例中的哪些调用将被静态分派取决于编译器的优化器的“激进”(或“智能”,如果您愿意)。
我会说每个理智的编译器都应该通过e1
静态调度调用,即使禁用了优化。在那里调用动态调度机制将是一个完全不必要的 pessimisation 。
对于通过aptr
和tptr
的调用,它取决于编译器的优化器的静态分析器是否能够消除aptr
和tptr
,将其替换为使用他们指向的实际对象(因为该信息在编译时是可用的)。一个体面的优化者应该能够做到并且静态地发送所有3个呼叫。
要确定编译器如何处理调用,请检查生成的程序集。
答案 1 :(得分:0)
正如其他评论所说,vtables的使用由编译器处理,编译器可能会尝试优化其访问权限,只要生成的输出是预期的输出即可。
但是,我们可能会将vtable视为包含虚方法地址的表。每次调用在父类中声明为“虚拟”的方法时,都应该在运行时检查vtable,以便知道跳转的具体地址。
这是程序员所期望的行为,尽管特定的机制可能更棘手,如果编译器可以在编译时确定地址,甚至可能根本不依赖于查询vtable。
因此,在所有这些情况下,编译器可能足够聪明,可以在编译时设置地址。但是你应该在“最坏的情况”中依赖它,在每种情况下都会对vtable进行访问,因为你正在调用虚方法 - 这是预期的行为 - 并让编译器进行它认为的优化它必须这样做。
正如对案例1中所述内容的澄清一样,vtable访问与对象是否已在堆中或堆栈中分配无关。这些是完全不同的概念。