c++ 为什么vptr不是静态的?

kx5bkwkv  于 2023-08-09  发布在  其他
关注(0)|答案(8)|浏览(142)

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


的数据

tpgth1q7

tpgth1q71#

对象的运行时类是对象本身的属性。实际上,vptr表示运行时类,因此不能是static。但是,它所指向的对象可以由同一个运行时类的所有示例共享。

6jygbczu

6jygbczu2#

你的图表错了。没有一个单独的vtable,每个多态类型都有一个vtable。A的vptr指向A的vtable,A1的vptr指向A1的vtable,等等。
给出:

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的vtable包含{ &A::foo, &A::bar }
A1的vtable包含{ &A1::foo, &A::bar }
A2的vtable包含{ &A2::foo, &A::bar }
A3的vtable包含{ &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中查找并找到类A的vptr,该类(根据您的想法)是A类型的static属性,而不是引用a绑定到的对象的成员。该vptr是否指向AA1A2的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()在应该调用A1::foo()时调用A::foo(),调用aa.foo()在应该调用A2::foo()时也调用A::foo()
很明显,你的想法无法实现所需的语义,证明使用你的想法的编译器不可能是C++编译器。如果不知道派生类型是什么,编译器就无法从a中获取A1的vtable(通常这是不可能的,对base的引用可能是从不同库中定义的函数返回的,并且可能引用甚至还没有编写的派生类型!)或通过将VPTR直接存储在对象中。
a1a2的vptr必须不同,并且在通过pointer或base引用访问它们时,必须可以在不知道动态类型的情况下访问它们,以便当您通过基类a的引用获得vptr时,它仍然指向正确的vtable,而不是基类vtable。最明显的方法是将vptr直接存储在对象中。另一种更复杂的解决方案是保持对象地址到vptr的Map,例如类似于std::map<void*, vtable*>,并通过查找&a找到a的vtable,但这仍然为每个对象存储一个vptr,而不是为每个类型存储一个vptr,并且每次创建和销毁多态对象时都需要更多的工作(和动态分配)来更新map,并且会增加整体内存使用,因为map结构会占用空间。将vptr嵌入到对象本身中会更简单。

ndasle7k

ndasle7k3#

虚表(顺便说一句,这是C标准中没有提到的一种实现机制)用于在运行时识别对象的动态类型。因此,对象本身必须持有指向它的指针。如果它是静态的,那么只有静态类型可以被它识别,它将是无用的。
如果你想在内部使用typeid()来标识动态类型,然后用它调用静态指针,请注意typeid()只返回属于虚函数类型的对象的动态类型;否则,它只返回静态类型(当前C
标准中的5.2.8节)。是的,这意味着它的工作方式相反:typeid()通常使用虚拟指针来标识动态类型。

tkqqtvp1

tkqqtvp14#

每个人都证明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被析构时,它应该析构数据的基础部分、派生部分,然后是客户端部分。为了帮助这个顺序,Base析构函数应该是虚拟的,对象Oc的析构函数指向Client的析构函数。当对象Oc的基类析构函数为虚时,编译器向对象Oc的析构函数添加代码,调用派生的析构函数,派生的析构函数调用基类析构函数。当客户端对象被销毁时,这种链接会看到所有的基数据、派生数据和客户端数据都被销毁。
如果该vptr是静态的,则Oc的vtable条目仍将指向Base的析构函数,并且仅销毁Oc的base部分。Oc的vptr应该总是指向大多数派生对象的析构函数,如果vptr是静态的,这是不可能的。

nkoocmlb

nkoocmlb5#

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

字符串
现在考虑上面的例子,如果我们使_vptr静态,那么它的内存将在编译时只分配一次。因此,_vptr对于类A和类B是相同的

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


现在,考虑上述情况。编译器如何知道哪个虚拟table _vptr指向哪个虚拟table _vptr?
因此,不能使其为静态的,因为它需要可用于每个对象来调用其虚拟表。

xxls0lw8

xxls0lw86#

vptr的全部意义在于,您无法确切知道对象在运行时拥有哪个类。如果你知道这一点,那么虚函数调用就没有必要了。实际上,这就是不使用虚函数时发生的情况。但是对于虚函数,如果我有

class Sub : Parent {};

字符串
和一个Parent*类型的值,在运行时我不知道这是一个Parent类型的对象还是一个Sub类型的对象。VPTR让我弄明白了。

mznpcxlj

mznpcxlj7#

虚拟方法表是每个类。对象包含指向运行时类型vptr的指针。
我不认为这是一个要求在标准的胸围所有编译,我已经这样做。
即使在你的例子中也是如此。

tmb3ates

tmb3ates8#

@凶孔雀:原因可能是,静态成员变量必须在程序中的Main函数之前定义。但是如果我们希望_vptr是静态的,那么谁的责任(编译器/程序员)在main之前定义程序中的_vptr。以及程序员如何知道VTABLE的指针并将其分配给_vptr。这就是为什么编译器负责将值分配给指针(_vptr)。这发生在类的构造函数中(隐藏功能)。现在,如果构造函数进入画面,每个对象都应该有一个_vptr。

相关问题