我对大多数OOP理论都有很好的理解,但最让我困惑的是虚拟析构函数。
我以为析构函数总是被调用,不管是什么,也不管是链中的每个对象。
你打算什么时候让它们虚拟化?为什么?
我对大多数OOP理论都有很好的理解,但最让我困惑的是虚拟析构函数。
我以为析构函数总是被调用,不管是什么,也不管是链中的每个对象。
你打算什么时候让它们虚拟化?为什么?
当前回答
关于virtual的一个基本定义是它确定类的成员函数是否可以在其派生类中被覆盖。
类的D-tor基本上在作用域的末尾被调用,但存在一个问题,例如,当我们在堆(动态分配)上定义一个实例时,我们应该手动删除它。
一旦指令被执行,就会调用基类析构函数,但不会调用派生的析构函数。
一个实际的例子是,在控制场中,你必须操纵效应器和致动器。
在范围结束时,如果没有调用其中一个动力元件(执行器)的析构函数,将产生致命的后果。
#include <iostream>
class Mother{
public:
Mother(){
std::cout<<"Mother Ctor"<<std::endl;
}
virtual~Mother(){
std::cout<<"Mother D-tor"<<std::endl;
}
};
class Child: public Mother{
public:
Child(){
std::cout<<"Child C-tor"<<std::endl;
}
~Child(){
std::cout<<"Child D-tor"<<std::endl;
}
};
int main()
{
Mother *c = new Child();
delete c;
return 0;
}
其他回答
我认为讨论“未定义”行为,或者至少讨论在通过没有虚拟析构函数的基类(/struct)删除时可能发生的“崩溃”未定义行为,或者更准确地说,没有vtable,是有益的。下面的代码列出了一些简单的结构(类也是如此)。
#include <iostream>
using namespace std;
struct a
{
~a() {}
unsigned long long i;
};
struct b : a
{
~b() {}
unsigned long long j;
};
struct c : b
{
~c() {}
virtual void m3() {}
unsigned long long k;
};
struct d : c
{
~d() {}
virtual void m4() {}
unsigned long long l;
};
int main()
{
cout << "sizeof(a): " << sizeof(a) << endl;
cout << "sizeof(b): " << sizeof(b) << endl;
cout << "sizeof(c): " << sizeof(c) << endl;
cout << "sizeof(d): " << sizeof(d) << endl;
// No issue.
a* a1 = new a();
cout << "a1: " << a1 << endl;
delete a1;
// No issue.
b* b1 = new b();
cout << "b1: " << b1 << endl;
cout << "(a*) b1: " << (a*) b1 << endl;
delete b1;
// No issue.
c* c1 = new c();
cout << "c1: " << c1 << endl;
cout << "(b*) c1: " << (b*) c1 << endl;
cout << "(a*) c1: " << (a*) c1 << endl;
delete c1;
// No issue.
d* d1 = new d();
cout << "d1: " << d1 << endl;
cout << "(c*) d1: " << (c*) d1 << endl;
cout << "(b*) d1: " << (b*) d1 << endl;
cout << "(a*) d1: " << (a*) d1 << endl;
delete d1;
// Doesn't crash, but may not produce the results you want.
c1 = (c*) new d();
delete c1;
// Crashes due to passing an invalid address to the method which
// frees the memory.
d1 = new d();
b1 = (b*) d1;
cout << "d1: " << d1 << endl;
cout << "b1: " << b1 << endl;
delete b1;
/*
// This is similar to what's happening above in the "crash" case.
char* buf = new char[32];
cout << "buf: " << (void*) buf << endl;
buf += 8;
cout << "buf after adding 8: " << (void*) buf << endl;
delete buf;
*/
}
我并不是建议你是否需要虚拟析构函数,尽管我认为一般来说,拥有它们是一个很好的做法。我只是指出了如果基类(/struct)没有vtable,而派生类(/struck)有vtable,并且通过基类(/ststruct)指针删除对象,那么可能会导致崩溃的原因。在这种情况下,传递给堆的空闲例程的地址是无效的,因此是崩溃的原因。
如果运行上述代码,您将清楚地看到问题发生的时间。当基类(/struct)的this指针与派生类(/struct)的this指示器不同时,您将遇到此问题。在上面的示例中,结构a和b没有vtables。结构c和d确实有vtables。因此,指向c或d对象实例的a或b指针将被修复以说明vtable。如果传递此a或b指针进行删除,则会由于地址对堆的空闲例程无效而崩溃。
如果计划从基类指针中删除具有vtable的派生实例,则需要确保基类具有vtable。一种方法是添加一个虚拟析构函数,您可能无论如何都希望它能正确地清理资源。
当您可能通过指向基类的指针删除派生类的实例时,虚拟析构函数非常有用:
class Base
{
// some virtual methods
};
class Derived : public Base
{
~Derived()
{
// Do some important cleanup
}
};
在这里,您会注意到我没有将Base的析构函数声明为虚拟。现在,让我们看一下以下片段:
Base *b = new Derived();
// use b
delete b; // Here's the problem!
由于Base的析构函数不是虚拟的,而b是指向派生对象的Base*,因此删除b具有未定义的行为:
[在delete b]中,如果要删除的对象与其动态类型不同,静态类型应为对象的动态类型的基类删除,静态类型应具有虚拟析构函数或行为未定义。
在大多数实现中,对析构函数的调用将像任何非虚拟代码一样被解析,这意味着将调用基类的析构函数,而不是派生类的析构器,从而导致资源泄漏。
总之,当基类的析构函数要以多态方式操作时,请始终将其设为虚拟。
如果要防止通过基类指针删除实例,可以使基类析构函数受保护且非虚拟;通过这样做,编译器不会允许您在基类指针上调用delete。
在本文中,您可以从HerbSutter了解更多关于虚拟性和虚拟基类析构函数的信息。
我喜欢思考接口和接口的实现。在C++中,speak接口是纯虚拟类。析构函数是接口的一部分,需要实现。因此析构函数应该是纯虚拟的。构造函数呢?构造函数实际上不是接口的一部分,因为对象总是显式实例化的。
通过指向基类的指针调用析构函数
struct Base {
virtual void f() {}
virtual ~Base() {}
};
struct Derived : Base {
void f() override {}
~Derived() override {}
};
Base* base = new Derived;
base->f(); // calls Derived::f
base->~Base(); // calls Derived::~Derived
虚拟析构函数调用与任何其他虚拟函数调用都没有区别。
对于base->f(),调用将被分派到Derived::f()中,对于base->~base()也是如此-它的重写函数-将调用Derived::~Derived()。
间接调用析构函数时也会发生同样的情况,例如delete base;。delete语句将调用base->~base(),该函数将被分派到Derived::~Derived()。
具有非虚拟析构函数的抽象类
若您不打算通过指向其基类的指针删除对象,那个么就不需要使用虚拟析构函数。只需保护它,使其不会被意外调用:
// library.hpp
struct Base {
virtual void f() = 0;
protected:
~Base() = default;
};
void CallsF(Base& base);
// CallsF is not going to own "base" (i.e. call "delete &base;").
// It will only call Base::f() so it doesn't need to access Base::~Base.
//-------------------
// application.cpp
struct Derived : Base {
void f() override { ... }
};
int main() {
Derived derived;
CallsF(derived);
// No need for virtual destructor here as well.
}
虚拟基类析构函数是“最佳实践”——您应该始终使用它们来避免(难以检测)内存泄漏。使用它们,可以确保类的继承链中的所有析构函数都被调用(按正确的顺序)。使用虚拟析构函数从基类继承也会使继承类的析构函数自动虚拟化(因此不必在继承类析构函数声明中重新键入“virtual”)。