何时使用虚拟析构函数?
我对大多数面向对象理论有一个很好的理解,但是让我困惑的是虚拟析构函数。
我认为无论对于链中的每个对象,析构函数总是被调用。
你什么时候想让他们变成虚拟的,为什么?
虚拟析构函数在通过指向基类的指针删除派生类的实例时非常有用:
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
]中,如果要删除的对象的静态types与其dynamictypes不同,则静态types应该是要删除的对象的dynamictypes的基类, 静态types应该具有虚拟析构函数或者行为是未定义的 。
在大多数实现中,对析构函数的调用将像任何非虚拟代码一样被parsing,这意味着基类的析构函数将被调用,但不是派生类的析构函数,从而导致资源泄漏。
总而言之,当基本类的析构函数意图被多态操作时,总是使其成为virtual
。
如果要防止通过基类指针删除实例,则可以使基类析构函数受保护而不受虚拟; 通过这样做,编译器不会让你调用基类指针上的delete
。
您可以从Herb Sutter的本文中了解更多关于虚拟性和虚拟基类析构函数的信息。
在多态基类中声明析构函数。 这是Scott Meyers的Effective C ++中的第7项。 Meyers继续总结说,如果一个类有任何虚函数,它应该有一个虚析构函数,而那些没有被devise为基类的类或者没有被devise为多态使用的类不应该声明虚析构函数。
虚拟构造函数是不可能的,但虚拟析构函数是可能的。 让我们试验….
#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
所以基本指针的销毁(对派生对象进行分配!)遵循破坏规则,即先导出基础。 另一方面,对于构造函数,没有什么像虚拟构造函数。
另外请注意,当没有虚拟析构函数时删除基类指针将导致未定义的行为 。 我最近学到的东西:
如何覆盖删除在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" (ie 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 ++中讲界面是纯虚拟的类。 析构函数是接口的一部分,并期望实现。 所以析构函数应该是纯虚拟的。 如何构造? 构造函数实际上不是接口的一部分,因为对象总是被显式地实例化。
简单的说,当你删除一个指向派生类对象的基类指针时,虚析构函数将以适当的顺序破坏资源。
#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
当你想要不同的析构函数应该遵循正确的顺序,而通过基类指针删除对象时,析构函数的虚拟关键字是必要的。 例如:
Base *myObj = new Derived(); // Some code which is using myObj object myObj->fun(); //Now delete the object delete myObj ;
如果你的派生类析构函数是虚拟的,那么对象将按顺序被破坏(首先派生的对象,然后基地)。 如果派生类析构函数不是虚拟的,那么只有基类对象将被删除(因为指针是基类“Base * myObj”)。 所以派生对象会有内存泄漏。
虚拟基类析构函数是“最佳实践” – 你应该总是使用它们来避免(很难察觉)内存泄漏。 使用它们,你可以确定类的inheritance链中的所有析构函数都被调用(以正确的顺序)。 inheritance自基类使用虚拟析构函数使得inheritance类的析构函数也自动虚拟(所以你不必在inheritance类析构函数声明中重新键入'虚拟')。
当你需要从基类调用派生类析构函数。 您需要在基类中声明虚拟基类析构函数。
什么是虚拟析构函数或如何使用虚拟析构函数
一个类析构函数是一个具有相同名称的函数,前面加上〜将重新分配该类所分配的内存。 为什么我们需要一个虚拟的析构函数
使用一些虚函数来查看下面的示例
该示例还告诉你如何将一个字母转换为上或下
#include "stdafx.h" #include<iostream> using namespace std; // program to convert the lower to upper orlower class convertch { public: //void convertch(){}; virtual char* convertChar() = 0; ~convertch(){}; }; class MakeLower :public convertch { public: MakeLower(char *passLetter) { tolower = true; Letter = new char[30]; strcpy(Letter, passLetter); } virtual ~MakeLower() { cout<< "called ~MakeLower()"<<"\n"; delete[] Letter; } char* convertChar() { size_t len = strlen(Letter); for(int i= 0;i<len;i++) Letter[i] = Letter[i] + 32; return Letter; } private: char *Letter; bool tolower; }; class MakeUpper : public convertch { public: MakeUpper(char *passLetter) { Letter = new char[30]; toupper = true; strcpy(Letter, passLetter); } char* convertChar() { size_t len = strlen(Letter); for(int i= 0;i<len;i++) Letter[i] = Letter[i] - 32; return Letter; } virtual ~MakeUpper() { cout<< "called ~MakeUpper()"<<"\n"; delete Letter; } private: char *Letter; bool toupper; }; int _tmain(int argc, _TCHAR* argv[]) { convertch *makeupper = new MakeUpper("hai"); cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" "; delete makeupper; convertch *makelower = new MakeLower("HAI");; cout<<"Eneterd : HAI = " <<makelower->convertChar()<<" "; delete makelower; return 0; }
从上面的示例中,您可以看到MakeUpper和MakeLower类的析构函数未被调用。
用虚拟析构函数查看下一个示例
#include "stdafx.h" #include<iostream> using namespace std; // program to convert the lower to upper orlower class convertch { public: //void convertch(){}; virtual char* convertChar() = 0; virtual ~convertch(){}; // defined the virtual destructor }; class MakeLower :public convertch { public: MakeLower(char *passLetter) { tolower = true; Letter = new char[30]; strcpy(Letter, passLetter); } virtual ~MakeLower() { cout<< "called ~MakeLower()"<<"\n"; delete[] Letter; } char* convertChar() { size_t len = strlen(Letter); for(int i= 0;i<len;i++) { Letter[i] = Letter[i] + 32; } return Letter; } private: char *Letter; bool tolower; }; class MakeUpper : public convertch { public: MakeUpper(char *passLetter) { Letter = new char[30]; toupper = true; strcpy(Letter, passLetter); } char* convertChar() { size_t len = strlen(Letter); for(int i= 0;i<len;i++) { Letter[i] = Letter[i] - 32; } return Letter; } virtual ~MakeUpper() { cout<< "called ~MakeUpper()"<<"\n"; delete Letter; } private: char *Letter; bool toupper; }; int _tmain(int argc, _TCHAR* argv[]) { convertch *makeupper = new MakeUpper("hai"); cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" \n"; delete makeupper; convertch *makelower = new MakeLower("HAI");; cout<<"Eneterd : HAI = " <<makelower->convertChar()<<"\n "; delete makelower; return 0; }
虚析构函数将显式地调用类的最派生的运行时析构函数,以便它能够以适当的方式清除对象。
或者访问链接
我认为讨论“未定义”行为,或者至less是在没有虚拟析构函数的基类(/ 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; */ }
我并不是在暗示你是否需要虚拟析构函数,尽pipe我认为这是一个很好的习惯。 我只是指出,如果你的基类(/结构)没有一个虚表和你的派生类(/结构),你可能最终崩溃的原因,你通过基类(/结构)删除一个对象指针。 在这种情况下,你传递给堆的空闲例程的地址是无效的,从而导致崩溃的原因。
如果你运行上面的代码,你会清楚地看到问题发生的时间。 当基类(/结构)的这个指针不同于派生类(/结构)的这个指针时,你会遇到这个问题。 在上面的示例中,struct a和b没有vtables。 结构c和d确实有vtable。 因此,一个指向ac或d对象实例的a或b指针将被固定,以便考虑vtable。 如果你通过这个a或b指针来删除它,将会因为地址对堆的空闲例程无效而崩溃。
如果打算从基类指针中删除具有vtable的派生实例,则需要确保基类具有一个vtable。 一种方法是添加一个虚拟析构函数,你可能想要正确地清理资源。
任何公开inheritance的类,多态或不应该有一个虚拟的析构函数。 换句话说,如果它可以被一个基类指针指向,它的基类应该有一个虚拟的析构函数。
如果是虚拟的,则派生类析构函数被调用,然后是基类构造函数。 如果不是虚拟的,只有基类析构函数被调用。
我认为这个问题的核心是关于虚拟方法和多态,而不是具体的析构函数。 这是一个更清晰的例子:
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.
没有virtual
它会打印出来:
This is A.
而现在你应该明白什么时候使用虚拟析构函数。