如果我把一个基类的析构函数从非虚改为虚,会发生什么呢?
If I change the destructor of one base class from non-virtual to virtual, what will happen?
我遇到一个基类,它的析构函数是非虚函数,尽管这个基类有一个虚函数fv()
。这个基类也有许多子类。这些子类中有许多定义了自己的fv()
。
我不知道程序中如何使用基类和子类的细节。我只知道程序工作良好,即使基类的析构函数应该是虚的。
我想把基类的析构函数从非虚函数改为虚函数。但我不确定后果。那么,会发生什么呢?我还需要做些什么来确保程序在我更改后正常工作?
跟进:在我将基类的析构函数从非虚改为虚之后,程序在一个测试用例中失败了。
结果让我很困惑。因为如果基类的析构函数不是虚函数,那么程序就不会使用基类的多态性。因为如果不是,就会导致未定义行为。例如:Base *pb = new Sub
。
因此,我认为如果我将析构函数从非虚函数改为虚函数,应该不会导致更多的bug。
除非存在其他问题,否则析构函数的虚拟性不会破坏现有代码中的任何内容。它甚至可能解决一些问题(见下文)。然而,类可能不是被设计为多态的,所以将virtual添加到它的析构函数中使它成为可多态的,这可能是不可取的。然而,你应该能够安全地将虚拟性添加到析构函数中,并且它本身应该不会引起问题。
多态性允许这样做:
class A
{
public:
~A() {}
};
class B : public A
{
~B() {}
int i;
};
int main()
{
A *a = new B;
delete a;
}
您可以获取指向类的A
类型对象的指针,该对象实际上是B
类型。这对于拆分接口(如A
)和实现(如B
)是很有用的。但是delete a;
会发生什么呢?
类型为A
的对象a
的部分被销毁。但是B
类型的部分呢?另外,那部分有资源,需要释放。这就是内存泄漏。通过调用delete a;
,您调用了A
类型的析构函数(因为a
是指向A
类型的指针),基本上您调用了a->~a();
。永远不会调用B
类型的析构函数。如何解决这个问题?
class A :
{
public:
virtual ~A() {}
};
通过在A
的析构函数中添加虚拟分派(注意,通过声明基类析构函数为virtual,它会自动使所有派生类的析构函数为virtual,即使没有这样声明)。然后,对delete a;
的调用将把析构函数的调用分派到虚表中,以找到要使用的正确析构函数(在本例中为B
类型)。该析构函数将像往常一样调用父析构函数。整洁的,对吧?
可能出现的问题
如您所见,这样做本身不会破坏任何东西。然而,在您的设计中可能存在不同的问题。例如,可能存在一个"依赖于"通过将析构函数设置为虚函数而暴露的非虚函数调用的bug,请考虑: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 allocatedn"; }
~Component() { delete[] data; std::cout << "data deletedn"; }
};
struct Base {
virtual void f() {}
};
struct Derived : Base {
Component c;
void f() override {}
};
int main()
{
Base* b = new Derived;
delete b;
}
输出:数据分配
但未删除。
结论当一个类层次结构有状态时,在纯粹的技术层面上,你需要一个从头到脚的虚析构函数。
一旦您向类中添加了虚析构函数,就可能触发了未经测试的析构逻辑。这里同样的选择是保留已添加的虚析构函数,并修复逻辑。否则,您将在进程中出现资源和/或内存泄漏。
技术细节示例中发生的情况是,虽然Base
有虚函数表,但它的析构函数本身不是虚函数,这意味着无论何时调用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
,可以修复未定义行为(UB),然后调用正确的析构函数。如果子类析构函数有bug,那么您可以通过另一个bug更改UB。
可以改变的是类的布局类型。添加虚析构函数可以将类从标准布局类型更改为非标准布局类型。所以你所依赖的类是一个POD或标准布局类型,例如
- 内存或围绕 的内存类
- 传递给C函数
为UB。
我只知道一个类型是
- 用作基类
- 有虚函数
- 析构函数不是虚函数
- 使析构函数虚化会破坏东西
,即对象符合外部ABI。Windows上的所有COM接口都满足这四个标准。这不是未定义的行为,而是关于虚拟调度机制的不可移植的特定于实现的保证。
不管你的操作系统是什么,它都归结为"单一定义规则"。除非根据新的定义重新编译使用该类型的每段代码,否则不能修改该类型。
- 什么时候调用组成单元对象的析构函数
- 什么时候调用析构函数
- C++:使用方法调用析构函数的顺序是什么?
- 当我从 std::vector 中的新放置调用析构函数时会发生什么?
- 在析构函数中删除单链表的正确方法是什么?
- 拥有"受保护的非虚拟析构函数"与"受保护虚拟析构构函数"有什么好处
- 什么是带有友元说明符的析构函数
- 我们什么时候应该在 C++ 中将析构函数声明为 =DELETE
- 当声明了虚拟析构函数但没有实现时会发生什么情况
- 我不明白析构函数有什么问题?
- 在C++中为临时库调用析构函数的顺序是什么
- 如果我在析构函数中创建一个对象,会发生什么
- 有人可以解释一下这里发生了什么(类和构造函数/析构函数)吗?
- 虚拟析构函数的用途是什么
- 可观察行为和未定义行为 -- 如果我不调用析构函数会发生什么?
- Qt:写这个类的析构函数的正确和安全的方法是什么
- 什么应该在一个适当的析构函数中
- 在C++中,析构函数的调用顺序和成员变量的销毁顺序是什么
- 我们什么时候必须在派生类 c++ 中定义析构函数
- "= default"析构函数和空析构函数有什么区别?