我认为短语“具有虚函数的类使用vtable实现”会让你产生误解。
这个短语让人觉得具有虚函数的类是用“方式A”实现的,而没有虚函数的类则是用“方式B”实现的。
事实上,具有虚函数的类不仅是作为类实现的,它们还有一个vtable。另一种看待此问题的方式是,“‘vtable’实现类的‘虚函数’部分”。
所有类(具有虚或非虚方法)都是结构体。C++中结构体和类的唯一区别是默认情况下,结构体成员是公共的,类成员是私有的。因此,在这里我将使用术语“类”来指代结构体和类。记住,它们几乎是同义词!
类(以及结构体)只是一个连续的内存块,其中每个成员按顺序存储。请注意,由于CPU架构原因,有时会在成员之间有间隙,因此该块可能比其部件的总和要大。
方法或“成员函数”是一种幻觉。实际上,没有所谓的“成员函数”。一个函数总是只是存储在某个内存位置的一系列机器代码指令。为了进行调用,处理器跳转到该内存位置并开始执行。你可以说所有的方法和函数都是“全局的”,任何反对这个观点的迹象都是编译器强制实施的方便幻觉。
显然,方法像是属于特定对象的,因此显然还有更多事情要做。为了将特定方法(函数)的特定调用与特定对象绑定,每个成员方法都有一个隐藏参数,它是指向相关对象的指针。成员方法“隐藏”在你的C++代码中,你不需要自己添加它,但它是非常真实的。当你这样说:
void CMyThingy::DoSomething(int arg);
{
// do something
}
编译器确实会这样做:它实际上将代码编译成计算机可以理解的语言。
void CMyThingy_DoSomething(CMyThingy* this, int arg)
{
/do something
}
最后,当你写下这段代码时:
myObj.doSomething(aValue);
编译器显示:
CMyThingy_DoSomething(&myObj, aValue);
不需要任何函数指针!编译器已经知道您正在调用哪个方法,因此直接调用它。
静态方法甚至更简单。它们没有this指针,因此它们的实现与您编写的完全相同。
就是这样!其余部分只是方便的语法糖:编译器知道一个方法属于哪个类,因此它确保不让您在未指定函数的情况下调用该方法。 它还将使用这些知识将myItem
转换为this->myItem
,当它明确时会这样做。
(是的,没错:即使您看不到它,方法中的成员访问也始终通过指针间接进行)
非虚成员函数在本质上只是一种语法糖,它们几乎像普通函数一样,但具有访问检查和隐式对象参数。
struct A
{
void foo ();
void bar () const;
};
基本上与以下相同:
struct A
{
};
void foo (A * this);
void bar (A const * this);
vtable是必需的,因为它可以确保我们针对特定对象实例调用正确的函数。例如,如果我们有:
struct A
{
virtual void foo ();
};
'foo'的实现可能近似于以下内容:
void foo (A * this) {
void (*realFoo)(A *) = lookupVtable (this->vtable, "foo");
(realFoo)(this); // Make the call to the most derived version of 'foo'
}
virtual
修饰符将该方法放在 VMT 中,以进行后期绑定,然后在运行时决定从哪个类执行哪个方法。(我从原始答案中提取了这一部分,以便可以单独进行批评。它更加简洁并且直接回答了你的问题,因此在某种程度上它是一个更好的答案)
不,没有函数指针;相反,编译器将问题内部处理。
编译器使用对象指针调用全局函数,而不是调用对象内部的指向函数。
为什么?因为这通常更加高效。间接调用是昂贵的指令。
在运行时期间,函数指针不需要更改。
分支语句直接生成到方法的编译代码中;就像如果您有未在类中定义的函数,分支语句也会直接生成到它们。