c++ - que - ¿Cuándo usar destructores virtuales?
que hace un destructor en c++ (15)
Llamando 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 del destructor virtual no es diferente de cualquier otra llamada de función virtual.
Para base->f()
, la llamada se enviará a Derived::f()
, y es lo mismo para base->~Base()
- su función de reemplazo - se llamará Derived::~Derived()
.
Lo mismo sucede cuando se llama indirectamente al destructor, por ejemplo, delete base;
. La declaración de delete
llamará base->~Base()
que se enviará a Derived::~Derived()
.
Clase abstracta con destructor no virtual.
Si no va a eliminar un objeto a través de un puntero a su clase base, entonces no es necesario tener un destructor virtual. Solo 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" (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.
}
Tengo un sólido conocimiento de la mayoría de las teorías de OO, pero lo único que me confunde mucho son los destructores virtuales.
Pensé que el destructor siempre es llamado sin importar qué y para cada objeto en la cadena.
¿Cuándo vas a hacerlos virtuales y por qué?
¿Qué es un destructor virtual o cómo usar un destructor virtual?
Un destructor de clase es una función con el mismo nombre de la clase precedente 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 a superior o inferior
#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;
}
En el ejemplo 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<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;
}
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 una manera adecuada.
O visita el enlace
Creo que el núcleo de esta pregunta es sobre los métodos virtuales y el polimorfismo, no el destructor específicamente. 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 deberías entender cuándo usar los destructores virtuales.
Cualquier clase que se hereda públicamente, sea polimórfica o no, debe tener un destructor virtual. Para decirlo de otra manera, si puede ser apuntado por un puntero de clase base, su clase base debería tener un destructor virtual.
Si es virtual, se llama al destructor de la clase derivada, luego al constructor de la clase base. Si no es virtual, solo se llama al destructor de la clase base.
Declarar destructores virtuales en clases base polimórficas. Este es el ítem 7 en el C ++ efectivo de Scott Meyers. Meyers continúa resumiendo que si una clase tiene alguna función virtual, debería tener un destructor virtual, y que las clases que no están diseñadas para ser clases básicas o que no están diseñadas para ser utilizadas de manera polimórfica no deben declarar destructores virtuales.
Haz el destructor virtual cada vez que tu clase sea polimórfica.
La palabra clave virtual para el destructor es necesaria cuando desea que los diferentes destructores sigan el orden correcto mientras los objetos se eliminan mediante el 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 destruirán en un orden (primero el objeto derivado y luego la base). Si su destructor de clase derivado NO es virtual, solo se eliminará el objeto de clase base (porque el puntero es de la clase base "Base * myObj"). Así que habrá pérdida de memoria para el objeto derivado.
Los destructores de clase base virtual son "mejores prácticas": siempre debe usarlos para evitar (difícil de detectar) pérdidas de memoria. Usá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). La herencia de una clase base que usa el destructor virtual hace que el destructor de la clase hereditaria también sea automáticamente virtual (por lo que no tiene que volver a escribir ''virtual'' en la declaración del destructor de la clase hereditaria).
Los destructores virtuales son útiles cuando potencialmente podría 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í, te darás cuenta de que no declaré el destructor de Base como virtual
. Ahora, echemos un vistazo al siguiente fragmento de código:
Base *b = new Derived();
// use b
delete b; // Here''s the problem!
Dado que el destructor de Base no es virtual
b
es una Base*
apunta a un objeto Derived
, delete b
tiene un comportamiento indefinido :
[En
delete b
], si el tipo estático del objeto que se va a eliminar es diferente de su tipo dinámico, el tipo estático será una clase base del tipo dinámico del objeto que se eliminará y el tipo estático tendrá un destructor virtual o el comportamiento es indefinido .
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 provocará una fuga de recursos.
Para resumir, siempre haga que los destructores de virtual
clases base sean virtual
cuando estén destinados a ser manipulados 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 clase base esté protegido y no sea virtual; Al hacerlo, el compilador 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 clases de base virtual en este artículo de Herb Sutter .
Me gusta pensar en interfaces e implementaciones de interfaces. En C ++, la interfaz es pura clase virtual. Destructor es parte de la interfaz y se espera que sea implementado. Por lo tanto, destructor debe ser puro virtual. ¿Qué hay de constructor? El constructor en realidad no es parte de la interfaz porque el objeto siempre es instanciado explícitamente.
Para ser simple, el destructor virtual es destruir los recursos en un orden adecuado, cuando se elimina un puntero de clase base que apunta al objeto de clase derivado.
#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
Pensé que sería beneficioso discutir el comportamiento "indefinido", o al menos el comportamiento indefinido "crash" que puede ocurrir cuando se elimina a través de una clase base (/ struct) sin un destructor virtual, o más precisamente no es vtable. El código siguiente enumera algunas estructuras simples (lo mismo sería cierto para las clases).
#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;
*/
}
No estoy sugiriendo si necesitas destructores virtuales o no, aunque creo que en general es una buena práctica tenerlos. Solo estoy señalando la razón por la que puede terminar con un bloqueo si su clase base (/ struct) no tiene una vtable y su clase derivada (/ struct) la tiene y elimina un objeto a través de una clase base (/ struct) puntero. En este caso, la dirección que usted pasa a la rutina libre del montón no es válida y, por lo tanto, el motivo del bloqueo.
Si ejecuta el código anterior, verá claramente cuando se produce el problema. Cuando el puntero de la clase base (/ struct) es diferente del puntero de la clase derivada (/ struct), se encontrará con este problema. En el ejemplo anterior, las estructuras ayb no tienen vtables. Las estructuras c y d tienen vtables. Por lo tanto, un puntero a o b para una instancia de objeto ac o d se corregirá para tener en cuenta el vtable. Si pasa este puntero a o b para eliminarlo, se bloqueará debido a que la dirección no es válida para la rutina libre del montón.
Si planea eliminar instancias derivadas que tienen vtables de los punteros de clase base, debe asegurarse de que la clase base tenga una vtable. Una forma de hacerlo es agregar un destructor virtual, que quizás desee limpiar de forma adecuada los recursos.
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í recientemente:
¿Cómo debe comportarse la eliminación de eliminar en C ++?
He estado usando C ++ durante años y todavía consigo colgarme.
Un constructor virtual no es posible pero un destructor virtual es posible. Experimentemos ...
#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;
}
El código anterior da como resultado 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), encontramos que solo se llama al destructor base. Pero esto no debe suceder. Para hacer lo apropiado tenemos que hacer que el destructor base sea virtual. Ahora veamos que pasa en lo siguiente:
#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;
}
La salida cambió de la siguiente manera:
Base Constructor Called
Derived constructor called
Derived destructor called
Base Destructor called
Entonces, la destrucción del puntero de base (que toma una asignación en un objeto derivado) sigue la regla de destrucción, es decir, primero la derivada y luego la base. Por otro lado para el constructor no hay nada como el constructor virtual.
cuando necesite llamar al destructor de la clase derivada de la clase base. necesitas declarar el destructor de la clase base virtual en la clase base.