【问题标题】:C++ virtual destructor & vtableC++ 虚拟析构函数和 vtable
【发布时间】:2017-05-31 14:52:43
【问题描述】:

我有一些关于虚拟析构函数和 vtable 的具体问题。

假设我有以下代码:

class Base
{
public:

    virtual ~Base();

};

class Child : public Base
{
public:

    ~Child();
};

问题:

  1. vtable 存储在哪里?它总是在基类中,所有子类都只是保持一个指向它的指针吗?
  2. 添加虚拟方法只会将 sizeof(class) 增加 8 个字节,对吗? (假设是64位系统)基类存储表呢?
  3. 通过 new 运算符创建 Child 类型的实例然后删除 ... 会调用 Base 析构函数吗? (我问是因为 Child 类的析构函数不是虚拟的……这是否意味着它只影响 Child 的子类?)。

【问题讨论】:

  • 1 和 2: 实现定义。 3: 是的。
  • 我假设您的意思是 Child 实际上是Base 的子类?
  • 1. (a) 每个类必须有自己的 vtable,否则该技术无法工作。 (b) 真的没有“在课堂上”这样的东西。 2.因此无关紧要。 3.子类的析构函数如果基类是虚拟的。
  • 如果基类析构函数是虚的,那么所有派生类的析构函数也是虚的——不管你写virtualoverride与否;这是可选的,但不会改变派生 dtor 虚拟的事实。
  • @DeiDei -- 正式地,不是“定义的实现”,而是“具体的实现”。在语言定义中,“实现定义”意味着实现必须记录它所做的事情。

标签: c++ polymorphism vtable


【解决方案1】:

每个具有虚拟方法(声明/继承)的类都有自己的虚拟表(vtable)。当一个虚拟方法(任何,不仅仅是析构函数)被声明为虚拟时,派生类中该方法的所有覆盖都是自动虚拟的。编译器还在任何具有虚拟方法的此类的开头添加 _vptr。这个 _vptr 将在创建类的对象时填充,并将指向该类的 vtable。虚拟析构函数的处理方式与任何其他虚拟函数相同。在您的示例中,因为 ~Base 是虚拟的,所以 ~Child 也将是虚拟的。

假设你这样做:

Child *child = new (class Child);
delete(child);

这里,child->_vptr 将指向 Child 的 vtable。所以在删除过程中,首先调用~Child,然后调用~Base(以相反的构造顺序)。

如果你这样做了,

Base *base = new (class Child);
delete(base);

在这里,base->_vptr 将指向 Child 的 vtable。因此,在删除过程中,首先调用 ~Child,然后调用 vtable 中的 ~Base。

在gdb中,我们可以通过运行这个命令来验证这个_vptr,

"info vtbl base" or "print *base"

【讨论】:

    【解决方案2】:

    下面的解释假设编译器使用的虚拟调度实现是基于虚拟表的。

    1. 每个具有虚拟方法(声明或继承)的类都有自己的虚拟表。如果子类覆盖了基类中的虚成员函数,则指向覆盖函数的指针被放置在类的 vtable 中;否则,将保留指向基类实现的指针。

    2. 添加第一个虚函数会将类instance 的大小增加vtable 指针的大小。第一个之后的虚函数不会增加实例的大小。

    3. 由于 ~Base 是虚拟的,~Child 也是虚拟的,即使 virtual 关键字被省略。在覆盖的情况下,virtual 关键字是可选的。

    【讨论】:

    • 我不明白为什么要根据假设(逻辑假设为真)而不是问题的来源来回答。程序员之间的主要语言是代码,代码否定假设
    • @JacekCz 从 OP 写的其他所有内容中可以清楚地看出,他忘记在 Child 中添加 : public Base
    • “一个指向覆盖函数的指针被放置在副本中” 在这个上下文中“复制”是什么意思?
    • @JordanMelo 我对这个答案中的“复制”一词并不感到疯狂。它不是一个副本,它只是另一个 vtable。
    • @CaptainGiraffe 我还是不明白你的问题。什么是“指向子类的 vtable 指针”?它指向什么?
    【解决方案3】:

    通过 new 操作符创建一个 Child 类型的实例然后删除 ... 会调用 Base 析构函数吗?

    不在原始问题代码中,因为您没有从 Base 继承的 Child

    假设这是一个错误,并且我们修复了它,那么当您销毁 Child 时会调用 ~Base,即使它不是虚拟的,只是因为基类子对象作为正常的破坏顺序。

    虚拟析构函数的原因是您可以删除Child 通过 Base * 并且仍然正确调用~Child

    例如,与:

    struct Base { ~Base(); };
    struct Child: Base { ~Child(); };
    
    struct VBase { virtual ~VBase(); };
    struct VChild: VBase { ~VChild(); };
    

    这适用于两个层次结构:

    template <typename Derived>
    void test_static() {
      Derived d;
    }
    test_static<Child>();  // ~Child then ~Base invoked when d is destroyed
    test_static<VChild>(); // ~VChild then ~VBase invoked when d is destroyed
    

    但这仅适用于虚拟析构函数:

    template <typename Derived, typename Base>
    void test_dynamic() {
      std::unique_ptr<Base> p(new Derived);
    }
    test_dynamic<Child, Base>;  // only ~Base invoked when p destroyed
    test_dynamic<VChild,VBase>; // ~VChild then ~VBase invoked as before.
    

    至于vtable的问题,是否存在,如果存在,在哪里,是一个实现细节,你不用担心。

    【讨论】:

      猜你喜欢
      • 2011-08-12
      • 2018-11-26
      • 2012-04-18
      • 1970-01-01
      • 2016-08-14
      • 1970-01-01
      • 2012-09-02
      • 1970-01-01
      • 2013-07-06
      相关资源
      最近更新 更多