Ce qu'est un destructeur
Sur la page précédente, vous avez vu les constructeurs - des fonctions spéciales qui s'exécutent quand un objet naît pour établir son état initial. Un destructeur est l'image miroir : une fonction spéciale qui s'exécute quand un objet meurt, pour faire le nettoyage après lui.
Vous le déclarez avec le nom de la classe précédé d'un tilde (~). Il ne prend aucun paramètre, ne renvoie rien, et une classe peut en avoir exactement un. Vous ne l'appelez presque jamais à la main - C++ l'appelle pour vous au bon moment.
Remarquez que le message du destructeur s'affiche après que main a terminé le corps de sa fonction, mais avant que le programme ne se termine. Quand log sort de sa portée à l'accolade fermante, C++ exécute ~Logger() pour vous.
Quand les destructeurs s'exécutent
Le moment exact dépend de l'endroit où vit l'objet :
- Les objets sur la pile (locaux) sont détruits quand ils sortent de leur portée - à l'accolade fermante
}du bloc. - Les objets sur le tas (créés avec
new) sont détruits quand vous appelezdelete. Si vous oubliez ledelete, le destructeur ne s'exécute jamais et vous provoquez une fuite.
Cet exemple rend la différence visible :
Les objets sont détruits dans l'ordre inverse de leur construction. a a été construit en premier, il meurt donc en dernier. Cet ordre LIFO (last-in, first-out - dernier entré, premier sorti) compte lorsque les objets dépendent les uns des autres.
Pourquoi les destructeurs comptent : RAII
La véritable puissance des destructeurs est qu'ils rendent le nettoyage automatique et sûr face aux exceptions. Au lieu de penser à libérer une ressource sur chaque chemin de code, vous placez la libération dans un destructeur et laissez le langage garantir qu'elle s'exécute. Ce motif s'appelle RAII - Resource Acquisition Is Initialization (l'acquisition de ressource est une initialisation) - et c'est l'ossature du C++ moderne.
Ici, une classe possède un tampon sur le tas : elle l'alloue dans le constructeur et le libère dans le destructeur, de sorte que les appelants ne touchent jamais à new/delete eux-mêmes.
L'idée clé : même si une exception était levée après la création de squares, la pile se dépilerait et ~IntArray() s'exécuterait quand même. C'est cette garantie qui rend RAII si fiable - et la raison pour laquelle vous écrivez rarement un delete nu dans du bon code C++.
La Règle de Trois (et de Cinq)
Une classe dotée d'un destructeur personnalisé possède presque toujours une ressource brute, et cela crée un danger caché. Le constructeur de copie et l'affectation de copie générés par le compilateur effectuent une copie superficielle - ils copient le pointeur, pas le tampon vers lequel il pointe. Désormais deux objets détiennent le même pointeur, et les deux destructeurs feront delete dessus, provoquant un plantage par double libération.
IntArray a(5);
IntArray b = a; // copie superficielle : a.data et b.data sont le MÊME pointeur
// en fin de portée : le destructeur de b libère le tampon,
// puis le destructeur de a le libère À NOUVEAU -> comportement indéfini (double libération)
Cela conduit à la Règle de Trois : si vous écrivez l'un des trois - destructeur, constructeur de copie ou opérateur d'affectation de copie - vous avez presque certainement besoin des trois. En C++11 et au-delà, elle s'étend à la Règle de Cinq, en ajoutant le constructeur de déplacement et l'affectation par déplacement.
Il existe pourtant une règle encore meilleure - la Règle de Zéro : concevez les classes de façon à ne gérer aucune ressource brute. Détenez plutôt un std::vector, un std::string ou un pointeur intelligent, et le destructeur généré par le compilateur fait ce qu'il faut gratuitement.
Optez pour la Règle de Zéro par défaut. N'écrivez un destructeur personnalisé que lorsque vous possédez vraiment une ressource brute qu'aucun type standard n'encapsule pour vous.
Destructeurs virtuels
Quand vous supprimez un objet via un pointeur sur la classe de base, le destructeur doit être virtual - sinon seule la partie de base est détruite et la partie dérivée fuit. C'est l'un des bugs les plus courants dans le code polymorphe, et le compilateur ne vous en avertit pas par défaut.
Sans virtual sur ~Base, delete p n'appellerait que ~Base() - comportement indéfini, et la partie Derived de l'objet n'est jamais nettoyée. Règle empirique : toute classe avec des fonctions virtuelles (une classe de base polymorphe) a besoin d'un destructeur virtuel. Vous verrez exactement pourquoi cela compte dès que vous commencerez à dériver des classes.
Erreurs et pièges courants
Quelques pièges font trébucher presque tout le monde :
new/delete qui ne correspondent pas. Si vous allouez avec new[], libérez avec delete[]. Mélanger new[] avec un delete simple (ou l'inverse) est un comportement indéfini.
Oublier virtual sur un destructeur de base. Comme ci-dessus, supprimer un objet dérivé via un pointeur de base sans destructeur virtuel fait fuir la partie dérivée. Si vous écrivez une classe destinée à être héritée, rendez le destructeur virtuel.
Laisser des exceptions s'échapper d'un destructeur. Un destructeur qui lève une exception pendant le dépilement de la pile met fin à votre programme. En C++ moderne, les destructeurs sont implicitement noexcept - empêchez le code de nettoyage de lever des exceptions, ou avalez l'exception à l'intérieur du destructeur.
Écrire un destructeur dont vous n'avez pas besoin. Si vos membres se nettoient déjà tout seuls, un ~NomClasse() {} vide ajoute du bruit et peut désactiver silencieusement les opérations de déplacement. Quand il n'y a rien à nettoyer, n'écrivez aucun destructeur.
Ensuite : l'héritage
Vous avez maintenant vu le cycle de vie complet d'un objet - les constructeurs lui donnent vie, les destructeurs le nettoient, et les destructeurs virtual maintiennent ce nettoyage correct quand une classe s'appuie sur une autre. Ce dernier point est un aperçu de la prochaine grande idée : l'héritage, où une classe réutilise et étend les données et le comportement d'une autre. La page suivante montre comment dériver une classe d'une autre, comment la construction et la destruction s'enchaînent à travers la hiérarchie, et comment les éléments que vous venez d'apprendre s'imbriquent.
Questions fréquentes
Qu'est-ce qu'un destructeur en C++ ?
Un destructeur est une fonction membre spéciale nommée ~NomClasse() qui s'exécute automatiquement quand un objet est détruit - lorsqu'il sort de sa portée ou que vous faites delete. Son rôle est le nettoyage : libérer de la mémoire, fermer des fichiers ou libérer toute ressource que l'objet possède. Il ne prend aucun paramètre, n'a pas de type de retour, et une classe ne peut en avoir qu'un seul.
Quand un destructeur s'exécute-t-il en C++ ?
Pour un objet local (sur la pile), le destructeur s'exécute quand il sort de sa portée, à l'accolade fermante }. Pour un objet sur le tas créé avec new, il s'exécute quand vous appelez delete. Les membres et les classes de base sont détruits automatiquement ensuite, dans l'ordre inverse de la construction.
Dois-je toujours écrire un destructeur en C++ ?
Non. Si votre classe ne contient que des membres qui se nettoient tout seuls (comme std::string, std::vector ou des pointeurs intelligents), le destructeur généré par le compilateur suffit - n'en écrivez pas. Vous n'avez besoin d'un destructeur personnalisé que lorsque votre classe possède une ressource brute, comme de la mémoire issue de new ou un descripteur de fichier ouvert.