我对大多数OOP理论都有很好的理解,但最让我困惑的是虚拟析构函数。
我以为析构函数总是被调用,不管是什么,也不管是链中的每个对象。
你打算什么时候让它们虚拟化?为什么?
我对大多数OOP理论都有很好的理解,但最让我困惑的是虚拟析构函数。
我以为析构函数总是被调用,不管是什么,也不管是链中的每个对象。
你打算什么时候让它们虚拟化?为什么?
当前回答
我认为这个问题的核心是关于虚拟方法和多态性,而不是具体的析构函数。下面是一个更清晰的例子:
class A
{
public:
A() {}
virtual void foo()
{
cout << "This is A." << endl;
}
};
class B : public A
{
public:
B() {}
void foo()
{
cout << "This is B." << endl;
}
};
int main(int argc, char* argv[])
{
A *a = new B();
a->foo();
if(a != NULL)
delete a;
return 0;
}
将打印出:
This is B.
如果没有虚拟,它将打印出:
This is A.
现在您应该了解何时使用虚拟析构函数。
其他回答
当您希望不同的析构函数在通过基类指针删除对象时遵循正确的顺序时,析构函数的虚拟关键字是必需的。例如:
Base *myObj = new Derived();
// Some code which is using myObj object
myObj->fun();
//Now delete the object
delete myObj ;
如果基类析构函数是虚拟的,那么对象将按顺序被析构函数(首先是派生对象,然后是基)。如果基类析构函数不是虚拟的,那么只有基类对象会被删除(因为指针是基类“base*myObj”)。因此派生对象将存在内存泄漏。
当您可能通过指向基类的指针删除派生类的实例时,虚拟析构函数非常有用:
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了解更多关于虚拟性和虚拟基类析构函数的信息。
将所有析构函数都设为虚拟,除非你有充分的理由不这样做。
否则会发生这样的邪恶:
假设您有一个包含Apple和Orange对象的Fruit指针数组。
从Fruit对象集合中删除时,除非~Fruit()是虚拟的,否则无法调用~Apple()和~Orange()。
正确完成示例:
#include <iostream>
using namespace std;
struct Fruit { // good
virtual ~Fruit() { cout << "peel or core should have been tossed" << endl; }
};
struct Apple: Fruit { virtual ~Apple() {cout << "toss core" << endl; } };
struct Orange: Fruit { virtual ~Orange() {cout << "toss peel" << endl; } };
int main() {
Fruit *basket[]={ new Apple(), new Orange() };
for (auto fruit: basket) delete fruit;
};
正品产出量
toss core
peel or core should have been tossed
toss peel
peel or core should have been tossed
错误示例:
#include <iostream>
using namespace std;
struct Fruit { // bad
~Fruit() { cout << "peel or core should have been tossed" << endl; }
};
struct Apple: Fruit { virtual ~Apple() {cout << "toss core" << endl; } };
struct Orange: Fruit { virtual ~Orange() {cout << "toss peel" << endl; } };
int main() {
Fruit *basket[]={ new Apple(), new Orange() };
for (auto fruit: basket) delete fruit;
};
不良输出
peel or core should have been tossed
peel or core should have been tossed
(注意:为了简洁起见,我使用了struct,通常使用class并指定public)
我认为这里的大多数答案都没有抓住重点,除了公认的答案,这是一件好事。然而,让我再补充一个对这个问题有不同看法的问题:如果你想多态地删除这个类的实例,你需要一个虚拟析构函数。
这种方式回避了这个问题,所以让我详细说明一下:正如许多人所指出的,如果调用delete base_ptr并且析构函数不是虚拟的,就会出现不希望的行为。然而,有几个假设需要明确:
如果您的类不是基类,那么希望您不会编写这样的代码。在本例中,我不是指手动内存管理,它本身就很糟糕,而是从这个类中公开派生出来的。不应继承未设计为基类的类,例如std::string。C++可以让你射自己的脚。这是你的错,而不是基类没有虚拟析构函数。如果析构函数不可访问(受保护的或私有的),则此代码不会编译,因此不会出现不希望的行为。有一个受保护的析构函数是有用的,特别是对于mixin,但对于接口(在较小程度上)也是有用的。除非您实际使用了虚拟函数,否则您不希望产生虚拟函数的开销。相反,使析构函数受到保护可以防止不期望的行为,但不会限制您的行为。如果您实际上编写了一个应该派生自的类,那么通常都会有虚拟函数。作为它们的用户,通常只能通过指向基类的指针来使用它们。当这种使用包括处理它们时,它也需要是多态的。当您应该将析构函数设为虚拟时,就会出现这种情况。
对于这个主题的一个类似的不同观点,也可以阅读“什么时候不应该使用虚拟析构函数?”?
虚拟基类析构函数是“最佳实践”——您应该始终使用它们来避免(难以检测)内存泄漏。使用它们,可以确保类的继承链中的所有析构函数都被调用(按正确的顺序)。使用虚拟析构函数从基类继承也会使继承类的析构函数自动虚拟化(因此不必在继承类析构函数声明中重新键入“virtual”)。