为什么vptr不是静态的?

16

每个包含一个或多个虚函数的类都有与之关联的 Vtable。一个名为 vptr 的 void 指针指向该 vtable。该类的每个对象都包含指向相同 Vtable 的 vptr。那么为什么 vptr 不是静态的?为什么不把它与类相关联,而是将其与对象相关联呢?

输入图像描述


4
如果它被静态化了,它还有什么意义呢? - R. Martinho Fernandes
2
你如何从一个对象中访问它呢? - StoryTeller - Unslander Monica
1
那将违背整个目的。关键在于名称:虚拟分派是动态的,而不是静态的。 - Kerrek SB
3
但是在运行时,如何知道这个类?编译器只知道基类,因此它只能插入基类的vptr。 - StoryTeller - Unslander Monica
5
如果地址是静态的(vptr不在对象中),那么我们如何在运行时知道哪个vtable与哪个对象相关联? - StoryTeller - Unslander Monica
显示剩余5条评论
8个回答

10

对象的运行时类是对象本身的属性。实际上,vptr 代表了运行时类,因此不能是 static的。然而,它指向的内容可以被同一运行时类的所有实例共享。


同意。实际上,它也用于RTTI。但我的观点是:由于类的所有对象都是相同类型,vptr将为每个对象表示相同的运行时类。为什么不创建一个静态方法getType(),它使用静态vptr来获取其类型。对象可以调用此静态方法以了解其类型。 - Samarsh
5
编译器如何知道调用哪个函数?使用 vptr 的重点在于编译器不知道实际类型;实际类型可以在运行时变化。 - James Kanze
3
如果你有一个 A& a 对象,那么它的“运行时类”是什么?AA1A2?还是其他东西?你如何知道?调用 A 的静态函数永远无法告诉你 a 的运行时类(正确术语是“动态类型”)。 - Jonathan Wakely
2
@HarshMaurya:这会造成一个先有鸡还是先有蛋的问题。要调用getType函数,你首先需要虚表指针(因为它必须是virtual才能工作)。但是要获取虚表指针,你建议我们调用getType。糟糕了。 - David Schwartz
明白了。我将其标记为答案。尽管@DavidSchwartz的评论让我理解了问题。谢谢。 - Samarsh
@NPE,vptr的类型是什么(它的类类型)?假设我针对类A及其对象A AO执行typeid(vptr).name(),输出会是什么? - uss

8
你的图示是错误的。每个多态类型都有一个虚函数表,而不是单一的虚函数表。对于 A 的 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();

编译器查找基类A并找到类A的vptr,该vptr(根据您的想法)是类型A的静态属性,而不是引用a绑定的对象的成员。该vptr指向A、A1、A2或其他内容的vtable?如果它指向A1的vtable,则当a引用a2时,50%的时间是错误的,反之亦然。
现在假设我们编写:
A1 a1;
A2 a2;
A& a = a1;
A& aa = a2;
a.foo();
aa.foo();
aaa都是指向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必须对于a1a2是不同的,并且必须在通过指向基类的指针或引用访问它们时无需知道动态类型即可访问,以便当您通过基类引用a获取vptr时,它仍然指向正确的vtable,而不是基类vtable。最明显的方法是直接将vptr嵌入对象中。另一种复杂的解决方案是保留一个对象地址到vptrs的映射,例如类似于std::map<void*, vtable*>,并通过查找&a来找到a的vtable,但这仍然在每个对象中存储一个vptr而不是每个类型,在创建和销毁多态对象时需要更多的工作(和动态分配),并且会增加整体内存使用,因为映射结构将占用空间。将vptr直接嵌入对象中更简单。


1
抱歉,但您误读了图表。A1、A2和A3不是类,而是类A的对象。无论如何,我已经找到了解决方案。谢谢。 - Samarsh
1
哦,所以你的图表中没有多态性。也许如果你考虑一下多态性,这是虚函数存在的原因,你会明白为什么你的想法行不通。如果你没有派生类型,那么你的想法可以实现,但如果你没有派生类型,你为什么要使用虚函数呢? - Jonathan Wakely
这是我见过的关于这个主题最清晰的解释。 - neevek

1

众所周知,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是静态的,则无法实现这一点。


1
虚拟表(顺便提一下,这是C++标准中未提及的实现机制)用于在运行时识别对象的动态类型。因此,对象本身必须持有指向它的指针。如果它是静态的,那么只能通过静态类型来识别它,这将毫无用处。
如果您想以某种方式在内部使用typeid()来识别动态类型,然后调用静态指针,请注意typeid()仅对具有虚函数的类型的对象返回动态类型;否则,它只返回静态类型(当前C++标准中的§5.2.8)。是的,这意味着它可以反过来工作:typeid()通常使用虚拟指针来识别动态类型。

typeof在哪里定义的?它只能用于多态类型吗?这并不适用于GCC的typeof,它的工作方式类似于decltype,可以用于非多态类型,它只告诉你静态类型(对于查找动态类型显然没有用处)。 - Jonathan Wakely
@JonathanWakely 抱歉,那是个打字错误:我想说的是 typeid。我刚刚看到 C++11 标准接受在非多态类型上使用它:在这种情况下,它不会抛出异常,而是返回静态类型。对于我们的目的来说,这与找不到动态类型是相同的:除非有虚拟指针,否则无法找到动态类型。我正在更新我的答案。 - Gorpik

1
class A{
public:
    virtual void f1(){}
}
class B: public A{
public:
    void f1(){}
}

现在考虑上面的例子,如果我们将 _vptr 设为静态,则其内存仅在编译时分配一次。 因此,_vptr 将对于类 A 和类 B 都是相同的

B b;
A *p=&b;
p->f1();

现在考虑上述情况。编译器如何知道_vptr指向哪个虚拟表呢?
因此,它不能被设为静态,因为每个对象都需要调用自己的虚拟表时,_vptr必须可用。

0
整个 vptr 的意义在于你不知道一个对象在运行时到底是哪个类。如果你知道了,那么虚函数调用就是不必要的。事实上,当你不使用虚函数时,这就是发生的情况。但是使用虚函数时,如果我有...
class Sub : Parent {};

以及一个类型为Parent*的值,我在运行时不知道这是否真的是Parent类型的对象还是Sub类型的对象。虚函数表指针让我能够弄清楚这一点。


-1
@Harsh Maurya:可能的原因是,静态成员变量必须在程序的Main函数之前定义。但是,如果我们希望_vptr是静态的,那么由谁(编译器/程序员)在主程序之前定义程序中的_vptr呢?程序员如何知道VTABLE指针并将其分配给_vptr。这就是为什么编译器负责将值分配给指针(_vptr)。这发生在类的构造函数中(隐藏功能)。现在,如果构造函数出现在图片中,则每个对象应该有一个_vptr。

-1

虚函数表是每个类的。一个对象包含指向运行时类型vptr的指针。

我不认为这是标准要求,但我使用过的所有编译器都是这样做的。

即使在你的例子中也是如此。


-1:没有回答问题。问题是“为什么”,而不是“什么”。 - John Dibling

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接