Pointeurs, références & allocation dynamique Rappels sur les fonctions Les spécificités du C++ Structures et classes Encapsulation des données Notions de constructeur et de destructeur Fonctions et classes amies Surcharge d'opérateur Héritage Notions de patrons de fonctions et de classes Introduction à la librairie standard STL
Compilation et directives de préprocesseur Convention d'écriture et organisation des programmes Écriture/lecture sur l'entrée/sortie standard Les membres données statiques Utilisation de enum et de typedef
Retour menu principal

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) et point (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").