我对大部分的对象导向编程(OOP
)理论有扎实的了解,但有一件事情经常让我困惑,那就是虚析构函数。
我曾以为无论如何,析构函数都会被调用,并且每个对象都要调用一次。
什么时候需要将析构函数声明为虚函数?为什么需要这样做呢?
我对大部分的对象导向编程(OOP
)理论有扎实的了解,但有一件事情经常让我困惑,那就是虚析构函数。
我曾以为无论如何,析构函数都会被调用,并且每个对象都要调用一次。
什么时候需要将析构函数声明为虚函数?为什么需要这样做呢?
虚析构函数在通过指向基类的指针删除派生类实例时非常有用:
class Base
{
// some virtual methods
};
class Derived : public Base
{
~Derived()
{
// Do some important cleanup
}
};
在这里,你会注意到我没有将Base的析构函数声明为virtual
。现在,让我们看一下下面的代码片段:
Base *b = new Derived();
// use b
delete b; // Here's the problem!
由于Base的析构函数不是 virtual
,而且b是指向Derived
对象的Base*
,所以delete b
存在未定义行为:
在
delete b
中,如果要删除的对象的静态类型与其动态类型不同,则静态类型应为要删除的对象的动态类型的基类,并且静态类型必须具有虚析构函数,否则行为未定义。
在大多数实现中,调用析构函数的过程将像任何非虚函数一样被解析,这意味着将调用基类的析构函数而不是派生类的析构函数,导致资源泄漏。
总之,当希望通过多态方式操作时,务必使基类的析构函数为 virtual
。
如果想要防止通过基类指针删除实例,可以将基类的析构函数设为受保护的和非虚拟的。 这样做,编译器将不允许您在基类指针上调用delete
。
您可以在Herb Sutter的这篇文章中了解更多关于虚拟性和虚基类析构函数的信息。
Base
和Derived
都只有自动存储变量,也就是说,在析构函数中没有“特殊”的或额外的自定义代码要执行,那么可以不编写任何析构函数吗?还是派生类仍然会有内存泄漏? - bobobobo虚构造函数不可能,但虚析构函数是可以的。 让我们进行实验......
#include <iostream>
using namespace std;
class Base
{
public:
Base(){
cout << "Base Constructor Called\n";
}
~Base(){
cout << "Base Destructor called\n";
}
};
class Derived1: public Base
{
public:
Derived1(){
cout << "Derived constructor called\n";
}
~Derived1(){
cout << "Derived destructor called\n";
}
};
int main()
{
Base *b = new Derived1();
delete b;
}
以上代码输出以下内容:
Base Constructor Called
Derived constructor called
Base Destructor called
派生对象的构造遵循构造规则,但当我们删除“b”指针(基准指针)时,发现只调用了基类的析构函数。这不应该发生。为了做正确的事情,我们必须使基类的析构函数成为虚函数。 现在让我们看一下以下内容会发生什么:
#include <iostream>
using namespace std;
class Base
{
public:
Base(){
cout << "Base Constructor Called\n";
}
virtual ~Base(){
cout << "Base Destructor called\n";
}
};
class Derived1: public Base
{
public:
Derived1(){
cout << "Derived constructor called\n";
}
~Derived1(){
cout << "Derived destructor called\n";
}
};
int main()
{
Base *b = new Derived1();
delete b;
}
输出结果如下所示:
Base Constructor Called
Derived Constructor called
Derived destructor called
Base destructor called
因此,基指针的销毁(需要在派生对象上进行分配!)遵循销毁规则,即先销毁Derived,然后再销毁Base。 另一方面,并不存在所谓的虚构造函数。
在多态的基类中声明析构函数为虚函数。这是Scott Meyers在他的《Effective C++》中的第7条建议。Meyers继续总结道:如果一个类有任何虚函数,则它应该有一个虚析构函数,并且不是设计为基类或者不是设计为多态使用的类不应该声明虚析构函数。
const Base& = make_Derived();
。在这种情况下,即使它不是虚拟的,也会调用Derived
prvalue的析构函数,因此可以节省vtables/vpointers引入的开销。当然,范围相当有限。Andrei Alexandrescu在他的书现代C++设计中提到了这一点。 - vsoftcoBase * b = new Derived1
,这是所有问题的源头,而应该使用Derived1 * b = new Derived1
。此外,如果没有new
,就不会有问题,因为编译器总是知道任何超出作用域的实际类型。 - Ciro Santilli OurBigBook.com如果你的类是多态的,那么请将析构函数声明为虚函数。
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()
。
如果您不打算通过其基类指针删除对象,则不需要拥有虚析构函数。只需将其设置为 protected
,以防止意外调用:
// 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.
}
~Derived() = default
,在所有派生类中都需要显式声明 ~Derived()
吗?或者这是语言所暗示的(因此可以安全地省略)? - Ponkadoodlebase->~Base()
是否合适?根据您所说,Base::~Base()
不会被调用,那么就会出现内存泄漏。我是对的吗? - John当您希望通过基类指针删除对象时,不同的析构函数应该按正确顺序进行时,虚拟析构关键字是必需的。
例如:
Base *myObj = new Derived();
// Some code which is using myObj object
myObj->fun();
//Now delete the object
delete myObj ;
如果你的基类析构函数是虚函数,那么对象将按顺序被销毁(首先是派生对象,然后是基类)。如果你的基类析构函数不是虚函数,那么只有基类对象会被删除(因为指针是基类 "Base *myObj")。因此,派生对象会造成内存泄漏。 #include<iostream>
using namespace std;
class B{
public:
B(){
cout<<"B()\n";
}
virtual ~B(){
cout<<"~B()\n";
}
};
class D: public B{
public:
D(){
cout<<"D()\n";
}
~D(){
cout<<"~D()\n";
}
};
int main(){
B *b = new D();
delete b;
return 0;
}
OUTPUT:
B()
D()
~D()
~B()
==============
If you don't give ~B() as virtual. then output would be
B()
D()
~B()
where destruction of ~D() is not done which leads to leak
delete
会导致未定义的行为。 - James Adkison我喜欢思考接口和接口实现。在C++中,接口是纯虚类。析构函数是接口的一部分,需要被实现。因此,析构函数应该是纯虚的。那么构造函数呢?构造函数实际上不是接口的一部分,因为对象总是显式地实例化。
virtual
,则即使在派生类中没有声明为 virtual
,它也会自动成为 virtual
。 - DavidRRfinal
,则应为其定义虚析构函数。
我知道这看起来像是过度保守的规则。但是,这是唯一确保从您的类派生的人在使用基指针删除时不会有UB的方法。例如,在下面的程序中,基类B没有任何虚函数,因此根据Meyer,您不需要编写虚析构函数。 但是,如果不这样做,您将具有UB:如果一个类有任何虚函数,则应该有一个虚析构函数,并且不设计为基类或不设计为多态使用的类不应声明虚析构函数。
#include <iostream>
struct A
{
~A()
{
std::cout << "A::~A()" << std::endl;
}
};
struct B
{
};
struct C : public B
{
A a;
};
int main(int argc, char *argv[])
{
B *b = new C;
delete b; // UB, and won't print "A::~A()"
return 0;
}
_down_
都会被调用。virtual
确保它从顶部开始而不是中间开始。 - Mooing Duck