C++: Constructeurs et Destructeurs
C++: Constructeurs et Destructeurs
Constructeurs et destructeurs
Centre Informatique pour les Lettres
et les Sciences Humaines
2 – Constructeurs...................................................................................................................4
3 - Listes d'initialisation........................................................................................................10
4 - Destructeurs ...................................................................................................................12
5 - Bon, c'est gentil tout ça, mais ça fait quand même 13 pages. Qu'est-ce que je dois
vraiment en retenir ? .......................................................................................................13
On dit qu'une fonction est surchargée lorsqu'il existe (au moins) une autre fonction portant
exactement le même nom.
Deux fonctions ne peuvent porter le même nom que si elles diffèrent par leur caractère
constant ou par le type d'au moins un de leurs paramètres.
Il est donc possible de définir deux fonctions différentes dont les déclarations seraient
1 void f( int n, double x);
2 void f(char n, double x);
puisque le premier paramètre de la première est de type int alors que le premier paramètre de
la seconde est de type char. L'ordre des paramètres est bien entendu significatif, et
1 void f(int n, double x);//déclaration d'une fonction
2 void f(double x, int); //déclaration d'une fonction homonyme de la précédente
sont également des déclarations correspondant à deux fonctions différentes.
Les noms attribués aux paramètres sont en revanche sans importance, et les lignes
1 void f(int n, double x); //déclaration d'une fonction
2 void f(int machin, double truc); //redéclaration de la fonction précédente
constituent en fait deux déclarations de la même fonction1 et non les déclarations de deux
fonctions homonymes. Comme une fonction ne peut être définie qu'une seule fois, toute
tentative pour définir la pseudo seconde fonction se soldera par un message indiquant que
cette définition est inacceptable, car l'unique fonction f() existe déjà.
1 Rappels : Un objet peut être déclaré plusieurs fois, à condition que toutes ces déclarations soient identiques. Les
noms des paramètres ne figurent dans la déclaration qu'à titre documentaire, et sont ignorés par le compilateur.
Dans le cas de fonctions membre, le fait que seule l'une d'entre elles soit privée du droit de
modifier les variables membre de l'instance au titre de laquelle elle est invoquée suffit à les
distinguer, et l'on peut donc avoir
1 class CExemple
2 {
3 public:
4 void f(int x, double y); //une première fonction membre
5 void f(int x, double y) const; //une seconde fonction membre
6 };
La constance et la liste (ordonnée) des types de ses arguments constituent ce qu'on appelle la
signature d'une fonction.
On peut donc dire que deux fonctions peuvent porter le même nom à condition que leurs
signatures soient différentes. C'est sur la base de ces signatures que le compilateur peut
déterminer quel code doit être exécuté lorsque l'évaluation d'une expression implique l'appel
d'une fonction surchargée.
Si nous disposons de deux fonctions déclarées ainsi
1 void g(bool x); //déclaration d'une fonction
2 void g(char * s); //déclaration d'une fonction homonyme de la précédente
il est en effet clair que des lignes telles que
1 bool test = true;
2 g(test);
3 g(false);
se traduiront par l'exécution de la première fonction g() (celle qui dispose d'un paramètre de
type bool pour recevoir la valeur booléenne transmise), alors que les lignes
1 char texte[] = "Bravo";
2 char c = 'a';
3 g(texte);
4 g(&c);
déclencheront l'exécution de la seconde (celle qui dispose d'un paramètre de type "pointeur sur
char" pour recevoir l'adresse transmise).
Dans le cas de fonctions membre dont la signature ne diffère que parce que l'une des deux est
constante, la règle appliquée est simple : la fonction "non constante" est exécutée, sauf lorsque
l'appel est effectué au titre d'une instance constante (seule la fonction qui ne peut pas modifier
les variables membre est alors légitime).
L'utilisation des signatures pour choisir entre les différentes fonctions portant le même nom
conduit parfois à des phénomènes assez inattendus, liés en particulier aux interactions entre
ce dispositif et deux autres mécanismes : celui des conversions automatiques de type et celui
des valeurs par défaut. Imaginons que nous disposions d'une fonction déclarée ainsi :
void h(int a);
Il est tout à fait possible d'appeler cette fonction en spécifiant la valeur du paramètre à l'aide
d'une constante littérale de type char :
h('x');
En effet, bien que 'x' soit une constante littérale de type char, il s'agit d'une valeur
numérique que le compilateur est capable de convertir automatiquement en un int
convenant à l'initialisation du paramètre de la fonction h(). Imaginons maintenant que
l'évolution du programme conduise à introduire une fonction déclarée ainsi :
void h(char a);
Les signatures de ces fonctions diffèrent, et l'homonymie est donc acceptée par le
compilateur. Il faut cependant bien comprendre que toutes les lignes de code qui utilisaient
une expression de type char pour déterminer la valeur transmise lors d'un appel de la
première fonction h() appellent désormais la seconde fonction h(). Ce changement de
comportement est généralement souhaitable, mais il faut en être conscient et éviter d'utiliser
la surcharge dans les cas où il serait inopportun.
Par ailleurs, le système des valeurs par défaut conduit certaines fonctions à avoir plusieurs
signatures : puisque certains paramètres peuvent être omis lors de l'appel de la fonction,
différentes listes de types des paramètres sont envisageables. Dans certains cas, valeurs par
défaut et surcharge sont donc incompatibles, car la mise en œuvre simultanée de ces deux
procédés rend le mécanisme des signatures insuffisant pour déterminer quel code doit être
2 - Constructeurs
Les classes que nous créons sont généralement destinées à nous permettre de stocker des
données. La définition d'une classe n'est alors qu'une première étape : il nous faut ensuite
l'instancier (soit en définissant des variables dont le type est cette classe, soit en utilisant
l'allocation dynamique). Ce sont ces instances qui nous permettent finalement de stocker et de
manipuler efficacement nos données. L'intérêt du mécanisme des classes repose donc en
grande partie sur le processus d'instanciation, processus que nous avons utilisé jusqu'à
présent sans nous inquiéter outre mesure des opérations qu'il implique. La plupart de ces
opérations restent sous la responsabilité exclusive du compilateur, et nous n'aurons jamais à
nous y intéresser. Le langage C++ nous offre cependant un moyen d'influer, lorsque nous le
jugeons nécessaire, sur le processus d'instanciation des classes que nous avons créées :
La création d'une instance d'une classe s'accompagne toujours de l'exécution d'une fonction
particulière, que l'on appelle un constructeur de la classe.
Les constructeurs présentent de nombreuses caractéristiques qui les distinguent des fonctions
"ordinaires". Une des caractéristiques les plus déroutantes pour le programmeur novice est
sans doute que ces fonctions sont très souvent appelées implicitement, c'est à dire sans que la
ligne de code qui déclenche leur exécution ne présente les caractéristiques qui permettent
habituellement de reconnaître un appel de fonction : le nom de la fonction, suivi d'un couple de
parenthèses encadrant la (ou les) expression(s) déterminant la (ou les) valeur(s) transmise(s)
comme argument(s). L'étrangeté de la situation est en outre amplifiée par le fait que le
compilateur crée automatiquement certains constructeurs, qui n'apparaissent alors pas dans
le code définissant la classe.
Imaginons, par exemple, une classe définie de la façon suivante :
1 class CTresSimple
2 {
3 public :
4 int leMembre;
5 };
A première vue, cette classe ne comporte aucune fonction, et il est donc assez difficile
d'imaginer que la création d'une variable locale dans une fonction quelconque, à l'aide d'une
ligne aussi anodine que
CTresSimple uneInstance; //instanciation innocente de notre classe
se traduit en fait par l'appel (invisible) d'une fonction membre qui n'est ni déclarée ni définie
par notre code. C'est pourtant très exactement ce qui se produit : le compilateur ajoute
automatiquement un constructeur à la classe, et ce constructeur est (implicitement) appelé
lors de la création d'une instance.
Le rôle du compilateur est de produire un exécutable à partir du texte source, et
certainement pas de modifier ou de compléter le texte source. Un constructeur ajouté
automatiquement à une classe par le compilateur n'existe donc à aucun moment sous forme
d'un texte en C++ que nous pourrions consulter.
Ce mécanisme quelque peu opaque s'éclaircit rapidement dès lors que les constructeurs sont
explicitement déclarés, définis et appelés.
Un constructeur est une fonction membre qui porte le même nom que la classe et est
dépourvue de type.
Attention : c'est bien de type que les constructeurs sont dépourvus, et pas simplement de
valeur de retour. En d'autres termes, les constructeurs ne sont même pas de type void.
Pour reprendre l'exemple précédent, la classe CTresSimple peut donc être définie ainsi :
1 class CTresSimple
2 {
3 public :
4 int leMembre;
5 CTresSimple(); //déclaration d'un constructeur : - pas de type
// - même nom que la classe
6 };
La définition du constructeur en question ne présente pas de particularités par rapport à celle
de n'importe quelle autre fonction membre : on reprend la déclaration (en prenant toutefois
soin d'utiliser le nom complet de la fonction), mais on remplace le point virgule final par un
couple d'accolades contenant le code de la fonction.
1 CTresSimple::CTresSimple()
2 {//le bloc de code définissant ce constructeur est vide
3 }
Il peut sembler choquant que le bloc de code définissant le constructeur reste vide.
Contrairement aux apparences, cela ne signifie pas que l'exécution du constructeur soit sans
intérêt. Une des caractéristiques très particulières des constructeurs est que leur exécution
provoque la création d'une instance avant que le bloc de code qui les définit ne commence à
être exécuté. Le véritable processus de création d'une instance est du ressort exclusif du
compilateur, il ne peut pas réellement être décrit en C++ "normal".
La déclaration et la définition explicites de ce constructeur ne changent rien aux possibilités
d'utilisation de la classe CTresSimple, qui peut toujours être instanciée normalement :
CTresSimple uneInstance; //appel implicite du constructeur
Notons également qu'il est aussi possible d'appeler explicitement le constructeur lors de
l'instanciation de la classe :
CTresSimple uneInstance = CTresSimple(); //appel explicite du constructeur
Un tel appel explicite est toujours possible, que le constructeur ait ou non été défini
explicitement. Il reste toutefois d'un usage assez rare dans le cas de la définition d'une
variable, car il en alourdit l'écriture sans présenter d'avantages vraiment significatifs.
Les constructeurs que nous avons rencontrés jusqu'à présent sont dépourvus d'arguments, et
permettent donc d'obtenir l'instanciation de la classe sans avoir à fournir de données.
Lorsque le code définissant une classe ne comporte aucun constructeur, le compilateur rend la
classe instanciable en lui ajoutant automatiquement un constructeur par défaut.
Le constructeur par défaut fourni par le compilateur est réellement minimal, puisqu'il
correspond à un constructeur par défaut dont le bloc d'instructions serait vide. Lorsque le
constructeur par défaut est défini explicitement, il est bien entendu possible de placer des
instructions dans son bloc de code, et celles-ci seront alors exécutées comme si le constructeur
avait été invoqué au titre de l'instance qui vient d'être créée.
Une des missions qui échoit naturellement à un constructeur est l'initialisation des variables
membre, et le constructeur par défaut de la classe que nous avons imaginée dans les exemples
précédents pourrait être défini ainsi :
1 CTresSimple::CTresSimple()
2 {
3 leMembre = 0;
4 }
L'intérêt des constructeurs devient alors évident : étant donné que l'instanciation de la classe
CTresSimple s'accompagne automatiquement de l'appel du constructeur, il devient
rigoureusement impossible qu'une variable de ce type soit créée sans que sa variable membre
ne reçoive une valeur initiale. Une source d'erreurs de programmation se trouve ainsi éliminée.
Le constructeur par défaut est également appelé lorsque l'opérateur new est utilisé pour
réserver une zone de mémoire destinée à stocker une instance de la classe :
1 CTresSimple * ptr;
2 ptr = new CTresSimple; //appel implicite du constructeur par défaut
Lorsque new[] est employé pour réserver une zone de mémoire destinée à stocker plusieurs
instances de la classe, le constructeur est exécuté pour chacune de ces instances :
1 CTresSimple * unFauxTableau;
2 unFauxTableau = new CTresSimple[100]; //le constructeur est appelé 100 fois
Le même phénomène intervient évidemment lors de la définition d'un vrai tableau :
CTresSimple unTableau[100]; //le constructeur est appelé 100 fois
La définition d'une variable et l'usage de l'opérateur new ne constituent pas les seuls cas où
une classe se trouve instanciée. Lors de l'appel d'une fonction utilisant comme paramètre une
instance de la classe, il est également nécessaire de créer une instance.
En effet, comme nous le savons depuis la Leçon 5, la fonction ne va pas travailler sur l'objet
utilisé pour spécifier la valeur de son paramètre, mais va simplement utiliser cette valeur pour
initialiser son propre objet (le paramètre lui-même). Ceci suppose donc une opération
d'initialisation de l'objet nouvellement créé (le paramètre) à l'aide des valeurs contenues dans
l'objet utilisé par la fonction appelante pour spécifier la valeur de ce paramètre. Chaque fois
que nous écrivons quelque chose comme :
1 CTresSimple uneInstance; //le constructeur par défaut initialise leMembre à 0
2 uneInstance.leMembre = 17; //on affecte une nouvelle valeur au membre
3 maFonction(uneInstance);
la fonction appelée se trouve en position de devoir initialiser son paramètre avec la valeur
transmise. Une situation analogue se présente si nous écrivons :
1 CTresSimple uneInstance;
2 uneInstance.leMembre = 18;
3 CTresSimple uneAutre = uneInstance; //initialisation d'une instance
Dans un cas comme dans l'autre, le constructeur ne doit pas initialiser leMembre de l'instance
en cours de création2 avec la valeur 0, mais avec la valeur contenue dans leMembre de
l'instance qui sert de "modèle", c'est à dire 17 dans le premier cas, et 18 dans le second. Quelle
que soit la façon dont il est défini, le constructeur par défaut est parfaitement incapable de
réaliser cette opération car, étant lui-même dépourvu de paramètre, il est condamné à utiliser
toujours la même valeur pour initialiser leMembre. Lorsque ce genre d'initialisation est
nécessaire, il nous faut donc disposer d'un autre type de constructeur, capable d'utiliser une
instance existant déjà comme "modèle" de l'instance à créer. Ce "modèle" sera communiqué au
constructeur par le biais d'un paramètre de type "référence à une instance constante".
Il n'est en effet pas envisageable que le paramètre soit de type "instance", puisque nous
venons de voir que la transmission de ce type de paramètre est précisément l'un des cas qui
exige la mise en œuvre du constructeur qu'il s'agit ici de définir ! L'usage d'une référence évite
ce cercle vicieux et, comme le propos du constructeur par copie n'est certainement pas de
modifier l'instance qui lui sert de modèle, il est préférable que son paramètre désigne l'objet
en question comme étant constant.
On appelle constructeur par copie un constructeur qui reçoit comme unique paramètre une
référence à une instance de la classe.
Un constructeur par copie a normalement vocation à s'inspirer de l'objet qui lui est
communiqué pour initialiser les variables membre de l'instance en cours de création. Dans de
nombreux cas, cette "inspiration" consiste purement et simplement à donner aux variables
membre de la nouvelle instance des valeurs identiques à celles rencontrées dans l'instance qui
sert de modèle, ensemble d'opérations que l'on qualifie souvent de "copie membre à membre".
Nous rencontrerons bientôt des cas où le constructeur par copie ne peut se contenter d'une
approche aussi rudimentaire. Un des cas classiques est celui des variables membre de type
pointeur : un constructeur par copie "membre à membre" produit une instance qui pointe au
même endroit que le modèle utilisé, ce qui n'est pas toujours l'effet souhaité, et peut même
s'avérer fort dangereux.
La présence d'un constructeur par copie est indispensable à l'utilisation d'une instance de la
classe pour en initialiser une autre. Etant donné qu'une telle initialisation est implicitement
effectuée dès qu'une fonction utilise un paramètre de type "instance de la classe", un
constructeur par copie est une fonction membre très souvent requise, ce qui justifie que
Le compilateur génère automatiquement un constructeur par copie pour toute classe qui en est
dépourvue alors que l'usage qui en est fait le nécessite.
Constructeurs de transtypage
Les constructeurs par défaut et par copie ne sont pas les seuls types de constructeurs
envisageables. Selon la nature de la classe, il peut en effet être assez naturel d'utiliser, pour
spécifier l'état initial d'une instance, autre chose qu'une autre instance de cette même classe.
Dans le cas de notre classe CTresSimple, il serait assez tentant de pouvoir initialiser
directement l'unique variable membre à l'aide d'une valeur entière. Ceci devient possible dès
lors que la classe dispose d'un constructeur admettant pour paramètre un int :
1 class CTresSimple
2 {
3 public :
4 int leMembre;
5 CTresSimple(); //constructeur par défaut
6 CTresSimple(int valeur); //constructeur de transtypage à partir d'int
7 };
Le constructeur en question peut être défini ainsi :
1 CTresSimple::CTresSimple(int valeur)
2 {
3 leMembre = valeur;
4 }
Il est alors possible d'appeler explicitement ce constructeur
CTresSimple uneInstance = CTresSimple(3);
On pourrait croire que la ligne précédente provoque d'abord l'appel du constructeur de
transtypage pour créer une instance temporaire et anonyme qui serait ensuite utilisée par un
constructeur par copie pour créer uneInstance. Il n'en est rien, et cette ligne ne génère
qu'un unique appel, au constructeur de transtypage.
Toutefois, on préfère généralement la simplicité syntaxique d'un appel implicite
CTresSimple uneInstance(3); //appel implicite du constructeur de transtypage
qui peut aussi être obtenu en utilisant la notation "traditionnelle" de l'initialisation
CTresSimple uneInstance = 3; //appel implicite du constructeur de transtypage
Il faut aussi remarquer que la disponibilité d'un constructeur de transtypage permet au
compilateur d'effectuer automatiquement certaines conversions, ce qui autorise par exemple
l'affectation suivante :
1 CTresSimple uneInstance; //appel implicite du constructeur par défaut
2 uneInstance = 15; //affectation avec transtypage automatique !
Cette affectation peut sembler un peu "magique", mais le déroulement des opérations est
finalement assez simple :
- L'expression placée à droite de l'opérateur d'affectation est évaluée. Il s'agit d'une constante
littérale, dont la valeur est 15 et le type est int.
- L'expression placée à gauche de l'opérateur d'affectation est évaluée. Il s'agit du nom d'une
variable dont le type est CTresSimple, et l'expression désigne donc une zone de mémoire
susceptible de stocker le résultat obtenu à droite, sous réserve que les types soient
"compatibles" (deux types sont compatibles lorsqu'ils sont identiques ou lorsque le
compilateur sait comment produire une valeur ayant le type de gauche à partir d'une valeur
ayant le type de droite).
- Les types int et CTresSimple ne sont pas identiques, mais le compilateur sait comment
produire une valeur de type CTresSimple à partir d'une valeur de type int : il suffit de
transmettre cette dernière au constructeur de CTresSimple qui attend ce type de
paramètre ! Une fois ceci fait, il ne reste plus qu'à appliquer l'opérateur d'affectation entre
deux objets de types CTresSimple : la variable uneInstance et l'instance (temporaire et
anonyme) produite par le constructeur à partir de l'entier 15.
Cette apparente possibilité d'affectation à une instance de la classe d'une valeur d'un type
différent explique la terminologie employée :
Il arrive (assez exceptionnellement, il faut bien l'avouer) que la disponibilité d'un constructeur
de transtypage conduise le compilateur à effectuer automatiquement des conversions qui ne
sont pas souhaitables. Lorsqu'un tel cas particulier se présente, le langage permet
d'empêcher l'usage automatique du constructeur concerné. Il suffit pour cela de faire
précéder la déclaration du constructeur du mot explicit. Si notre classe est définie ainsi
class CTresSimple
{
public :
int leMembre;
CTresSimple(); //constructeur par défaut
explicit CTresSimple(int valeur); //constructeur de transtypage
};
la conversion d'un int en CTresSimple n'est plus effectuée que si elle explicitement
demandée
CTresSimple uneInstance; //appel implicite du constructeur par défaut
//uneInstance = 15; //ERREUR : transtypage automatique interdit !
uneInstance = CTresSimple(15); //OK : appel explicite du constructeur
ou si elle intervient lors d'une instanciation
CTresSimple incroyable(3); //appel implicite d'un constructeur explicit !
La possibilité d'effectuer l'initialisation d'une instance à partir d'une valeur d'un autre type
dépend bien entendu de la nature de la classe considérée et de l'usage qui en est fait. Il n'est
donc pas possible pour le compilateur d'inventer des constructeurs de transtypage, et ceux-ci
n'existent que dans la mesure où l'auteur de la classe les a définis. Il est, par ailleurs, tout à
fait possible de définir des constructeurs exigeant plusieurs arguments (et qui ne sont donc ni
"par défaut", ni "par copie", ni "de transtypage").
Imaginons que nous disposions d'une classe comportant plusieurs variables membre. Si nous
souhaitons pouvoir initialiser plusieurs de ces variables avec des valeurs quelconques, il nous
faut bien entendu un constructeur disposant d'autant de paramètres.
1 class CDuo
2 {
3 public:
4 double m_decimal;
5 int m_entier;
6 CDuo (double unDecimal, int unEntier); //constructeur à deux arguments
7 };
La définition d'un constructeur de ce genre est sans surprise :
1 CDuo::CDuo (double unDecimal, int unEntier)
2 {
3 m_decimal = unDecimal;
4 m_entier = unEntier;
5 }
Définie ainsi, notre classe se prête à l'instanciation grâce à l'une des syntaxes suivantes :
1 CDuo uneInstance = CDuo(0.0, 0); //appel explicite du constructeur
2 CDuo uneAutre(3.14, 18); //appel implicite du constructeur
Il faut toutefois noter que, étant donné qu'un constructeur comportant plusieurs arguments
n'est pas un constructeur de transtypage, il n'est pas possible d'en utiliser un appel implicite
pour effectuer l'affectation d'une liste de valeurs à une instance de notre classe :
uneInstance = {2.7, 36}; // IMPOSSIBLE !
Une affectation n'est ici possible qu'entre deux objets de même type, ce qui exige qu'une
instance (temporaire et anonyme) soit créée par appel explicite du constructeur :
uneInstance = CDuo(2.7, 36); // OK
Le fait que certains constructeurs puissent parfois être générés automatiquement par le
compilateur simplifie considérablement la mise en œuvre des classes les plus simples. Ce
phénomène risque, en revanche, de générer une certaine confusion dans l'esprit du
programmeur débutant, qui connaît l'existence de ces constructeurs, mais maîtrise encore mal
les règles qui les gouvernent. Le tableau ci-dessous résume les caractéristiques essentielles des
quatre types de constructeurs :
La définition explicite d'un constructeur, quel qu'il soit, inhibe la génération automatique d'un
constructeur par défaut.
3 - Listes d'initialisations
Nous avons jusqu'à présent évoqué l'utilisation des constructeurs pour initialiser les variables
membre des instances en cours de création. Si cet emploi du mot "initialisation" semble justifié
par le fait que le constructeur est, par définition, exécuté avant que l'instance ait pu recevoir
quelque valeur que ce soit, il n'en reste pas moins que, dans les exemples précédents, les
constructeurs effectuent des opérations qui restent, du point de vue syntaxique, de simples
affectations. La distinction est, le plus souvent, sans grande importance pratique. Il existe
cependant des circonstances où une affectation s'avère impossible, et c'est notamment le cas
lorsque les données membre auxquelles le constructeur est censé conférer une valeur initiale
sont des constantes, des références, ou des instances d'une classe ne comportant pas de
constructeur par défaut.
Il est, bien entendu, impossible de procéder à l'affectation d'une valeur à une constante ou à
une référence, mais le problème n'est pas là : la création d'une constante ou d'une référence
exige absolument qu'il y ait une véritable initialisation (au sens syntaxique du terme). La
difficulté n'est donc pas que le constructeur n'a pas le droit de procéder à des affectations,
mais bien que l'instance ne peut même pas être créée pour être ensuite confiée au
constructeur. Le même problème se pose lorsque l'une des variables membre est elle-même
une instance d'une classe qui ne comporte pas de constructeur par défaut : la création de ce
membre exige une initialisation. Rendre l'affectation possible (en définissant un constructeur
de transtypage, par exemple) n'est donc d'aucun secours, car la variable membre sur laquelle
le constructeur aurait alors le droit de procéder à une affectation ne peut pas être créée.
La solution adoptée par les concepteurs de C++ est de doter les constructeurs d'un moyen de
spécifier avec quelles valeurs doivent être initialisées les variables membre de l'instance sur
laquelle ils opéreront dès qu'elle aura été créée. La syntaxe employée pour obtenir ce résultat
consiste à insérer la spécification des valeurs d'initialisation entre la parenthèse qui clôt la liste
des paramètres et l'accolade qui ouvre le corps de la fonction. Cette "liste d'initialisation"
débute par le symbole "deux points" et énumère les noms des membres devant être initialisés,
3 Il est d'ailleurs généralement préférable de profiter de l'occasion pour initialiser les variables membre avec des valeurs
suivis chacun d'un couple de parenthèses encadrant la valeur devant être utilisée. Le
constructeur par défaut de notre classe élémentaire, que nous avions définie ainsi
1 CTresSimple::CTresSimple()
2 {
3 leMembre = 0;
4 }
pourrait donc également être définie comme ceci :
1 CTresSimple::CTresSimple() : leMembre(0)
2 {}//l'initialisation est faite, le constructeur n'a plus rien à faire…
Comme nous l'avons vu dans la Leçon 3, on choisit souvent de définir les fonctions membre
très brèves dans la définition de la classe elle-même. Si nous adoptons cette stratégie, la
définition de la classe devient
class CTresSimple
{
public :
int leMembre; //déclaration de la variable membre
CTresSimple() : leMembre(0) { } //DEFINITION directe du constructeur
};
Bien que, comme nous l'avons vu, les valeurs d'initialisations soient utilisées avant que le
constructeur ne soit effectivement appelé, il est tout à fait possible que la liste d'initialisation
utilise les valeurs qui seront reçues par le constructeur lors de son appel4. Le constructeur de
la classe CDuo, que nous avions défini ainsi
1 CDuo::CDuo (double unDecimal, int unEntier)
2 {
3 m_decimal = unDecimal;
4 m_entier = unEntier;
5 }
pourrait donc être réécrit comme cela :
1 CDuo::CDuo (double unDecimal, int unEntier)
2 : m_decimal(unDecimal), m_entier(unEntier)
3 {}//les initialisations sont faites, le constructeur n'a plus rien à faire…
Dans les deux exemples qui précèdent, le recours à une liste d'initialisation relève de la pure
coquetterie : il n'y a aucun inconvénient réel à "initialiser" les variables membre à l'aide
d'affectations effectuées dans le corps du constructeur. Ce n'est en revanche pas le cas de
l'exemple suivant, qui concerne une classe définie ainsi:
1 class CMoinsSimple
2 {
3 public:
4 const int m_constante; //une constante non initialisée ?
5 double & m_ref; //une référence non initialisée ?
6 CDuo m_duo; //une instance de CDuo non initialisée ?
//mais un constructeur qui arrange tout ça !
7 CMoinsSimple (int uneConstante, double &uneRef, CDuo unDuo);
8 };
Si l'on est conscient du problème posé par la nature des membre de la classe CMoinsSimple, la
définition de son constructeur ne pose pas réellement de problème :
1 CMoinsSimple::CMoinsSimple(int uneConstante, double &uneRef, CDuo unDuo)
2 : m_constante(uneConstante), m_ref(uneRef), m_duo(unDuo)
3 {}//les initialisations sont faites, le constructeur n'a plus rien à faire…
4 Vous pouvez, au choix, considérer cela comme un des charmes du langage ou trouver qu'il s'agit d'une abomination
sans nom, mais c'est vraiment comme cela que les choses se passent : les valeurs qui seront reçues comme paramètres
peuvent, dans ce cas, être utilisées avant que la fonction ne soit appelée. Une autre façon de se représenter le
phénomène serait de dire que le constructeur est appelé avant que l'instance n'existe, qu'il appelle ensuite de façon
invisible le processus de création en lui communiquant la liste d'initialisation, et que ce n'est que dans un troisième
temps qu'il exécute les instructions présentes dans son corps. Le problème est qu'il faut alors accepter l'idée qu'une
fonction membre (le constructeur) peut être appelée au titre d'une instance qui n'existe pas encore, ce qui n'est guère
moins abominable que l'hypothèse précédente.
En effet, du fait que le membre deux est déclaré avant le membre un, il est le premier à être
initialisé, ce qui signifie que le calcul effectué à cette occasion fait intervenir la valeur de un
avant que ce membre n'ait été initialisé… d'où un résultat imprévisible.
4 - Destructeurs
De même que la naissance d'une instance s'accompagne toujours de l'appel d'un constructeur,
la "mort" d'une instance provoque l'appel du destructeur de la classe.
Un destructeur est une fonction membre qui porte le même nom que la classe, précédé du
signe ~, et est dépourvue de type et dépourvue de paramètre.
L'absence de paramètre exclut la surcharge, puisqu'une seule signature est possible5 (celle qui
est réduite à la liste vide). Une classe peut donc comporter de nombreux constructeurs, mais
elle aura toujours un seul et unique destructeur.
L'appel explicite du destructeur d'une classe est possible, mais n'est nécessaire que dans des
cas extrêmement particuliers, qui ne nous intéressent pas pour l'instant.
Attention : le destructeur est automatiquement appelé lorsqu'une instance "meurt", mais ceci
ne signifie pas que l'appel du destructeur "tue" l'instance. La mémoire occupée par une
variable locale, par exemple, ne sera jamais libérée par l'appel explicite du destructeur. Cet
appel provoque l'exécution des instructions définissant le destructeur, mais celles-ci ne
peuvent en aucun cas appliquer à une variable un traitement équivalent à celui que delete
peut faire subir à une instance créée par allocation dynamique.
Lors de la disparition d'un tableau d'instances, chacun des éléments du tableau fait l'objet
d'un appel du destructeur.
1 { //début d'un bloc
2 CTresSimple tab[100]; //variable locale au bloc
3 } //fin du bloc : le destructeur est appelé 100 fois
Lors de la libération de la mémoire attribuée à un (faux) tableau d'instances, le destructeur est
appelé par l'opérateur delete[] pour chacune des instances.
1 CTresSimple * tab = new CTresSimple[100];
2 delete[] tab; //si la création a réussi, le destructeur est appelé 100 fois
Les opérations effectuées par un destructeur dépendent étroitement de la nature de la classe
concernée. Il s'agit le plus souvent d'opérations "symétriques" de celles effectuées lors de la
construction : libération de mémoire si le constructeur en alloue, fermeture d'un fichier que le
constructeur aurait ouvert, etc. Les constructeurs générés automatiquement par le
5 Ni les constructeurs ni les destructeurs ne peuvent être déclarés const, ce qui exclut toute différence de signatures
reposant sur autre chose que le type des paramètres.
compilateur se bornant à initialiser les variables membre, ils ne nécessitent aucune opération
"symétrique" au moment de la destruction, ce qui explique que :
Lorsqu'une classe est dépourvue de destructeur, le compilateur lui en ajoute automatiquement
un, qui n'effectue aucun traitement.
Si l'on rend explicite la présence du destructeur, la définition de notre exemple le plus simple
devient :
1 class CTresSimple
2 {
3 public :
4 int leMembre;
5 ~CTresSimple(); //déclaration du destructeur : pas de type
6 // nom de la classe précédé de ~
7 };
et la définition d'une fonction équivalente au destructeur généré automatiquement serait :
1 CTresSimple::~CTresSimple
2 {}//ce destructeur ne fait rien
5 - Bon, c'est gentil tout ça, mais ça fait quand même 13 pages.
Qu'est-ce que je dois vraiment en retenir ?
1) La signature d'une fonction est déterminée par sa constance et la liste des types de ses
paramètres.
2) Deux fonctions peuvent porter le même nom si leurs signatures diffèrent.
3) Lorsque deux fonctions sont homonymes, on dit qu'elles sont surchargées.
4) Lorsqu'une classe est instanciée, il y a toujours exécution d'un de ses constructeurs.
5) Le rôle d'un constructeur n'est pas de créer l'objet sur lequel il opère.
6) Un constructeur est une fonction membre dépourvue de type et portant le nom de la classe.
7) Un constructeur sans arguments est appelé constructeur par défaut.
8) Si la définition d'une classe ne mentionne aucun constructeur, le compilateur crée
automatiquement un constructeur par défaut.
9) Un constructeur par copie est un constructeur recevant comme paramètre une référence à
une instance de la classe.
10) L'initialisation d'une instance au moyen d'une autre nécessite un constructeur par copie.
11) Si, lors d'un appel de fonction, la valeur d'un objet est transmise, le paramètre
correspondant ne peut être initialisé que par le constructeur par copie.
12) Si une classe a besoin d'un constructeur par copie alors qu'elle en est dépourvue, le
compilateur en génère automatiquement un.
13) Les constructeurs générés automatiquement par le compilateur s'avèrent parfois
insuffisants, et l'on peut être conduit d'une part à créer des versions plus élaborées des
constructeurs par défaut et par copie, et d'autre part à créer d'autres types de
constructeurs.
14) Les listes d'initialisation permettent de procéder à de véritables initialisations des membres
de l'instance créée, et non à de simples affectations, ce qui est parfois indispensable.
15) Lorsqu'une instance disparaît, il y a toujours exécution du destructeur de la classe.
16) Un destructeur est une fonction membre dépourvue de type et de paramètre et dont le nom
est composé en faisant précéder celui de la classe du signe ~.