orientada - ¿Cuándo se llama un destructor de C++?
programacion orientada a objetos (9)
Pregunta básica: ¿cuándo un programa llama a un método destructor de clase en C ++? Me han dicho que se llama siempre que un objeto sale del alcance o está sujeto a una delete
Preguntas más específicas:
1) Si el objeto se crea mediante un puntero y ese puntero se elimina más tarde o se le asigna 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ías llamar a un destructor manualmente?
1) Si el objeto se crea mediante un puntero y ese puntero se elimina más tarde o se le asigna una nueva dirección para señalar, ¿el objeto al que apuntaba llamaba su destructor (suponiendo que nada más lo señala)?
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, 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?
Eso depende de la implementación de la lista vinculada. Las colecciones típicas destruyen todos sus objetos contenidos cuando se destruyen.
Entonces, 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 tienen más referencias. Todo depende de ti para elegir las piezas que hacen lo que quieres.
3) ¿Alguna vez querrías llamar a un destructor manualmente?
Por supuesto. 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, 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");
}
- Cuando crea un objeto con
new
, usted es responsable de llamar adelete
. Cuando creas un objeto conmake_shared
, elshared_ptr
resultante es responsable de mantener el conteo y llamar adelete
cuando el recuento de usos va a cero. - Salir del alcance significa dejar un bloque. Esto es cuando se llama al destructor, suponiendo que el objeto no fue asignado con
new
(es decir, es un objeto de pila). - El único momento en que necesita llamar a un destructor explícitamente es cuando asigna el objeto con una ubicación
new
.
Punteros : los punteros regulares no son compatibles con RAII. Sin una
delete
explícita, habrá basura. ¡Afortunadamente C ++ tiene punteros automáticos que manejan esto por ti!Alcance : piense cuándo una variable se vuelve invisible para su programa. Por lo general, esto es al final de
{block}
, como usted señala.Destrucción manual : nunca intente esto. Solo deja que scope y RAII hagan la magia por ti.
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.
Otros ya han abordado 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 es capaz de contener más objetos que los que están actualmente en el vector.
Para gestionar eso, lo vector
hace el vector
debajo de las cubiertas es asignar memoria sin procesar a través del objeto Allocator
(que, a menos que 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 una 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
, eso liberaría todo su bloque de memoria; necesita destruir un objeto en esa memoria sin destruir a los demás, o liberar cualquier bloque de memoria que controle (por ejemplo, si erase
5 elementos de un vector, inmediatamente push_back
5 elementos más, se garantiza que el vector no reasignar memoria cuando lo haces.
Para hacer eso, el vector destruye directamente los objetos en la memoria llamando explícitamente al destructor, no utilizando delete
.
Si, por casualidad, alguien más escribiera un contenedor usando almacenamiento contiguo más o menos como lo hace un vector
(o una variante de eso, como std::deque
realmente lo hace), casi seguramente querrás usar la misma técnica.
Solo por ejemplo, consideremos cómo podría escribir código para un buffer de anillo 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 usa el operator new
y el 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).
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 desacoplar la asignación de memoria de la construcción de objetos.
Sí, se llama a un destructor (aka dtor) cuando un objeto sale del alcance si está en la pila o cuando se llama delete
en un puntero a un objeto.
Si el puntero se borra mediante
delete
se llamará al dtor. Si reasigna el puntero sin llamar adelete
primero, obtendrá una pérdida de memoria porque el objeto todavía existe en algún lugar de la memoria. En la última instancia, no se llama al dtor.Una buena implementación de lista enlazada llamará al controlador 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.
Lo dudo, pero no me sorprendería si hay alguna circunstancia extraña.
Si el objeto no se crea mediante un puntero (por ejemplo, A a1 = A ();), se llama al destructor cuando se destruye el objeto, siempre que la función donde se encuentra el objeto haya finalizado. 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 = nuevo A ();), se llama al destructor cuando se borra el puntero (eliminar a2;). Si el usuario no elimina el punto explícitamente o le da un punto nueva dirección antes de eliminarla, se produce la pérdida de memoria. Eso es un error.
En una lista enlazada, 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.
Raramente llamamos un destructor de forma manual. Es una función que proporciona para el sistema.
¡Disculpa mi pobre ingles!
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.yes, cuando eliminas 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".