每个包含一个或多个虚函数的类都有一个与之关联的 Vtable。一个名为 vptr 的 void 指针指向该 vtable。该类的每个对象都包含指向同一个 Vtable 的 vptr。那为什么不是 vptr static ?与其将 vptr 与对象相关联,不如将其与类相关联?
7 回答
对象的运行时类是对象本身的一个属性。实际上,vptr
表示运行时类,因此不能是static
. 但是,它所指向的内容可以由同一运行时类的所有实例共享。
你的图表是错误的。没有一个 vtable,每个多态类型都有一个 vtable。vptr forA
指向 vtable for A
, vptr forA1
指向 vtable forA1
等。
鉴于:
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 查找 vtable,然后调用 vtable 中的第一个函数。
假设一个编译器使用了你的想法,我们写:
A1 a1;
A2 a2;
A& a = (std::rand() % 2) ? a1 : a2;
a.foo();
编译器在基类中A
查找并找到该类的 vptr A
(根据您的想法),该类是static
类型的属性,而不是引用绑定到A
的对象的成员。a
该 vptr 是否指向 vtable for A
, or A1
orA2
或其他东西?如果它指向 vtable ,A1
则在 50% 的时间里a
引用是错误的a2
,反之亦然。
现在假设我们写:
A1 a1;
A2 a2;
A& a = a1;
A& aa = a2;
a.foo();
aa.foo();
a
和aa
都是对 的引用A
,但它们需要两个不同的 vptr,一个指向 vtable for A1
,一个指向 vtable for A2
。如果 vptr 是一个静态成员,A
它怎么能同时有两个值呢?唯一合乎逻辑、一致的选择是静态 vptrA
指向 vtable for A
。
但这意味着a.foo()
调用A::foo()
在它应该调用A1::foo()
的时候调用,并且调用aa.foo()
也在A::foo()
它应该调用的时候调用A2::foo()
。
显然,您的想法未能实现所需的语义,证明使用您的想法的编译器不能是 C++ 编译器。A1
编译器无法在a
不知道派生类型是什么的情况下获取 vtable for甚至尚未编写的派生类型!)或将 vptr 直接存储在对象中。
和 的 vptr 必须不同,a1
并且a2
在通过指针或对 base 的引用访问它们时,必须在不知道动态类型的情况下可访问,这样当您通过对基类的引用获取 vptr 时a
,它仍然指向右侧vtable,而不是基类 vtable。最明显的方法是将 vptr 直接存储在对象中。另一种更复杂的解决方案是将对象地址映射到 vptrs,例如类似的东西,并通过查找std::map<void*, vtable*>
找到 vtablea
&a
,但这仍然为每个对象存储一个 vptr 而不是每个类型一个,并且每次创建和销毁多态对象时都需要更多的工作(和动态分配)来更新映射,并且会增加整体内存使用量,因为映射结构会占用空间。将 vptr 嵌入对象本身更简单。
虚拟表(顺便说一下,C++ 标准中没有提到的一种实现机制)用于在运行时识别对象的动态类型。因此,对象本身必须持有一个指向它的指针。如果它是静态的,那么它只能识别静态类型,它是无用的。
如果您正在考虑以某种方式在typeid()
内部使用来识别动态类型,然后用它调用静态指针,请注意typeid()
仅返回属于具有虚函数类型的对象的动态类型;否则它只返回静态类型(当前 C++ 标准中的第 5.2.8 节)。是的,这意味着它以相反的方式工作:typeid()
通常使用虚拟指针来标识动态类型。
正如每个人都证明 Vptr 是对象的属性。让我们看看为什么?
假设我们有三个对象
Class Base{
virtual ~Base();
//Class Definition
};
Class Derived: public Base{
//Class Definition
};
Class Client: public Derived{
//Class Definition
};
持有关系 Base<---Derived<----Client. 客户端类派生自派生类,派生类又派生自基类
Base * Ob = new Base;
Derived * Od = new Derived;
Client* Oc = new Client;
每当 Oc 被破坏时,它应该破坏数据的基础部分、派生部分和客户端部分。为了帮助这个序列,基本析构函数应该是虚拟的,并且对象 Oc 的析构函数指向客户端的析构函数。当对象 Oc 的基析构函数是虚拟编译器时,将代码添加到对象 Oc 的析构函数以调用派生的析构函数,派生的析构函数调用基的析构函数。当 Client 对象被销毁时,此链接会看到所有基础数据、派生数据和客户端数据都被销毁。
如果 vptr 是静态的,那么 Oc 的 vtable 条目仍将指向 Base 的析构函数,并且只有 Oc 的基本部分被销毁。oc 的 vptr 应该始终指向大多数派生对象的析构函数,如果 vptr 是静态的,这是不可能的。
的全部要点vptr
是因为您不确切知道对象在运行时具有哪个类。如果您知道这一点,那么虚拟函数调用将是不必要的。也就是说,事实上,当您不使用虚函数时会发生什么。但是对于虚拟功能,如果我有
class Sub : Parent {};
和 type 的值Parent*
,我不知道在运行时这是否真的是 type 的对象Parent
或 type 之一Sub
。vptr让我弄清楚了。
虚拟方法表是每个类。一个对象包含一个指向运行时类型 vptr 的指针。
我不认为这是标准半身像中的要求,我使用过的所有编译都以这种方式进行。
即使在您的示例中也是如此。
@Harsh Maurya:原因可能是,静态成员变量必须在程序中的 Main 函数之前定义。但是,如果我们希望 _vptr 是静态的,其责任(编译器/程序员)在 main 之前在程序中定义 _vptr。以及程序员如何知道 VTABLE 的指针以将其分配给 _vptr。这就是编译器负责将值分配给指针(_vptr)的原因。这发生在类的构造函数(隐藏功能)中。现在,如果 Constructor 出现,每个对象都应该有一个 _vptr。