Reprenons. Nous avons maintenant 3 fichiers :
- main.cpp : il contient le main, dans lequel on a créé 2 objets de type Personnage : david et goliath.
- Personnage.h : c'est le header de la classe Personnage. On y liste les prototypes des méthodes et les attributs. On y définit la portée (public / private) de chacun des éléments. Pour respecter le principe d'encapsulation, tous nos attributs sont privés, c'est-à-dire non accessibles de l'extérieur.
- Personnage.cpp : c'est le fichier dans lequel on implémente nos méthodes, c'est-à-dire qu'on écrit le code source des méthodes.
Pour l'instant, nous avons défini et implémenté pas mal de méthodes. Je voudrais vous parler ici de 2 méthodes particulières que l'on retrouve dans la plupart des classes : le constructeur et le destructeur.
- Le constructeur : c'est une méthode qui est appelée automatiquement à chaque fois que l'on crée un objet basé sur cette classe.
- Le destructeur : c'est une méthode qui est automatiquement appelée lorsqu'un objet est détruit, par exemple à la fin de la fonction dans laquelle il a été déclaré ou lors d'un delete si l'objet a été alloué dynamiquement avec new.
Voyons voir plus en détail comment fonctionnent ces méthodes un peu particulières...
Le constructeur
Comme son nom l'indique, c'est une méthode qui sert à
construire l'objet. Dès qu'on crée un objet, le constructeur est automatiquement appelé.
Par exemple, lorsqu'on fait dans notre main :
Code : C++ | Personnage david, goliath;
|
Le constructeur de l'objet david est appelé, et de même pour le constructeur de l'objet goliath.
Un constructeur par défaut est automatiquement créé par le compilateur. C'est un constructeur vide, qui ne fait rien de particulier.
On a cependant très souvent besoin de créer nous-mêmes un constructeur, qui remplace ce constructeur vide par défaut.
Le rôle du constructeur
Si le constructeur est appelé lors de la création de l'objet, ce n'est pas pour faire joli. En fait, le rôle principal du constructeur est d'
initialiser les attributs.
En effet, souvenez-vous : nos attributs sont déclarés dans Personnage.h, mais pas initialisés !
Revoici Personnage.h :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | #include <string>
class Personnage
{
public:
void recevoirDegats(int nbDegats);
void attaquer(Personnage &cible);
void boirePotionDeVie(int quantitePotion);
void changerArme(std::string nomNouvelleArme, int degatsNouvelleArme);
bool estVivant();
private:
int m_vie;
int m_mana;
std::string m_nomArme;
int m_degatsArme;
};
|
Nos attributs m_vie, m_mana, et m_degatsArmes ne sont pas initialisés ! Pourquoi ? Parce qu'on n'a pas le droit d'initialiser les attributs ici. C'est justement dans le constructeur qu'il faut le faire.
En fait, le constructeur est indispensable pour initialiser les attributs qui ne sont pas des objets (type classique : int, double, char...). En effet, ceux-ci ont une valeur inconnue en mémoire (ça peut être 0 comme -3451).
En revanche, les attributs qui sont des objets, comme c'est le cas de m_nomArme ici qui est un string, sont automatiquement initialisés par le langage C++ avec une valeur par défaut.
Créer un constructeur
Le constructeur est une méthode, mais une méthode un peu particulière.
En effet, pour créer un constructeur, il y a 2 règles à respecter :
- Il faut que la méthode ait le même nom que la classe. Dans notre cas, la méthode devra s'appeler "Personnage".
- La méthode ne doit RIEN renvoyer, pas même void ! C'est une méthode sans aucun type de retour.
Si on déclare son prototype dans Personnage.h, ça donne ça :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | #include <string>
class Personnage
{
public:
Personnage(); // Constructeur
void recevoirDegats(int nbDegats);
void attaquer(Personnage &cible);
void boirePotionDeVie(int quantitePotion);
void changerArme(std::string nomNouvelleArme, int degatsNouvelleArme);
bool estVivant();
private:
int m_vie;
int m_mana;
std::string m_nomArme;
int m_degatsArme;
};
|
Le constructeur se voit du premier coup d'oeil : déjà parce qu'il n'a aucun type de retour (pas de void ni rien), et ensuite parce qu'il a le même nom que la classe.
Et si on en profitait pour implémenter ce constructeur dans Personnage.cpp maintenant ?

Voici à quoi pourrait ressembler son implémentation :
Code : C++ | Personnage::Personnage()
{
m_vie = 100;
m_mana = 100;
m_nomArme = "Epée rouillée";
m_degatsArme = 10;
}
|
Vous noterez une fois de plus qu'il n'y a pas de type de retour, pas même void (très important, c'est une erreur que l'on fait souvent

).
J'ai choisi de mettre la vie et la mana à 100, le maximum, ce qui est logique. J'ai mis par défaut une arme appelée "Epée rouillée" qui fait 10 de dégâts à chaque coup.
Et voilà ! Notre classe Personnage a un constructeur qui initialise les attributs, elle est désormais pleinement utilisable.

Maintenant, à chaque fois que l'on crée un objet de type Personnage, celui-ci est initialisé à 100 points de vie et de mana, avec l'arme "Epée rouillée". Nos deux compères david et goliath commencent donc à égalité lorsqu'ils sont créés dans le main :
Code : C++ | Personnage david, goliath; // Les constructeurs de david et goliath sont appelés.
|
Autre façon d'initialiser avec un constructeur : la liste d'initialisation
Le C++ permet d'initialiser les attributs de la classe d'une autre manière (un peu déroutante) appelée
liste d'initialisation. C'est une technique que je vous recommande d'utiliser quand vous le pouvez (et c'est celle que nous utiliserons dans ce cours).
Reprenons le constructeur qu'on vient de créer :
Code : C++ | Personnage::Personnage()
{
m_vie = 100;
m_mana = 100;
m_nomArme = "Epée rouillée";
m_degatsArme = 10;
}
|
Le code que vous allez voir ci-dessous produit le même effet :
Code : C++ | Personnage::Personnage() : m_vie(100), m_mana(100), m_nomArme("Epée rouillée"), m_degatsArme(10)
{
// Rien à mettre dans le corps du constructeur, tout a déjà été fait !
}
|
La nouveauté, c'est qu'on rajoute un symbole deux-points (
:) suivi de la liste des attributs que l'on veut initialiser avec la valeur entre parenthèses. Avec ce code, on initialise la vie à 100, la mana à 100, l'attribut m_nomArme à "Epée rouillée", etc.
Cette technique est un peu surprenante, surtout que du coup on n'a plus rien à mettre dans le corps du constructeur entre les accolades, vu que tout a déjà été fait avant ! Elle a toutefois l'avantage d'être "plus propre" et se révèlera pratique dans la suite du chapitre.
On va donc utiliser autant que possible les listes d'initialisation avec les constructeurs, c'est une bonne habitude à prendre.
Le prototype du constructeur (dans le .h) ne change pas. Toute la partie après les deux-points n'apparaît pas dans le prototype.
Surcharger le constructeur
Vous savez qu'en C++ on a le droit de surcharger les fonctions, donc de surcharger les méthodes. Et comme le constructeur est une méthode, on a le droit de le surcharger lui aussi.
Pourquoi je vous en parle ? Ce n'est pas par hasard : en fait, le constructeur est une méthode que l'on a tendance à beaucoup surcharger. Cela permet de créer un objet de plusieurs façons différentes.
Pour l'instant, on a créé un constructeur sans paramètres :
Code : C++
On appelle ça :
le constructeur par défaut (il fallait bien lui donner un nom le pauvre

).
Supposons que l'on souhaite créer un personnage qui ait dès le départ une meilleure arme... comment diable faire ?
C'est là que la surcharge devient utile. On va créer un 2ème constructeur qui prendra en paramètre le nom de l'arme et ses dégâts.
Dans Personnage.h, on va donc rajouter ce prototype :
Code : C++ | Personnage(std::string nomArme, int degatsArme);
|
Le préfixe std:: est obligatoire ici comme je vous l'ai dit plus tôt car on n'utilise pas la directive using namespace std; dans le .h (cf chapitre précédent).
L'implémentation dans Personnage.cpp sera la suivante :
Code : C++ | Personnage::Personnage(string nomArme, int degatsArme) : m_vie(100), m_mana(100), m_nomArme(nomArme), m_degatsArme(degatsArme)
{
}
|
Vous noterez ici tout l'intérêt de mettre le préfixe
m_ au début des attributs : comme ça on peut faire la différence dans notre code entre
m_nomArme, qui est un attribut, et
nomArme, qui est le paramètre envoyé au constructeur.
Ici, on place juste dans l'attribut de l'objet le nom de l'arme envoyé en paramètre. On recopie juste la valeur. C'est tout bête, mais il faut le faire, sinon l'objet ne se "souviendra pas" du nom de l'arme qu'il possède.
La vie et la mana, eux, sont toujours fixés à 100 (il faut bien les initialiser), mais l'arme, elle, peut maintenant être indiquée par l'utilisateur lorsqu'il crée l'objet.
Quel utilisateur ?

Souvenez-vous, l'utilisateur c'est celui qui crée et utilise les objets. Le concepteur c'est celui qui crée les classes.
Dans notre cas, la création des objets est faite dans le main. Pour le moment, la création de nos objets ressemble à ça :
Code : C++ | Personnage david, goliath;
|
Comme on n'a spécifié aucun paramètre, c'est le constructeur par défaut (celui sans paramètres) qui sera appelé.
Maintenant supposons que l'on veuille donner dès le départ une meilleure arme à Goliath (c'est lui le plus fort après tout

). On va indiquer entre parenthèses le nom et la puissance de cette arme :
Code : C++ | Personnage david, goliath("Epée aiguisée", 20);
|
Goliath est équipé de l'épée aiguisée dès sa création. David est équipé de l'arme par défaut, l'épée rouillée.
Comme on n'a spécifié aucun paramètre lors de la création de david, c'est le constructeur par défaut qui sera appelé pour lui. Pour goliath, comme on a spécifié des paramètres, c'est le constructeur qui prend en paramètre un string et un int qui sera appelé.
Exercice : on aurait aussi pu permettre à l'utilisateur de modifier la vie et la mana de départ, mais je ne l'ai pas fait ici. Ce n'est pas compliqué, vous pouvez le faire pour vous entraîner. Ca vous fera un troisième constructeur surchargé.
Le destructeur
Le destructeur est une méthode appelée lorsque l'objet est supprimé de la mémoire. Son principal rôle est de désallouer la mémoire (via des delete) qui a été allouée dynamiquement.
Dans le cas de notre classe Personnage, on n'a fait aucune allocation dynamique (il n'y a aucun new). Le destructeur est donc inutile. Cependant, vous en aurez certainement besoin un jour où l'autre, car on est souvent amené à faire des allocations dynamiques.
Tenez, l'objet string par exemple, vous croyez qu'il fonctionne comment ? Il a un destructeur qui lui permet, juste avant la destruction de l'objet, de supprimer le tableau de char qu'il a alloué dynamiquement en mémoire. Il fait donc un delete sur le tableau de char, ce qui permet de garder une mémoire propre et d'éviter les fameuses "fuites de mémoire".
Créer un destructeur
Bien que ce soit inutile dans notre cas (je n'ai pas mis d'allocations dynamiques pour ne pas trop compliquer de suite

), je vais vous montrer comment on crée un destructeur. Voici les règles à suivre :
- Un destructeur est une méthode qui commence par un tilde ~ suivi du nom de la classe
- Un destructeur ne renvoie aucune valeur, pas même void (comme le constructeur)
- Et, nouveauté : le destructeur ne peut prendre aucun paramètre. Il y a donc toujours un seul destructeur, il ne peut pas être surchargé.
Dans Personnage.h, le prototype du destructeur sera donc :
Code : C++
Dans Personnage.cpp, l'implémentation sera :
Code : C++ | Personnage::~Personnage()
{
/* Rien à mettre ici car on ne fait pas d'allocation dynamique
dans la classe Personnage. Le destructeur est donc inutile mais
je le mets pour montrer à quoi ça ressemble.
En temps normal, un destructeur fait souvent des delete et quelques
autres vérifications si nécessaire avant la destruction de l'objet */
}
|
Bon vous l'aurez compris, mon destructeur ne fait rien. C'était même pas la peine de le créer (il n'est pas obligatoire après tout).
Cela vous montre néanmoins la procédure à suivre. Soyez rassurés, nous ferons des allocations dynamiques plus tôt que vous ne le pensez (je sais je suis diabolique

), et nous aurons alors grand besoin du destructeur pour désallouer la mémoire !