当前位置:  开发笔记 > 编程语言 > 正文

如何实现虚拟功能和vtable?

如何解决《如何实现虚拟功能和vtable?》经验,为你挑选了3个好方法。

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

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

vtable是否适用于所有类,或仅适用于至少具有一个虚函数的类?

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

有一个虚拟函数会减慢整个班级的速度吗?或者只调用虚拟函数?如果虚拟功能实际被覆盖了,速度是否会受到影响,或者只要它是虚拟的,它就没有效果.



1> Zach Burling..:

虚拟功能如何在深层次实施?

来自"C++中的虚函数"

每当程序声明了虚函数时,就会为该类构造av-table.v表包含包含一个或多个虚函数的类的虚函数的地址.包含虚函数的类的对象包含一个指向内存中虚拟表的基址的虚拟指针.每当有虚函数调用时,v表用于解析函数地址.包含一个或多个虚函数的类的对象在内存中对象的最开头包含一个名为vptr的虚拟指针.因此,在这种情况下,对象的大小增加了指针的大小.此vptr包含内存中虚拟表的基址.请注意,虚拟表是特定于类的,即 一个类只有一个虚拟表,而不管它包含的虚函数的数量.该虚拟表又包含该类的一个或多个虚函数的基地址.在对象上调用虚函数时,该对象的vptr为内存中的该类提供虚拟表的基址.此表用于解析函数调用,因为它包含该类的所有虚函数的地址.这是在虚函数调用期间解析动态绑定的方式.该对象的vptr为内存中的该类提供虚拟表的基址.此表用于解析函数调用,因为它包含该类的所有虚函数的地址.这是在虚函数调用期间解析动态绑定的方式.该对象的vptr为内存中的该类提供虚拟表的基址.此表用于解析函数调用,因为它包含该类的所有虚函数的地址.这是在虚函数调用期间解析动态绑定的方式.

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

一般来说,我认为答案是"不".你可以做一些内存修改来找到vtable,但你仍然不知道函数签名是什么样的.如果没有直接访问vtable或在运行时修改它,应该可以使用此功能(语言支持)实现的任何功能.另请注意,C++语言规范没有指定需要vtable - 但这是大多数编译器实现虚函数的方式.

vtable是否适用于所有对象,或仅存在至少具有一个虚函数的对象?

相信这里的答案是"它取决于实现",因为规范首先不需要vtable.但是,在实践中,我相信如果一个类至少有一个虚函数,所有现代编译器都只创建一个vtable.存在与vtable相关联的空间开销以及与调用虚拟功能与非虚拟功能相关联的时间开销.

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

答案是它没有通过语言规范指定,因此它取决于实现.如果未定义(通常不是),则调用纯虚函数会导致未定义的行为(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查找.对于dtor尤其如此,必须在相同的堆栈范围内调用dtor.
编译器将不必要的vtable指针放在不需要它的对象中,这与Stroustrup的C ++哲学不符。规则是除非您要求,否则您不会获得C中没有的开销,并且编译器破坏它是不礼貌的。
我同意对于任何在没有虚拟函数存在的情况下认真对待自己使用vtable的编译器来说都是愚蠢的。但是,我觉得有必要指出,据我所知,C ++标准不是/ require /它,因此请在使用前警告它。
常见实现:每个对象都有一个指向vtable的指针; 该类拥有该表.构造魔法只包括在基本ctor完成后更新派生的ctor中的vtable指针.

2> puetzk..:

可以在运行时修改甚至直接访问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位的过程中,它可能会失败.


嗯.这得到了赏金感到不祥.我希望这并不意味着@Mobilewits认为这样的诡计实际上是个好主意......

3> MvG..:

有一个虚拟函数会减慢整个班级的速度吗?

或者只调用虚拟函数?如果虚拟功能实际被覆盖了,速度是否会受到影响,或者只要它是虚拟的,它就没有效果.

只要在处理这样一个类的对象时,必须初始化,复制另一个数据项,虚函数就会减慢整个类的速度.对于有六个左右成员的班级,差异应该是可以忽略的.对于只包含单个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来实现类似于上面代码的东西,因此更容易看到低级别.

父类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!
}

派生类吧

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只是内存中的一个静态块,主要包含函数指针.多态类的每个对象都将指向与其动态类型对应的vtable.这也使得RTTI和虚函数之间的连接更加清晰:您可以通过查看它指向的vtable来检查类的类型.以上是以多种方式简化的,例如多重继承,但一般概念是合理的.

如果arg是类型Foo*而你采取arg->vtable,但实际上是类型的对象Bar,那么你仍然得到正确的地址vtable.这是因为vtable它始终是对象地址的第一个元素,无论它是被调用vtable还是base.vtable在正确类型的表达式中.

推荐阅读
手机用户2402851335
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有