提问人:MainID 提问时间:2/18/2009 更新时间:4/20/2016 访问量:13666
虚拟类的每个对象都有一个指向 vtable 的指针吗?
Does every object of virtual class have a pointer to vtable?
答:
所有虚拟类通常都有一个 vtable,但 C++ 标准不需要它,存储方法取决于编译器。
Vtable 是每个类的实例,即,如果我有一个具有虚拟方法的类的 10 个对象,则所有 10 个对象之间只有一个 vtable 共享。
在这种情况下,所有 10 个对象都指向同一个 vtable。
评论
每个多态类型的对象都有一个指向 Vtable 的指针。
VTable 的存储位置取决于编译器。
所有具有虚拟方法的类都将具有一个由类的所有对象共享的 vtable。
每个对象实例都有一个指向该 vtable 的指针(这就是 vtable 的查找方式),通常称为 vptr。编译器隐式生成代码以初始化构造函数中的 vptr。
请注意,这些都不是由 C++ 语言强制执行的 - 如果需要,实现可以以其他方式处理虚拟调度。但是,这是我熟悉的每个编译器都使用的实现。Stan Lippman 的著作《Inside the C++ Object Model》很好地描述了它是如何工作的。
评论
不一定
几乎每个具有虚拟函数的对象都会有一个 v-table 指针。对于具有对象派生自的虚函数的每个类,不需要有一个 v-table 指针。
不过,在某些情况下,充分分析代码的新编译器可能能够消除 v-table。
例如,在一个简单的例子中:如果你只有一个抽象基类的具体实现,编译器知道它可以将虚拟调用更改为常规函数调用,因为每当调用虚拟函数时,它总是解析为完全相同的函数。
此外,如果只有几个不同的具体函数,编译器可以有效地更改调用站点,以便它使用“if”来选择要调用的正确具体函数。
因此,在这种情况下,不需要 v 表,对象最终可能没有 v-table。
评论
在家试试这个:
#include <iostream>
struct non_virtual {};
struct has_virtual { virtual void nop() {} };
struct has_virtual_d : public has_virtual { virtual void nop() {} };
int main(int argc, char* argv[])
{
std::cout << sizeof non_virtual << "\n"
<< sizeof has_virtual << "\n"
<< sizeof has_virtual_d << "\n";
}
评论
left as an assignment
VTable 是一个实现细节,语言定义中没有任何内容表明它存在。事实上,我已经阅读了有关实现虚拟函数的替代方法。
但是:所有常见的编译器(即我所知道的编译器)都使用 VTabels。
那么是的。任何具有虚拟方法或派生自具有虚拟方法的类(直接或间接)的类都将具有指向 VTable 的指针的对象。
您提出的所有其他问题将取决于编译器/硬件,这些问题没有真正的答案。
就像其他人说的,C++标准不强制要求虚拟方法表,但允许使用虚拟方法表。我已经使用 gcc 和这段代码完成了我的测试,这是最简单的场景之一:
class Base {
public:
virtual void bark() { }
int dont_do_ebo;
};
class Derived1 : public Base {
public:
virtual void bark() { }
int dont_do_ebo;
};
class Derived2 : public Base {
public:
virtual void smile() { }
int dont_do_ebo;
};
void use(Base* );
int main() {
Base * b = new Derived1;
use(b);
Base * b1 = new Derived2;
use(b1);
}
添加了 data-members 以防止编译器将基类的大小指定为零(称为 empty-base-class-optimization)。这是 GCC 选择的布局:(使用 -fdump-class-hierarchy 打印)
Vtable for Base
Base::_ZTV4Base: 3u entries
0 (int (*)(...))0
4 (int (*)(...))(& _ZTI4Base)
8 Base::bark
Class Base
size=8 align=4
base size=8 base align=4
Base (0xb7b578e8) 0
vptr=((& Base::_ZTV4Base) + 8u)
Vtable for Derived1
Derived1::_ZTV8Derived1: 3u entries
0 (int (*)(...))0
4 (int (*)(...))(& _ZTI8Derived1)
8 Derived1::bark
Class Derived1
size=12 align=4
base size=12 base align=4
Derived1 (0xb7ad6400) 0
vptr=((& Derived1::_ZTV8Derived1) + 8u)
Base (0xb7b57ac8) 0
primary-for Derived1 (0xb7ad6400)
Vtable for Derived2
Derived2::_ZTV8Derived2: 4u entries
0 (int (*)(...))0
4 (int (*)(...))(& _ZTI8Derived2)
8 Base::bark
12 Derived2::smile
Class Derived2
size=12 align=4
base size=12 base align=4
Derived2 (0xb7ad64c0) 0
vptr=((& Derived2::_ZTV8Derived2) + 8u)
Base (0xb7b57c30) 0
primary-for Derived2 (0xb7ad64c0)
正如你所看到的,每个类都有一个 vtable。前两个条目很特别。第二个指向类的 RTTI 数据。第一个 - 我知道但忘记了。它在更复杂的情况下有一些用处。好吧,如布局所示,如果你有一个类 Derived1 的对象,那么 vptr (v-table-pointer) 将指向类 Derived1 的 v-table,它只有一个条目用于其函数 bark 指向 Derived1 的版本。Derived2 的 vptr 指向 Derived2 的 vtable,它有两个条目。另一个是它添加的新方法,微笑。它重复了 Base::bark 的条目,当然,这将指向 Base 的函数版本,因为它是它最派生的版本。
我还用 -fdump-tree-optimized 转储了 GCC 在完成一些优化后生成的树(构造函数内联,...)。输出使用 GCC 的中端语言,该语言是独立于前端的,缩进到一些类似 C 的块结构中:GIMPL
;; Function virtual void Base::bark() (_ZN4Base4barkEv)
virtual void Base::bark() (this)
{
<bb 2>:
return;
}
;; Function virtual void Derived1::bark() (_ZN8Derived14barkEv)
virtual void Derived1::bark() (this)
{
<bb 2>:
return;
}
;; Function virtual void Derived2::smile() (_ZN8Derived25smileEv)
virtual void Derived2::smile() (this)
{
<bb 2>:
return;
}
;; Function int main() (main)
int main() ()
{
void * D.1757;
struct Derived2 * D.1734;
void * D.1756;
struct Derived1 * D.1693;
<bb 2>:
D.1756 = operator new (12);
D.1693 = (struct Derived1 *) D.1756;
D.1693->D.1671._vptr.Base = &_ZTV8Derived1[2];
use (&D.1693->D.1671);
D.1757 = operator new (12);
D.1734 = (struct Derived2 *) D.1757;
D.1734->D.1682._vptr.Base = &_ZTV8Derived2[2];
use (&D.1734->D.1682);
return 0;
}
正如我们所看到的,它只是设置了一个指针 - vptr - 它将指向我们之前在创建对象时看到的相应 vtable。我还转储了用于创建 Derived1 的汇编程序代码并调用使用($4 是第一个参数寄存器,$2 是返回值寄存器,$0 是始终 0-register),然后通过工具:)去掉其中的名称c++filt
# 1st arg: 12byte
add $4, $0, 12
# allocate 12byte
jal operator new(unsigned long)
# get ptr to first function in the vtable of Derived1
add $3, $0, vtable for Derived1+8
# store that pointer at offset 0x0 of the object (vptr)
stw $3, $2, 0
# 1st arg is the address of the object
add $4, $0, $2
jal use(Base*)
如果我们想调用会发生什么?bark
void doit(Base* b) {
b->bark();
}
GIMPL 代码:
;; Function void doit(Base*) (_Z4doitP4Base)
void doit(Base*) (b)
{
<bb 2>:
OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call];
return;
}
OBJ_TYPE_REF
是一个 GIMPL 结构,它被很好地打印出来(它记录在 gcc SVN 源代码中)gcc/tree.def
OBJ_TYPE_REF(<first arg>; <second arg> -> <third arg>)
它的含义是:在对象上使用表达式,并将前端 (c++) 特定值(它是 vtable 中的索引)存储。最后,它作为“this”参数传递。我们是否可以调用出现在 vtable 中第二个索引处的函数(注意,我们不知道哪个 vtable 属于哪种类型!),GIMPL 将如下所示:*b->_vptr.Base
b
0
b
OBJ_TYPE_REF(*(b->_vptr.Base + 4);b->1) (b) [tail call];
当然,这里再次是汇编代码(堆栈帧的东西被切断了):
# load vptr into register $2
# (remember $4 is the address of the object,
# doit's first arg)
ldw $2, $4, 0
# load whatever is stored there into register $2
ldw $2, $2, 0
# jump to that address. note that "this" is passed by $4
jalr $2
请记住,vptr 正好指向第一个函数。(在该条目之前,存储了 RTTI 插槽)。因此,该插槽中出现的任何东西都被称为。它还将调用标记为尾调用,因为它发生在我们函数中的最后一个语句中。doit
要回答有关哪些对象(从现在开始的实例)具有 vtable 以及在哪里的问题,考虑何时需要 vtable 指针会很有帮助。
对于任何继承层次结构,对于该层次结构中特定类定义的每组虚拟函数,都需要一个 vtable。换言之,给定以下几点:
class A { virtual void f(); int a; };
class B: public A { virtual void f(); virtual void g(); int b; };
class C: public B { virtual void f(); virtual void g(); virtual void h(); int c; };
class D: public A { virtual void f(); int d; };
class E: public B { virtual void f(); int e; };
因此,您需要五个 vtable:A、B、C、D 和 E 都需要自己的 vtable。
接下来,您需要知道给定指向特定类的指针或引用要使用哪个 vtable。例如,给定一个指向 A 的指针,您需要对 A 的布局有足够的了解,以便您可以获得一个 vtable,告诉您在哪里调度 A::f()。给定指向 B 的指针,您需要充分了解 B 的布局才能调度 B::f() 和 B::g()。依此类推。
一种可能的实现可以将 vtable 指针作为任何类的第一个成员。这意味着 A 实例的布局为:
A's vtable;
int a;
B 的实例是:
A's vtable;
int a;
B's vtable;
int b;
您可以从此布局中生成正确的虚拟调度代码。
您还可以通过组合具有相同布局的 vtables 的 vtable 指针来优化布局,或者如果一个指针是另一个 vtable 的子集。因此,在上面的示例中,您还可以将 B 布局为:
B's vtable;
int a;
int b;
因为 B 的 vtable 是 A 的超集。B 的 vtable 有 A::f 和 B::g 的条目,A 的 vtable 有 A::f 的条目。
为了完整起见,以下是我们目前看到的所有 vtables 的布局方式:
A's vtable: A::f
B's vtable: A::f, B::g
C's vtable: A::f, B::g, C::h
D's vtable: A::f
E's vtable: A::f, B::g
实际条目为:
A's vtable: A::f
B's vtable: B::f, B::g
C's vtable: C::f, C::g, C::h
D's vtable: D::f
E's vtable: E::f, B::g
对于多重继承,执行相同的分析:
class A { virtual void f(); int a; };
class B { virtual void g(); int b; };
class C: public A, public B { virtual void f(); virtual void g(); int c; };
生成的布局将是:
A:
A's vtable;
int a;
B:
B's vtable;
int b;
C:
C's A vtable;
int a;
C's B vtable;
int b;
int c;
您需要一个指向与 A 兼容的 vtable 的指针和一个指向与 B 兼容的 vtable 的指针,因为对 C 的引用可以转换为对 A 或 B 的引用,并且您需要将虚函数分派给 C。
由此可以看出,特定类的 vtable 指针数至少是它派生的根类数(直接派生或由于超类)。根类是具有 vtable 的类,该 vtable 不继承自同样具有 vtable 的类。
虚拟继承在混合中抛出另一个间接因素,但您可以使用相同的指标来确定 vtable 指针的数量。
评论