Notions de constructeur et de destructeur
Le constructeur et le destructeur sont deux méthodes particulières qui sont
appelées respectivement à la création et à la destruction d’un objet. Toute
classe a un constructeur et un destructeur par défaut, fournis par le
compilateur. Toutefois, il est souvent nécessaire de les redéfinir afin de gérer
certaines actions (initialisation des membres, appel de méthodes…) qui doivent
avoir lieu lors de la création d’un objet puis lors de sa destruction. À titre
d’exemple, si l’objet contient des variables allouées dynamiquement, il faut
leur réserver de la mémoire à la création de l’objet ou, à défaut, mettre les
pointeurs correspondants à 0
(ou NULL
). À la destruction de l’objet, il convient
de restituer la mémoire éventuellement allouée.
Définition des constructeurs et des destructeurs
Le constructeur se définit comme une méthode à part entière. Cependant, pour que le compilateur puisse la considérer en tant que telle, les deux conditions suivantes doivent être validées :
- le constructeur doit porter le même nom que la classe,
- le constructeur ne doit avoir aucun type,
void
compris.
Considérons la classe point
précédemment utilisée. La fonction membre
initialise
est naturellement remplacée par un constructeur, c’est-à-dire par
une méthode dépourvue de type et nommée point
. La déclaration de cette classe
devient alors
class point { private : int m_X; int m_Y; public : point(int abs, int ord); // Constructeur de la classe point };
La définition du constructeur reste identique à celle de la méthode
initialise
, à savoir l’assignation de la valeur abs
, respectivement ord
,
au membre m_X
, respectivement m_Y
, mais le prototype s’écrit
point::point(int abs, int ord) { m_X = abs; m_Y = ord; }
Lors de la construction d’un objet, il est recommandé d'initialiser les
membres au travers du constructeur plutôt que d'assigner leurs
valeurs. Autrement dit, à l’assignation m_X = abs; m_Y = ord
, on préférera
l’initialisation suivante
point::point(int abs, int ord) : m_X(abs), m_Y(ord) {}
Cette possibilité devient indispensable en cas :
- d’initialisation d’un membre constant,
- d’initialisation d’un membre qui est une référence.
Le destructeur doit respecter les mêmes règles que le constructeur sachant que
son nom est toujours précédé du signe tilde. Comme nous l’avons souligné en
introduction, l’utilisation d’un destructeur est naturelle lors de la libération
de l’espace mémoire alloué. Supposons l’ajout d’un pointeur de caractère char
*p_geometry_type
(portant sur le type de géométrie mis en jeu, euclidienne par
exemple) à la classe point
. La déclaration de la classe point
devient
class point { private : ... char *p_geometry_type; public : point(); point(int abs, int ord); ~point(); // Destructeur de la classe point };
tandis que la restitution de la mémoire allouée pour p_geometry_type
se
formalise de la façon suivante
point::~point() { delete[] p_geometry_type; }
Le constructeur est appelé après l’allocation de la mémoire de l’objet alors que
le destructeur est appelé avant la libération de cette mémoire. La gestion de
l’allocation dynamique de mémoire avec les classes est ainsi simplifiée. Par
ailleurs, les constructeurs peuvent avoir des paramètres. Ils peuvent donc être
surchargés (cf. fiche sur les spécificités du C++), à la différence des
destructeurs qui n’ont jamais d’arguments. Cela tient au fait qu’en général, le
contexte dans lequel un objet est crée, est connu ce qui n’est pas le cas lors
de sa destruction : il n’existe donc qu’un seul destructeur pour une classe
donnée. La classe point
peut ainsi proposer plusieurs constructeurs, l’appel
se faisant suivant le contexte, c’est-à-dire suivant l’instanciation d’un objet
point
. On pourra imaginer ainsi la déclaration suivante
class point { private : int m_X; int m_Y; char *p_geometry_type; public : point(); point(int abs, int ord); point(int abs, int ord, unsigned int n_size); ~point(); };
où les constructeurs sont définis ainsi
// Constructeur par défaut point::point() : m_X(0), m_Y(0), p_geometry_type(0) {} // Simple initialisation des coordonnées point::point(int abs, int ord) : m_X(abs), m_Y(ord), p_geometry_type(0) {} // Initialisation des coordonées et allocation dynamique point::point(int abs, int ord, unsigned int n_size) : m_X(abs), m_Y(ord), p_geometry_type(new char[n_size + 1]) {}
Selon le besoin, l’utilisateur de la classe choisira l’un des constructeurs
précédents lors de la création d’un objet point
. À titre d’exemple, les
instanciations suivantes seront toutes différentes tout en étant chacune
valables.
point my_point1; // construction par défaut point my_point2(1, 2); // initialisation des coordonnées point my_point3(1, 2, 256); // allocation dynamique pour le pointeur p_geometry_type
Remarques
- Lorsqu’un constructeur se contente d’attribuer des valeurs initiales aux données membres, le destructeur est rarement indispensable. En revanche, il le devient dés lors que l’objet est amené (par le biais de son constructeur ou d’autres fonctions membres) à allouer dynamiquement de la mémoire.
- En théorie, constructeurs et destructeurs peuvent être publics ou privés. En pratique, à moins d’avoir de bonnes raisons de faire le contraire, il vaut mieux les rendre publics. En effet, un destructeur privé ne pourra pas être appelé directement, ce qui dans l’absolu n’est pas dramatique dès lors qu’aucune allocation dynamique n’a été nécessaire. En revanche, si le constructeur d’une classe est privé, il ne sera plus possible de créer d’objets via ce constructeur. Seule exception notable à cette règle, la possibilité de déclarer le constructeur par défaut (celui sans argument) comme privé, afin d’interdire l’utilisation d’un objet sans avoir explicitement initialiser ces valeurs membres. Cette condition assure une plus grande robustesse au programme, l’utilisateur devant nécessairement préciser les valeurs initiales de chacun des membres de l’objet.
Objets membres
Il est tout à fait possible qu’une classe possède un membre donnée lui-même de
type classe. Par exemple, la classe point
préalablement définie, pourra être
un membre d’une classe cercle
ainsi définie
class cercle { private: point m_centre; double m_rayon; ... };
La situation d’objets membres correspond à une relation entre classes du type
relation de possession. Effectivement, on peut dire qu’un cercle possède un
centre (de type point
). Ce type de relation s’oppose à la relation de type
“relation est” inhérente à la notion d’héritage (cf. Chapitre "Héritage").
La présence d’un constructeur de classe point
impose la création d’un
constructeur de classe cercle
. En effet, en l’absence de constructeur, le
membre m_centre
se verrait certes attribuer un emplacement en mémoire, mais
son constructeur ne pourrait être appelé. Il est donc nécessaire d’une part de
définir un constructeur pour cercle
et d’autre part, de spécifier les
arguments à fournir au constructeur de point
. Le constructeur suivant
class cercle { private: point m_centre; double m_rayon; public: cercle(int abs, int ord, double rayon); }; cercle::cercle(int abs, int ord, double rayon) : centre(abs, ord), m_rayon(rayon) { ... }
propose ainsi une solution. Les constructeurs seront appelés dans l’ordre
suivant: point
, cercle
tandis que les destructeurs seront appelés dans
l’ordre inverse.
Remarques
Il pourrait être envisagé de définir le constructeur de cercle
de la façon
suivante:
cercle::cercle(int abs, int ord, double rayon) : m_rayon(rayon) { centre = point (abs, ord); ... }
Cependant, on créerait alors un objet temporaire de type point
supplémentaire
provoquant le ralentissement du programme lors de l’exécution (a fortiori si
la classe impliquée est de taille conséquente). En outre, si la classe point
dispose de membres dynamiques, p_geometry_type
par exemple, seules les valeurs
des pointeurs seront recopiées et non leurs emplacements mémoire. Il conviendra
alors de surcharger l’opérateur d’affectation =
de telle sorte que les membres
pointeurs soient également affectés (cf. Chapitre "Surcharge d'opérateur").
Les objets dynamiques
Les objets dynamiques, par opposition aux objets automatiques dont la durée de
vie se limite à l’appel d’une fonction ou à la taille d’un bloc (boucle for
,
par exemple), ne sont explicitement détruits qu’à l’appel de l’opérateur
delete
. Cette instruction a pour conséquence l’exécution du destructeur de la
classe et donc, la désallocation de l’espace mémoire réservé. En outre, la
déclaration d’objets dynamiques se fait via l’opérateur new
; le ou les
constructeurs constituent alors le passage obligé lors de la création de
l’objet. Plus précisément, après l’allocation dynamique de l’emplacement mémoire
requis, l’opérateur new
appellera le constructeur de l’objet adéquat selon la
nature des arguments figurant à la suite de son appel. Ainsi, les déclarations
suivantes
point * ptr_point1 = new point; point * ptr_point2 = new point(2,5);
permettront soit l’appel du constructeur par défaut (premier cas), soit l’appel du constructeur initialisant les membres de la classe aux valeurs 2 et 5 (second cas).
L’accès aux méthodes de l’objet pointé ptr_point1
ou ptr_point2
se fera par
des appels de la forme ptr_point1->affiche();
équivalents aux instructions de
type (*ptr_point1).affiche();
Dès lors que l’objet dynamique n’est plus nécessaire, l’utilisation de
l’opérateur delete
, indissociable de l’opérateur new
, entrainera alors
l’appel du destructeur de classe.
Constructeur de recopie
Nous venons de voir que le C++ garantissait l’appel d’un constructeur pour un
objet créé par une déclaration ou par un new
. Ce point est fondamental
puisqu’il certifie qu’un objet ne pourra être créé sans avoir été placé, au
préalable, dans “un état initial convenable” (du moins jugé comme tel par le
concepteur de l’objet).
Cependant, il existe des circonstances dans lesquelles il est nécessaire de construire un objet quand bien même le programmeur n’a pas prévu de constructeur pour cela. La situation la plus fréquente est celle où la valeur d’un objet doit être transmise en argument à une fonction. Dans ce cas précis, il est indispensable de créer, dans un emplacement local à la fonction, un objet qui soit une copie de l’argument effectif. Le même problème se pose dans le cas d’un objet renvoyé par valeur comme résultat d’une fonction; il faut alors créer un objet qui soit une copie du résultat. Une troisième situation se rencontre lors de l’initialisation d’un objet par copie d’un objet du même type.
De manière générale, on regroupe ces trois situations sous le nom d’initialisation par recopie i.e. la création d’un objet par recopie d’un objet existant de même type. Pour réaliser une telle opération, C++ a prévu d’utiliser un constructeur particulier dit constructeur de recopie. En l’absence d’un tel constructeur, un traitement par défaut est prévu. Toutefois, ce comportement par défaut, se contente d’effectuer une copie de chacun des membres de la classe. On retrouve ainsi une situation analogue à celle qui est mise en place (par défaut) lors d’une affectation entre objets de même type. Le problème se pose alors pour des objets contenant des pointeurs sur des emplacements dynamiques. Par défaut, seules les valeurs des pointeurs seront recopiées, les emplacements pointées ne le seront pas.
Afin de conserver les espaces mémoires alloués, il est possible de fournir explicitement, dans la classe, un constructeur de recopie. Il s’agit d’un constructeur disposant d’un seul argument du type de la classe et transmis obligatoirement par référence. Son entête doit donc être de l’une de ces deux formes :
point(point& the_point);
ou
point(const point& the_point);
C’est à ce constructeur et donc au concepteur de la classe, de prendre en charge l’intégralité du travail de copie : copie superficielle c’est-à-dire copie des membres et copie profonde à savoir copie des espaces alloués dynamiquement. Le code suivant fournit un exemple d’implémentation de constructeur de recopie
class point { private : int m_X; int m_Y; char *p_geometry_type; public : point(); point(const point& the_point); ~point(); }; point::point(const point& the_point) { // Copie superficielle m_X = the_point.m_X; m_Y = the_point.m_Y; // Copie profonde if (the_point.p_geometry_type) { p_geometry_type = new char[strlen(the_point.p_geometry_type) + 1]; strcpy(p_geometry_type, the_point.p_geometry_type); } }
Remarques
- Le C++ impose au constructeur par recopie que son unique argument soit
transmis par référence ce qui relève d’une logique implacable puisque, dans le
cas contraire, l’appel du constructeur de recopie impliquerait une
intialisation par recopie de l’argument et donc un appel du constructeur de
recopie qui, lui même, etc etc etc. Quoiqu’il en soit la forme
point(point the_point);
serait rejetée lors de la compilation. - Les deux formes précédentes
point(point& the_point)
etpoint (const point& the_point)
peuvent exister au sein d’une même classe. Dans ce cas, la première serait utilisée en cas d’initialisation par un objet quelconque tandis que la seconde serait utilisée en cas d’initialisation par un objet constant. En général, comme un tel constructeur de recopie n’a logiquement aucune raison de vouloir modifier l’objet reçu en argument, il est conseillé de ne définir que la seconde forme qui restera ainsi applicable aux deux situations évoquées. - Bien que l’initialisation par recopie et l’affectation présente un traitement
par défaut semblable (copie superficielle), la prise en compte d’une copie
profonde passe par des mécanismes différents : définition d’un constructeur de
recopie pour l’initialisation, surdéfinition de l’opérateur
=
pour l’affectation (cf. Chapitre "Surcharge d'opérateur").