每个包含一个或多个虚函数的类都有与之关联的 Vtable。一个名为 vptr 的 void 指针指向该 vtable。该类的每个对象都包含指向相同 Vtable 的 vptr。那么为什么 vptr 不是静态的?为什么不把它与类相关联,而是将其与对象相关联呢?
每个包含一个或多个虚函数的类都有与之关联的 Vtable。一个名为 vptr 的 void 指针指向该 vtable。该类的每个对象都包含指向相同 Vtable 的 vptr。那么为什么 vptr 不是静态的?为什么不把它与类相关联,而是将其与对象相关联呢?
对象的运行时类是对象本身的属性。实际上,vptr
代表了运行时类,因此不能是 static
的。然而,它指向的内容可以被同一运行时类的所有实例共享。
vptr
的重点在于编译器不知道实际类型;实际类型可以在运行时变化。 - James KanzeA& a
对象,那么它的“运行时类”是什么?A
?A1
?A2
?还是其他东西?你如何知道?调用 A
的静态函数永远无法告诉你 a
的运行时类(正确术语是“动态类型”)。 - Jonathan WakelygetType
函数,你首先需要虚表指针(因为它必须是virtual
才能工作)。但是要获取虚表指针,你建议我们调用getType
。糟糕了。 - David Schwartzvptr
的类型是什么(它的类类型)?假设我针对类A
及其对象A AO
执行typeid(vptr).name()
,输出会是什么? - ussA
的 vptr 指向 A
的虚函数表,对于 A1
的 vptr 指向 A1
的虚函数表,以此类推。class A {
public:
virtual void foo();
virtual void bar();
};
class A1 : public A {
virtual void foo();
};
class A2 : public A {
virtual void foo();
};
class A3 : public A {
virtual void bar();
virtual void baz();
};
A
的虚函数表包含{ &A::foo, &A::bar }
A1
的虚函数表包含{ &A1::foo, &A::bar }
A2
的虚函数表包含{ &A2::foo, &A::bar }
A3
的虚函数表包含{ &A::foo, &A3::bar, &A3::baz }
因此,当您调用a.foo()
时,编译器会跟随对象的vptr查找虚函数表,然后调用虚函数表中的第一个函数。
假设编译器使用了您的想法,我们写下:
A1 a1;
A2 a2;
A& a = (std::rand() % 2) ? a1 : a2;
a.foo();
A1 a1;
A2 a2;
A& a = a1;
A& aa = a2;
a.foo();
aa.foo();
a
和aa
都是指向A
的引用,但它们需要两个不同的vptr,一个指向A1
的vtable,另一个指向A2
的vtable。如果vptr是A
的静态成员,那么它怎么可能同时具有两个值呢?唯一合理、一致的选择是A
的静态vptr指向A
的vtable。
但这意味着调用a.foo()
会调用A::foo()
,而应该调用A1::foo()
,调用aa.foo()
也会调用A::foo()
,而应该调用A2::foo()
。
显然,你的想法无法实现所需的语义,证明使用你的想法的编译器不能是C++编译器。编译器无法从a
中获取A1
的vtable,除非知道派生类型是什么(通常是不可能的,引用到基类可能已从定义在不同库中的函数返回,并且可能引用尚未编写的派生类型!)或者直接在对象中存储vptr。
vptr必须对于a1
和a2
是不同的,并且必须在通过指向基类的指针或引用访问它们时无需知道动态类型即可访问,以便当您通过基类引用a
获取vptr时,它仍然指向正确的vtable,而不是基类vtable。最明显的方法是直接将vptr嵌入对象中。另一种复杂的解决方案是保留一个对象地址到vptrs的映射,例如类似于std::map<void*, vtable*>
,并通过查找&a
来找到a
的vtable,但这仍然在每个对象中存储一个vptr而不是每个类型,在创建和销毁多态对象时需要更多的工作(和动态分配),并且会增加整体内存使用,因为映射结构将占用空间。将vptr直接嵌入对象中更简单。
众所周知,Vptr是对象的一个属性。那么为什么呢?
假设我们有三个对象:
Class Base{
virtual ~Base();
//Class Definition
};
Class Derived: public Base{
//Class Definition
};
Class Client: public Derived{
//Class Definition
};
它们之间的关系是:Base<---Derived<----Client。Client类继承自Derived类,Derived类又继承自Base类。
Base * Ob = new Base;
Derived * Od = new Derived;
Client* Oc = new Client;
每当Oc被析构时,它应该先销毁基类部分,然后是派生类部分,最后是客户端类部分的数据。为了帮助这种顺序,Base类的析构函数应该是虚拟的,并且Oc对象的析构函数指向Client类的析构函数。当Oc对象的基类析构函数是虚拟的时,编译器会在Oc对象的析构函数中添加代码来调用派生类的析构函数和基类的析构函数。这种链接方式可以确保在销毁Client对象时销毁所有的基类、派生类和客户端类数据。
如果vptr是静态的,那么Oc的vtable条目仍将指向Base的析构函数,只有Oc的基类部分会被销毁。Oc的vptr应该始终指向最派生对象的析构函数,如果vptr是静态的,则无法实现这一点。
typeid()
来识别动态类型,然后调用静态指针,请注意typeid()
仅对具有虚函数的类型的对象返回动态类型;否则,它只返回静态类型(当前C++标准中的§5.2.8)。是的,这意味着它可以反过来工作:typeid()
通常使用虚拟指针来识别动态类型。typeof
在哪里定义的?它只能用于多态类型吗?这并不适用于GCC的typeof
,它的工作方式类似于decltype
,可以用于非多态类型,它只告诉你静态类型(对于查找动态类型显然没有用处)。 - Jonathan Wakelytypeid
。我刚刚看到 C++11 标准接受在非多态类型上使用它:在这种情况下,它不会抛出异常,而是返回静态类型。对于我们的目的来说,这与找不到动态类型是相同的:除非有虚拟指针,否则无法找到动态类型。我正在更新我的答案。 - Gorpikclass A{
public:
virtual void f1(){}
}
class B: public A{
public:
void f1(){}
}
现在考虑上面的例子,如果我们将 _vptr 设为静态,则其内存仅在编译时分配一次。 因此,_vptr 将对于类 A 和类 B 都是相同的
B b;
A *p=&b;
p->f1();
vptr
的意义在于你不知道一个对象在运行时到底是哪个类。如果你知道了,那么虚函数调用就是不必要的。事实上,当你不使用虚函数时,这就是发生的情况。但是使用虚函数时,如果我有...class Sub : Parent {};
以及一个类型为Parent*
的值,我在运行时不知道这是否真的是Parent
类型的对象还是Sub
类型的对象。虚函数表指针让我能够弄清楚这一点。
虚函数表是每个类的。一个对象包含指向运行时类型vptr的指针。
我不认为这是标准要求,但我使用过的所有编译器都是这样做的。
即使在你的例子中也是如此。