虚拟函数和vtable如何实现?

时间:2020-03-06 14:24:32  来源:igfitidea点击:

我们都知道C ++中有什么虚函数,但是如何在深层次上实现它们呢?

是否可以在运行时修改vtable甚至直接访问vtable?

该vtable是否适用于所有类,或者仅适用于至少具有一个虚函数的类?

对于至少一个条目的函数指针,抽象类是否仅具有NULL?

拥有一个虚拟函数会减慢整个课堂的学习速度吗?还是仅调用虚拟函数?如果虚拟函数是否被实际覆盖,速度是否会受到影响?或者,只要它是虚拟的,对速度没有影响。

解决方案

通常与VTable一起,是指向函数的指针数组。

每个对象都有一个vtable指针,该指针指向成员函数的数组。

虚拟功能如何在深层次上实现?

摘自" C ++中的虚函数"

只要程序声明了虚函数,就会为该类构造一个v表。 v表由包含一个或者多个虚拟功能的类的虚拟功能的地址组成。包含虚拟函数的类的对象包含一个虚拟指针,该指针指向内存中虚拟表的基地址。每当有虚拟函数调用时,v表都会用于解析到函数地址。包含一个或者多个虚拟函数的类的对象在内存中对象的最开始处包含一个称为vptr的虚拟指针。因此,在这种情况下,对象的大小增加了指针的大小。此vptr包含内存中虚拟表的基地址。注意,虚拟表是特定于类的,即,一个类只有一个虚拟表,而与它包含的虚函数的数量无关。该虚拟表又包含该类的一个或者多个虚拟函数的基地址。在对象上调用虚拟函数时,该对象的vptr为内存中的该类提供虚拟表的基地址。该表用于解析函数调用,因为它包含该类的所有虚函数的地址。这是在虚拟函数调用期间解决动态绑定的方式。

是否可以在运行时修改vtable甚至直接访问vtable?

一般来说,我相信答案是"否"。我们可以进行一些内存修改来找到vtable,但是我们仍然不知道函数签名的外观。无需直接访问vtable或者在运行时对其进行修改,就可以使用该功能(语言支持)实现任何目标。还要注意,C ++语言规范没有指定需要vtable,但是大多数编译器都是通过这种方式实现虚函数的。

该vtable是否适用于所有对象,或者仅适用于具有至少一个虚拟功能的对象?

我相信这里的答案是"取决于实现",因为该规范首先不需要vtables。但是,实际上,我认为所有现代编译器仅在一个类至少具有1个虚函数的情况下才创建vtable。与vtable相关联的空间开销与与调用虚拟函数与非虚拟函数相关联的时间开销。

对于至少一个条目的函数指针,抽象类是否仅具有NULL?

答案是语言规范未指定,因此取决于实现。如果未定义(通常不是),则调用纯虚函数会导致未定义的行为(ISO / IEC 14882:2003 10.4-2)。实际上,它确实在vtable中为该功能分配了一个插槽,但并未为其分配地址。这使vtable不完整,这要求派生类实现该功能并完成vtable。有些实现只是在vtable条目中放置了一个NULL指针。其他实现将指针指向执行与断言类似的操作的虚拟方法。

请注意,抽象类可以为纯虚函数定义实现,但是该函数只能使用限定ID语法来调用(即,在方法名称中完全指定该类,类似于从a调用基类方法派生类)。这样做是为了提供易于使用的默认实现,同时仍然需要派生类提供替代。

拥有单个虚拟函数会降低整个类的速度,还是只会降低对虚拟函数的调用?

这是我所学的知识,所以如果我错了,请有人在这里帮助我!

我相信只有类中的虚拟函数会遇到与调用虚拟函数和非虚拟函数有关的时间性能损失。该类的空间开销是任意一种。请注意,如果有一个vtable,则每个类只有1个,而不是每个对象一个。

如果虚拟函数实际上被覆盖或者不被覆盖,速度是否会受到影响?或者,只要它是虚拟的,对速度没有影响吗?

我不相信被覆盖的虚函数的执行时间与调用基本虚函数相比会减少。但是,与为派生类和基类定义另一个vtable关联的类存在额外的空间开销。

其他资源:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx(通过返回机器)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/cxx-abi/abi.html#vtable

该答案已合并到社区Wiki答案中

  • 对于至少一个条目的函数指针,抽象类是否仅具有NULL?

答案是未指定(如果未定义),则调用纯虚函数会导致未定义的行为(通常不是)(ISO / IEC 14882:2003 10.4-2)。有些实现只是在vtable条目中放置了一个NULL指针。其他实现将指针指向执行与断言类似的操作的虚拟方法。

请注意,抽象类可以定义纯虚函数的实现,但是只能使用限定ID语法来调用该函数(即,在方法名称中完全指定该类,类似于从调用基类方法派生类)。这样做是为了提供易于使用的默认实现,同时仍然需要派生类提供替代。

除以下问题外,Burly的答案在这里是正确的:

对于至少一个条目的函数指针,抽象类是否仅具有NULL?

答案是根本没有为抽象类创建虚拟表。不需要,因为无法创建这些类的对象!

换句话说,如果我们有:

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

通过pB访问的vtbl指针将是类D的vtbl。这正是实现多态的方式。即,如何通过pB访问D方法。 B类不需要vtbl。

为了回应Mike在下方的评论...

如果我描述中的B类具有未被D覆盖的虚拟方法foo()和未被覆盖的虚拟方法bar(),则D的vtbl将具有指向B的foo()及其自身的bar()的指针。 。仍然没有为B创建vtbl。

  • 是否可以在运行时修改vtable甚至直接访问vtable?

不是可移植的,但是,如果我们不介意卑鄙的把戏,请确保!

WARNING: This technique is not recommended for use by children, adults under the age of 969, or small furry creatures from Alpha Centauri. Side effects may include demons which fly out of your nose, the abrupt appearence of Yog-Sothoth as a required approver on all subsequent code reviews, or the retroactive addition of IHuman::PlayPiano() to all existing instances]

在大多数编译器中,我看到的是vtbl *是对象的前4个字节,而vtbl内容只是那里的成员指针数组(通常按它们声明的顺序,基类是第一个)。当然,还有其他可能的布局,但这就是我通常观察到的。

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

现在要拉一些恶作剧...

在运行时更改类:

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

为所有实例替换方法(monkeypatching类)

这有点棘手,因为vtbl本身可能位于只读内存中。

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

由于进行了mprotect操作,后者很可能会使病毒检查程序和链接唤醒并引起注意。在使用NX位的过程中,它很可能会失败。

我们可以使用函数指针作为类的成员并使用静态函数作为实现,或者使用指向成员函数和实现的成员函数的指针来重新创建C ++中虚拟函数的功能。两种方法之间仅存在符号上的优势...实际上,虚拟函数调用本身只是一种符号上的便利。实际上,继承只是一种符号上的方便...可以在不使用语言功能进行继承的情况下实现所有继承。 :)

下面是未经测试的废话,可能是错误的代码,但希望可以证明这个想法。

例如

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};