El siguiente código causa una pérdida de memoria:
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class base
{
void virtual initialize_vector() = 0;
};
class derived : public base
{
private:
vector<int> vec;
public:
derived()
{
initialize_vector();
}
void initialize_vector()
{
for (int i = 0; i < 1000000; i++)
{
vec.push_back(i);
}
}
};
int main()
{
for (int i = 0; i < 100000; i++)
{
unique_ptr<base> pt = make_unique<derived>();
}
}
No tenía mucho sentido para mí, ya que la clase derivada no asigna memoria dinámica en bruto, y unique_ptr se desasigna solo. Recibo el destructor implícito de esa base de clase en lugar de derivado, pero no entiendo por qué eso es un problema aquí. Si tuviera que escribir un destructor explícito para derivado, no escribiría nada para vec.
c++
inheritance
memory
allocation
Ignorancia inercial
fuente
fuente
~derived()
que delega al destructor de vec. Alternativamente, está asumiendo queunique_ptr<base> pt
conocería el destructor derivado. Sin un método virtual, este no puede ser el caso. Si bien un unique_ptr puede recibir una función de eliminación que es un parámetro de plantilla sin ninguna representación de tiempo de ejecución, y esa característica no sirve para este código.Respuestas:
Cuando el compilador ejecuta el implícito
delete _ptr;
dentro delunique_ptr
destructor (donde_ptr
está el puntero almacenado en elunique_ptr
), sabe exactamente dos cosas:_ptr
es. Como el puntero está adentrounique_ptr<base>
, eso significa que_ptr
es del tipobase*
.Esto es todo lo que el compilador sabe. Entonces, dado que está eliminando un objeto de tipo
base
, invocará~base()
.Entonces ... ¿dónde está la parte donde destruye el
dervied
objeto al que realmente apunta? Porque si el compilador no sabe que está destruyendo aderived
, entonces no sabe quederived::vec
existe en absoluto , y mucho menos que debe ser destruido. Entonces has roto el objeto dejando la mitad sin destruir.El compilador no puede asumir que cualquier
base*
ser destruido es en realidad underived*
; después de todo, podría haber cualquier número de clases derivadas debase
. ¿Cómo sabría a qué tipobase*
apunta realmente este particular ?Lo que el compilador tiene que hacer es averiguar el destructor correcto para llamar (sí,
derived
tiene un destructor. A menos que sea= delete
un destructor, cada clase tiene un destructor, ya sea que escriba uno o no). Para hacer esto, tendrá que usar cierta información almacenadabase
para obtener la dirección correcta del código destructor para invocar, información que establece el constructor de la clase real. Luego tiene que usar esta información para convertir elbase*
puntero a la dirección de laderived
clase correspondiente (que puede o no estar en una dirección diferente. Sí, realmente). Y luego puede invocar a ese destructor.¿Ese mecanismo que acabo de describir? Es comúnmente llamado "despacho virtual": también conocido como lo que sucede cada vez que llama a una función marcada
virtual
cuando tiene un puntero / referencia a una clase base.Si desea llamar a una función de clase derivada cuando todo lo que tiene es un puntero / referencia de clase base, esa función debe declararse
virtual
. Los destructores no son fundamentalmente diferentes a este respecto.fuente
Herencia
El punto principal de la herencia es compartir una interfaz y un protocolo comunes entre muchas implementaciones diferentes, de modo que una instancia de una clase derivada se pueda tratar de manera idéntica a cualquier otra instancia desde cualquier otro tipo derivado.
En la herencia de C ++ también trae detalles de implementación, marcar (o no marcar) el destructor como virtual es uno de esos detalles de implementación.
Enlace de funciones
Ahora, cuando se llama a una función, o cualquiera de sus casos especiales, como un constructor o destructor, el compilador debe elegir a qué implementación de función se refería. Entonces debe generar un código de máquina que siga esta intención.
La forma más sencilla de resolver esto sería seleccionar la función en tiempo de compilación y emitir el código de máquina suficiente para que, independientemente de cualquier valor, cuando se ejecute ese fragmento de código, siempre ejecute el código de la función. Esto funciona muy bien, excepto por la herencia.
Si tenemos una clase base con una función (podría ser cualquier función, incluido el constructor o el destructor) y su código llama a una función, ¿qué significa esto?
Tomando de su ejemplo, si llamó
initialize_vector()
al compilador tiene que decidir si realmente quiso llamar a la implementación que se encuentraBase
, o la implementación que se encuentra enDerived
. Hay dos formas de decidir esto:Base
tipo, quiso decir la implementación enBase
.Base
valor escrito podría serBase
, oDerived
que la decisión sobre qué llamada hacer, debe tomarse en tiempo de ejecución cuando se llama (cada vez que se llama).El compilador en este punto está confundido, ambas opciones son igualmente válidas. Esto es cuando
virtual
entra en la mezcla. Cuando esta palabra clave está presente, el compilador elige la opción 2 retrasando la decisión entre todas las implementaciones posibles hasta que el código se ejecute con un valor real. Cuando esta palabra clave está ausente, el compilador elige la opción 1 porque ese es el comportamiento normal.El compilador aún podría elegir la opción 1 en el caso de una llamada de función virtual. Pero solo si puede probar que este es siempre el caso.
Constructores y Destructores
Entonces, ¿por qué no especificamos un constructor virtual?
Más intuitivamente, ¿cómo elegiría el compilador entre implementaciones idénticas del constructor para
Derived
yDerived2
? Esto es bastante simple, no puede. No existe un valor preexistente del cual el compilador pueda aprender lo que realmente se pretendía. No hay un valor preexistente porque ese es el trabajo del constructor.Entonces, ¿por qué necesitamos especificar un destructor virtual?
Más intuitivamente, ¿cómo elegiría el compilador entre implementaciones para
Base
yDerived
? Son solo llamadas de función, por lo que ocurre el comportamiento de la llamada de función. Sin un destructor virtual declarado, el compilador decidirá vincularse directamente alBase
destructor independientemente del tipo de tiempo de ejecución de los valores.En muchos compiladores, si el derivado no declara ningún miembro de datos, ni hereda de otros tipos, el comportamiento en el
~Base()
será adecuado, pero no está garantizado. Funcionaría puramente por casualidad, al igual que pararse frente a un lanzallamas que aún no se había encendido. Estás bien por un tiempo.La única forma correcta de declarar cualquier tipo de base o interfaz en C ++ es declarar un destructor virtual, de modo que se llame al destructor correcto para cualquier instancia dada de la jerarquía de tipos de ese tipo. Esto permite que la función con el mayor conocimiento de la instancia limpie esa instancia correctamente.
fuente