【问题标题】:Why vptr is not static?为什么 vptr 不是静态的?
【发布时间】:2012-12-04 13:11:56
【问题描述】:

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

【问题讨论】:

  • 如果它是静态的……那有什么意义呢?
  • 你将如何从 object 中访问它而不是?
  • 这将破坏整个目的。线索就在名称中:虚拟调度是动态,而不是静态
  • 但是如何在运行时知道该类?编译器只知道基类,所以只能插入基类的vptr。
  • @HarshMaurya,没错。那么如果地址是静态的(vptr 不在对象中),我们如何在运行时知道哪个 vtable 与哪个对象关联?

标签: c++ function virtual vtable vptr


【解决方案1】:

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

【讨论】:

  • 同意。事实上,它也用于 RTTI。但我的观点是:由于 Class 的所有对象都属于同一类型,vptr 将代表每个对象的相同运行时类。为什么不创建一个使用静态 vptr 获取其类型的静态方法 getType()。对象可以调用这个静态方法来知道它们的类型。
  • @HarshMaurya 编译器如何知道要调用哪个函数?使用vptr 的全部意义在于编译器不知道实际类型。实际类型在运行时可能会有所不同。
  • @HarshMaurya,如果您有A& a,该对象的“运行时类”是什么? A? A1? A2?还有什么?你怎么知道?调用A的静态函数永远无法告诉你a的运行时类(正确的说法是“动态类型”)
  • @HarshMaurya:这会造成鸡与蛋的问题。要调用getType 函数,您首先需要虚拟表指针(因为它必须是virtual 才能工作)。但是要获取虚拟表指针,您建议我们调用getType。哎呀。
  • 知道了。我将此标记为答案。尽管@DavidSchwartz 的评论让我理解了这个问题。谢谢。
【解决方案2】:

vptr 的全部意义在于您不知道对象在运行时具有哪个类。如果您知道这一点,那么虚拟函数调用将是不必要的。也就是说,事实上,当您不使用虚函数时会发生什么。但是如果我有虚函数的话

class Sub : Parent {};

Parent* 类型的值,我不知道在运行时这是否真的是Parent 类型的对象或Sub 类型之一。 vptr 让我弄清楚了。

【讨论】:

    【解决方案3】:

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

    我不认为这是标准半身像中的要求,我使用过的所有编译都这样做。

    即使在您的示例中也是如此。

    【讨论】:

    • -1:不回答问题。问题是为什么,而不是什么
    【解决方案4】:

    虚拟表(顺便说一下,C++ 标准中没有提到的一种实现机制)用于在运行时识别对象的动态类型。因此,对象本身必须持有一个指向它的指针。如果是静态的,那么它只能识别静态类型,就没用了。

    如果您正在考虑以某种方式在内部使用typeid() 来识别动态类型,然后用它调用静态指针,请注意typeid() 只返回属于具有虚函数类型的对象的动态类型;否则它只返回静态类型(当前 C++ 标准中的第 5.2.8 节)。是的,这意味着它可以反过来工作:typeid() 通常使用虚拟指针来标识动态类型。

    【讨论】:

    • typeof 是在哪里定义的,它在哪里说它只能用于多态类型? GCC 的 typeof 并非如此,它的工作方式类似于 decltype,可用于非多态类型,它只是告诉您静态类型(这对于尝试查找动态类型显然无用。)
    • @JonathanWakely 抱歉,打错了:我的意思是typeid。而且我刚刚看到 C++11 标准接受在非多态类型上使用它:在这种情况下,它不会抛出异常,而是返回静态类型。就我们的目的而言,这等同于:除非您有虚拟指针,否则您无法找到动态类型。我正在更新我的答案。
    【解决方案5】:

    你的图表是错误的。没有一个 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,它(根据您的想法)是static 类型的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(这通常是不可能的,对基址的引用可能已经从定义在不同的库,并且可以引用尚未编写的派生类型!)或将 vptr 直接存储在对象中。

    a1a2的vptr必须不同,并且在通过指针或对base的引用访问它们时必须在不知道动态类型的情况下可访问,这样当您通过对base的引用获取vptr时类,a,它仍然指向正确的 vtable,而不是基类 vtable。最明显的方法是将 vptr 直接存储在对象中。另一种更复杂的解决方案是将对象地址映射到 vptrs,例如类似于std::map<void*, vtable*>,并通过查找&a 找到a 的vtable,但这仍然为每个对象存储一个vptr,而不是每种类型一个,并且需要更多的工作(和动态分配)来更新地图每次创建和销毁多态对象时,都会增加整体内存使用量,因为映射结构会占用空间。将 vptr 嵌入对象本身会更简单。

    【讨论】:

    • 抱歉,您看错了图表。 A1、A2 和 A3 不是类,而是 A 类的对象。无论如何,我得到了解决方案。谢谢。
    • 哦,所以你的图中没有多态性。好吧,也许如果您考虑多态性,这是虚函数存在的原因,您会明白为什么您的想法行不通。如果您没有派生类型,那么您的想法会奏效,但如果您没有派生类型,那么为什么要首先使用虚函数?
    • 这是我见过的关于这个话题最清晰的解释。
    【解决方案6】:

    正如每个人都证明 Vptr 是对象的属性。 让我们看看为什么?

    假设我们有三个对象 Class Base{ virtual ~Base(); //Class Definition }; Class Derived: public Base{ //Class Definition }; Class Client: public Derived{ //Class Definition };

    持有关系Base

    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 是静态的,这是不可能的。

    【讨论】:

      【解决方案7】:

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

      【讨论】:

        猜你喜欢
        • 2021-12-21
        • 2013-12-31
        • 1970-01-01
        • 1970-01-01
        • 2016-12-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-27
        相关资源
        最近更新 更多