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.)
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.
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.
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.
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.
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).
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.
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;
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".
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:
delete[]
(notez les crochets), free
ou tout autre moyenSi 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.)
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:
delete
, free
ou tout autre moyenSi 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.)
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.)
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.
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.
Celles-ci sont pour tous les sens et les mêmes objectifs que les variables globales en termes de durée de vie.
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.
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:
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
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.
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.
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
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.