Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Un destructeur est une fonction membre appelée automatiquement lorsque l’objet sort du champ d'application 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
,volatile
oustatic
. Toutefois, ils peuvent être appelés pour la destruction d’objets déclarés en tant queconst
,volatile
oustatic
. - 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.
- Utilisez
delete
pour libérer un objet alloué à l'aide denew
. L’utilisationdelete[]
entraîne un comportement non défini. - Utilisez
delete[]
pour libérer un objet alloué à l'aide denew[]
. L’utilisationdelete
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 obtenir 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 :
Le destructeur de la classe est appelé et le corps de la fonction destructeur est exécuté.
Les destructeurs d'objets membres non statiques sont appelés dans l'ordre inverse de celui 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.
Les destructeurs pour les classes de base non virtuelles sont appelés dans l’ordre inverse de déclaration.
Les destructeurs pour les classes de base virtuelles sont appelés dans l'ordre inverse de l'ordre de 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.
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 :
- Balayez le graphique vers la gauche, en démarrant au point le plus élevé dans le graphique (dans ce cas,
E
). - 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.
- 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.
- 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.
- Si le nœud mémorisé n’est pas encore dans la liste, ajoutez-le au bas de la liste.
- Balayez le graphique vers le haut et le long du tracé suivant vers la droite.
- Passez à l’étape 2.
- Lorsque le dernier tracé ascendant est épuisé, notez le nom du nœud actuel.
- Passez à l'étape 3.
- 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 :
- Classe de base
E
non virtuelle . - Classe de base
D
non virtuelle . - Classe de base
C
non virtuelle . - Classe de base virtuelle
B
. - 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 parcourue 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 destructeurs d'une classe dépendent de manière critique de l'autre composant créé en premier ou persistant plus longtemps—par exemple, si le destructeur pour A
(dans la figure indiquée précédemment) dépend de façon cruciale que B
soit toujours présent lors de l'exécution de son code, et vice versa.
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 new
défini par 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 Opérateurs new et delete). 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 si elle acquiert une ressource, et pour gérer la ressource en toute sécurité, elle doit probablement implémenter un constructeur de copie et une affectation par 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 fera que les pointeurs str1.text
et str2.text
se réfèrent à la même mémoire, et lorsque nous reviendrons de copy_strings()
, cette mémoire sera libérée deux fois, ce qui constitue un 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’assignation de copie empêche la définition implicite du constructeur de déplacement et de l’opérateur d’assignation 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