如果我将一个基类的析构函数从非虚拟更改为虚拟,会发生什么?

Yua*_*Wen 21 c++ virtual destructor virtual-destructor

我遇到了一个基类,它的析构函数是非虚拟的,尽管基类有1个虚函数fv().该基类也有许多子类.其中许多子类定义了它自己的子类fv().

我不知道程序中如何使用基类和子类的细节.我只知道程序工作正常,即使基类的析构函数应该是虚拟的.

我想将基类的析构函数从非虚拟更改为虚拟.但我不确定后果.那么,会发生什么?在更改程序后,我还需要做些什么来确保程序正常工作?

跟进:在我将基类的析构函数从非虚拟更改为虚拟后,程序失败了一个测试用例.
结果让我很困惑.因为如果基类的析构函数不是虚拟的,那么程序将不会使用基类的多态.因为如果没有,它会导致未定义的行为.例如,Base *pb = new Sub.
所以,我认为如果我将析构函数从非虚拟更改为虚拟,它不应该导致更多的错误.

Res*_*ion 7

除非存在其他问题,否则析构函数的虚拟性不会破坏现有代码中的任何内容.它甚至可以解决一些问题(见下文).但是,该类可能不是设计为多态的,因此在其析构函数中添加virtual可以使其具有多态性,这可能是不可取的.然而,您应该能够安全地向析构函数添加虚拟性,它本身不会引起任何问题.

说明

多态性允许这样:

class A 
{
public:
    ~A() {}
};

class B : public A
{
    ~B() {}

    int i;
};

int main()
{
    A *a = new B;
    delete a;
}
Run Code Online (Sandbox Code Playgroud)

您可以将指针指向实际类型A的类类型的对象B.这对于例如分割接口(例如A)和实现(例如B)是有用的.然而会发生delete a;什么?

部分a类型的对象A被破坏.但那种类型B呢?此外,该部分有资源,他们需要被释放.那就是内存泄漏.通过调用delete a;你调用类型的析构函数A(因为a是一个指向类型的指针A),基本上你调用a->~a();.B永远不会调用类型的析构函数.怎么解决这个?

class A :
{
public:
    virtual ~A() {}
};
Run Code Online (Sandbox Code Playgroud)

通过向A析构函数添加虚拟调度(请注意,通过声明基本析构函数virtual,它会自动使所有派生类的析构函数为虚拟,即使未声明这样也是如此).然后调用delete a;将析构函数的调用分配到虚拟表中以找到要使用的正确析构函数(在本例中为类型B).析构函数将像往常一样调用父析构函数.干净吧?

可能的问题

正如你所看到的,你不能破坏任何东西本身.但是,您的设计可能存在不同的问题.例如,可能有一个错误"依赖"您通过虚拟化而暴露的析构函数的非虚拟调用,请考虑:

int main()
{
    B *b = new B;
    A *a = b;
    delete a;
    b->i = 10; //might work without virtual destructor, also undefined behvaiour
}
Run Code Online (Sandbox Code Playgroud)

基本上是对象切片,但由于之前没有虚拟析构函数,因此B部分创建的对象未被销毁,因此赋值i可能有效.如果你使析构函数成为虚拟的,那么它就不存在了,它可能会崩溃或做任何事情(未定义的行为).

像这样的事情可能会发生,在复杂的代码中可能很难找到.但是如果你的析构函数在虚拟化之后导致崩溃,你可能会在那里的某个地方出现这样的错误,你可以在那里开始使用它,因为正如我所说的那样,只需使析构函数虚拟化就不会破坏任何东西.

  • `delete a;`在你的最后一个例子中,如果`A`的析构函数不是虚拟的,则触发UB.关于"我"或其他任何事物的生存都没有任何保证. (2认同)

Yam*_*vic 6

看看这里,

struct Component {
    int* data;

    Component() { data = new int[100]; std::cout << "data allocated\n"; }
    ~Component() { delete[] data; std::cout << "data deleted\n"; }
};

struct Base {
    virtual void f() {}
};

struct Derived : Base {
    Component c;
    void f() override {}

};

int main()
{
    Base* b = new Derived;
    delete b;
}
Run Code Online (Sandbox Code Playgroud)

输出:

分配的数据

但没有删除.

结论

每当类层次结构具有状态时,在纯技术级别上,您需要从顶部开始一直使用虚拟析构函数.

有可能一旦您将虚拟析构函数添加到类中,就会触发未经测试的销毁逻辑.这里理智的选择是保留您添加的虚拟析构函数,并修复逻辑.否则,您的进程中将出现资源和/或内存泄漏.

技术细节

这个例子中发生的事情是,虽然Base有一个vtable,但它的析构函数本身并不是虚拟的,这意味着无论什么时候Base::~Base()被调用,它都不会通过vptr.换句话说,它只是调用Base::Base(),就是这样.

main()函数中,Derived分配新对象并将其分配给类型的变量Base*.当下一个delete语句运行时,它实际上首先尝试调用直接传递类型的析构函数,这是简单的Base*,然后它释放该对象占用的内存.现在,由于编译器发现它Base::~Base()不是虚拟的,因此它不会尝试通过对象的vptrd.这意味着Derived::~Derived()任何人都不会调用它.但是既然Derived::~Derived()是编译器生成破坏的地方Component Derived::c,那么该组件也永远不会被破坏.因此,我们从未看到打印过的数据.

如果Base::~Base()是虚拟的,那么会发生的事情是该delete d语句将通过对象的vptrd,调用析构函数,Derived::~Derived().根据定义,析构函数将首先调用Base::~Base()(这是由编译器自动生成的),然后销毁其内部状态,即Component c.因此,整个销毁过程将按预期完成.

  • 例如,有可能你的派生类之一有一个成员,比如我的例子中的`Component`,其破坏逻辑在特定的上下文中是错误的,例如你的测试失败.现在,因为之前它甚至没有被调用,所以你通过了测试,但是你可能泄漏了资源和/或内存. (4认同)
  • @YuanWen:这可能意味着有人依赖于某些数据成员被泄露的事实,或者派生类的某些破坏逻辑有问题(并且程序运行正常,因为错误地未调用它). (4认同)

Pet*_*ter 6

这显然取决于你的代码在做什么.

一般来说,virtual只有你有类似的用法,才需要制作基类的析构函数

 Base *base = new SomeDerived;
    // whatever
 delete base;
Run Code Online (Sandbox Code Playgroud)

使用非虚拟析构函数Base会导致上述内容显示未定义的行为.使析构函数虚拟化可消除未定义的行为.

但是,如果你做了类似的事情

{   // start of some block scope

     Derived derived;

      //  whatever

}
Run Code Online (Sandbox Code Playgroud)

然后,析构函数不必是虚拟的,因为行为已被很好地定义(析构函数Derived及其基数以其构造函数的相反顺序调用).

如果改变从非析构函数virtualvirtual原因测试用例失败,那么你需要检查测试用例明白为什么.一种可能性是测试用例依赖于某些未定义行为的特定咒语 - 这意味着测试用例存在缺陷,并且在不同情况下可能无法成功(例如,使用不同的编译器构建程序).但是,如果没有看到测试用例(或代表它的MCVE),我会毫不犹豫地宣称它依赖于未定义的行为


Jar*_*d42 3

您可以“安全地”添加virtual到析构函数。

如果调用了等效函数,则可以修复未定义行为(UB)delete base,然后调用正确的析构函数。如果子类析构函数有错误,那么您可以通过其他错误更改 UB。

  • @YuanWen 如果行为发生变化,则表明析构函数应该是虚拟的。因此,您需要修复代码中的其他错误。 (7认同)
  • 是的,如果使析构函数虚拟化会导致错误,那么有些事情看起来很可疑...... (2认同)
  • 也许无论是谁一开始编写它都发现,如果他们偷偷地使析构函数不是虚拟的,他们就可以假装修复了原来的错误。在这种情况下:玩得开心! (2认同)