虚拟函数和vtable如何实现?

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

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

这个vtable是否存在于所有的类,或者只有那些至less有一个虚函数的类?

抽象类是否至less有一个条目的函数指针是NULL?

单个虚拟function是否会减慢整个class级的速度? 或者只有对虚拟函数的调用? 如果虚拟函数被实际覆盖,速度会受到影响,或者只要虚拟函数没有效果,速度会受到影响。

虚拟function是如何在深层次上实现的?

从“C ++中的虚函数”

每当一个程序有一个虚函数声明,av – table是为类构造的。 v表由虚拟函数的地址组成,虚拟函数用于包含一个或多个虚拟函数的类。 包含虚拟函数的类的对象包含一个指向内存中虚拟表的基地址的虚拟指针。 每当有虚函数调用时,v表就被用来parsing函数地址。 包含一个或多个虚函数的类的对象包含一个虚拟指针,该虚指针在内存中对象的最开始处称为vptr。 因此,在这种情况下,对象的大小增加了指针的大小。 这个vptr包含了虚拟表的内存地址。 请注意,虚拟表是特定于类的,也就是说,只有一个类的虚拟表,而不pipe它包含的虚拟函数的数量。 该虚拟表又包含该类的一个或多个虚拟function的基地址。 在对象上调用虚函数时,该对象的vptr将为该类在内存中提供虚表的基地址。 该表用于parsing函数调用,因为它包含该类的所有虚函数的地址。 这是在虚拟函数调用期间如何解决dynamic绑定。

虚拟表可以修改,甚至可以直接在运行时访问?

普遍而言,我相信答案是“不”。 你可以做一些内存寻找vtable,但你仍然不知道函数签名看起来像什么称呼它。 任何你想用这个能力(语言支持)都可以实现的东西,不需要直接访问vtable或者在运行时修改它。 还要注意,C ++语言规范没有规定vtables是必需的 – 但是这是大多数编译器实现虚函数的方式。

vtable是否存在于所有的对象,或只有那些至less有一个虚函数的对象?

相信这里的答案是“这取决于实现”,因为规范并不需要vtables。 然而,在实践中,如果一个类至less有一个虚拟函数,我相信所有的现代编译器只会创build一个虚拟表。 与vtable相关联的空间开销和与调用虚函数vs非虚函数相关的时间开销。

抽象类是否至less有一个条目的函数指针是NULL?

答案是语言规范没有说明,所以取决于实现。 如果没有定义(通常不是这样),则调用纯虚函数会导致未定义的行为(ISO / IEC 14882:2003 10.4-2)。 在实践中,它确实在函数的vtable中分配了一个槽,但是没有给它分配一个地址。 这使得vtable不完整,需要派生类来实现函数并完成vtable。 有些实现只是在vtable条目中放置一个NULL指针; 其他的实现把一个指向一个虚拟方法的指针做一些与断言类似的东西。

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

是否有一个单一的虚拟function减慢了整个class级或只有对虚拟function的调用?

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

相信只有在课堂上虚拟的function才能体验与调用虚拟function和非虚拟function相关的时间性能。 这个class的空间开销是有的。 请注意,如果有一个vtable,每个只有一个,而不是每个对象一个。

如果虚拟函数被实际覆盖,速度会受到影响吗?或者只要虚拟函数没有影响,速度会受到影响吗?

与调用基本虚函数相比,我不相信被覆盖的虚函数的执行时间减less了。 但是,与为派生类和基类定义另一个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

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

不能移植,但如果你不介意肮脏的技巧,当然!

警告 :这项技术不build议儿童, 969岁以下的成年人或Alpha Centauri的小毛茸茸生物使用。 副作用可能包括从鼻子飞出的恶魔, Yog-Sothoth作为所有后续代码评论的必要批准者的突然出现,或者追溯添加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! 

replace所有实例的方法(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位的过程中,它可能会失败。

单个虚拟function是否会减慢整个class级的速度?

或者只有对虚拟函数的调用? 如果虚拟函数被实际覆盖,速度会受到影响,或者只要虚拟函数没有效果,速度会受到影响。

虚拟函数的速度减慢了整个类的范围,因为在处理这样一个类的对象时,还有一个数据项需要被初始化,复制。 对于有六七成左右的class级来说,差距应该是可以忽略的。 对于只包含一个char成员的类,或者根本没有成员,差别可能是显着的。

除此之外,重要的是要注意,不是每个虚函数的调用都是虚函数调用。 如果你有一个已知types的对象,编译器可以发出一个正常的函数调用代码,甚至可以内联这个函数。 只有当你通过一个可能指向基类的对象或某个派生类的对象的指针或引用来进行多态调用时,才需要vtable间接寻址,并在性能方面付出代价。

 struct Foo { virtual ~Foo(); virtual int a() { return 1; } }; struct Bar: public Foo { int a() { return 2; } }; void f(Foo& arg) { Foo x; xa(); // non-virtual: always calls Foo::a() Bar y; ya(); // 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 za(); // non-virtual Foo::a, since z is a Foo, even if arg was not } 

无论函数是否被覆盖,硬件所采取的步骤基本相同。 从对象中读取vtable的地址,从适当的槽中检索的函数指针,以及由指针调用的函数。 就实际业绩而言,分行预测可能会产生一些影响。 例如,如果大多数对象引用了给定虚拟函数的相同实现,那么分支预测器有可能在检索到指针之前正确地预测要调用哪个函数。 但是哪个函数是常用的函数并不重要:它可能是大多数委托给未覆盖的基本情况的对象,也可能是属于同一个子类的大多数对象,因此委托给相同的被覆盖的情况。

他们如何在深层次上实施?

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

父类Foo

 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 } 

函数f执行虚函数调用

 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只是一个内存中的静态块,大部分都包含函数指针。 多态类的每个对象都将指向与其dynamictypes对应的vtable。 这也使得RTTI和虚拟函数之间的连接变得更加清晰:通过查看它指向的vtable,你可以检查一个类是什么types。 以上在许多方面被简化了,例如多重inheritance,但总的概念是合理的。

如果arg的types是Foo*并且采用了arg->vtable ,但实际上是Bartypes的对象,那么您仍然可以获得vtable的正确地址。 这是因为vtable始终是对象地址中的第一个元素,不pipe是在正确types的expression式中调用vtable还是base.vtable

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

这个答案已被纳入社区Wiki的答案

  • 抽象类是否至less有一个条目的函数指针是NULL?

答案是没有指定 – 如果没有定义(通常不是这样),调用纯虚函数会导致未定义的行为(ISO / IEC 14882:2003 10.4-2)。 有些实现只是在vtable条目中放置一个NULL指针; 其他的实现把一个指向一个虚拟方法的指针做一个类似于断言的东西。

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

您可以使用函数指针作为类的成员和静态函数作为实现,或者使用指向成员函数和成员函数的指针来实现C ++中的虚函数的function。 这两种方法之间只有符号优势…实际上,虚拟函数调用本身就是符号方便。 实际上,inheritance只是一个符号方便…它可以全部实现,而不需要使用inheritance的语言function。 🙂

下面是垃圾未经testing,可能是错误的代码,但希望展示的想法。

例如

 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; } ... }; 

通常使用一个VTable,一个指向函数的指针数组。

这里没有提到的所有这些答案是,在多重inheritance的情况下,基类都有虚方法。 inheritance类有多个指向vmt的指针。 结果是这样的对象的每个实例的大小更大。 大家都知道一个带虚拟方法的类对于vmt来说有4个字节的额外空间,但是在多重inheritance的情况下,对于每个具有虚拟方法的基类来说,这个时间就是指针的大小。

我会尽量简单:)

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

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

当多态类从另一个多态类派生时,我们可能有以下情况:

  • 派生类不添加新的虚拟函数也不覆盖任何。 在这种情况下,这个类与基类共享vtable。
  • 派生类添加和覆盖虚拟方法。 在这种情况下,它将获得自己的虚拟表,其中添加的虚拟函数的索引从最后一个派生的索引开始。
  • inheritance中的多个多态类。 在这种情况下,我们有第二个和下一个基地之间的索引转移和它在派生类中的索引

虚拟表可以修改,甚至可以直接在运行时访问?

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

这个vtable是否存在于所有的类,或者只有那些至less有一个虚函数的类?

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

抽象类是否至less有一个条目的函数指针是NULL?

这是一个可能的实现,而不是实践。 相反,通常会有一个函数打印“纯虚函数调用”,并执行abort() 。 如果您尝试在构造函数或析构函数中调用抽象方法,可能会发生这种情况。

单个虚拟function是否会减慢整个class级的速度? 或者只有对虚拟函数的调用? 如果虚拟函数被实际覆盖,速度会受到影响,或者只要虚拟函数没有效果,速度会受到影响。

减速仅取决于呼叫是作为直接呼叫还是作为虚拟呼叫解决。 没有别的事情。 🙂

如果你通过一个指针或对一个对象的引用来调用一个虚拟函数,那么它将总是作为虚拟调用来实现 – 因为编译器永远不会知道在运行时将会为这个指针分配什么types的对象,以及它是否是这个方法被覆盖的类。 只有在两种情况下,编译器才能将呼叫parsing为直接呼叫的虚拟function:

  • 如果通过一个值(一个返回值的函数的variables或结果)调用方法 – 在这种情况下,编译器毫不怀疑实际的对象类是什么,并且可以在编译时“硬解决”它。
  • 如果虚拟方法在类中被声明为final的,那么你可以通过它来指向它( 仅在C ++ 11中 )。 在这种情况下,编译器知道这个方法不能进一步覆盖,只能是这个类的方法。

请注意,虽然虚拟调用只有取消引用两个指针的开销。 使用RTTI(虽然只适用于多态类)比调用虚拟方法慢,你应该找两种方法来实现同样的事情。 例如,定义virtual bool HasHoof() { return false; } virtual bool HasHoof() { return false; }然后重写只有bool Horse::HasHoof() { return true; } bool Horse::HasHoof() { return true; }会为你提供调用if (anim->HasHoof()) ,比if(dynamic_cast<Horse*>(anim))要快。 这是因为在某些情况下, dynamic_cast必须遍历类层次结构,即使是recursion地查看是否可以从实际的指针types和期望的类types构buildpath。 虽然虚拟调用总是相同的 – 解引用两个指针。

除了这个问题,Burly的答案是正确的:

抽象类是否至less有一个条目的函数指针是NULL?

答案是根本没有为抽象类创build虚拟表。 没有必要,因为没有这些类的对象可以创build!

换句话说,如果我们有:

 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类有一个虚拟方法foo() ,而不是由D覆盖的虚拟方法bar()被覆盖,那么D的vtbl将有一个指向B的foo()和它自己的bar() 。 还没有为B创buildvtbl

非常可爱的概念certificate,我提前了一点(看inheritance的顺序事宜); 让我知道如果你的C ++的实现实际上拒绝它(我的版本的gcc只给出一个警告分配匿名结构,但这是一个错误),我很好奇。

CCPolite.h

 #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_constructor.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 

main.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!!!! 

注意,因为我从来没有分配我的假对象,没有必要做任何破坏; 析构函数会自动放在dynamic分配对象范围的最后,以回收对象文本本身和vtable指针的内存。