Destructeurs (C++)

Un destructeur est une fonction membre appelée automatiquement lorsque l’objet sort de l’étendue ou est explicitement détruit par un appel à delete ou delete[]. Un destructeur porte le même nom que la classe et est précédé d’un tilde (~). Par exemple, le destructeur de la classe String est déclaré : ~String().

Si vous ne définissez pas de destructeur, le compilateur fournit un destructeur par défaut et, pour certaines classes, cela suffit. Vous devez définir un destructeur personnalisé lorsque la classe gère les ressources qui doivent être explicitement libérées, telles que des handles vers des ressources système ou des pointeurs vers la mémoire qui doivent être libérés lorsqu’une instance de la classe est détruite.

Prenons la déclaration suivante d'une classe String :

// spec1_destructors.cpp
#include <string> // strlen()

class String
{
    public:
        String(const char* ch);  // Declare the constructor
        ~String();               // Declare the destructor
    private:
        char* _text{nullptr};
};

// Define the constructor
String::String(const char* ch)
{
    size_t sizeOfText = strlen(ch) + 1; // +1 to account for trailing NULL

    // Dynamically allocate the correct amount of memory.
    _text = new char[sizeOfText];

    // If the allocation succeeds, copy the initialization string.
    if (_text)
    {
        strcpy_s(_text, sizeOfText, ch);
    }
}

// Define the destructor.
String::~String()
{
    // Deallocate the memory that was previously reserved for the string.
    delete[] _text;
}

int main()
{
    String str("We love C++");
}

Dans l’exemple précédent, le destructeur String::~String utilise l’opérateur delete[] pour libérer l’espace alloué dynamiquement pour le stockage de texte.

Déclaration des destructeurs

Les destructeurs sont des fonctions ayant le même nom que la classe, mais précédé d'un tilde (~).

Plusieurs règles régissent la déclaration des destructeurs. Les destructeurs :

  • N’acceptez pas les arguments.
  • Ne retournez pas de valeur (ou void).
  • Ne peut pas être déclaré en tant que const, volatileou static. Toutefois, ils peuvent être appelés pour la destruction d’objets déclarés en tant que const, volatileou static.
  • Peut être déclaré en tant que virtual. À l’aide de destructeurs virtuels, vous pouvez détruire des objets sans connaître leur type : le destructeur correct pour l’objet est appelé à l’aide du mécanisme de fonction virtuelle. Les destructeurs peuvent également être déclarés comme des fonctions virtuelles pures pour les classes abstraites.

Utilisation de destructeurs

Les destructeurs sont appelés lorsque l'un des événements suivants se produit :

  • Un objet (automatique) local avec portée de bloc passe hors de portée.
  • Permet delete de libérer un objet alloué à l’aide newde . L’utilisation delete[] entraîne un comportement non défini.
  • Permet delete[] de libérer un objet alloué à l’aide new[]de . L’utilisation delete entraîne un comportement non défini.
  • La durée de vie d'un objet temporaire se termine.
  • Un programme se termine et des objets globaux ou statiques existent.
  • Le destructeur est appelé explicitement à l'aide du nom complet de la fonction destructeur.

Les destructeurs peuvent librement appeler des fonctions membres de classe et accéder aux données de membres de classe.

Il existe deux restrictions sur l’utilisation des destructeurs :

  • Vous ne pouvez pas prendre son adresse.

  • Les classes dérivées n’héritent pas du destructeur de leur classe de base.

Ordre de destruction

Lorsqu'un objet bascule hors de portée ou est supprimé, la séquence d'événements de sa suppression complète est la suivante :

  1. Le destructeur de la classe est appelé et le corps de la fonction destructeur est exécuté.

  2. Les destructeurs des objets membres non statiques sont appelés dans l'ordre inverse dans lequel ils apparaissent dans la déclaration de classe. La liste facultative d’initialisation des membres utilisée dans la construction de ces membres n’affecte pas l’ordre de construction ou de destruction.

  3. Les destructeurs pour les classes de base non virtuelles sont appelés dans l’ordre inverse de déclaration.

  4. Les destructeurs pour les classes de base virtuelles sont appelés dans l'ordre inverse de leur déclaration.

// order_of_destruction.cpp
#include <cstdio>

struct A1      { virtual ~A1() { printf("A1 dtor\n"); } };
struct A2 : A1 { virtual ~A2() { printf("A2 dtor\n"); } };
struct A3 : A2 { virtual ~A3() { printf("A3 dtor\n"); } };

struct B1      { ~B1() { printf("B1 dtor\n"); } };
struct B2 : B1 { ~B2() { printf("B2 dtor\n"); } };
struct B3 : B2 { ~B3() { printf("B3 dtor\n"); } };

int main() {
   A1 * a = new A3;
   delete a;
   printf("\n");

   B1 * b = new B3;
   delete b;
   printf("\n");

   B3 * b2 = new B3;
   delete b2;
}
A3 dtor
A2 dtor
A1 dtor

B1 dtor

B3 dtor
B2 dtor
B1 dtor

Classes de base virtuelles

Les destructeurs pour les classes de base virtuelles sont appelés dans l'ordre inverse d'apparition dans un graphique acyclique dirigé (balayage à profondeur prioritaire, de gauche à droite, post-ordre). L'illustration suivante représente un graphique d'héritage.

Inheritance graph that shows virtual base classes.

Cinq classes, étiquetées A à E, sont organisées dans un graphique d’héritage. La classe E est la classe de base B, C et D. Classes C et D sont la classe de base A et B.

Les listes suivantes répertorient les définitions de classes des classes indiquées dans la figure :

class A {};
class B {};
class C : virtual public A, virtual public B {};
class D : virtual public A, virtual public B {};
class E : public C, public D, virtual public B {};

Pour déterminer l'ordre de destruction des classes de base virtuelles d'un objet de type E, le compilateur génère une liste en appliquant l'algorithme suivant :

  1. Balayez le graphique vers la gauche, en démarrant au point le plus élevé dans le graphique (dans ce cas, E).
  2. Exécutez des balayages vers la gauche jusqu'à ce que tous les nœuds aient été consultés. Notez le nom du nœud actuel.
  3. Reprenez le nœud précédent (en bas et à droite) pour déterminer si le nœud mémorisé est une classe de base virtuelle.
  4. Si le nœud mémorisé est une classe de base virtuelle, analysez la liste pour voir si elle a déjà été entrée. Si ce n’est pas une classe de base virtuelle, ignorez-la.
  5. Si le nœud mémorisé n’est pas encore dans la liste, ajoutez-le au bas de la liste.
  6. Balayez le graphique vers le haut et le long du tracé suivant vers la droite.
  7. Passez à l’étape 2.
  8. Lorsque le dernier tracé vers le haut est écoulé, notez le nom du nœud actuel.
  9. Passez à l’étape 3.
  10. Continuez ainsi jusqu'à ce que le nœud inférieur soit de nouveau le nœud actuel.

Par conséquent, pour la classe E, l'ordre de destruction est le suivant :

  1. Classe de base Enon virtuelle .
  2. Classe de base Dnon virtuelle .
  3. Classe de base Cnon virtuelle .
  4. Classe de base virtuelle B.
  5. Classe de base virtuelle A.

Ce processus crée une liste triée d'entrées uniques. Aucun nom de classe n'apparaît deux fois. Une fois la liste construite, elle est décrite dans l’ordre inverse et le destructeur de chacune des classes de la liste est appelé du dernier au premier.

L’ordre de construction ou de destruction est principalement important lorsque les constructeurs ou les destructeurs d’une classe s’appuient sur l’autre composant créé en premier ou persistant plus long( par exemple, si le destructeur pour A (dans la figure indiquée précédemment) s’appuie toujours sur B être présent lorsque son code est exécuté, ou inversement.

Ces interdépendances entre les classes dans un graphique d'héritage sont fondamentalement dangereuses car les classes dérivées ultérieurement peuvent modifier le tracé à l'extrême gauche, modifiant ainsi l'ordre de construction et de destruction.

Classes de base non virtuelles

Les destructeurs pour les classes de base non virtuelles sont appelés dans l’ordre inverse dans lequel les noms de classes de base sont déclarés. Prenons la déclaration de classe suivante :

class MultInherit : public Base1, public Base2
...

Dans l'exemple précédent, le destructeur de Base2 est appelé avant le destructeur de Base1.

Appels de destructeur explicites

Appeler un destructeur explicitement est rarement nécessaire. Toutefois, il peut être utile d'effectuer un nettoyage des objets placés à des adresses absolues. Ces objets sont généralement alloués à l’aide d’un opérateur défini par new l’utilisateur qui prend un argument de placement. L’opérateur delete ne peut pas libérer cette mémoire, car elle n’est pas allouée à partir du magasin gratuit (pour plus d’informations, consultez Les opérateurs nouveaux et supprimés). Un appel au destructeur, toutefois, permet d'effectuer un nettoyage approprié. Pour appeler explicitement le destructeur pour un objet, s, de classe String, utilisez l'une des instructions suivantes :

s.String::~String();     // non-virtual call
ps->String::~String();   // non-virtual call

s.~String();       // Virtual call
ps->~String();     // Virtual call

La notation pour les appels explicites aux destructeurs, illustrée dans l'exemple précédent, peut être utilisée que le type définisse ou non un destructeur. Vous pouvez ainsi effectuer ce type d'appels explicites sans savoir si un destructeur est défini pour le type. Un appel explicite à un destructeur n'a aucun effet lorsqu'aucun destructeur n'est défini.

Programmation fiable

Une classe a besoin d’un destructeur s’il acquiert une ressource et qu’elle gère la ressource en toute sécurité, il doit probablement implémenter un constructeur de copie et une affectation de copie.

Si ces fonctions spéciales ne sont pas définies par l’utilisateur, elles sont implicitement définies par le compilateur. Les constructeurs et opérateurs d’affectation générés implicitement effectuent une copie peu profonde, dans le sens des membres, ce qui est presque certainement incorrect si un objet gère une ressource.

Dans l’exemple suivant, le constructeur de copie généré implicitement rend les pointeurs str1.text et str2.text fait référence à la même mémoire, et lorsque nous revenons de copy_strings(), cette mémoire sera supprimée deux fois, ce comportement non défini :

void copy_strings()
{
   String str1("I have a sense of impending disaster...");
   String str2 = str1; // str1.text and str2.text now refer to the same object
} // delete[] _text; deallocates the same memory twice
  // undefined behavior

La définition explicite d’un destructeur, d’un constructeur de copie ou d’un opérateur d’affectation de copie empêche la définition implicite du constructeur de déplacement et l’opérateur d’affectation de déplacement. Dans ce cas, l’échec de la fourniture d’opérations de déplacement est généralement, si la copie est coûteuse, une opportunité d’optimisation manquée.

Voir aussi

Constructeurs de copie et opérateurs d’assignation de copie
Constructeurs de déplacement et opérateurs d’assignation de déplacement