如果实现具有虚函数的类使用了虚表,那么没有虚函数的类如何实现?

6
特别是,无论如何都必须有一种函数指针存在,对吧?

这个问题的背景是什么?你是在寻找特定的问题还是只是想了解一般知识? - Greg Whitfield
9个回答

15

我认为短语“具有虚函数的类使用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,当它明确时会这样做。

(是的,没错:即使您看不到它,方法中的成员访问也始终通过指针间接进行)


一个结构体对于基类和成员拥有公共访问权限。成员并不一定是“按顺序存储”的,非 POD 类的布局可能与实际“声明顺序”不同。最后,非常挑剔地说,静态成员确实具有隐式对象参数,但该参数被丢弃。 - Richard Corden
静态成员确实没有隐式对象参数。抱抱? - curiousguy

12

非虚成员函数在本质上只是一种语法糖,它们几乎像普通函数一样,但具有访问检查和隐式对象参数。

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'
}

3
虚方法用于实现多态。使用 virtual 修饰符将该方法放在 VMT 中,以进行后期绑定,然后在运行时决定从哪个类执行哪个方法。
如果该方法不是虚拟的,则在编译时确定将从哪个类实例执行它。
函数指针主要用于回调。

1
如果一个带有虚函数的类使用了虚表来实现,那么一个没有虚函数的类就不会使用虚表来实现。
虚表包含了调度一个方法所需的函数指针。如果该方法不是虚函数,调用将直接转到类的已知类型,不需要间接寻址。

1
对于非虚方法,编译器可以生成普通的函数调用(例如,调用特定地址并将this指针作为参数传递的CALL),甚至可以内联它。对于虚函数,编译器通常在编译时不知道要调用代码的地址,因此它会生成在运行时查找vtable中地址然后调用方法的代码。当然,即使对于虚函数,编译器有时也可以在编译时正确地解析正确的代码(例如,在没有指针/引用的情况下调用局部变量上的方法)。

1

(我从原始答案中提取了这一部分,以便可以单独进行批评。它更加简洁并且直接回答了你的问题,因此在某种程度上它是一个更好的答案)

不,没有函数指针;相反,编译器将问题内部处理

编译器使用对象指针调用全局函数,而不是调用对象内部的指向函数

为什么?因为这通常更加高效。间接调用是昂贵的指令。


0

在运行时期间,函数指针不需要更改。


0

分支语句直接生成到方法的编译代码中;就像如果您有未在类中定义的函数,分支语句也会直接生成到它们。


0
编译器/链接器直接链接将被调用的方法。不需要vtable间接寻址。顺便问一下,这与“堆栈 vs. 堆”有什么关系?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接