首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >虚拟函数和vtable是如何实现的?

虚拟函数和vtable是如何实现的?
EN

Stack Overflow用户
提问于 2008-09-19 03:29:45
回答 12查看 63K关注 0票数 125

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

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

vtable是适用于所有类,还是只适用于那些至少有一个虚函数的类?

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

只有一个虚函数会减慢整个类的运行速度吗?还是只调用虚拟的函数?如果虚拟函数实际上被重写了,速度是否会受到影响,或者只要它是虚拟的,这就没有影响吗?

EN

回答 12

Stack Overflow用户

回答已采纳

发布于 2008-09-19 03:36:25

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

来自"Virtual Functions in C++"

只要程序声明了一个虚函数,就会为该类构造一个v表。V表由指向包含一个或多个虚拟函数的类的虚拟函数的地址组成。包含虚拟函数的类的对象包含一个虚拟指针,该指针指向内存中虚拟表的基地址。只要有虚函数调用,就会使用v-table来解析函数地址。包含一个或多个虚拟函数的类的对象在存储器中对象的最开始处包含称为vptr的虚拟指针。因此,在这种情况下,对象的大小增加了指针的大小。该vptr包含存储器中的虚拟表的基地址。请注意,虚拟表是特定于类的,也就是说,对于一个类,无论它包含多少虚拟函数,都只有一个虚拟表。该虚拟表又包含该类的一个或多个虚拟函数的基地址。在对象上调用虚拟函数时,该对象的vptr在内存中提供该类的虚拟表的基地址。此表用于解析函数调用,因为它包含该类的所有虚函数的地址。这就是在虚函数调用期间解析动态绑定的方式。

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

总的来说,我相信答案是否定的。你可以做一些内存损坏来找到vtable,但是你仍然不知道调用它的函数签名是什么样子的。您希望使用此功能(该语言支持的)实现的任何功能都应该可以实现,而无需直接访问vtable或在运行时修改它。还要注意,C++语言规范没有指定需要vtables然而,这是大多数编译器实现虚函数的方式。

vtable是适用于所有对象,还是只适用于那些至少具有一个虚拟函数的对象?

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

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

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

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

只有一个虚函数是会减慢整个类的速度,还是只会减慢对虚函数的调用?

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

我相信只有类中的虚函数才会经历与调用虚函数与非虚函数相关的时间性能影响。无论哪种方式,类的空间开销都是存在的。请注意,如果有一个vtable,则每个类只有一个,而不是每个对象一个。

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

我不相信与调用基本虚函数相比,被重写的虚函数的执行时间会减少。然而,对于与为派生类定义另一个vtable与为基类定义另一个vtable相关联的类,存在额外的空间开销。

其他资源:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (via way back machine)

http://en.wikipedia.org/wiki/Virtual_table

http://www.codesourcery.com/public/cxx-abi/abi.html#vtable

票数 133
EN

Stack Overflow用户

发布于 2008-09-19 13:39:42

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

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

WARNING:不建议儿童、969年龄以下的成年人或来自半人马座阿尔法星的小型毛皮动物使用此技术。副作用可能包括demons which fly out of your nose,突然出现Yog-Sothoth作为所有后续代码审查的必要批准者,或者将IHuman::PlayPiano()追溯添加到所有现有实例]

在我见过的大多数编译器中,vtbl *是对象的前4个字节,而vtbl内容只是其中的一个成员指针数组(通常按照它们被声明的顺序,基类的第一个)。当然,还有其他可能的布局,但这是我通常观察到的。

代码语言:javascript
运行
复制
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;

现在来搞点恶作剧吧。

在运行时更改类:

代码语言:javascript
运行
复制
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本身可能在只读内存中。

代码语言:javascript
运行
复制
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位的进程中,它很可能会失败。

票数 41
EN

Stack Overflow用户

发布于 2015-04-10 02:51:04

只有一个虚函数会减慢整个类的运行速度吗?

还是仅对虚拟函数的调用?如果虚拟函数实际上被重写了,速度是否会受到影响,或者只要它是虚拟的,这就没有影响吗?

使用虚函数会降低整个类的速度,因为必须初始化、复制、…在处理这种类的对象时。对于一个只有六个左右成员的类,差异应该是不可否认的。对于只包含一个char成员的类,或者根本不包含任何成员的类,差异可能是显著的。

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

代码语言:javascript
运行
复制
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来实现与上面的代码类似的东西,以便更容易看到低级。

父类Foo

代码语言:javascript
运行
复制
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!
}

派生类栏

代码语言:javascript
运行
复制
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执行虚函数调用

代码语言:javascript
运行
复制
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

票数 18
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/99297

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档