繁体   English   中英

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

[英]If I change the destructor of one base class from non-virtual to virtual, what will happen?

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

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

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

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

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

说明

多态性允许这样:

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

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

    int i;
};

int main()
{
    A *a = new B;
    delete a;
}

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

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

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

通过向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
}

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

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

看看这里

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;
}

输出:

分配的数据

但没有删除

结论

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

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

技术细节

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

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

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

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

一般而言,只有具有类似的用法,才需要使基类的析构函数成为virtual

 Base *base = new SomeDerived;
    // whatever
 delete base;

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

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

{   // start of some block scope

     Derived derived;

      //  whatever

}

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

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

如果从基类派生的人改变了类资源的所有权策略,它可能会破坏一些测试:

struct A 
{ 
    int * data; // does not take ownership of data
    A(int* d) : data(d) {}
     ~A() { }
};

struct B : public A // takes ownership of data
{
    B(int * d) : A (d) {}
    ~B() { delete data; }
};

用法:

int * t = new int(8);
{
    A* a = new B(t);
    delete a;
}
cout << *t << endl;

这里使A的析构函数虚拟将导致UB。 但我不认为这种用法可以称为良好做法。

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

如果调用了等效的delete base ,则可以修复Undefined Behavior(UB),然后调用右析构函数。 如果子类析构函数是错误的,那么您可以通过其他错误更改UB。

可能改变的一件事是类的布局类型。 添加虚拟析构函数可以将类从标准布局类型更改为非标准布局类。 所以你依赖于POD或标准布局类型的任何东西,例如

  • memcpy或memmve类
  • 传递给C函数

将是UB。

我知道一个类型的确切情况

  • 用作基类
  • 有虚拟功能
  • 析构函数不是虚拟的
  • 使析构函数虚拟破坏事物

那就是当对象符合外部ABI时。 Windows上的所有COM接口都满足所有四个条件。 这不是未定义的行为,而是与虚拟调度机制有关的非便携式特定于实现的保证。

无论您的操作系统如何,它都归结为“单一定义规则”。 除非根据新定义重新编译每段代码,否则无法修改类型。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM