Destruição de objects em C ++

Quando exatamente objects são destruídos em C ++ e o que isso significa? Eu tenho que destruí-los manualmente, já que não há Garbage Collector? Como as exceções entram em jogo?

(Nota: Esta é uma input para o C ++ FAQ do Stack Overflow . Se você quiser criticar a idéia de fornecer um FAQ neste formulário, então o post no meta que iniciou tudo isso seria o lugar para fazer isso. essa questão é monitorada na sala de chat do C ++ , onde a ideia do FAQ começou em primeiro lugar, então é muito provável que sua resposta seja lida por aqueles que surgiram com a ideia.)

No texto a seguir, vou distinguir entre objects com escopo , cujo tempo de destruição é determinado estaticamente pelo escopo de inclusão (funções, blocos, classs, expressões) e objects dynamics , cujo tempo exato de destruição geralmente não é conhecido até o tempo de execução.

Enquanto a semântica de destruição de objects de class é determinada por destruidores, a destruição de um object escalar é sempre um não operacional. Especificamente, a destruição de uma variável de ponteiro não destrói o pointee.

Objetos com escopo

objects automáticos

Objetos automáticos (comumente chamados de “variables ​​locais”) são destruídos, na ordem inversa de sua definição, quando o stream de controle deixa o escopo de sua definição:

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 

Se uma exceção for lançada durante a execução de uma function, todos os objects automáticos previamente construídos serão destruídos antes que a exceção seja propagada para o chamador. Esse processo é chamado de desenrolamento de pilha . Durante o desenrolamento da pilha, nenhuma outra exceção pode deixar os destruidores dos objects automáticos anteriormente construídos anteriormente mencionados. Caso contrário, a function std::terminate é chamada.

Isso leva a uma das diretrizes mais importantes em C ++:

Destrutores nunca devem jogar.

objects estáticos não locais

Objetos estáticos definidos no escopo do namespace (comumente chamados de "variables ​​globais") e membros de dados estáticos são destruídos, na ordem inversa de sua definição, após a execução do 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; 

Observe que a ordem relativa de construção (e destruição) de objects estáticos definidos em diferentes unidades de tradução é indefinida.

Se uma exceção deixa o destruidor de um object estático, a function std::terminate é chamada.

objects estáticos locais

Objetos estáticos definidos dentro de funções são construídos quando (e se) o stream de controle passa pela sua definição pela primeira vez. 1 Eles são destruídos em ordem inversa após a execução do 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* 

Se uma exceção deixa o destruidor de um object estático, a function std::terminate é chamada.

1: Este é um modelo extremamente simplificado. Os detalhes de boot de objects estáticos são realmente muito mais complicados.

subobjects da class base e subobjects de membros

Quando o stream de controle deixa o corpo do destruidor de um object, seus sub-objects de membro (também conhecidos como seus "membros de dados") são destruídos na ordem inversa de sua definição. Depois disso, seus subobjects de class base são destruídos na ordem inversa da lista de especificador de base:

 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 

Se uma exceção é lançada durante a construção de um dos sub- Foo de Foo , então todos os seus sub-objects previamente construídos serão destruídos antes que a exceção seja propagada. O destruidor de Foo , por outro lado, não será executado, já que o object Foo nunca foi totalmente construído.

Observe que o corpo do destruidor não é responsável por destruir os próprios membros de dados. Você só precisa gravar um destruidor se um membro de dados for um identificador para um recurso que precisa ser liberado quando o object for destruído (como um arquivo, um soquete, uma conexão com o database, um mutex ou uma memory heap).

elementos de matriz

Elementos de matriz são destruídos em ordem decrescente. Se uma exceção é lançada durante a construção do n-ésimo elemento, os elementos n-1 a 0 são destruídos antes que a exceção seja propagada.

objects temporários

Um object temporário é construído quando uma expressão prvalue do tipo de class é avaliada. O exemplo mais proeminente de uma expressão prvalue é a chamada de uma function que retorna um object por valor, como T operator+(const T&, const T&) . Em circunstâncias normais, o object temporário é destruído quando a expressão completa que contém lexicamente o prvalor é completamente avaliada:

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

A function acima chamada some_function(a + " " + b) é uma expressão completa porque não faz parte de uma expressão maior (em vez disso, é parte de uma expressão-instrução). Portanto, todos os objects temporários construídos durante a avaliação das subexpressões serão destruídos no ponto-e-vírgula. Existem dois objects temporários: o primeiro é construído durante a primeira adição e o segundo é construído durante a segunda adição. O segundo object temporário será destruído antes do primeiro.

Se uma exceção for lançada durante a segunda adição, o primeiro object temporário será destruído corretamente antes da propagação da exceção.

Se uma referência local for inicializada com uma expressão prvalue, a vida útil do object temporário será estendida para o escopo da referência local, portanto, você não obterá uma referência pendente:

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

Se uma expressão prvalue de um tipo não de class for avaliada, o resultado será um valor , não um object temporário. No entanto, um object temporário será construído se o prvalue for usado para inicializar uma referência:

 const int& r = i + j; 

Objetos e matrizes dynamics

Na seção seguinte, destruir X significa "primeiro destruir X e depois liberar a memory subjacente". Da mesma forma, criar X significa "primeiro alocar memory suficiente e depois construir X aí".

objects dynamics

Um object dynamic criado via p = new Foo é destruído via delete p . Se você esquecer de delete p , você terá um vazamento de resources. Você nunca deve tentar seguir um dos seguintes procedimentos, pois todos levam a um comportamento indefinido:

  • destruir um object dynamic via delete[] (observe os colchetes), free ou qualquer outro meio
  • destruir um object dynamic várias vezes
  • acessar um object dynamic depois de ter sido destruído

Se uma exceção é lançada durante a construção de um object dynamic, a memory subjacente é liberada antes que a exceção seja propagada. (O destruidor não será executado antes do lançamento da memory, porque o object nunca foi totalmente construído.)

matrizes dinâmicas

Um array dynamic criado via p = new Foo[n] é destruído via delete[] p (note os colchetes). Se você esquecer de delete[] p , você tem um vazamento de resources. Você nunca deve tentar seguir um dos seguintes procedimentos, pois todos levam a um comportamento indefinido:

  • destruir um array dynamic via delete , free ou qualquer outro meio
  • destruir um array dynamic várias vezes
  • acessar um array dynamic depois de ter sido destruído

Se uma exceção é lançada durante a construção do n-ésimo elemento, os elementos n-1 a 0 são destruídos em ordem decrescente, a memory subjacente é liberada e a exceção é propagada.

(Geralmente, você deve preferir std::vector over Foo* para matrizes dinâmicas. Isso torna a escrita de código correto e robusto muito mais fácil).

pointers inteligentes de contagem de referência

Um object dynamic gerenciado por vários objects std::shared_ptr é destruído durante a destruição do último object std::shared_ptr envolvido no compartilhamento desse object dynamic.

(Você deve geralmente preferir std::shared_ptr over Foo* para objects compartilhados. Isso torna a escrita de código correto e robusto muito mais fácil).

O destruidor de um object é chamado automaticamente quando a vida útil do object termina e é destruído. Você não deve geralmente chamá-lo manualmente.

Nós vamos usar este object como um exemplo:

 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";} }; 

Existem três (quatro em C ++ 11) tipos distintos de object em C ++ e o tipo de object define a vida útil dos objects.

  • Objetos de duração do armazenamento estático
  • Objetos de duração do armazenamento automático
  • Objetos de duração do armazenamento dynamic
  • (Em C ++ 11) objects de duração de armazenamento de segmentos

Objetos de duração do armazenamento estático

Estes são os mais simples e equivalem a variables ​​globais. A vida útil desses objects é (geralmente) o tamanho do aplicativo. Estes são (geralmente) construídos antes que o main seja inserido e destruído (na ordem inversa de ser criado) após sairmos do main.

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

Nota 1: Existem dois outros tipos de object de duração de armazenamento estático.

variables ​​de membro estático de uma class.

Estes são para todos os sentidos e propósitos iguais às variables ​​globais em termos de tempo de vida.

variables ​​estáticas dentro de uma function.

Estes são objects de duração de armazenamento estático criados de forma lenta. Eles são criados no primeiro uso (em uma mansão segura para encadeamento do C ++ 11). Assim como outros objects de duração de armazenamento estático, eles são destruídos quando o aplicativo é finalizado.

Ordem de construção / destruição

  • A ordem de construção dentro de uma unidade de compilation é bem definida e é igual à declaração.
  • A ordem de construção entre as unidades de compilation é indefinida.
  • A ordem de destruição é exatamente o inverso da ordem de construção.

Objetos de duração do armazenamento automático

Esses são os tipos mais comuns de objects e o que você deve usar 99% do tempo.

Estes são três tipos principais de variables ​​automáticas:

  • variables ​​locais dentro de uma function / bloco
  • variables ​​de membro dentro de uma class / matriz.
  • variables ​​temporárias.

Variáveis ​​Locais

Quando uma function / bloco é encerrada, todas as variables ​​declaradas dentro dessa function / bloco serão destruídas (na ordem inversa da criação).

 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 ​​de membro

O tempo de vida de uma variável membro é ligado ao object que a possui. Quando a vida útil de um dono termina, todos os membros da sua vida útil também acabam. Então você precisa olhar para o tempo de vida de um proprietário que obedece às mesmas regras.

Nota: Os membros são sempre destruídos antes do proprietário na ordem inversa da criação.

  • Assim, para os membros da class, eles são criados na ordem de declaração
    e destruído na ordem inversa da declaração
  • Assim, para os membros da matriz, eles são criados em ordem 0 -> top
    e destruído na ordem inversa top -> 0

variables ​​temporárias

Estes são objects que são criados como resultado de uma expressão, mas não são atribuídos a uma variável. Variáveis ​​temporárias são destruídas como outras variables ​​automáticas. É justamente que o fim de seu escopo é o fim da afirmação em que são criados (isso é usualmente o ';').

 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: Existem situações em que a vida de um temporário pode ser prolongada.
Mas isso não é relevante para essa discussão simples. No momento em que você entende que este documento será de segunda natureza para você e antes de estender a vida de um temporário não é algo que você quer fazer.

Objetos de duração do armazenamento dynamic

Esses objects têm uma vida útil dinâmica e são criados com new e destruídos com uma chamada para 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 os desenvolvedores que vêm de idiomas coletados como lixo, isso pode parecer estranho (gerenciar a vida útil de seu object). Mas o problema não é tão ruim quanto parece. É incomum em C ++ usar objects alocados dinamicamente diretamente. Temos objects de gerenciamento para controlar sua vida útil.

O mais próximo da maioria dos outros idiomas coletados pelo GC é o std::shared_ptr . Isso manterá o controle do número de usuários de um object criado dinamicamente e quando todos eles forem eliminados, chamará delete automaticamente (penso nisso como uma versão melhor de um object 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 

Objetos de Duração de Armazenamento de Encadeamento

Estes são novos para o idioma. Eles são muito parecidos com objects de duração de armazenamento estático. Mas, em vez de viver a mesma vida que a aplicação em que vivem, desde que o segmento de execução a que estão associados.