C++虚析构函数和虚函数表

3
我是一名有用的助手,可以为您提供翻译。以下是需要翻译的内容:

我对虚析构函数和虚表有一些具体问题。

假设我有以下代码:

class Base
{
public:

    virtual ~Base();

};

class Child : public Base
{
public:

    ~Child();
};

问题:

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

2
1和2: 实现定义的。 3: 是的。 - DeiDei
5
我假设您的意思是Child实际上是Base的子类? - Some programmer dude
  1. (a) 每个类必须有自己的虚函数表,否则该技术无法工作。 (b) 实际上并不存在所谓的“在类中”。2. 因此是不相关的。
  2. 如果基类的析构函数是虚函数,则子类的析构函数也是虚函数。
- user207421
如果基类析构函数是虚拟的,那么所有派生类的析构函数也是虚拟的——无论您是否编写了 virtualoverride;这是可选的,但不会改变派生类析构函数是虚拟的这一事实。 - Jesper Juhl
1
@DeiDei -- 正式来说,不是“实现定义”,而是“实现特定”。在语言定义中,“实现定义”意味着实现必须记录它所做的事情。 - Pete Becker
3个回答

5

以下解释假定编译器所采用的虚函数实现方式是基于虚表的。

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

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

  3. 由于 ~Base 是虚的,因此 ~Child 也是虚的,即使省略了 virtual 关键字。在进行覆盖时, virtual 关键字是可选的。


我不明白为什么答案是基于假设(逻辑上的假设),而不是问题的起源。程序员之间的主要语言是代码,而代码否定了假设。 - Jacek Cz
2
@JacekCz 从OP的其他写作中可以很清楚地看出,他忘记在“Child”中添加“:public Base”。 - Sergey Kalinichenko
“覆盖函数的指针被放置在拷贝中”,这里的“拷贝”指的是对象或者数据结构的一个复制品。 - Jordan Melo
1
@JordanMelo 我对这个答案中的“复制”一词并不感到满意。它不是一个复制,而只是另一个虚函数表。 - user207421
@CaptainGiraffe 这个答案中没有说明如果没有重写,虚函数表是否会被重用。RTTI 为单独使用虚函数表提供了另一个原因。这回答了你的问题吗? - user207421
显示剩余6条评论

2
通过 new 操作符创建 Child 类型的实例,然后 delete ...,Base 析构函数会被调用吗?
在原始问题代码中不会,因为您没有让 Child 继承 Base。
假设这是一个错误,并且我们进行了修复,即使它不是虚拟的,当你销毁 Child 的时候, ~Base 也会被调用,因为基类子对象是作为正常销毁序列的一部分被销毁的。
虚析构函数的原因是允许您通过 Base * 删除 Child 并正确地调用 ~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 的问题,它是否存在以及它在哪里是实现细节,您不需要担心。


1
每个具有虚方法(声明/继承)的类都有自己的虚表(vtable)。当一个虚方法(任何方法,不仅限于析构函数)被声明为虚函数时,派生类中该方法的所有覆盖都会自动成为虚函数。编译器还会在任何具有虚方法的这种类的开头添加_vptr。当创建类的对象时,_vptr将被填充,并指向该类的虚表。虚析构函数与任何其他虚函数处理方式相同。在您的示例中,由于~Base是虚拟的,因此~Child也将是虚拟的。
假设您执行以下操作:
Child *child = new (class Child);
delete(child);

在这里,child->_vptr将指向Child的虚函数表。因此,在删除时,将首先调用~Child,然后是~Base(以构造的相反顺序)。
或者,如果您这样做,
Base *base = new (class Child);
delete(base);

这里,base->_vptr将指向Child的虚函数表。因此,在删除时,将首先调用~Child,然后从虚函数表中调用~Base。
在gdb中,我们可以通过运行以下命令来验证_vptr:
"info vtbl base" or "print *base"

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