web-dev-qa-db-fra.com

Destruction d'objets en C ++

Quand exactement les objets sont-ils détruits en C++, et qu'est-ce que cela signifie? Dois-je les détruire manuellement, car il n'y a pas de garbage collector? Comment les exceptions entrent-elles en jeu?

(Remarque: il s'agit d'une entrée de FAQ C++ de Stack Overflow . Si vous voulez critiquer l'idée de fournir un FAQ dans ce formulaire, alors la publication sur la méta qui a commencé tout cela serait l'endroit pour le faire. Les réponses à cette question sont surveillées dans le C++ chatroom , où l'idée FAQ a commencé en premier lieu, donc votre réponse est très susceptible d'être lue par ceux qui sont venus avec l'idée.)

65
fredoverflow

Dans le texte suivant, je distinguerai les objets délimités , dont le temps de destruction est statiquement déterminé par leur portée englobante (fonctions, blocs, classes, expressions) et les objets dynamiques , dont l'heure exacte de destruction n'est généralement pas connue avant l'exécution.

Alors que la sémantique de destruction des objets de classe est déterminée par des destructeurs, la destruction d'un objet scalaire est toujours un no-op. Plus précisément, la destruction d'une variable de pointeur ne pas détruit la pointe.

Objets délimités

objets automatiques

Les objets automatiques (communément appelés "variables locales") sont détruits, dans l'ordre inverse de leur définition, lorsque le flux de contrôle quitte le champ d'application de leur définition:

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 une exception est levée pendant l'exécution d'une fonction, tous les objets automatiques précédemment construits sont détruits avant que l'exception ne soit propagée à l'appelant. Ce processus est appelé déroulement de la pile . Pendant le déroulement de la pile, aucune autre exception ne peut laisser les destructeurs des objets automatiques précédemment construits susmentionnés. Sinon, la fonction std::terminate Est appelée.

Cela conduit à l'une des directives les plus importantes en C++:

Les destructeurs ne devraient jamais lancer.

objets statiques non locaux

Les objets statiques définis au niveau de l'espace de noms (communément appelés "variables globales") et les membres de données statiques sont détruits, dans l'ordre inverse de leur définition, après l'exécution 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;

Notez que l'ordre relatif de construction (et de destruction) des objets statiques définis dans différentes unités de traduction n'est pas défini.

Si une exception quitte le destructeur d'un objet statique, la fonction std::terminate Est appelée.

objets statiques locaux

Les objets statiques définis à l'intérieur des fonctions sont construits lorsque (et si) le flux de contrôle passe par leur définition pour la première fois.1 Ils sont détruits dans l'ordre inverse après l'exécution 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 une exception quitte le destructeur d'un objet statique, la fonction std::terminate Est appelée.

1: Il s'agit d'un modèle extrêmement simplifié. Les détails d'initialisation des objets statiques sont en réalité beaucoup plus compliqués.

sous-objets de classe de base et sous-objets membres

Lorsque le flux de contrôle quitte le corps destructeur d'un objet, ses sous-objets membres (également appelés "membres de données") sont détruits dans l'ordre inverse de leur définition. Après cela, ses sous-objets de classe de base sont détruits dans l'ordre inverse de la liste des spécificateurs 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

Si une exception est levée pendant la construction de l'un des sous-objets de Foo, alors tous ses sous-objets précédemment construits seront détruits avant que l'exception ne soit propagé. Le destructeur Foo, d'autre part, ne sera pas exécuté, car l'objet Foo n'a jamais été entièrement construit .

Notez que le corps du destructeur n'est pas responsable de la destruction des membres de données eux-mêmes. Vous n'avez besoin d'écrire un destructeur que si un membre de données est un handle vers une ressource qui doit être libérée lorsque l'objet est détruit (tel qu'un fichier, un socket, une connexion à une base de données, un mutex ou une mémoire de tas).

éléments de tableau

Les éléments du tableau sont détruits dans l'ordre décroissant. Si une exception est levée lors de la construction du n-ième élément, les éléments n-1 à 0 sont détruits avant la propagation de l'exception.

objets temporaires

Un objet temporaire est construit lorsqu'une expression de valeur de type classe est évaluée. L'exemple le plus important d'une expression de valeur est l'appel d'une fonction qui renvoie un objet par valeur, comme T operator+(const T&, const T&). Dans des circonstances normales, l'objet temporaire est détruit lorsque l'expression complète qui contient lexicalement la valeur est complètement évaluée:

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

L'appel de fonction ci-dessus some_function(a + " " + b) est une expression complète car elle ne fait pas partie d'une expression plus grande (à la place, elle fait partie d'une expression-instruction). Par conséquent, tous les objets temporaires qui sont construits lors de l'évaluation des sous-expressions seront détruits au point-virgule. Il existe deux de ces objets temporaires: le premier est construit lors du premier ajout et le second est construit lors du deuxième ajout. Le deuxième objet temporaire sera détruit avant le premier.

Si une exception est levée lors du deuxième ajout, le premier objet temporaire sera détruit correctement avant de propager l'exception.

Si une référence locale est initialisée avec une expression prvalue, la durée de vie de l'objet temporaire est étendue à la portée de la référence locale, vous n'obtiendrez donc pas de référence pendant:

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

Si une expression de valeur de type non-classe est évaluée, le résultat est une valeur , pas un objet temporaire. Cependant, un objet temporaire sera construit si la valeur est utilisée pour initialiser une référence:

const int& r = i + j;

Objets et tableaux dynamiques

Dans la section suivante, détruire X signifie "d'abord détruire X puis libérer la mémoire sous-jacente". De même, créer X signifie "allouer d'abord suffisamment de mémoire puis y construire X".

objets dynamiques

Un objet dynamique créé via p = new Foo Est détruit via delete p. Si vous oubliez de delete p, Vous avez une fuite de ressources. Vous ne devez jamais essayer d'effectuer l'une des actions suivantes, car elles conduisent toutes à un comportement non défini:

  • détruire un objet dynamique via delete[] (notez les crochets), free ou tout autre moyen
  • détruire un objet dynamique plusieurs fois
  • accéder à un objet dynamique après sa destruction

Si une exception est levée lors de la construction d'un objet dynamique, la mémoire sous-jacente est libérée avant que l'exception ne se propage. (Le destructeur ne sera pas exécuté avant la libération de la mémoire, car l'objet n'a jamais été entièrement construit.)

tableaux dynamiques

Un tableau dynamique créé via p = new Foo[n] Est détruit via delete[] p (Notez les crochets). Si vous oubliez de delete[] p, Vous avez une fuite de ressources. Vous ne devez jamais essayer d'effectuer l'une des actions suivantes, car elles conduisent toutes à un comportement non défini:

  • détruire un tableau dynamique via delete, free ou tout autre moyen
  • détruire plusieurs fois un tableau dynamique
  • accéder à un tableau dynamique après sa destruction

Si une exception est levée lors de la construction du n-ième élément, les éléments n-1 à 0 sont détruits dans l'ordre décroissant, la mémoire sous-jacente est libéré, et l'exception se propage.

(Vous devriez généralement préférer std::vector<Foo> À Foo* Pour les tableaux dynamiques. Cela facilite l'écriture de code correct et robuste.)

pointeurs intelligents de comptage de références

Un objet dynamique géré par plusieurs objets std::shared_ptr<Foo> Est détruit lors de la destruction du dernier objet std::shared_ptr<Foo> Impliqué dans le partage de cet objet dynamique.

(Vous devriez généralement préférer std::shared_ptr<Foo> À Foo* Pour les objets partagés. Cela facilite l'écriture de code correct et robuste.)

81
fredoverflow

Le destructeur d'un objet est appelé automatiquement lorsque la durée de vie de l'objet se termine et qu'il est détruit. Vous ne devez généralement pas l'appeler manuellement.

Nous utiliserons cet objet comme exemple:

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

Il existe trois (quatre en C++ 11) types d'objets distincts en C++ et le type de l'objet définit la durée de vie des objets.

  • Objets de durée de stockage statique
  • Objets de durée de stockage automatique
  • Objets de durée de stockage dynamique
  • (En C++ 11) Objets de durée de stockage de thread

Objets de durée de stockage statique

Ce sont les plus simples et correspondent aux variables globales. La durée de vie de ces objets est (généralement) la durée de l'application. Ceux-ci sont (généralement) construits avant l'entrée de main et détruits (dans l'ordre inverse de leur création) après la sortie de main.

Test  global;
int main()
{
    std::cout << "Main\n";
}

> ./a.out
Created    0x10fbb80b0
Main
Destroyed  0x10fbb80b0

Remarque 1: Il existe deux autres types d'objet de durée de stockage statique.

variables membres statiques d'une classe.

Celles-ci sont pour tous les sens et les mêmes objectifs que les variables globales en termes de durée de vie.

variables statiques à l'intérieur d'une fonction.

Ce sont des objets de durée de stockage statique créés paresseusement. Ils sont créés lors de la première utilisation (dans un manoir thread-safe pour C++ 11). Tout comme les autres objets de durée de stockage statique, ils sont détruits à la fin de l'application.

Ordre de construction/destruction

  • L'ordre de construction au sein d'une unité de compilation est bien défini et identique à la déclaration.
  • L'ordre de construction entre les unités de compilation n'est pas défini.
  • L'ordre de destruction est l'inverse exact de l'ordre de construction.

Objets de durée de stockage automatique

Ce sont les types d'objets les plus courants et ce que vous devriez utiliser 99% du temps.

Ce sont trois types principaux de variables automatiques:

  • variables locales à l'intérieur d'une fonction/d'un bloc
  • variables membres dans une classe/tableau.
  • variables temporaires.

Variables locales

Lorsqu'une fonction/un bloc est quitté, toutes les variables déclarées à l'intérieur de cette fonction/bloc seront détruites (dans l'ordre inverse de la création).

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 membres

La durée de vie d'une variable membre est liée à l'objet qui la possède. Quand la durée de vie d'un propriétaire se termine, la durée de vie de tous ses membres se termine également. Vous devez donc regarder la durée de vie d'un propriétaire qui obéit aux mêmes règles.

Remarque: Les membres sont toujours détruits devant le propriétaire dans l'ordre inverse de la création.

  • Ainsi, pour les élèves, ils sont créés dans l'ordre de déclaration
    et détruit dans l'ordre inverse de la déclaration
  • Ainsi, pour les membres du tableau, ils sont créés dans l'ordre 0 -> top
    et détruit dans l'ordre inverse haut -> 0

variables temporaires

Ce sont des objets qui sont créés à la suite d'une expression mais qui ne sont pas affectés à une variable. Les variables temporaires sont détruites comme les autres variables automatiques. C'est juste que la fin de leur portée est la fin de la instruction dans laquelle ils sont créés (c'est généralement le ';').

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 ';'

Remarque: Il existe des situations où la durée de vie d'un temporaire peut être prolongée.
Mais cela n'est pas pertinent pour cette simple discussion. Au moment où vous comprenez que ce document sera une seconde nature pour vous et avant qu'il ne prolonge la durée de vie d'un temporaire n'est pas quelque chose que vous voulez faire.

Objets de durée de stockage dynamique

Ces objets ont une durée de vie dynamique et sont créés avec new et détruits avec un appel à 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

Pour les développeurs qui proviennent de langages récupérés, cela peut sembler étrange (gérer la durée de vie de votre objet). Mais le problème n'est pas aussi grave qu'il n'y paraît. Il est inhabituel en C++ d'utiliser directement des objets alloués dynamiquement. Nous avons des objets de gestion pour contrôler leur durée de vie.

La chose la plus proche de la plupart des autres langues collectées par GC est le std::shared_ptr. Cela gardera une trace du nombre d'utilisateurs d'un objet créé dynamiquement et quand tous seront partis, il appellera delete automatiquement (je pense que c'est une meilleure version d'un objet Java normal).

int main()
{
    std::cout << "Main Start\n";
    std::shared_ptr<Test>  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

Objets de durée de stockage des threads

Ce sont nouveaux dans la langue. Ils ressemblent beaucoup à des objets de durée de stockage statique. Mais plutôt que de vivre la même vie que l'application, ils vivent aussi longtemps que le fil d'exécution auquel ils sont associés.

35
Martin York