Yua*_*Wen 21 c++ virtual destructor virtual-destructor
我遇到了一个基类,它的析构函数是非虚拟的,尽管基类有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;
}
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可能有效.如果你使析构函数成为虚拟的,那么它就不存在了,它可能会崩溃或做任何事情(未定义的行为).
像这样的事情可能会发生,在复杂的代码中可能很难找到.但是如果你的析构函数在虚拟化之后导致崩溃,你可能会在那里的某个地方出现这样的错误,你可以在那里开始使用它,因为正如我所说的那样,只需使析构函数虚拟化就不会破坏任何东西.
看看这里,
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.因此,整个销毁过程将按预期完成.
这显然取决于你的代码在做什么.
一般来说,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及其基数以其构造函数的相反顺序调用).
如果改变从非析构函数virtual来virtual原因测试用例失败,那么你需要检查测试用例明白为什么.一种可能性是测试用例依赖于某些未定义行为的特定咒语 - 这意味着测试用例存在缺陷,并且在不同情况下可能无法成功(例如,使用不同的编译器构建程序).但是,如果没有看到测试用例(或代表它的MCVE),我会毫不犹豫地宣称它依赖于未定义的行为
您可以“安全地”添加virtual到析构函数。
如果调用了等效函数,则可以修复未定义行为(UB)delete base,然后调用正确的析构函数。如果子类析构函数有错误,那么您可以通过其他错误更改 UB。