多态类中的虚析构函数

4
我知道,当您拥有一个多态的基类时,基类应该定义一个虚析构函数。这样,当删除指向派生类对象的基类指针时,它将首先调用派生类的析构函数。如果我理解错误,请纠正我。
此外,如果基类的析构函数不是虚函数,则删除指向派生对象的基类指针将导致未定义的行为。如果我也理解错误,请纠正我。
所以我的问题是:为什么当基类析构函数是非虚函数时,对象不能被正确销毁?
我假设这是因为虚函数有一种表格,每当调用虚函数时,就会记住并查询该表格。编译器知道当对象应该被删除时,它应该首先调用派生析构函数。
我的假设是正确的吗?

1
你读过这个吗:https://dev59.com/IHRB5IYBdhLWcg3w77on?rq=1? - EdChum
@EdChum 是的,我确实读了它,但它并没有完全回答我的假设。这就是为什么我问了,但我在路上解决了。 - CantThinkOfAnything
2个回答

7
如果在删除对象时,变量的静态类型是基本类型,则将调用基本类型的析构函数,但不会调用子类的析构函数(因为它不是虚拟的)。因此,由基类分配的资源将被释放,但由子类分配的资源不会被释放。
因此,对象将无法正确地被销毁。关于那个表格,你说得对:它被称为虚方法表或“vtable”。但非虚析构函数的结果不是析构函数没有按正确顺序调用,而是子类的析构函数根本没有被调用!

啊,明白了。这是因为虚函数表所持有的虚函数。这样C++就知道在析构函数是虚函数时该调用哪个析构函数。当它不是虚函数时,它只会调用静态类型的析构函数。 - CantThinkOfAnything
3
正确,+1。从技术上讲,虚函数表只是实现虚函数调用的一种可能方式(尽管迄今为止是最常见的一种)。从标准C++的角度来看,只有规定的行为才是重要的。如何实现取决于编译器/平台ABI。这就是为什么标准简单地说“行为未定义”-在不同的虚拟调度实现下,可能会发生其他事情而不是“静态类型析构函数”。 - Angew is no longer proud of SO

6

考虑一下

struct Base {
    void f() { printf("Base::f"); }
};
struct Derived : Base {
    void f() { printf("Derived::f"); }
};
Base* p = new Derived;
p->f();

这段代码打印出了 Base::f,因为 Base::f 不是虚函数。现在让我们来看一下析构函数:

struct Base {
    ~Base() { printf("Base::~Base"); }
};
struct Derived : Base {
    ~Derived() { printf("Derived::~Derived"); }
};
Base* p = new Derived;
p->~Base();

这将打印出 Base::~Base。现在,如果我们使析构函数成为虚函数,那么和其他任何虚函数一样,将会调用对象动态类型中的最终覆盖者。一个析构函数会覆盖基类中的虚析构函数(即使它的“名称”不同):

struct Base {
    virtual ~Base() { printf("Base::~Base"); }
};
struct Derived : Base {
    ~Derived() override { printf("Derived::~Derived"); }
};
Base* p = new Derived;
p->~Base();

调用

p->~Base()

实际上调用了 Derived::~Derived()。由于这是一个析构函数,在它的主体执行完后,它会自动调用基类和成员的析构函数。因此输出为:
Derived::~Derived
Base::~Base

现在,一个delete-expression通常等同于一个析构函数调用后跟着一个内存释放函数的调用。在这个特定的情况下,表达式为:
delete p;

等同于

p->~Base();
::operator delete(p);

因此,如果析构函数是虚拟的,那么它会做正确的事情:首先调用Derived::~Derived,然后在完成后自动调用Base::~Base。如果析构函数不是虚拟的,则可能的结果是只调用Base::~Base


你的派生结构体没有继承你的基础结构体。Base* p = new Derived 无法编译。 - Ludwig Schulze
@LudwigSchulze 哎呀,这就是我凌晨 2 点写答案的下场。我会修复它的。 - Brian Bi

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