Cuándo usar destructores virtuales?

Tengo una sólida comprensión de la mayoría de las teorías de OO, pero una de las cosas que más me confunde son los destructores virtuales.

Pensé que siempre se llama al destructor sin importar qué y para cada objeto en la cadena.

¿Cuándo se supone que debes hacerlos virtuales y por qué?

Los destructores virtuales son útiles cuando puede eliminar una instancia de una clase derivada a través de un puntero a la clase base:

 class Base { // some virtual methods }; class Derived : public Base { ~Derived() { // Do some important cleanup } }; 

Aquí, notarás que no declare que el destructor de Base sea virtual . Ahora, echemos un vistazo al siguiente fragmento:

 Base *b = new Derived(); // use b delete b; // Here's the problem! 

Como el destructor de Base no es virtual b es un Base* apunta a un objeto Derived , el delete b tiene un comportamiento indefinido :

[En la delete b ], si el tipo estático del objeto a eliminar es diferente de su tipo dynamic, el tipo estático será una clase base del tipo dynamic del objeto que se eliminará y el tipo estático tendrá un destructor virtual o el comportamiento no está definido .

En la mayoría de las implementaciones, la llamada al destructor se resolverá como cualquier código no virtual, lo que significa que se llamará al destructor de la clase base, pero no al de la clase derivada, lo que generará una pérdida de recursos.

En resumen, siempre los destructores de virtual clases base son virtual cuando deben manipularse polimórficamente.

Si desea evitar la eliminación de una instancia a través de un puntero de clase base, puede hacer que el destructor de la clase base esté protegido y no sea virtual; al hacerlo, el comstackdor no le permitirá llamar a delete en un puntero de clase base.

Puede obtener más información sobre la virtualidad y el destructor de la clase base virtual en este artículo de Herb Sutter .

Declarar destructores virtuales en clases base polimórficas. Este es el Artículo 7 en Cype Effective C ++ de Scott Meyers. Meyers resume que si una clase tiene alguna función virtual, debe tener un destructor virtual, y que las clases que no están diseñadas para ser clases base o no están diseñadas para ser utilizadas polimórficamente no deberían declarar destructores virtuales.

Un constructor virtual no es posible, pero el destructor virtual es posible. Vamos a experimentar …

 #include  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; } 

El código anterior muestra lo siguiente:

 Base Constructor Called Derived constructor called Base Destructor called 

La construcción del objeto derivado sigue la regla de construcción, pero cuando eliminamos el puntero "b" (puntero base), hemos encontrado que solo se llama al destructor base. Pero esto no debe suceder. Para hacer lo correcto, tenemos que hacer que el destructor base sea virtual. Ahora veamos qué sucede en lo siguiente:

 #include  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; } 

El resultado cambió de la siguiente manera:

 Base Constructor Called Derived constructor called Derived destructor called Base Destructor called 

Entonces, la destrucción del puntero base (que toma una asignación en el objeto derivado) sigue la regla de destrucción, es decir, primero la derivada luego la base. Por otro lado, para el constructor no hay nada como el constructor virtual.

También tenga en cuenta que eliminar un puntero de clase base cuando no hay un destructor virtual resultará en un comportamiento indefinido . Algo que aprendí hace poco:

¿Cómo se debe comportar la eliminación de anulación en C ++?

He estado usando C ++ durante años y todavía me las arreglé para ahorcarme.

Haga el destructor virtual cada vez que su clase sea polimórfica.

Llamar destructor a través de un puntero a una clase base

 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 

La llamada a un destructor virtual no es diferente de cualquier otra llamada a función virtual.

Para base->f() , la llamada se enviará a Derived::f() , y es la misma para base->~Base() -su función de anulación- se llamará a Derived::~Derived() .

Lo mismo ocurre cuando se llama al destructor indirectamente, por ejemplo, delete base; . La instrucción delete llamará a base->~Base() que se enviará a Derived::~Derived() .

Clase abstracta con destructor no virtual

Si no va a eliminar el objeto a través de un puntero a su clase base, entonces no es necesario tener un destructor virtual. Solo hazlo protected para que no se llame accidentalmente:

 // 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. } 

Me gusta pensar en interfaces e implementaciones de interfaces. En C ++, la interfaz de habla es pura clase virtual. Destructor es parte de la interfaz y se espera que se implemente. Por lo tanto, destructor debe ser puramente virtual. ¿Qué hay de constructor? Constructor en realidad no es parte de la interfaz porque el objeto siempre se crea una instancia de forma explícita.

Para ser simple, Virtual destructor consiste en destruir los recursos en el orden correcto, cuando elimina un puntero de clase base que apunta al objeto de clase derivado.

  #include 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 

La palabra clave virtual para destructor es necesaria cuando desee que diferentes destructores sigan el orden correcto mientras los objetos se eliminan a través del puntero de la clase base. por ejemplo:

 Base *myObj = new Derived(); // Some code which is using myObj object myObj->fun(); //Now delete the object delete myObj ; 

Si su destructor de clase derivado es virtual, los objetos se destruntarán en un orden (primero el objeto derivado y luego la base). Si su destructor de clase derivado NO es virtual, entonces solo el objeto de clase base será eliminado (porque el puntero es de clase base “Base * myObj”). Por lo tanto, habrá pérdida de memoria para el objeto derivado.

Los destructores de la clase base virtual son las “mejores prácticas”: siempre debe usarlos para evitar memory leaks (difíciles de detectar). Utilizándolos, puede estar seguro de que todos los destructores en la cadena de herencia de sus clases están siendo llamados (en el orden correcto). Heredar de una clase base utilizando el destructor virtual también hace que el destructor de la clase heredada sea virtual (para que no tenga que volver a escribir “virtual” en la statement heredada de destructor de clase).

¿Qué es un destructor virtual o cómo usar el destructor virtual?

Un destructor de clase es una función con el mismo nombre de la clase anterior con ~ que reasignará la memoria asignada por la clase. Por qué necesitamos un destructor virtual

Vea la siguiente muestra con algunas funciones virtuales

La muestra también indica cómo puede convertir una letra en superior o inferior

 #include "stdafx.h" #include 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 

De la muestra anterior, puede ver que no se llama al destructor para las clases MakeUpper y MakeLower.

Vea la siguiente muestra con el destructor virtual

 #include "stdafx.h" #include 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 

El destructor virtual llamará explícitamente al destructor de tiempo de ejecución más derivado de la clase para que pueda borrar el objeto de manera adecuada.

O visite el enlace

https://web.archive.org/web/20130822173509/http://www.programminggallery.com/article_details.php?article_id=138

Pensé que sería beneficioso discutir el comportamiento “indefinido”, o al menos el comportamiento indefinido “accidente” que puede ocurrir al eliminar a través de una clase base (/ struct) sin un destructor virtual, o más precisamente sin vtable. El siguiente código enumera algunas estructuras simples (lo mismo sería cierto para las clases).

 #include  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; */ } 

No estoy sugiriendo si necesita destructores virtuales o no, aunque creo que, en general, es una buena práctica tenerlos. Solo señalo la razón por la que puede terminar con un locking si su clase base (/ struct) no tiene un vtable y su clase derivada (/ struct) lo hace y elimina un objeto a través de una clase base (/ struct) puntero. En este caso, la dirección que pasa a la rutina gratuita del montón no es válida y, por lo tanto, es el motivo del locking.

Si ejecuta el código anterior, verá claramente cuando ocurre el problema. Cuando este puntero de la clase base (/ struct) es diferente del puntero de la clase derivada (/ struct), se encontrará con este problema. En el ejemplo anterior, struct a y b no tienen tablas vtables. las estructuras cyd tienen vtables. Por lo tanto, se fijará un puntero aob para la instancia de objeto ac o d para tener en cuenta el vtable. Si pasa este puntero ao b para eliminar, se bloqueará debido a que la dirección no es válida para la rutina gratuita del montón.

Si planea eliminar instancias derivadas que tienen tablas virtuales desde punteros de clase base, debe asegurarse de que la clase base tenga una tabla virtual. Una forma de hacerlo es agregar un destructor virtual, que de todos modos puede desear para limpiar correctamente los recursos.

cuando necesite llamar al destructor de clase derivado de la clase base. necesita declarar el destructor de la clase base virtual en la clase base.

Creo que el núcleo de esta pregunta es acerca de los métodos virtuales y el polymorphism, no específicamente el destructor. Aquí hay un ejemplo más claro:

 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; } 

Se imprimirá:

 This is B. 

Sin virtual se imprimirá:

 This is A. 

Y ahora debes entender cuándo usar destructores virtuales.

Cualquier clase que se hereda públicamente, polimórfica o no, debe tener un destructor virtual. Para decirlo de otra manera, si un apuntador de clase base puede señalarlo, su clase base debería tener un destructor virtual.

Si es virtual, se llama al destructor de clase derivado, luego al constructor de la clase base. Si no es virtual, solo se llama al destructor de la clase base.