Destrucción de objetos en C ++

¿Cuándo exactamente se destruyen los objetos en C ++, y qué significa eso? ¿Debo destruirlos manualmente, ya que no hay Recolector de basura? ¿Cómo entran en juego las excepciones?

(Nota: Esto debe ser una entrada a las preguntas frecuentes de C ++ de Stack Overflow . Si desea criticar la idea de proporcionar una pregunta frecuente en este formulario, entonces la publicación en meta que inició todo esto sería el lugar para hacerlo). esa pregunta se monitorea en la sala de chat de C ++ , donde la idea de las preguntas frecuentes comenzó en primer lugar, por lo que es muy probable que su respuesta sea leída por aquellos a quienes se les ocurrió la idea).

    En el siguiente texto, distinguiré entre objetos delimitados , cuyo tiempo de destrucción está determinado estáticamente por su scope (funciones, bloques, clases, expresiones) y objetos dynamics , cuyo tiempo exacto de destrucción generalmente no se conoce hasta el tiempo de ejecución.

    Mientras que la destrucción de la semántica de los objetos de clase está determinada por destructores, la destrucción de un objeto escalar siempre es no operativa. Específicamente, la destrucción de una variable de puntero no destruye la punta.

    Objetos con scope

    objetos automáticos

    Los objetos automáticos (comúnmente denominados “variables locales”) se destruyen, en orden inverso a su definición, cuando el flujo de control deja el ámbito de su definición:

    void some_function() { Foo a; Foo b; if (some_condition) { Foo y; Foo z; } < --- z and y are destructed here } <--- b and a are destructed here 

    Si se lanza una excepción durante la ejecución de una función, todos los objetos automáticos construidos previamente se destruyen antes de que la excepción se propague a la persona que llama. Este proceso se llama desenrollamiento de la stack . Durante el desenrollado de la stack, ninguna otra excepción puede dejar los destructores de los objetos automáticos previamente construidos anteriormente mencionados. De lo contrario, se llama a la función std::terminate .

    Esto lleva a una de las pautas más importantes en C ++:

    Los destruidores nunca deberían tirar.

    objetos estáticos no locales

    Los objetos estáticos definidos en el ámbito del espacio de nombres (comúnmente denominados "variables globales") y los miembros de datos estáticos se destruyen, en orden inverso a su definición, después de la ejecución de main :

     struct X { static Foo x; // this is only a *declaration*, not a *definition* }; Foo a; Foo b; int main() { } < --- y, x, b and a are destructed here Foo X::x; // this is the respective definition Foo y; 

    Tenga en cuenta que el orden relativo de construcción (y destrucción) de los objetos estáticos definidos en diferentes unidades de traducción no está definido.

    Si una excepción abandona el destructor de un objeto estático, se llama a la función std::terminate .

    objetos estáticos locales

    Los objetos estáticos definidos dentro de las funciones se construyen cuando (y si) el flujo de control pasa a través de su definición por primera vez. 1 Se destruyen en orden inverso después de la ejecución de main :

     Foo& get_some_Foo() { static Foo x; return x; } Bar& get_some_Bar() { static Bar y; return y; } int main() { get_some_Bar().do_something(); // note that get_some_Bar is called *first* get_some_Foo().do_something(); } < --- x and y are destructed here // hence y is destructed *last* 

    Si una excepción abandona el destructor de un objeto estático, se llama a la función std::terminate .

    1: Este es un modelo extremadamente simplificado. Los detalles de inicialización de objetos estáticos son mucho más complicados.

    Subobjetos de clase base y subobjetos de miembros

    Cuando el flujo de control abandona el cuerpo destructor de un objeto, sus subobjetos miembros (también conocidos como sus "miembros de datos") se destruyen en orden inverso a su definición. Después de eso, sus subobjetos de clase de base se destruyen en orden inverso de la base-especificador-lista:

     class Foo : Bar, Baz { Quux x; Quux y; public: ~Foo() { } < --- y and x are destructed here, }; followed by the Baz and Bar base class subobjects 

    Si se lanza una excepción durante la construcción de uno de los subobjetos de Foo , todos sus subobjetos previamente construidos serán destruidos antes de que se propague la excepción. El destructor Foo , por otro lado, no se ejecutará, ya que el objeto Foo nunca se construyó por completo.

    Tenga en cuenta que el cuerpo del destructor no es responsable de la destrucción de los miembros de los datos. Solo necesita escribir un destructor si un miembro de datos maneja un recurso que necesita liberarse cuando se destruye el objeto (como un archivo, un socket, una conexión de base de datos, un mutex o memoria de montón).

    elementos de matriz

    Los elementos de matriz se destruyen en orden descendente. Si se lanza una excepción durante la construcción del elemento n-ésimo, los elementos n-1 a 0 se destruyen antes de que se propague la excepción.

    objetos temporales

    Se construye un objeto temporal cuando se evalúa una expresión prvalue del tipo de clase. El ejemplo más destacado de una expresión prvalue es la llamada de una función que devuelve un objeto por valor, como T operator+(const T&, const T&) . En circunstancias normales, el objeto temporal se destruye cuando la expresión completa que contiene léxicamente el valor prvalue completamente evaluada:

     __________________________ full-expression ___________ subexpression _______ subexpression some_function(a + " " + b); ^ both temporary objects are destructed here 

    La función anterior llama a some_function(a + " " + b) es una expresión completa porque no es parte de una expresión más grande (en cambio, es parte de una expresión-statement). Por lo tanto, todos los objetos temporales que se construyen durante la evaluación de las subexpresiones serán destruidos en el punto y coma. Hay dos objetos temporales de este tipo: el primero se construye durante la primera adición y el segundo se construye durante la segunda adición. El segundo objeto temporal será destruido antes que el primero.

    Si se lanza una excepción durante la segunda adición, el primer objeto temporal será destruido adecuadamente antes de propagar la excepción.

    Si se inicializa una referencia local con una expresión de prvalue, la duración del objeto temporal se amplía al scope de la referencia local, por lo que no obtendrá una referencia colgante:

     { const Foo& r = a + " " + b; ^ first temporary (a + " ") is destructed here // ... } < --- second temporary (a + " " + b) is destructed not until here 

    Si se evalúa una expresión prvalue de tipo no de clase, el resultado es un valor , no un objeto temporal. Sin embargo, se construirá un objeto temporal si el prvalue se usa para inicializar una referencia:

     const int& r = i + j; 

    Arreglos y objetos dynamics

    En la siguiente sección, destruir X significa "primero destruir X y luego liberar la memoria subyacente". Del mismo modo, crear X significa "primero asignar suficiente memoria y luego construir X allí".

    objetos dynamics

    Un objeto dynamic creado mediante p = new Foo se destruye mediante delete p . Si olvida delete p , tiene una fuga de recursos. Nunca intente hacer una de las siguientes acciones, ya que todas conducen a un comportamiento indefinido:

    • destruir un objeto dynamic a través de delete[] (tenga en cuenta los corchetes), free o de cualquier otro medio
    • destruir un objeto dynamic varias veces
    • acceder a un objeto dynamic después de que ha sido destruido

    Si se lanza una excepción durante la construcción de un objeto dynamic, la memoria subyacente se libera antes de que se propague la excepción. (El destructor no se ejecutará antes de la liberación de la memoria, porque el objeto nunca se construyó por completo).

    matrices dinámicas

    Una matriz dinámica creada mediante p = new Foo[n] se destruye mediante delete[] p (tenga en cuenta los corchetes). Si olvida delete[] p , tiene una fuga de recursos. Nunca intente hacer una de las siguientes acciones, ya que todas conducen a un comportamiento indefinido:

    • destruir una matriz dinámica a través de delete , free o cualquier otro medio
    • destruir una matriz dinámica varias veces
    • acceder a una matriz dinámica después de que ha sido destruida

    Si se lanza una excepción durante la construcción del elemento n-ésimo, los elementos n-1 a 0 se destruyen en orden descendente, se libera la memoria subyacente y se propaga la excepción.

    (En general, debe preferir std::vector sobre Foo* para matrices dinámicas. Hace que escribir código sea correcto y más sólido).

    indicadores inteligentes de recuento de referencias

    Un objeto dynamic administrado por varios objetos std::shared_ptr se destruye durante la destrucción del último objeto std::shared_ptr involucrado en el intercambio de ese objeto dynamic.

    (En general, debe preferir std::shared_ptr sobre Foo* para objetos compartidos. Hace que escribir código sea correcto y más sólido).

    El destructor de un objeto se llama automáticamente cuando la vida útil del objeto finaliza y se destruye. Por lo general, no debes llamarlo manualmente.

    Usaremos este objeto como un ejemplo:

     class Test { public: Test() { std::cout < < "Created " << this << "\n";} ~Test() { std::cout << "Destroyed " << this << "\n";} Test(Test const& rhs) { std::cout << "Copied " << this << "\n";} Test& operator=(Test const& rhs) { std::cout << "Assigned " << this << "\n";} }; 

    Hay tres (cuatro en C ++ 11) distintos tipos de objetos en C ++ y el tipo de objeto define la vida útil de los objetos.

    • Objetos de duración de almacenamiento estático
    • Objetos de duración de almacenamiento automático
    • Objetos de duración de almacenamiento dynamic
    • (En C ++ 11) objetos de duración de almacenamiento de subprocesos

    Objetos de duración de almacenamiento estático

    Estas son las más simples y equivalentes a las variables globales. La vida útil de estos objetos es (normalmente) la duración de la aplicación. Estos son (usualmente) construidos antes de que se ingrese y destruya main (en el orden inverso al que se crearon) después de salir de main.

     Test global; int main() { std::cout < < "Main\n"; } > ./a.out Created 0x10fbb80b0 Main Destroyed 0x10fbb80b0 

    Nota 1: hay otros dos tipos de objetos de duración de almacenamiento estático.

    variables miembro estáticas de una clase.

    Estos son para todo sentido y propósito lo mismo que las variables globales en términos de vida útil.

    variables estáticas dentro de una función.

    Estos son objetos de duración de almacenamiento estático creados de forma perezosa. Se crean en el primer uso (en un feudo seguro para C ++ 11). Al igual que otros objetos de duración de almacenamiento estáticos, se destruyen cuando finaliza la aplicación.

    Orden de construcción / destrucción

    • El orden de construcción dentro de una unidad de comstackción está bien definido y es lo mismo que la statement.
    • El orden de construcción entre las unidades de comstackción no está definido.
    • El orden de destrucción es el inverso exacto del orden de construcción.

    Objetos de duración de almacenamiento automático

    Estos son los tipos de objetos más comunes y lo que debe usar el 99% del tiempo.

    Estos son tres tipos principales de variables automáticas:

    • variables locales dentro de una función / bloque
    • variables miembro dentro de una clase / matriz.
    • variables temporales

    Variables locales

    Cuando se sale una función / bloque, todas las variables declaradas dentro de esa función / bloque serán destruidas (en el orden inverso de creación).

     int main() { std::cout < < "Main() START\n"; Test scope1; Test scope2; std::cout << "Main Variables Created\n"; { std::cout << "\nblock 1 Entered\n"; Test blockScope; std::cout << "block 1 about to leave\n"; } // blockScope is destrpyed here { std::cout << "\nblock 2 Entered\n"; Test blockScope; std::cout << "block 2 about to leave\n"; } // blockScope is destrpyed here std::cout << "\nMain() END\n"; }// All variables from main destroyed here. > ./a.out Main() START Created 0x7fff6488d938 Created 0x7fff6488d930 Main Variables Created block 1 Entered Created 0x7fff6488d928 block 1 about to leave Destroyed 0x7fff6488d928 block 2 Entered Created 0x7fff6488d918 block 2 about to leave Destroyed 0x7fff6488d918 Main() END Destroyed 0x7fff6488d930 Destroyed 0x7fff6488d938 

    variables miembro

    La vida útil de una variable miembro está ligada al objeto que la posee. Cuando la vida útil de un propietario finaliza, la vida útil de todos sus miembros también finaliza. Por lo tanto, debe observar la vida útil de un propietario que obedece a las mismas reglas.

    Nota: los miembros siempre se destruyen antes que el propietario en orden inverso a la creación.

    • Por lo tanto, para los miembros de la clase se crean en el orden de la statement
      y destruido en el orden inverso de la statement
    • Por lo tanto, para los miembros de la matriz, se crean por orden 0 -> arriba
      y destruido en el orden inverso superior -> 0

    variables temporales

    Estos son objetos que se crean como resultado de una expresión pero no están asignados a una variable. Las variables temporales se destruyen al igual que otras variables automáticas. Es solo que el final de su scope es el final de la statement en la que se crean (esto es usualmente el ';').

     std::string data("Text."); std::cout < < (data + 1); // Here we create a temporary object. // Which is a std::string with '1' added to "Text." // This object is streamed to the output // Once the statement has finished it is destroyed. // So the temporary no longer exists after the ';' 

    Nota: Hay situaciones en las que se puede extender la vida de un temporal.
    Pero esto no es relevante para esta simple discusión. Para cuando comprenda que este documento será de su propia naturaleza y que antes de que se extienda la vida de un temporal no es algo que desee hacer.

    Objetos de duración de almacenamiento dynamic

    Estos objetos tienen una vida útil dinámica y se crean con new y destruidos con una llamada a delete .

     int main() { std::cout < < "Main()\n"; Test* ptr = new Test(); delete ptr; std::cout << "Main Done\n"; } > ./a.out Main() Created 0x1083008e0 Destroyed 0x1083008e0 Main Done 

    Para los desarrolladores que provienen de los lenguajes recolectados, esto puede parecer extraño (administrando la vida útil de su objeto). Pero el problema no es tan malo como parece. En C ++ es inusual utilizar objetos dinámicamente asignados directamente. Tenemos objetos de administración para controlar su vida útil.

    Lo más parecido a la mayoría de los demás lenguajes recostackdos por GC es std::shared_ptr . Esto hará un seguimiento de la cantidad de usuarios de un objeto creado dinámicamente y cuando todos se hayan ido, se llamará a delete automáticamente (creo que esto es una mejor versión de un objeto Java normal).

     int main() { std::cout < < "Main Start\n"; std::shared_ptr smartPtr(new Test()); std::cout < < "Main End\n"; } // smartPtr goes out of scope here. // As there are no other copies it will automatically call delete on the object // it is holding. > ./a.out Main Start Created 0x1083008e0 Main Ended Destroyed 0x1083008e0 

    Subprocesos objetos de duración de almacenamiento

    Estos son nuevos en el lenguaje. Son muy parecidos a los objetos de duración de almacenamiento estáticos. Pero en lugar de vivir la misma vida que la aplicación que viven, siempre y cuando el hilo de ejecución que están asociados.