Nous avons maintenant 3 fichiers :
main.cpp
: il contient lemain()
, dans lequel nous avons créé deux objets de typePersonnage
:david
etgoliath
.Personnage.hpp
: c'est leheader
de la classePersonnage
. Nous y faisons figurer les prototypes des méthodes et les attributs. Nous y définissons 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 nous implémentons nos méthodes, c'est-à-dire dans lequel nous écrivons 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 appelée automatiquement à chaque fois que l'on crée un objet basé sur cette classe.
Le destructeur : c'est une méthode appelée automatiquement lorsqu'un objet est détruit, par exemple à la fin de la fonction dans laquelle il a été déclaré ou, si l'objet a été alloué dynamiquement avec
new
, lors d'undelete
.
Voyons plus en détail comment fonctionnent ces méthodes un peu particulières…
Initialisez les attributs avec un 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 écrit dans le main()
:
Personnage david, goliath;
le constructeur de la classe Personnage
est appelé pour créer l'objet david
et une deuxième fois pour créer l'objet goliath
.
Comprenez le rôle du constructeur
Le rôle principal du constructeur est d'initialiser les attributs. En effet, souvenez-vous : nos attributs sont déclarés dans Personnage.hpp
mais ils ne sont pas initialisés !
Revoici le code du fichier Personnage.hpp
:
#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.
Créez un constructeur
Pour créer un constructeur, il y a deux règles à respecter :
Il faut que la méthode ait le même nom que la classe. Dans notre cas, la méthode devra donc 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.hpp
, cela donne le code suivant :
#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'œil : déjà parce qu'il n'a aucun type de retour, ensuite parce qu'il porte le même nom que la classe.
Et si on en profitait pour coder ce constructeur dans Personnage.cpp
?
Voici à quoi pourrait ressembler son implémentation :
Personnage::Personnage()
{
m_vie = 100;
m_mana = 100;
m_nomArme = "Epee rouillee";
m_degatsArme = 10;
}
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 "Épée rouillée". Nos deux compères david
et goliath
commencent donc à égalité lorsqu'ils sont créés dans le main()
:
Personnage david, goliath; //Les constructeurs de david et goliath sont appelés
Alternative : utilisez la liste d'initialisation
Reprenons le constructeur que nous venons de créer :
Personnage::Personnage()
{
m_vie = 100;
m_mana = 100;
m_nomArme = "Epee rouillee";
m_degatsArme = 10;
}
Le code que vous allez voir ci-dessous produit le même effet :
Personnage::Personnage() : m_vie(100), m_mana(100), m_nomArme("Epee rouillee"), 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 :
(suivi de la liste des attributs que l'on veut initialiser avec, entre parenthèses, la valeur).
Avec ce code, on initialise la vie à 100, le mana à 100, l'attribut m_nomArme
à "Épé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 : 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 utilisera donc autant que possible les listes d'initialisation avec les constructeurs, c'est une bonne habitude à prendre.
Surchargez 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. Cela permet de créer un objet de plusieurs façons différentes.
Pour l'instant, on a créé un constructeur sans paramètre :
Personnage();
Supposons que l'on souhaite créer un personnage qui ait dès le départ une meilleure arme… comment faire ? C'est là que la surcharge devient utile. On va créer un deuxième constructeur qui prendra en paramètre le nom de l'arme et ses dégâts.
Dans Personnage.hpp
, on rajoute donc ce prototype :
Personnage(std::string nomArme, int degatsArme);
L'implémentation dans Personnage.cpp
sera la suivante :
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 préfixer les attributs par "m_ " : ainsi, on peut faire la différence dans le code entre :
m_nomArme
qui est un attribut.Et
nomArme
qui est le paramètre envoyé au constructeur.
Ici, on place simplement dans l'attribut de l'objet le nom de l'arme envoyé en paramètre. On recopie juste la valeur.
La vie et le mana, eux, sont toujours fixés à 100 (il faut bien les initialiser) ; mais l'arme, quant à elle, peut maintenant être renseigné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 à cela :
Personnage david, goliath;
Comme on n'a spécifié aucun paramètre, c'est le constructeur par défaut (celui sans paramètre) qui sera appelé.
Maintenant, supposons que l'on veuille donner dès le départ une meilleure arme à Goliath ; on indique entre parenthèses le nom et la puissance de cette arme :
Personnage david, goliath("Epee aiguisee", 20);
Goliath est équipé dès sa création de l'épée aiguisée. 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 une string
et un int
qui sera appelé.
Créez aussi le constructeur de copie
Par exemple si l'on souhaite que david
soit une copie conforme de goliath
, il nous suffit d'écrire :
Personnage goliath("Epee aiguisee", 20); //On crée goliath en utilisant un constructeur normal
Personnage david(goliath); //On crée david en copiant tous les attributs de goliath
Le compilateur crée ce constructeur automatiquement pour vous ! C'est donc toute une partie du travail qui nous est épargnée. Merci le compilateur.
Si toutefois, vous désirez changer le comportement du constructeur de copie, il faut simplement :
le déclarer dans votre classe :
Personnage(Personnage const& autre);
et définir son implémentation :
Personnage::Personnage(Personnage const& autre): m_vie(autre.m_vie), m_mana(autre.m_mana), m_nomArme(autre.m_nomArme), m_degatsArme(autre.m_degatsArme)
{
}
On accède directement aux attributs de l'objet à copier (que j'ai appelé autre
) dans la liste d'initialisation. C'est simple et concis.
Créez un destructeur
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 ou 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".
Bien que ce soit inutile dans notre cas (je n'ai pas utilisé d'allocation dynamique pour ne pas trop compliquer les choses tout de suite), je vais vous montrer comment on crée un destructeur.
Voici les règles à suivre pour créer un destructeur :
Un destructeur commence par
~
(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.hpp
, le prototype du destructeur sera donc :
~Personnage();
Dans Personnage.cpp
, l'implémentation sera :
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 cela ressemble.
En temps normal, un destructeur fait souvent des delete et quelques
autres vérifications si nécessaire avant la destruction de l'objet. */
}
Utilisez les méthodes constantes
Quand vous dites "ma méthode est constante", vous indiquez au compilateur que votre méthode ne modifie pas l'objet, c'est-à-dire qu'elle ne modifie la valeur d'aucun de ses attributs.
Par exemple :
une méthode qui se contente d'afficher à l'écran des informations sur l'objet est une méthode constante : elle ne fait que lire les attributs ;
en revanche, une méthode qui met à jour le niveau de vie d'un personnage ne peut pas être constante.
On l'utilise ainsi :
//Prototype de la méthode (dans le .hpp) :
void maMethode(int parametre) const;
//Déclaration de la méthode (dans le .cpp) :
void MaClasse::maMethode(int parametre) const
{
}
On utilisera le mot-clé const
sur des méthodes qui se contentent de renvoyer des informations sans modifier l'objet. C'est le cas par exemple de la méthode estVivant()
, qui indique si le personnage est toujours vivant ou non. Elle ne modifie pas l'objet, elle vérifie le niveau de vie.
bool Personnage::estVivant() const
{
return m_vie > 0;
}
On pourrait trouver d'autres exemples de méthodes concernées. Pensez par exemple à la méthode size()
de la classe string
: elle ne modifie pas l'objet, elle ne fait que nous informer de la longueur du texte contenu dans la chaîne.
Concrètement, à quoi cela sert-il de créer des méthodes constantes ?
Cela sert principalement à 3 choses :
Pour vous : votre méthode ne fait que lire les attributs, et vous vous interdisez dès le début de les modifier. Si par erreur vous tentez d'en modifier un, le compilateur plante en vous reprochant de ne pas respecter la règle que vous vous êtes fixée. Et cela, c'est bien.
Pour les utilisateurs de votre classe : cela leur indique que la méthode se contente de renvoyer un résultat et qu'elle ne modifie pas l'objet. Dans une documentation, le mot-clé
const
apparaît dans le prototype de la méthode et c'est un excellent indicateur de ce qu'elle fait, ou plutôt de ce qu'elle ne peut pas faire (cela pourrait se traduire par : "cette méthode ne modifiera pas votre objet").Pour le compilateur : si vous vous rappelez le chapitre sur les variables, je vous conseillais de toujours déclarer
const
ce qui peut l'être. Nous sommes ici dans le même cas. On offre des garanties aux utilisateurs de la classe, et on aide le compilateur à générer du code binaire de meilleure qualité.
En résumé
Le constructeur est une méthode appelée automatiquement lorsqu'on crée l'objet. Le destructeur, lui, est appelé lorsque l'objet est supprimé.
On peut surcharger un constructeur, c'est-à-dire créer plusieurs constructeurs. Cela permet de créer un objet de différentes manières.
Une méthode constante est une méthode qui ne change pas l'objet. Cela signifie que les attributs ne sont pas modifiés par la méthode.
Allez, respirez, maintenant, on va voir comment associer des classes entre elles, un des grands intérêts de la programmation orientée objet !