lalahop

Mise au point à propos des destructeurs

La programmation orientée objet introduit le concept de destructeur. Pour résumer, il s'agit d'une unique méthode de classe (dans le sens où une classe peut implémenter plusieurs constructeurs mais un seul destructeur) qui est exécutée lors de la destruction d'un objet de la classe afin de récupérer les ressources, mémoire notamment, empruntées lors de la création de l'objet.

Sur le net, on peut parfois lire qu'il ne sert à rien d'écrire soit-même le code du destructeur ou qu'il faut écrire le destructeur que lorsque l'on utilise des pointeurs. Parfois, c'est encore pire : on peut lire qu'un destructeur ne sert à rien. Faisons une petite mise au point.

La première chose à savoir est que l'on n'a pas besoin de destructeur dans les langages de programmation (Java, PHP, etc. ) qui implémentent un système de ramasse-miettes (en anglais, juste pour épater les filles : garbage collector) car la libération de la mémoire est alors automatique. Certains vont dire : dans ce cas, à quoi sert la méthode java.lang.object.finalize() en Java ?

La réponse est simple : d'une part, la méthode finalize() ne détruit pas l'objet : elle permet au programmeur de réaliser une action avant la destruction à proprement parlé de l'objet par le ramasse-miettes. D'autre part, elle n'est pas à utiliser, même pour fermer un descripteur de fichier car Java ne peut ni garantir que cette méthode sera bien exécutée ni quand elle le sera. De plus cela ruine la portabilité de l'application, ce qui est un peu dommage vu que c'est un des principaux objectifs de Java. Ce n'est pas moi qui le dit mais Joshua Bloch dans son livre "Effective Java: Programming Language Guide". Vu les antécédents de cette personne dans le domaine Java, on peut, peut-être, lui faire confiance.

Prenons un exemple rapide : réaliser un close() sur un objet de la classe FileOuputStream à l'intérieur d'une méthode finalize() redéfinie est une mauvaise méthode en plus d'être inutile. Le close() doit être fait dans le bloc finally (vous savez : try ... catch ... finally) qui suit le try dans lequel vous avez ouvert le fichier. Plus précisément, elle doit être faite dans un bloc try ... catch à l'intérieur d'un bloc finally car cette méthode peut soulever une exception de type IOException.

Pour lever tout ambiguïté concernant mes propos :
En PHP, la méthode magique __destruct permet de personnaliser le processus de destruction d'un objet (exemple : supprimer une ligne dans une table de la base de donnée lors de la destruction d'une instance d'une classe donnée). Néanmoins, PHP implémente un mécanisme de libération automatique de la mémoire. Avant PHP 5.3, il était basé sur un compteur de référence : dès qu'une variable n'est plus référencée, elle est supprimée. Depuis PHP 5.3, le mécanisme a été amélioré et on obtient un garbage collector qui, en plus de faire la même chose que le mécanisme du compteur de référence, permet d'éviter les problèmes liés aux références circulaires. C'est pour cela que je pense que, comme en Java, il n'est pas nécessaire de redéfinir un destructeur sauf à vouloir personnaliser la destruction d'un objet comme évoqué précédemment.

Ensuite, pour les langages orientés objet sans ramasse-miettes (C++ par exemple) :

- Pour tout ce qui est allocation statique (int, double, array, objet) à l'intérieur d'une classe et qui sera donc stocké sur la pile, vous n'avez pas besoin d'un destructeur. Même avec un pointeur (un tableau est un pointeur), tout sera supprimé automatiquement par le destructeur par défaut. C'est pour ça qu'il est faux de dire qu'il faut écrire soit-même le code du destructeur dès que l'on utilise un pointeur. Le must c'est que même les appels récursifs sont pris en compte. Par exemple : Soit une classe A qui créer un objet de classe B qui elle même créer un tableau. Le tableau sera bel et bien supprimé.

- Pour tout ce qui est allocation dynamique (utilisation de l'opérateur new ou malloc) à l'intérieur d'une classe et qui sera donc stocké sur le tas, il est impératif d'écrire soit-même le code du destructeur. C'est pour ça qu'il est faux de dire qu'un destructeur ne sert à rien ou qu'il ne faut jamais le re-implémenter.

Pour résumer :

  • Langage objet avec ramasse-miettes : pas de destructeur à implémenter
  • Langage objet sans ramasse-miettes :
    • utilisation de l'allocation statique à l'intérieur d'une classe : pas besoin de redéfinir le destructeur par défaut.
    • utilisation de l'allocation dynamique à l'intérieur d'une classe : il faut redéfinir le destructeur par défaut (mais aussi le constructeur de recopie et l'opérateur d'affectation pour bien faire ;)).

ÉDIT du 28/10/2011 à 14h : Et si vous avez un doute sur la libération de la mémoire, vous pouvez utiliser Valgrind. Je l'ai utilisé uniquement sur du C et du C++ mais, d’après le grand Web, il fonctionne aussi avec du Python. Fin de l'édit.

Les commentaires sont fermés