我们都知道C ++中的虚函数是什么,但它们是如何在深层次实现的?
可以在运行时修改甚至直接访问vtable吗?
vtable是否适用于所有类,或仅存在至少具有一个虚函数的类?
抽象类对于至少一个条目的函数指针是否只有NULL?
有一个虚拟功能会减慢整个课程的速度吗?或者只调用虚拟函数?如果虚拟功能实际被覆盖,速度是否会受到影响,或者只要虚拟功能是虚拟的,它就会受到影响。
答案 0 :(得分:115)
每当程序声明了虚函数时,就会为该类构造一个v表。 v表包含包含一个或多个虚函数的类的虚函数的地址。包含虚函数的类的对象包含一个指向内存中虚拟表的基址的虚拟指针。每当有虚函数调用时,v表用于解析函数地址。包含一个或多个虚函数的类的对象在内存中对象的最开头包含一个名为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
答案 1 :(得分:28)
不便携,但如果你不介意肮脏的伎俩,当然!
警告:不建议儿童,969以下的成年人或Alpha Centauri的小型毛茸茸生物使用此技术。副作用可能包括demons which fly out of your nose,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 a 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位的过程中,它可能会失败。
答案 2 :(得分:17)
或者只调用虚拟函数?如果虚拟功能实际被覆盖,速度是否会受到影响,或者只要虚拟功能是虚拟的,它就会受到影响。
虚拟函数会减慢整个类的范围,因为在处理这样一个类的对象时,必须初始化,复制一个数据项。对于有六个左右成员的班级,差异应该是可以忽略的。对于只包含一个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
类型的对象,那么您仍然可以获得{{1}的正确地址1}}。这是因为vtable
始终是对象地址的第一个元素,无论它是在正确类型的表达式中被称为vtable
还是vtable
。
答案 3 :(得分:2)
通常使用VTable,指向函数的指针数组。
答案 4 :(得分:2)
此答案已合并到Community Wiki answer
中答案是它未指定 - 如果未定义(通常不是),调用纯虚函数会导致未定义的行为(ISO / IEC 14882:2003 10.4-2)。有些实现只是在vtable条目中放置一个NULL指针;其他实现将指针放置到一个虚拟方法,该方法执行与断言类似的操作。
请注意,抽象类可以为纯虚函数定义实现,但该函数只能使用qualified-id语法调用(即,在方法名称中完全指定类,类似于调用基类)派生类的方法)。这样做是为了提供易于使用的默认实现,同时仍然要求派生类提供覆盖。
答案 5 :(得分:2)
您可以使用函数指针作为类的成员和静态函数作为实现,或使用指向实现的成员函数和成员函数的指针,在C ++中重新创建虚函数的功能。这两种方法之间只有符号优势......事实上,虚函数调用本身只是一种符号方便。事实上,继承只是一种符号方便性......它可以在不使用语言特性进行继承的情况下实现。 :)
下面是未经测试的废话,可能是错误的代码,但希望能够证明这个想法。
e.g。
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; }
...
};
答案 6 :(得分:2)
我试着简化:)
这是一个带有指向函数的指针的数组,这些函数是特定虚函数的实现。此数组中的索引表示为类定义的虚函数的特定索引。这包括纯虚函数。
当多态类派生自另一个多态类时,我们可能会遇到以下情况:
不是标准方式 - 没有API可以访问它们。编译器可能有一些扩展或私有API来访问它们,但这可能只是一个扩展。
只有那些至少有一个虚函数(甚至是析构函数)或派生至少一个具有vtable的类("是多态的")。
这是一种可能的实施方式,而不是实践方式。相反,通常会有一个函数可以打印类似"纯虚函数,称为"并abort()
。如果您尝试在构造函数或析构函数中调用抽象方法,则可能会发生对此的调用。
减速仅取决于呼叫是作为直接呼叫还是作为虚拟呼叫解决。没有其他事情重要。 :)
如果通过指针或对象的引用来调用虚函数,那么它将始终实现为虚拟调用 - 因为编译器永远不会知道在运行时将为此指针分配哪种对象,以及是否是一个类被覆盖或不被覆盖的类。只有在两种情况下,编译器才能将对虚拟函数的调用解析为直接调用:
final
,则通过该方法调用它(仅在C ++ 11中)。在这种情况下,编译器知道此方法不能进行任何进一步的覆盖,它只能是此类中的方法。请注意,虚拟调用只有解除引用两个指针的开销。使用RTTI(尽管只适用于多态类)比调用虚方法慢,如果你找到一个案例来实现同样的两种方式。例如,定义virtual bool HasHoof() { return false; }
然后仅覆盖bool Horse::HasHoof() { return true; }
将使您能够调用if (anim->HasHoof())
,这比尝试if(dynamic_cast<Horse*>(anim))
更快。这是因为dynamic_cast
在某些情况下甚至必须递归地遍历类层次结构,以查看是否可以从实际指针类型和所需的类类型构建路径。虽然虚拟调用总是相同的 - 取消引用两个指针。
答案 7 :(得分:1)
每个对象都有一个vtable指针,指向一个成员函数数组。
答案 8 :(得分:1)
在所有这些答案中没有提到的是在多重继承的情况下,基类都有虚拟方法。继承类有多个指向vmt的指针。 结果是这样一个对象的每个实例的大小更大。 每个人都知道一个带有虚方法的类对于vmt有4个字节的额外值,但是在多重继承的情况下,每个基类的虚拟方法都是4。4是指针的大小。
答案 9 :(得分:1)
这是现代C ++中可运行的虚拟表手动实现。它具有定义明确的语义,没有黑客,也没有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
}
答案 10 :(得分:0)
除了问题之外,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。
如果我的描述中的B类有一个虚拟方法 foo()没有被D覆盖,而虚拟方法 bar()被覆盖,那么D's vtbl将有一个指向B的 foo()和它自己的 bar()的指针。仍然没有为B创建vtbl。
答案 11 :(得分:0)
非常可爱的概念证明我提前做了一点(看看遗产的顺序是否重要);让我知道你的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!!!!
请注意,因为我从不分配我的假对象,所以不需要做任何破坏;析构函数自动放在动态分配对象范围的末尾,以回收对象文字本身和vtable指针的内存。