我们都知道C++中的虚函数是什么,但它们是如何在深层次实现的?
可以在运行时修改甚至直接访问vtable吗?
vtable是否适用于所有类,或仅适用于至少具有一个虚函数的类?
抽象类对于至少一个条目的函数指针只有一个NULL吗?
有一个虚拟函数会减慢整个班级的速度吗?或者只调用虚拟函数?如果虚拟功能实际被覆盖了,速度是否会受到影响,或者只要它是虚拟的,它就没有效果.
来自"C++中的虚函数"
每当程序声明了虚函数时,就会为该类构造av-table.v表包含包含一个或多个虚函数的类的虚函数的地址.包含虚函数的类的对象包含一个指向内存中虚拟表的基址的虚拟指针.每当有虚函数调用时,v表用于解析函数地址.包含一个或多个虚函数的类的对象在内存中对象的最开头包含一个名为vptr的虚拟指针.因此,在这种情况下,对象的大小增加了指针的大小.此vptr包含内存中虚拟表的基址.请注意,虚拟表是特定于类的,即 一个类只有一个虚拟表,而不管它包含的虚函数的数量.该虚拟表又包含该类的一个或多个虚函数的基地址.在对象上调用虚函数时,该对象的vptr为内存中的该类提供虚拟表的基址.此表用于解析函数调用,因为它包含该类的所有虚函数的地址.这是在虚函数调用期间解析动态绑定的方式.该对象的vptr为内存中的该类提供虚拟表的基址.此表用于解析函数调用,因为它包含该类的所有虚函数的地址.这是在虚函数调用期间解析动态绑定的方式.该对象的vptr为内存中的该类提供虚拟表的基址.此表用于解析函数调用,因为它包含该类的所有虚函数的地址.这是在虚函数调用期间解析动态绑定的方式.
一般来说,我认为答案是"不".你可以做一些内存修改来找到vtable,但你仍然不知道函数签名是什么样的.如果没有直接访问vtable或在运行时修改它,应该可以使用此功能(语言支持)实现的任何功能.另请注意,C++语言规范没有指定需要vtable - 但这是大多数编译器实现虚函数的方式.
我相信这里的答案是"它取决于实现",因为规范首先不需要vtable.但是,在实践中,我相信如果一个类至少有一个虚函数,所有现代编译器都只创建一个vtable.存在与vtable相关联的空间开销以及与调用虚拟功能与非虚拟功能相关联的时间开销.
答案是它没有通过语言规范指定,因此它取决于实现.如果未定义(通常不是),则调用纯虚函数会导致未定义的行为(ISO/IEC 14882:2003 10.4-2).实际上,它确实在vtable中为函数分配了一个槽,但是没有为它分配地址.这使得vtable不完整,这需要派生类来实现该函数并完成vtable.有些实现只是在vtable条目中放置一个NULL指针; 其他实现将指针指向一个虚拟方法,该方法执行类似于断言的操作.
请注意,抽象类可以定义纯虚函数的实现,但该函数只能使用qualified-id语法调用(即,在方法名称中完全指定类,类似于从中调用基类方法)派生类).这样做是为了提供易于使用的默认实现,同时仍然要求派生类提供覆盖.
这是我的知识的边缘,所以如果我错了,有人请帮助我!
我相信只有类中虚拟的函数才会遇到与调用虚函数和非虚函数相关的时间性能.这个类的空间开销是两种方式.请注意,如果存在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吗?
不便携,但如果你不介意肮脏的技巧,当然!
警告:不建议儿童,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!
替换所有实例的方法(monkeypatching class)
这个有点棘手,因为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位的过程中,它可能会失败.
或者只调用虚拟函数?如果虚拟功能实际被覆盖了,速度是否会受到影响,或者只要它是虚拟的,它就没有效果.
只要在处理这样一个类的对象时,必须初始化,复制另一个数据项,虚函数就会减慢整个类的速度.对于有六个左右成员的班级,差异应该是可以忽略的.对于只包含单个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! }
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来检查类的类型.以上是以多种方式简化的,例如多重继承,但一般概念是合理的.
如果arg
是类型Foo*
而你采取arg->vtable
,但实际上是类型的对象Bar
,那么你仍然得到正确的地址vtable
.这是因为vtable
它始终是对象地址的第一个元素,无论它是被调用vtable
还是base.vtable
在正确类型的表达式中.