如何实现虚函数和虚表?

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

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

Vtable 是存在于所有类中,还是只存在于那些至少有一个虚函数的类中?

抽象类是否至少有一个条目的函数指针只有一个空值?

使用单个虚函数会降低整个类的速度吗?还是只调用虚函数?如果虚函数真的被覆盖了,速度会受到影响吗? 或者只要它是虚函数,速度就不会受到影响。

67448 次浏览

通常使用 VTable,它是指向函数的指针数组。

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

如何在深层次上实现虚函数?

来自 “ C + + 中的虚函数”:

每当一个程序声明了一个虚函数,就会为这个类构造一个 v 表。V 表由包含一个或多个虚函数的类的虚函数的地址组成。包含虚函数的类的对象包含一个虚拟指针,该指针指向内存中虚表的基地址。每当有虚函数调用时,就使用 v 表来解析函数地址。包含一个或多个虚函数的类的对象在内存中对象的开头包含一个名为 vptr 的虚指针。因此,在这种情况下,对象的大小会随着指针的大小而增加。此 vptr 包含内存中虚表的基地址。请注意,虚表是特定于类的,也就是说,一个类只有一个虚表,而不管它包含多少个虚函数。这个虚表依次包含类的一个或多个虚函数的基地址。在对一个对象调用虚函数时,该对象的 vptr 为内存中的该类提供虚表的基地址。此表用于解析函数调用,因为它包含该类的所有虚函数的地址。这就是在虚函数调用期间解析动态绑定的方式。

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

一般来说,我认为答案是“不”。您可以对内存进行一些破坏以找到 vtable,但是您仍然不知道函数签名调用它是什么样子的。您希望通过这种能力(语言支持的)实现的任何事情都应该是可能的,而不需要直接访问 vtable 或在运行时修改它。还要注意,C + + 语言规范 没有指定需要 vtables-但是大多数编译器是这样实现虚函数的。

Vtable 是存在于所有对象中,还是只存在于那些至少具有一个虚函数的对象中?

这里的答案是“它取决于实现”,因为规范本身并不需要 vtables。然而,在实践中,我认为所有现代编译器只有在一个类至少有一个虚函数的情况下才会创建 vtable。与 vtable 相关的空间开销和与调用虚函数相关的时间开销与调用非虚函数相关的时间开销。

抽象类是否至少有一个条目的函数指针只有一个空值?

答案是它没有被语言规范指定,所以它取决于实现。如果没有定义纯虚函数(通常没有定义) ,调用纯虚函数会导致未定义行为(ISO/IEC 14882:200310.4-2)。实际上,它确实在 vtable 中为函数分配了一个槽,但是没有为它分配地址。这使得 vtable 不完整,需要派生类实现函数并完成 vtable。一些实现只是在 vtable 条目中放置一个 NULL 指针; 另一些实现放置一个指向虚方法的指针,该虚方法执行类似于断言的操作。

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

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

这已经超出了我的知识范围,所以如果我错了,谁来帮帮我!

只有类中的虚函数才会遇到与调用虚函数和非虚函数相关的时间性能损失。无论如何,类的空间开销都是存在的。注意,如果有 vtable,那么每个 同学们只有1个,而不是每个 对象只有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

这个问题的答案已经纳入了 社区维基解答

  • 抽象类是否至少有一个条目的函数指针只有一个空值?

答案是没有指定——如果没有定义纯虚函数(通常没有定义) ,调用纯虚函数会导致未定义行为(ISO/IEC 14882:200310.4-2)。一些实现只是在 vtable 条目中放置一个 NULL 指针; 另一些实现放置一个指向虚方法的指针,该虚方法执行类似于断言的操作。

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

Burly 的回答在这里是正确的,除了一个问题:

抽象类是否至少有一个条目的函数指针只有一个空值?

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

换句话说,如果我们有:

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。

作为对迈克下面评论的回应..。

如果我的描述中的 B 类有一个不被 D 重写的虚方法 Foo ()和一个被重写的虚方法 酒吧,那么 D 的 vtbl 将有一个指向 B 的 Foo ()和它自己的 酒吧的指针。仍然没有为 B 创建 vtbl。

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

不是便携式的,但如果你不介意肮脏的把戏,当然!

警告 : 这项技术不推荐给儿童、 969年龄以下的成年人或半人马座阿尔法星的小型毛茸茸动物使用。副作用可能包括 从你鼻子里飞出来的恶魔瑜伽索托斯突然出现在所有后续代码审查中作为必需的审批人,或者将 IHuman::PlayPiano()追溯添加到所有现有实例]

在我见过的大多数编译器中,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

后者很可能使病毒检查器和链接醒来并注意到,由于 mprotected 操作。在使用 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; }
...
};

使用单个虚函数会降低整个类的速度吗?

还是只调用虚函数?如果虚函数真的被覆盖了,速度会受到影响吗? 或者只要它是虚函数,速度就不会受到影响。

使用虚函数会降低整个类的速度,因为在处理这样一个类的对象时,必须初始化、复制... ... 另一个数据项。对于一个有六个左右成员的类来说,差别应该是可以忽略不计的。对于只包含单个 char成员或根本不包含成员的类,这种差异可能是显著的。

除此之外,重要的是要注意,并非每个对虚函数的调用都是虚函数调用。如果您有一个已知类型的对象,编译器可以发出正常函数调用的代码,甚至可以内联函数(如果它喜欢的话)。只有当您通过指针或引用(可能指向基类的对象或某个派生类的对象)进行多态调用时,才需要 vtable 间接并为其性能付费。

struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
Foo x; x.a(); // non-virtual: always calls Foo::a()
Bar y; y.a(); // non-virtual: always calls Bar::a()
arg.a();      // virtual: must dispatch via vtable
Foo z = arg;  // copy constructor Foo::Foo(const Foo&) will convert to Foo
z.a();        // non-virtual Foo::a, since z is a Foo, even if arg was not
}

无论函数是否被覆盖,硬件必须采取的步骤本质上是相同的。从对象读取 vtable 的地址,从适当的槽检索函数指针,以及通过指针调用的函数。就实际性能而言,分支预测可能会产生一些影响。例如,如果大多数对象都引用了给定虚函数的相同实现,那么即使在检索到指针之前,分支预测器也有可能正确地预测调用哪个函数。但是哪个函数是公共函数并不重要: 它可以是大多数委托给非重写基本情况的对象,或者大多数属于同一个子类的对象,因此委托给同一个重写情况。

它们是如何在深层次上实施的?

我喜欢使用 jheriko 的想法来使用模拟实现来演示这一点。但是我会使用 C 来实现类似于上面代码的东西,这样低级别的代码更容易看到。

家长级福

typedef struct Foo_t Foo;   // forward declaration
struct slotsFoo {           // list all virtual functions of Foo
const void *parentVtable; // (single) inheritance
void (*destructor)(Foo*); // virtual destructor Foo::~Foo
int (*a)(Foo*);           // virtual function Foo::a
};
struct Foo_t {                      // class Foo
const struct slotsFoo* vtable;    // each instance points to vtable
};
void destructFoo(Foo* self) { }     // Foo::~Foo
int aFoo(Foo* self) { return 1; }   // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
0,                                // no parent class
destructFoo,
aFoo
};
void constructFoo(Foo* self) {      // Foo::Foo()
self->vtable = &vtableFoo;        // object points to class vtable
}
void copyConstructFoo(Foo* self,
Foo* other) { // Foo::Foo(const Foo&)
self->vtable = &vtableFoo;        // don't copy from other!
}

派生类 Bar

typedef struct Bar_t {              // class Bar
Foo base;                         // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { }     // Bar::~Bar
int aBar(Bar* self) { return 2; }   // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
&vtableFoo,                       // can dynamic_cast to Foo
(void(*)(Foo*)) destructBar,      // must cast type to avoid errors
(int(*)(Foo*)) aBar
};
void constructBar(Bar* self) {      // Bar::Bar()
self->base.vtable = &vtableBar;   // point to Bar vtable
}

执行虚函数调用

void f(Foo* arg) {                  // same functionality as above
Foo x; constructFoo(&x); aFoo(&x);
Bar y; constructBar(&y); aBar(&y);
arg->vtable->a(arg);              // virtual function call
Foo z; copyConstructFoo(&z, arg);
aFoo(&z);
destructFoo(&z);
destructBar(&y);
destructFoo(&x);
}

可以看到,vtable 只是内存中的一个静态块,主要包含函数指针。多态类的每个对象都将指向与其动态类型相对应的 vtable。这也使 RTTI 和虚函数之间的连接更加清晰: 您可以通过查看类所指向的 vtable 来检查类的类型。以上内容在很多方面都被简化了,例如多重继承,但总体概念是合理的。

如果 argFoo*类型的,并且取 arg->vtable,但实际上是 Bar类型的对象,那么仍然可以得到 vtable的正确地址。这是因为 vtable始终是对象地址的第一个元素,无论在类型正确的表达式中是称为 vtable还是 base.vtable

在所有这些答案中都没有提到的是,在多重继承的情况下,基类都有虚方法。继承类有多个指向 vmt 的指针。 结果是这样一个对象的每个实例的大小都更大。 每个人都知道,使用虚方法的类对于 vmt 来说有4个字节的额外多重继承,但是对于每个基类来说,虚方法乘以4。4是指针的大小。

我会尽量让事情变得简单:)

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

这是一个带有函数指针的数组,函数是特定虚函数的实现。此数组中的索引表示为类定义的虚函数的特定索引。这包括纯虚函数。

当一个多态类派生自另一个多态类时,我们可能会遇到以下情况:

  • 派生类不添加新的虚函数,也不重写任何。在本例中,这个类与基类共享 vtable。
  • 派生类添加和重写虚方法。在这种情况下,它得到自己的 vtable,其中添加的虚函数的索引从上一个派生函数后面开始。
  • 继承中的多个多态类。在这种情况下,我们在第二个和第二个基和它在派生类中的索引之间有一个索引位移

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

不是标准的方式-没有 API 来访问它们。编译器可能有一些扩展或私有 API 来访问它们,但那可能只是一个扩展。

Vtable 是存在于所有类中,还是只存在于那些至少有一个虚函数的类中?

只有那些至少有一个虚函数(甚至是析构函数)或至少派生一个具有 vtable 的类(“是多态的”)的类。

抽象类是否至少有一个条目的函数指针只有一个空值?

这是一个可能的实现,但是并没有经过实践。相反,通常有一个函数打印类似于“纯虚函数调用”的内容并执行 abort()。如果您尝试在构造函数或析构函数中调用抽象方法,则可能会发生对该方法的调用。

使用单个虚函数会降低整个类的速度吗?还是只调用虚函数?如果虚函数真的被覆盖了,速度会受到影响吗? 或者只要它是虚函数,速度就不会受到影响。

减速只取决于调用是作为直接调用解析还是作为虚拟调用解析。其他的都不重要。:)

如果你通过一个指针或者一个对象的引用来调用一个虚函数,那么它总是会被实现为一个虚函数——因为编译器永远不会知道在运行时什么样的对象会被赋值给这个指针,以及它是否属于一个方法被重写的类。只有在两种情况下,编译器才能将对虚函数的调用解析为直接调用:

  • 如果你通过一个值(一个变量或者返回值的函数的结果)来调用这个方法——在这种情况下,编译器不会怀疑对象的实际类是什么,并且可以在编译时“硬解析”它。
  • 如果虚方法在类中声明为 final,那么您可以通过对该类的指针或引用来调用它(只有在 C + + 11中)。在这种情况下,编译器知道这个方法不能进行任何进一步的重写,它只能是来自这个类的方法。

不过请注意,虚拟调用只有取消引用两个指针的开销。使用 RTTI (尽管只对多态类可用)比调用虚方法要慢,如果您发现用两种方法实现同样的事情的话。例如,定义 virtual bool HasHoof() { return false; }然后仅重写为 bool Horse::HasHoof() { return true; }将使您能够调用比尝试 if(dynamic_cast<Horse*>(anim))更快的 if (anim->HasHoof())。这是因为在某些情况下,dynamic_cast必须遍历类层次结构,甚至递归地查看是否可以从实际的指针类型和所需的类类型构建路径。虽然虚拟调用总是相同的-取消引用两个指针。

非常可爱的概念证明(看看继承顺序是否重要) ; 让我知道如果你的 C + + 实现实际上拒绝它(我的版本的 gcc 只提供了一个警告,指定匿名结构,但这是一个错误) ,我很好奇。

译者:

#ifndef CCPOLITE_H
#define CCPOLITE_H


/* the vtable or interface */
typedef struct {
void (*Greet)(void *);
void (*Thank)(void *);
} ICCPolite;


/**
* the actual "object" literal as C++ sees it; public variables be here too
* all CPolite objects use(are instances of) this struct's structure.
*/
typedef struct {
ICCPolite *vtbl;
} CPolite;


#endif /* CCPOLITE_H */

CCPolite _ structor.h :

/**
* unconventionally include me after defining OBJECT_NAME to automate
* static(allocation-less) construction.
*
* note: I assume CPOLITE_H is included; since if I use anonymous structs
*     for each object, they become incompatible and cause compile time errors
*     when trying to do stuff like assign, or pass functions.
*     this is similar to how you can't pass void * to windows functions that
*         take handles; these handles use anonymous structs to make
*         HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
*         require a cast.
*/
#ifndef OBJECT_NAME
#error CCPolite> constructor requires object name.
#endif


CPolite OBJECT_NAME = {
&CCPolite_Vtbl
};


/* ensure no global scope pollution */
#undef OBJECT_NAME

C :

#include <stdio.h>
#include "CCPolite.h"


// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
virtual void Greet() = 0;
};


// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
virtual void Thank() = 0;
};


// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};


// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
void Greet()
{
puts("hello!");
}


void Thank()
{
puts("thank you!");
}
};


// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
void Greet()
{
puts("hi!");
}


void Thank()
{
puts("ty!");
}
};


// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
puts("HI I AM C!!!!");
}


// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
puts("THANK YOU, I AM C!!");
}


// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
CCPolite_Thank,
CCPolite_Greet
};


CPolite CCPoliteObj = {
&CCPolite_Vtbl
};


int main(int argc, char **argv)
{
puts("\npart 1");
CPolite1 o1;
o1.Greet();
o1.Thank();


puts("\npart 2");
CPolite2 o2;
o2.Greet();
o2.Thank();


puts("\npart 3");
CPolite1 *not1 = (CPolite1 *)&o2;
CPolite2 *not2 = (CPolite2 *)&o1;
not1->Greet();
not1->Thank();
not2->Greet();
not2->Thank();


puts("\npart 4");
CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
fake->Thank();
fake->Greet();


puts("\npart 5");
CPolite2 *fake2 = (CPolite2 *)fake;
fake2->Thank();
fake2->Greet();


puts("\npart 6");
#define OBJECT_NAME fake3
#include "CCPolite_constructor.h"
fake = (CPolite1 *)&fake3;
fake->Thank();
fake->Greet();


puts("\npart 7");
#define OBJECT_NAME fake4
#include "CCPolite_constructor.h"
fake2 = (CPolite2 *)&fake4;
fake2->Thank();
fake2->Greet();


return 0;
}

产出:

part 1
hello!
thank you!


part 2
hi!
ty!


part 3
ty!
hi!
thank you!
hello!


part 4
HI I AM C!!!!
THANK YOU, I AM C!!


part 5
THANK YOU, I AM C!!
HI I AM C!!!!


part 6
HI I AM C!!!!
THANK YOU, I AM C!!


part 7
THANK YOU, I AM C!!
HI I AM C!!!!

注意,因为我从来没有分配我的假对象,所以没有必要进行任何销毁; 销毁函数会自动放在动态分配的对象的作用域的末尾,以回收对象文字本身和 vtable 指针的内存。

下面是现代 C + + 中虚表的 可以逃跑手动实现。它具有定义良好的语义,没有 Hacks,也没有 void*

注意: .*->**->是不同的操作符。成员函数指针的工作方式不同。

#include <iostream>
#include <vector>
#include <memory>


struct vtable; // forward declare, we need just name


class animal
{
public:
const std::string& get_name() const { return name; }


// these will be abstract
bool has_tail() const;
bool has_wings() const;
void sound() const;


protected: // we do not want animals to be created directly
animal(const vtable* vtable_ptr, std::string name)
: vtable_ptr(vtable_ptr), name(std::move(name)) { }


private:
friend vtable; // just in case for non-public methods


const vtable* const vtable_ptr;
std::string name;
};


class cat : public animal
{
public:
cat(std::string name);


// functions to bind dynamically
bool has_tail() const { return true; }
bool has_wings() const { return false; }
void sound() const
{
std::cout << get_name() << " does meow\n";
}
};


class dog : public animal
{
public:
dog(std::string name);


// functions to bind dynamically
bool has_tail() const { return true; }
bool has_wings() const { return false; }
void sound() const
{
std::cout << get_name() << " does whoof\n";
}
};


class parrot : public animal
{
public:
parrot(std::string name);


// functions to bind dynamically
bool has_tail() const { return false; }
bool has_wings() const { return true; }
void sound() const
{
std::cout << get_name() << " does crrra\n";
}
};


// now the magic - pointers to member functions!
struct vtable
{
bool (animal::* const has_tail)() const;
bool (animal::* const has_wings)() const;
void (animal::* const sound)() const;


// constructor
vtable (
bool (animal::* const has_tail)() const,
bool (animal::* const has_wings)() const,
void (animal::* const sound)() const
) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};


// global vtable objects
const vtable vtable_cat(
static_cast<bool (animal::*)() const>(&cat::has_tail),
static_cast<bool (animal::*)() const>(&cat::has_wings),
static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
static_cast<bool (animal::*)() const>(&dog::has_tail),
static_cast<bool (animal::*)() const>(&dog::has_wings),
static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
static_cast<bool (animal::*)() const>(&parrot::has_tail),
static_cast<bool (animal::*)() const>(&parrot::has_wings),
static_cast<void (animal::*)() const>(&parrot::sound));


// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }


// implement dynamic dispatch
bool animal::has_tail() const
{
return (this->*(vtable_ptr->has_tail))();
}


bool animal::has_wings() const
{
return (this->*(vtable_ptr->has_wings))();
}


void animal::sound() const
{
(this->*(vtable_ptr->sound))();
}


int main()
{
std::vector<std::unique_ptr<animal>> animals;
animals.push_back(std::make_unique<cat>("grumpy"));
animals.push_back(std::make_unique<cat>("nyan"));
animals.push_back(std::make_unique<dog>("doge"));
animals.push_back(std::make_unique<parrot>("party"));


for (const auto& a : animals)
a->sound();


// note: destructors are not dispatched virtually
}