我们都知道 C++ 中的虚函数是什么,但是它们是如何在深层次上实现的呢?
可以在运行时修改甚至直接访问 vtable 吗?
vtable 是否存在于所有类,还是只存在于至少具有一个虚函数的类?
抽象类是否只是为至少一个条目的函数指针设置了一个 NULL 值?
拥有一个虚函数会减慢整个班级的速度吗?还是只调用虚拟函数?如果虚拟功能实际上是否被覆盖,速度是否会受到影响,或者只要它是虚拟的就没有影响。
我们都知道 C++ 中的虚函数是什么,但是它们是如何在深层次上实现的呢?
可以在运行时修改甚至直接访问 vtable 吗?
vtable 是否存在于所有类,还是只存在于至少具有一个虚函数的类?
抽象类是否只是为至少一个条目的函数指针设置了一个 NULL 值?
拥有一个虚函数会减慢整个班级的速度吗?还是只调用虚拟函数?如果虚拟功能实际上是否被覆盖,速度是否会受到影响,或者只要它是虚拟的就没有影响。
来自“C++ 中的虚拟函数”:
每当程序声明了虚函数时,都会为该类构造 av - table。v-table 由包含一个或多个虚函数的类的虚函数地址组成。包含虚函数的类的对象包含一个虚指针,它指向内存中虚表的基地址。每当有虚函数调用时,都会使用 v-table 解析到函数地址。包含一个或多个虚拟函数的类的对象在内存中对象的最开始包含一个称为 vptr 的虚拟指针。因此,在这种情况下,对象的大小会增加指针的大小。这个 vptr 包含内存中虚拟表的基地址。请注意,虚拟表是特定于类的,即 一个类只有一个虚表,与它包含的虚函数数量无关。该虚拟表又包含该类的一个或多个虚拟函数的基地址。在对象上调用虚函数时,该对象的 vptr 提供了该类在内存中的虚拟表的基地址。该表用于解析函数调用,因为它包含该类的所有虚函数的地址。这就是在虚函数调用期间解决动态绑定的方式。该对象的 vptr 提供了该类在内存中的虚拟表的基地址。该表用于解析函数调用,因为它包含该类的所有虚函数的地址。这就是在虚函数调用期间解决动态绑定的方式。该对象的 vptr 提供了该类在内存中的虚拟表的基地址。该表用于解析函数调用,因为它包含该类的所有虚函数的地址。这就是在虚函数调用期间解决动态绑定的方式。
总的来说,我相信答案是“不”。你可以做一些内存修改来找到 vtable,但你仍然不知道函数签名是什么样子的。您想通过这种能力(语言支持)实现的任何事情都应该可以在不直接访问 vtable 或在运行时修改它的情况下实现。另请注意,C++ 语言规范没有指定 vtable 是必需的 - 但是大多数编译器都是这样实现虚函数的。
我相信这里的答案是“它取决于实现”,因为规范首先不需要 vtables。然而,在实践中,我相信所有现代编译器只有在一个类至少有 1 个虚函数时才会创建一个 vtable。存在与 vtable 相关的空间开销和与调用虚拟函数与非虚拟函数相关的时间开销。
答案是语言规范未指定它,因此它取决于实现。如果未定义(通常未定义),则调用纯虚函数会导致未定义的行为(ISO/IEC 14882:2003 10.4-2)。实际上,它确实在 vtable 中为函数分配了一个槽,但没有为其分配地址。这使得 vtable 不完整,需要派生类来实现功能并完成 vtable。一些实现只是在 vtable 条目中放置一个 NULL 指针;其他实现会放置一个指向虚拟方法的指针,该方法执行类似于断言的操作。
请注意,抽象类可以定义纯虚函数的实现,但该函数只能使用限定 ID 语法调用(即,在方法名称中完全指定类,类似于从派生类)。这样做是为了提供易于使用的默认实现,同时仍需要派生类提供覆盖。
这是我知识的边缘,所以如果我错了,请有人在这里帮助我!
我相信只有类中的虚拟函数才会经历与调用虚拟函数与非虚拟函数相关的时间性能损失。无论哪种方式,课程的空间开销都是存在的。请注意,如果有 vtable,则每个class只有 1 个,而不是每个object一个。
与调用基本虚函数相比,我不相信被覆盖的虚函数的执行时间会减少。但是,与为派生类和基类定义另一个 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 一个类)
这有点棘手,因为 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
在正确类型的表达式中。
通常带有一个 VTable,一个指向函数的指针数组。
这是现代 C++ 中虚拟表的可运行手动实现。它具有明确定义的语义,没有 hack 并且没有void*
.
注意:.*
and是与and->*
不同的运算符。成员函数指针的工作方式不同。*
->
#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
}
此答案已纳入社区 Wiki 答案
答案是它是未指定的 - 如果未定义(通常未定义)调用纯虚函数会导致未定义的行为(ISO/IEC 14882:2003 10.4-2)。一些实现只是在 vtable 条目中放置一个 NULL 指针;其他实现会放置一个指向虚拟方法的指针,该方法执行类似于断言的操作。
请注意,抽象类可以定义纯虚函数的实现,但该函数只能使用限定 ID 语法调用(即,在方法名称中完全指定类,类似于从派生类)。这样做是为了提供易于使用的默认实现,同时仍需要派生类提供覆盖。
您可以在 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; }
...
};
我会尽量让它简单:)
这是一个带有函数指针的数组,函数是特定虚函数的实现。此数组中的索引表示为类定义的虚函数的特定索引。这包括纯虚函数。
当一个多态类派生自另一个多态类时,我们可能有以下几种情况:
不是标准方式 - 没有 API 可以访问它们。编译器可能有一些扩展或私有 API 来访问它们,但这可能只是一个扩展。
只有那些具有至少一个虚函数(甚至是析构函数)或派生至少一个具有其 vtable 的类(“是多态的”)。
这是一种可能的实现,但没有实践。相反,通常有一个函数可以打印类似“调用的纯虚函数”并执行abort()
. 如果您尝试在构造函数或析构函数中调用抽象方法,则可能会调用它。
减速仅取决于呼叫是作为直接呼叫还是虚拟呼叫解决。其他都不重要。:)
如果你通过一个指针或一个对象的引用来调用一个虚函数,那么它总是会被实现为虚调用——因为编译器永远无法知道在运行时会给这个指针分配什么样的对象,以及它是否属于是否覆盖此方法的类。只有在两种情况下,编译器才能将对虚函数的调用解析为直接调用:
final
您有一个指针或引用通过该类调用它(仅在 C++11 中)。在这种情况下,编译器知道这个方法不能再被重写,它只能是这个类的方法。请注意,尽管虚拟调用仅具有取消引用两个指针的开销。使用 RTTI(尽管仅适用于多态类)比调用虚拟方法要慢,如果您找到一种以两种方式实现相同事物的案例。例如,virtual bool HasHoof() { return false; }
仅定义然后覆盖 asbool Horse::HasHoof() { return true; }
将为您提供if (anim->HasHoof())
比尝试更快的调用能力if(dynamic_cast<Horse*>(anim))
。这是因为dynamic_cast
在某些情况下必须遍历类层次结构,甚至递归地查看是否可以从实际指针类型和所需的类类型构建路径。虽然虚拟调用始终相同 - 取消引用两个指针。
每个对象都有一个指向成员函数数组的 vtable 指针。
在所有这些答案中都没有提到的是,在多重继承的情况下,基类都具有虚拟方法。继承类有多个指向 vmt 的指针。结果是这样一个对象的每个实例的大小都更大。每个人都知道一个具有虚方法的类有 4 个额外的 vmt 字节,但在多重继承的情况下,它是每个具有虚方法的基类乘以 4。4 是指针的大小。
除了问题之外,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 类有一个未被 D 覆盖的虚拟方法foo()和一个被覆盖的虚拟方法bar(),那么 D 的 vtbl 将有一个指向 B 的foo()和它自己的bar()的指针. 仍然没有为 B 创建 vtbl。
我早些时候做了非常可爱的概念证明(看看继承顺序是否重要);让我知道你的 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
主.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 指针的内存。