我对大多数OO理论有了深刻的理解,但让我困惑的一件事是虚拟析构函数.
我认为无论什么以及链中的每个对象,析构函数总是会被调用.
你什么时候打算让它们成为虚拟的?为什么?
当您可能通过指向基类的指针删除派生类的实例时,虚拟析构函数很有用:
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
是Base*
指向Derived
对象,因此delete b
具有未定义的行为:
[In
delete b
],如果要删除的对象的静态类型与其动态类型不同,则静态类型应为要删除的对象的动态类型的基类,静态类型应具有虚拟析构函数或者行为未定义.
在大多数实现中,对析构函数的调用将像任何非虚拟代码一样被解析,这意味着将调用基类的析构函数,但不会调用派生类的析构函数,从而导致资源泄漏.
总而言之,virtual
当它们被多态地操纵时,总是使基类的析构函数成为可能.
如果要防止通过基类指针删除实例,可以使基类析构函数受保护且非虚拟; 通过这样做,编译器将不允许您调用delete
基类指针.
您可以在Herb Sutter的这篇文章中了解有关虚拟性和虚拟基类析构函数的更多信息.
虚拟构造函数是不可能的,但虚拟析构函数是可能的.让我们实验......
#includeusing 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"指针(基指针)时,我们发现只有基本析构函数被调用.但这不能发生.要做适当的事情,我们必须使基础析构函数成为虚拟的.现在让我们看看下面发生了什么:
#includeusing 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
因此,基指针的破坏(在派生对象上进行分配!)遵循破坏规则,即首先导出然后是基数.另一方面,对于构造函数,没有像虚构造函数那样的东西.
在多态基类中声明析构函数是虚拟的.这是Scott Meyers的Effective C++中的第7项.迈尔斯继续总结,如果一个类有任何虚函数,它应该有一个虚析构函数,而不是类设计为基类或不是设计用于多态应不声明虚析构函数.
还要注意,在没有虚析构函数时删除基类指针将导致未定义的行为.我刚刚学到的东西:
如何覆盖C++中的删除行为?
我已经使用C++多年了,我仍然设法挂起自己.
当您的类具有多态性时,使析构函数成为虚拟的.
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. }
我喜欢考虑接口的接口和实现.在C++中,speak接口是纯虚拟类.析构函数是界面的一部分,有望实现.因此析构函数应该是纯虚拟的.构造函数怎么样?构造函数实际上不是接口的一部分,因为对象始终是显式实例化的.
简单来说,当您删除指向派生类对象的基类指针时,Virtual析构函数将以正确的顺序销毁资源.
#includeusing 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
当您希望不同的析构函数在通过基类指针删除对象时遵循正确的顺序时,析构函数的虚拟关键字是必需的.例如:
Base *myObj = new Derived(); // Some code which is using myObj object myObj->fun(); //Now delete the object delete myObj ;
如果派生类析构函数是虚拟的,那么对象将按顺序被驱逐(首先是派生对象然后是基础).如果派生类析构函数不是虚拟的,那么只会删除基类对象(因为指针是基类"Base*myObj").因此派生对象会有内存泄漏.