[英]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()
不是虚拟的,因此它不会尝试通过对象d
的vptr 。 这意味着任何人都不会调用Derived::~Derived()
。 但由于Derived::~Derived()
是编译器生成Component
Derived::c
销毁的地方,因此该组件也不会被销毁。 因此,我们从未看到打印过的数据 。
如果Base::~Base()
是虚拟的,那么delete d
语句将通过对象d
的vptr ,调用析构函数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或标准布局类型的任何东西,例如
将是UB。
我知道一个类型的确切情况
那就是当对象符合外部ABI时。 Windows上的所有COM接口都满足所有四个条件。 这不是未定义的行为,而是与虚拟调度机制有关的非便携式特定于实现的保证。
无论您的操作系统如何,它都归结为“单一定义规则”。 除非根据新定义重新编译每段代码,否则无法修改类型。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.