¿Cuándo se llama a un destructor de C ++?


118

Pregunta básica: ¿cuándo un programa llama al método destructor de una clase en C ++? Me han dicho que se llama cuando un objeto sale del alcance o se somete a unadelete

Preguntas más específicas:

1) Si el objeto se crea a través de un puntero y ese puntero se elimina más tarde o se le da una nueva dirección a la que apuntar, ¿el objeto al que apuntaba llama a su destructor (asumiendo que nada más lo apunta)?

2) Continuando con la pregunta 1, qué define cuándo un objeto sale del alcance (sin tener en cuenta cuándo un objeto sale de un {bloque} dado). Entonces, en otras palabras, ¿cuándo se llama a un destructor en un objeto en una lista vinculada?

3) ¿Alguna vez querría llamar a un destructor manualmente?


3
Incluso sus preguntas específicas son demasiado amplias. "Ese puntero se elimina más tarde" y "se le da una nueva dirección para apuntar" son bastante diferentes. Busque más (algunas de estas respuestas han sido respondidas) y luego haga preguntas separadas para las partes que no pudo encontrar.
Matthew Flaschen

Respuestas:


74

1) Si el objeto se crea a través de un puntero y ese puntero se elimina más tarde o se le da una nueva dirección a la que apuntar, ¿el objeto al que apuntaba llama a su destructor (asumiendo que nada más lo apunta)?

Depende del tipo de punteros. Por ejemplo, los punteros inteligentes a menudo eliminan sus objetos cuando se eliminan. Los punteros ordinarios no lo hacen. Lo mismo ocurre cuando se hace que un puntero apunte a un objeto diferente. Algunos punteros inteligentes destruirán el objeto antiguo o lo destruirán si no tiene más referencias. Los punteros ordinarios no tienen esa inteligencia. Solo tienen una dirección y le permiten realizar operaciones en los objetos a los que apuntan al hacerlo específicamente.

2) Continuando con la pregunta 1, qué define cuándo un objeto sale del alcance (sin tener en cuenta cuándo un objeto sale de un {bloque} dado). Entonces, en otras palabras, ¿cuándo se llama a un destructor en un objeto en una lista vinculada?

Eso depende de la implementación de la lista vinculada. Las colecciones típicas destruyen todos sus objetos contenidos cuando son destruidos.

Por lo tanto, una lista vinculada de punteros normalmente destruiría los punteros, pero no los objetos a los que apuntan. (Lo cual puede ser correcto. Pueden ser referencias de otros punteros). Sin embargo, una lista enlazada diseñada específicamente para contener punteros podría eliminar los objetos por su propia destrucción.

Una lista vinculada de punteros inteligentes podría eliminar automáticamente los objetos cuando se eliminan los punteros, o hacerlo si no tuvieran más referencias. Depende de usted elegir las piezas que hacen lo que quiere.

3) ¿Alguna vez querría llamar a un destructor manualmente?

Por supuesto. Un ejemplo sería si desea reemplazar un objeto con otro objeto del mismo tipo pero no desea liberar memoria solo para asignarlo nuevamente. Puede destruir el objeto antiguo en su lugar y construir uno nuevo en su lugar. (Sin embargo, generalmente esta es una mala idea).

// pointer is destroyed because it goes out of scope,
// but not the object it pointed to. memory leak
if (1) {
 Foo *myfoo = new Foo("foo");
}


// pointer is destroyed because it goes out of scope,
// object it points to is deleted. no memory leak
if(1) {
 Foo *myfoo = new Foo("foo");
 delete myfoo;
}

// no memory leak, object goes out of scope
if(1) {
 Foo myfoo("foo");
}

2
Pensé que el último de sus ejemplos declaraba una función. Es un ejemplo del "análisis sintáctico más irritante". (El otro punto más trivial es que supongo que quisiste decir new Foo()con una 'F' mayúscula).
Stuart Golodetz

1
Creo que Foo myfoo("foo")no es el análisis más irritante, pero lo char * foo = "foo"; Foo myfoo(foo);es.
Coseno

Puede ser una pregunta estúpida, pero ¿no debería delete myFoollamarse antes Foo *myFoo = new Foo("foo");? De lo contrario, eliminaría el objeto recién creado, ¿no?
Matheus Rocha

No hay myFooantes de la Foo *myFoo = new Foo("foo");línea. Esa línea crea una nueva variable llamada myFoo, que sombrea cualquier existente. Aunque en este caso, no existe ninguno ya que lo myFooanterior está en el alcance del if, que ha finalizado.
David Schwartz

1
@galactikuh Un "puntero inteligente" es algo que actúa como un puntero a un objeto, pero que también tiene características que facilitan la administración de la vida útil de ese objeto.
David Schwartz

20

Otros ya han abordado los otros problemas, por lo que solo miraré un punto: ¿alguna vez desea eliminar manualmente un objeto?

La respuesta es sí. @DavidSchwartz dio un ejemplo, pero es bastante inusual. Daré un ejemplo que está bajo el capó de lo que muchos programadores de C ++ usan todo el tiempo: std::vector(y std::deque, aunque no se usa tanto).

Como la mayoría de la gente sabe, std::vectorasignará un bloque de memoria más grande cuando / si agrega más elementos de los que puede contener su asignación actual. Sin embargo, cuando hace esto, tiene un bloque de memoria que es capaz de contener más objetos de los que hay actualmente en el vector.

Para administrar eso, lo que vectorhace bajo las sábanas es asignar memoria sin procesar a través del Allocatorobjeto (que, a menos que especifique lo contrario, significa que usa ::operator new). Luego, cuando usa (por ejemplo) push_backpara agregar un elemento al vector, internamente el vector usa a placement newpara crear un elemento en la parte (previamente) no utilizada de su espacio de memoria.

Ahora, ¿qué sucede cuando / si tienes eraseun elemento del vector? No puede simplemente usar delete, eso liberaría todo su bloque de memoria; necesita destruir un objeto en esa memoria sin destruir ningún otro, o liberar ninguno de los bloques de memoria que controla (por ejemplo, si usted erase5 elementos de un vector, luego inmediatamente push_back5 elementos más, se garantiza que el vector no se reasignará memoria cuando lo hagas.

Para hacer eso, el vector destruye directamente los objetos en la memoria llamando explícitamente al destructor, no usando delete.

Si, por casualidad, alguien más escribiera un contenedor usando almacenamiento contiguo más o menos como lo vectorhace (o alguna variante de eso, como std::dequerealmente lo hace), es casi seguro que querrá usar la misma técnica.

Por ejemplo, consideremos cómo podría escribir código para un búfer circular circular.

#ifndef CBUFFER_H_INC
#define CBUFFER_H_INC

template <class T>
class circular_buffer {
    T *data;
    unsigned read_pos;
    unsigned write_pos;
    unsigned in_use;
    const unsigned capacity;
public:
    circular_buffer(unsigned size) :
        data((T *)operator new(size * sizeof(T))),
        read_pos(0),
        write_pos(0),
        in_use(0),
        capacity(size)
    {}

    void push(T const &t) {
        // ensure there's room in buffer:
        if (in_use == capacity) 
            pop();

        // construct copy of object in-place into buffer
        new(&data[write_pos++]) T(t);
        // keep pointer in bounds.
        write_pos %= capacity;
        ++in_use;
    }

    // return oldest object in queue:
    T front() {
        return data[read_pos];
    }

    // remove oldest object from queue:
    void pop() { 
        // destroy the object:
        data[read_pos++].~T();

        // keep pointer in bounds.
        read_pos %= capacity;
        --in_use;
    }
  
~circular_buffer() {
    // first destroy any content
    while (in_use != 0)
        pop();

    // then release the buffer.
    operator delete(data); 
}

};

#endif

A diferencia de los contenedores estándar, este usa operator newy operator deletedirectamente. Para uso real, probablemente quieras usar una clase de asignador, pero por el momento haría más para distraer que para contribuir (en mi opinión, de todos modos).


9
  1. Cuando crea un objeto con new, es responsable de llamar delete. Cuando crea un objeto con make_shared, el resultado shared_ptres responsable de llevar el recuento y llamar deletecuando el recuento de uso llega a cero.
  2. Salirse del alcance significa dejar un bloqueo. Esto es cuando se llama al destructor, asumiendo que el objeto no fue asignado con new(es decir, es un objeto de pila).
  3. Casi la única vez que necesita llamar a un destructor explícitamente es cuando asigna el objeto con una ubicaciónnew .

1
Hay un recuento de referencias (shared_ptr), aunque obviamente no para punteros simples.
Pubby

1
@Pubby: Buen punto, fomentemos las buenas prácticas. Respuesta editada.
MSalters

6

1) Los objetos no se crean 'mediante punteros'. Hay un puntero que se asigna a cualquier objeto que sea "nuevo". Suponiendo que esto es lo que quiere decir, si llama a 'eliminar' en el puntero, en realidad eliminará (y llamará al destructor en) el objeto que el puntero desreferencia. Si asigna el puntero a otro objeto, habrá una pérdida de memoria; nada en C ++ recogerá su basura por usted.

2) Estas son dos preguntas separadas. Una variable sale del alcance cuando el marco de pila en el que se declara se saca de la pila. Por lo general, esto es cuando dejas un bloque. Los objetos en un montón nunca salen de su alcance, aunque sus punteros en la pila pueden hacerlo. Nada en particular garantiza que se llamará a un destructor de un objeto en una lista vinculada.

3) No realmente. Puede haber Deep Magic que sugiera lo contrario, pero por lo general desea hacer coincidir sus 'nuevas' palabras clave con sus palabras clave 'eliminar' y poner todo lo necesario en su destructor para asegurarse de que se limpia correctamente. Si no hace esto, asegúrese de comentar el destructor con instrucciones específicas para cualquier persona que use la clase sobre cómo deben limpiar los recursos de ese objeto manualmente.


3

Para dar una respuesta detallada a la pregunta 3: sí, hay ocasiones (raras) en las que puede llamar al destructor explícitamente, en particular como la contraparte de una ubicación nueva, como observa dasblinkenlight.

Para dar un ejemplo concreto de esto:

#include <iostream>
#include <new>

struct Foo
{
    Foo(int i_) : i(i_) {}
    int i;
};

int main()
{
    // Allocate a chunk of memory large enough to hold 5 Foo objects.
    int n = 5;
    char *chunk = static_cast<char*>(::operator new(sizeof(Foo) * n));

    // Use placement new to construct Foo instances at the right places in the chunk.
    for(int i=0; i<n; ++i)
    {
        new (chunk + i*sizeof(Foo)) Foo(i);
    }

    // Output the contents of each Foo instance and use an explicit destructor call to destroy it.
    for(int i=0; i<n; ++i)
    {
        Foo *foo = reinterpret_cast<Foo*>(chunk + i*sizeof(Foo));
        std::cout << foo->i << '\n';
        foo->~Foo();
    }

    // Deallocate the original chunk of memory.
    ::operator delete(chunk);

    return 0;
}

El propósito de este tipo de cosas es desacoplar la asignación de memoria de la construcción de objetos.


2
  1. Punteros : los punteros regulares no son compatibles con RAII. Sin un explícito delete, habrá basura. ¡Afortunadamente, C ++ tiene punteros automáticos que manejan esto por usted!

  2. Alcance : piense en cuándo una variable se vuelve invisible para su programa. Por lo general, esto es al final de {block}, como señala.

  3. Destrucción manual : nunca intente esto. Simplemente deje que el alcance y RAII hagan la magia por usted.


Una nota: auto_ptr está obsoleto, como lo menciona su enlace.
tnecniv

std::auto_ptrestá en desuso en C ++ 11, sí. Si el OP realmente tiene C ++ 11, debería usarlo std::unique_ptrpara propietarios únicos o std::shared_ptrpara múltiples propietarios contados por referencia.
chrisaycock

'Destrucción manual: nunca intente esto'. Muy a menudo pongo en cola los punteros de objeto a un hilo diferente usando una llamada al sistema que el compilador no comprende. 'Depender' de los punteros de alcance / automático / inteligente haría que mis aplicaciones fallaran catastróficamente ya que el hilo de llamada eliminaba los objetos antes de que pudieran ser manejados por el hilo del consumidor. Este problema afecta a los objetos e interfaces de alcance limitado y refCounted. Solo sirven los punteros y la eliminación explícita.
Martin James

@MartinJames ¿Puede publicar un ejemplo de una llamada al sistema que el compilador no entiende? ¿Y cómo estás implementando la cola? No std::queue<std::shared_ptr>?he descubierto que pipe()entre un hilo de productor y de consumidor hace que la concurrencia sea mucho más fácil, si la copia no es demasiado cara.
chrisaycock

myObject = new myClass (); PostMessage (aHandle, WM_APP, 0, LPPARAM (myObject));
Martin James

1

Siempre que use "nuevo", es decir, adjunte una dirección a un puntero, o para decir, reclame espacio en el montón, debe "eliminarlo".
1.Sí, cuando borra algo, se llama al destructor.
2.Cuando se llama al destructor de la lista enlazada, se llama al destructor de sus objetos. Pero si son punteros, debe eliminarlos manualmente. 3. cuando el espacio sea reclamado por "nuevo".


0

Sí, se llama a un destructor (también conocido como dtor) cuando un objeto sale del alcance si está en la pila o cuando llama deletea un puntero a un objeto.

  1. Si el puntero se elimina mediante, deletese llamará al dtor. Si reasigna el puntero sin llamar deleteprimero, obtendrá una pérdida de memoria porque el objeto todavía existe en la memoria en algún lugar. En el último caso, no se llama al dtor.

  2. Una buena implementación de lista enlazada llamará al dtor de todos los objetos de la lista cuando la lista se esté destruyendo (porque llamó a algún método para destruirla o se salió del alcance). Esto depende de la implementación.

  3. Lo dudo, pero no me sorprendería si hubiera alguna circunstancia extraña por ahí.


1
"Si reasigna el puntero sin llamar a eliminar primero, obtendrá una pérdida de memoria porque el objeto todavía existe en la memoria en algún lugar". No necesariamente. Podría haber sido eliminado a través de otro puntero.
Matthew Flaschen

0

Si el objeto no se crea a través de un puntero (por ejemplo, A a1 = A ();), se llama al destructor cuando se destruye el objeto, siempre cuando la función donde se encuentra el objeto está terminada. Por ejemplo:

void func()
{
...
A a1 = A();
...
}//finish


se llama al destructor cuando el código se ejecuta en la línea "finalizar".

Si el objeto se crea a través de un puntero (por ejemplo, A * a2 = new A ();), se llama al destructor cuando se elimina el puntero (eliminar a2;). Si el usuario no elimina el punto de forma explícita o nueva dirección antes de eliminarla, se produjo la pérdida de memoria. Eso es un error.

En una lista enlazada, si usamos std :: list <>, no debemos preocuparnos por el desctructor o la pérdida de memoria porque std :: list <> ha terminado todos estos por nosotros. En una lista enlazada escrita por nosotros mismos, deberíamos escribir el desctructor y borrar el puntero explícitamente, de lo contrario provocará pérdida de memoria.

Rara vez llamamos a un destructor manualmente. Es una función que proporciona el sistema.

¡Disculpa mi pobre ingles!


No es cierto que no pueda llamar a un destructor manualmente, puede (vea el código en mi respuesta, por ejemplo). Lo que es cierto es que la gran mayoría de las veces no debería :)
Stuart Golodetz

0

Recuerde que el constructor de un objeto se llama inmediatamente después de que se asigna la memoria para ese objeto y mientras que el destructor se llama justo antes de desasignar la memoria de ese objeto.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.