2012-04-10 25 views
79

Pregunta básica: cuando un programa llama a un método destructor de clase en C++? Se me ha dicho que se llama cuando un objeto sale del ámbito o se somete a un delete¿Cuándo se llama un destructor de C++?

preguntas más específicas:

1) Si se crea el objeto a través de un puntero y ese puntero adelante se borra o dada una nueva dirección para señalar, ¿el objeto al que apuntaba llamaba su destructor (suponiendo que nada más lo señala)?

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

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

+2

Incluso sus preguntas específicas son demasiado amplias. "Ese puntero se borra más tarde" y "se le da una nueva dirección para señalar" son bastante diferentes. Busque más (algunos de ellos han sido respondidos), y luego haga preguntas separadas para las partes que no pudo encontrar. –

Respuesta

49

1) Si se crea el objeto a través de un puntero y ese puntero adelante se borra o da una nueva dirección para apuntar a, ¿el objeto al que estaba apuntando a llamar a su nada destructor (suponiendo que los demás están apuntando lo)?

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

2) Siguiendo con la pregunta 1, lo que define cuándo un objeto queda fuera del alcance (sin importar cuándo un objeto abandona un {bloque} dado). Entonces, en otras palabras, ¿cuándo se llama un destructor a 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 se destruyen.

Por lo tanto, una lista vinculada de punteros típicamente destruiría los punteros pero no los objetos a los que apuntan. (Lo cual puede ser correcto. Pueden ser referencias mediante otros punteros). Sin embargo, una lista vinculada específicamente diseñada para contener punteros podría eliminar los objetos en 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 tenían más referencias. Todo depende de ti para elegir las piezas que hacen lo que quieres.

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

Sure. Un ejemplo sería si desea reemplazar un objeto por otro del mismo tipo, pero no desea liberar memoria solo para asignarlo nuevamente. Puedes destruir el objeto viejo en su lugar y construir uno nuevo en su lugar. (Sin embargo, en general, 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 tus ejemplos declaró una función? Es un ejemplo del "análisis más irritante". (El otro punto más trivial es que supongo que quería decir 'nuevo Foo()' con una 'F' mayúscula). –

+2

Buena captura. Gracias. –

+1

Creo que 'Foo myfoo (" foo ")' no es más molesto, sino 'char * foo =" foo "; Foo myfoo (foo); 'es. – Cosine

2
  1. Punteros - punteros regulares no son compatibles con RAII. Sin un delete explícito, habrá basura. ¡Afortunadamente C++ tiene auto pointers que manejan esto por ti!

  2. Ámbito - Piense en cuando una variable se vuelve invisible a su programa. Por lo general, esto es al final de {block}, como usted señala.

  3. Destrucción manual - Nunca intente esto. Solo deja que scope y RAII hagan la magia por ti.

+0

Una nota: auto_ptr está en desuso, como su enlace menciona. – tnecniv

+0

'std :: auto_ptr' está en desuso en C++ 11, sí. Si el OP tiene realmente C++ 11, debería usar 'std :: unique_ptr' para propietarios únicos, o' std :: shared_ptr' para múltiples propietarios contados por referencia. – chrisaycock

+0

'Destrucción manual: nunca intente esto'. A menudo hago cola de punteros a un hilo diferente usando una llamada al sistema que el compilador no entiende. 'Confiar' en el alcance/punteros automáticos/inteligentes provocaría que mis aplicaciones fallaran de manera catastrófica ya que los objetos fueron eliminados por el hilo de llamada antes de que pudieran ser manejados por el hilo de consumidor. Este problema afecta a los objetos e interfaces de alcance limitado y refCounted. Solo los punteros y la eliminación explícita lo harán. –

5
  1. Cuando se crea un objeto con new, que es responsable de llamar delete. Cuando crea un objeto con make_shared, el shared_ptr resultante es responsable de mantener el recuento y llamar al delete cuando el conteo de usos va a cero.
  2. Salir del alcance significa dejar un bloque. Esto es cuando se llama al destructor, suponiendo que el objeto era no asignado con new (es decir, es un objeto de pila).
  3. El único momento en el que necesita llamar a un destructor explícitamente es cuando asigna el objeto con un placement new.
+1

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

+1

@Pubby: Buen punto, promovamos las buenas prácticas. Respuesta editada – MSalters

3

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

2) Estas son dos preguntas separadas. Una variable queda fuera del alcance cuando el marco de pila en el que está declarado se elimina de la pila. Por lo general, esto es cuando sales de un bloque. Los objetos en un montón nunca salen de su alcance, aunque sí sus punteros en la pila. Nada en particular garantiza que se invocará un destructor de un objeto en una lista vinculada.

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

1

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

0

Sí, se llama a un destructor (a.k.a. dtor) cuando un objeto sale del ámbito si está en la pila o cuando se llama al delete en un puntero a un objeto.

  1. Si el puntero se elimina a través de delete entonces el dtor se llamará.Si reasigna el puntero sin llamar primero al delete, obtendrá una pérdida de memoria porque el objeto aún existe en la memoria en alguna parte. En la última instancia, no se llama al dtor.

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

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

+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 haberse eliminado a través de otro puntero. –

0

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

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


el destructor se llama cuando el código se execused a la línea "acabado".

Si el objeto se crea mediante un puntero (por ejemplo, A * a2 = nuevo A();), se llama al destructor cuando se elimina el puntero (eliminar a2;). Si el usuario no elimina el punto explícitamente o se le da una nueva dirección antes de eliminarla, se produce una fuga de memoria. Eso es un error.

En una lista vinculada, si usamos std :: list <>, no necesitamos preocuparnos por el desctructor o la pérdida de memoria porque std :: list <> ha terminado todo esto por nosotros. En una lista vinculada escrita por nosotros mismos, debemos escribir el desctructor y eliminar el puntero de forma explícita. De lo contrario, se producirá una pérdida de memoria.

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

¡Perdón por mi pobre inglés!

+0

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 del tiempo no deberías :) –

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 dasblinkenlight observa.

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 disociar la asignación de memoria a partir de la construcción de objetos.

10

Otros ya han solucionado los otros problemas, así que solo veré un punto: ¿alguna vez deseas eliminar un objeto manualmente?

La respuesta es sí. @DavidSchwartz dio un ejemplo, pero es bastante inusual. Daré un ejemplo que está debajo del 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::vector asignará 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 puede contener más objetos que los que están actualmente en el vector.

para gestionar dicho, lo que hace vector bajo las sábanas es asignar prima de memoria a través de laAllocator objeto (que, a menos que se especifique lo contrario, significa que utiliza ::operator new). Luego, cuando usa (por ejemplo) push_back para agregar un elemento al vector, internamente el vector usa un placement new para crear un elemento en la parte (previamente) no utilizada de su espacio de memoria.

Ahora, ¿qué sucede cuando/si erase un elemento del vector? No puede simplemente usar delete, que liberaría todo su bloque de memoria; necesita destruir un objeto en esa memoria sin destruir ningún otro, o liberar cualquier bloque de memoria que controle (por ejemplo, si erase 5 elementos de un vector, inmediatamente push_back 5 elementos más, es garantizado que el vector se no memoria reallocate cuando hacerlo.

Para ello, el vector destruye directamente los objetos en la memoria mediante una llamada explícita al destructor, no utilizando delete.

Si, por ventura, alguien más debía escribir un contenedor usando almacenamiento contiguo aproximadamente como un vector (o alguna variante de eso, como std::deque realmente lo hace), casi seguramente querrás utilizar la misma técnica.

Solo por ejemplo, consideremos cómo podría escribir el código para un anillo de memoria 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; 
    } 

    // release the buffer: 
~circular_buffer() { operator delete(data); } 
}; 

#endif 

A diferencia de los contenedores estándar, este utiliza operator new y operator delete directamente. Para uso real, es probable que desee utilizar una clase de asignador, pero por el momento haría más para distraer que contribuir (IMO, de todos modos).