• 50 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

course.header.alt.is_certifying

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 30/09/2019

Créez les classes (Partie 2/2)

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Allez, on enchaîne ! Pas question de s'endormir, on est en plein dans la POO, là. Au chapitre précédent, nous avons appris à créer une classe basique, à rendre le code modulaire en POO et surtout nous avons découvert le principe d'encapsulation (je vous rappelle que l'encapsulation est très importante, c'est la base de la POO).

Dans ce chapitre, nous allons découvrir comment initialiser nos attributs à l'aide d'un constructeur, élément indispensable à toute classe qui se respecte. Puisqu'on parlera de constructeur, on parlera aussi de destructeur, vous verrez que cela va de pair.
Nous complèterons notre classe Personnageet nous l'associerons à une nouvelle classe Armeque nous allons créer. Nous découvrirons alors tout le pouvoir qu'offrent les combinaisons de classes et vous devriez normalement commencer à imaginer pas mal de possibilités à partir de là.

Constructeur et destructeur

Reprenons. Nous avons maintenant 3 fichiers :

  • main.cpp: il contient lemain(), dans lequel nous avons créé deux objets de typePersonnage:davidetgoliath.

  • Personnage.h: c'est leheaderde 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…

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 écrit dans le main():

Personnage david, goliath;

le constructeur de la classe Personnageest appelé pour créer l'objet davidet une deuxième fois pour créer l'objet goliath.

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.hmais ils ne sont pas initialisés !

Revoici le code du fichierPersonnage.h:

#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_manaet m_degatsArmesne 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é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 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 dansPersonnage.h, 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 = "Épé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 (c'est une erreur que l'on fait souvent, c'est pourquoi j'insiste sur ce point).
J'ai choisi de mettre la vie et le mana à 100, le maximum, ce qui est logique. J'ai affecté par défaut une arme appelée « Épée rouillée » qui fait 10 de dégâts à chaque coup.

Et voilà ! Notre classe Personnagea un constructeur qui initialise les attributs, elle est désormais pleinement utilisable.
Maintenant, à chaque fois que l'on crée un objet de typePersonnage, celui-ci est initialisé à 100 points de vie et de mana, avec l'arme « Épée rouillée ». Nos deux compères davidetgoliathcommencent donc à égalité lorsqu'ils sont créés dans le main():

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, c'est-à-dire presque toujours (c'est aussi la technique que nous utiliserons dans ce cours).

Reprenons le constructeur que nous venons de créer :

Personnage::Personnage()
{
    m_vie = 100;
    m_mana = 100;
    m_nomArme = "Épée rouillée";
    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("Épé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, entre parenthèses, la valeur. Avec ce code, on initialise la vie à 100, le mana à 100, l'attributm_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.
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ètre :

Personnage();

On appelle cela : 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 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.

DansPersonnage.h, on rajoute donc ce prototype :

Personnage(std::string nomArme, int degatsArme);

L'implémentation dansPersonnage.cppsera 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 entrem_nomArme, qui est un attribut, etnomArme, 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. 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 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 lemain(). 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("Épée aiguisée", 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 dedavid, c'est le constructeur par défaut qui sera appelé pour lui. Pourgoliath, comme on a spécifié des paramètres, c'est le constructeur qui prend en paramètre unstringet unintqui sera appelé.

Exercice : on aurait aussi pu permettre à l'utilisateur de modifier la vie et le mana de départ mais je ne l'ai pas fait ici. Ce n'est pas compliqué, vous pouvez l'écrire pour vous entraîner. Cela vous fera un troisième constructeur surchargé.

Le constructeur de copie

Je vous ai dit au début de ce chapitre, que le compilateur créait automatiquement un constructeur par défaut qui ne fait rien. Ce n'est pas tout, il crée aussi ce qu'on appelle un "constructeur de copie". C'est une surcharge du constructeur qui initialise notre objet en copiant les valeurs des attributs de l'autre objet.
Par exemple si l'on souhaite quedavidsoit une copie conforme degoliath, il nous suffit d'écrire:

Personnage goliath("Épée aiguisée", 20);  //On crée goliath en utilisant un constructeur normal

Personnage david(goliath);                //On crée david en copiant tous les attributs de goliath

Ce constructeur est donc très simple à utiliser. Et comme je vous l'ai dit, le compilateur le crée automatiquement pour vous! C'est donc toute une partie du travail qui nous est épargnée. Merci le compilateur.

Si toute fois, vous désirez changer le comportement du constructeur de copie, il faut simplement le déclarer dans votre classe de la manière suivante:

Personnage(Personnage const& autre);

et de définir son implémentation comme suit:

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)
{
}

Vous remarquerez qu'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.

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 desdelete) qui a été allouée dynamiquement.

Dans le cas de notre classePersonnage, on n'a fait aucune allocation dynamique (il n'y a aucunnew). 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'objetstringpar 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 decharqu'il a alloué dynamiquement en mémoire. Il fait donc undeletesur le tableau dechar, ce qui permet de garder une mémoire propre et d'éviter les fameuses « fuites de mémoire ».

Créez un destructeur

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 :

  • 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êmevoid(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é.

DansPersonnage.h, le prototype du destructeur sera donc :

~Personnage();

DansPersonnage.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. */
}

Bon, vous l'aurez compris, mon destructeur ne fait rien. Ce n'é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 et nous aurons alors grand besoin du destructeur pour désallouer la mémoire !

Les méthodes constantes

Les méthodes constantes sont des méthodes de « lecture seule ». Elles possèdent le mot-cléconstà la fin de leur prototype et de leur déclaration.

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 .h) :
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éconstsur des méthodes qui se contentent de renvoyer des informations sans modifier l'objet. C'est le cas par exemple de la méthodeestVivant(), qui indique si le personnage est toujours vivant ou non. Elle ne modifie pas l'objet, elle se contente de vérifier 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 : vous savez que 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 : c'est très important aussi pour eux, 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éconstapparaî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éclarerconstce 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é.

Associez des classes entre elles

La programmation orientée objet devient vraiment intéressante et puissante lorsqu'on se met à combiner plusieurs objets entre eux. Pour l'instant, nous n'avons créé qu'une seule classe :Personnage.
Or en pratique, un programme objet est un programme constitué d'une multitude d'objets différents !

Il n'y a pas de secret, c'est en pratiquant que l'on apprend petit à petit à penser objet.
Ce que nous allons voir par la suite ne sera pas nouveau : vous allez réutiliser tout ce que vous savez déjà sur la création de classes, de manière à améliorer notre petit RPG et à vous entraîner à manipuler encore plus d'objets.

La classeArme

Je vous propose dans un premier temps de créer une nouvelle classeArme. Plutôt que de mettre les informations de l'arme (m_nomArme,m_degatsArme) directement dansPersonnage, nous allons l'équiper d'un objet de typeArme. Le découpage de notre programme sera alors un peu plus dans la logique d'un programme orienté objet.

Qui dit nouvelle classe dit deux nouveaux fichiers :

  • Arme.h: contient la définition de la classe ;

  • Arme.cpp: contient l'implémentation des méthodes de la classe.

Arme.h

Voici ce que je propose de mettre dansArme.h:

#ifndef DEF_ARME
#define DEF_ARME

#include <iostream>
#include <string>
 
class Arme
{
    public:
 
    Arme();
    Arme(std::string nom, int degats);
    void changer(std::string nom, int degats);
    void afficher() const;
 
    private:
 
    std::string m_nom;
    int m_degats;
};
 
#endif

Mis à part les includequ'il ne faut pas oublier, le reste de la classe est très simple.

On met le nom de l'arme et ses dégâts dans des attributs et, comme ce sont des attributs, on vérifie qu'ils sont bien privés (pensez à l'encapsulation). Vous remarquerez qu'au lieu de m_nomArmeet m_degatsArme, j'ai choisi de nommer mes attributs m_nomet m_degatstout simplement. Si l'on y réfléchit, c'est en effet plus logique : on est déjà dans la classeArme, ce n'est pas la peine de repréciser dans les attributs qu'il s'agit de l'arme, on le sait !

Ensuite, on ajoute un ou deux constructeurs, une méthode pour changer d'arme à tout moment, et une autre (allez, soyons fous) pour afficher le contenu de l'arme.

Reste à implémenter toutes ces méthodes dans Arme.cpp. Mais c'est facile, vous savez déjà le faire.

Arme.cpp

Entraînez-vous à écrire Arme.cpp, c'est tout bête, les méthodes font au maximum deux lignes. Bref, c'est à la portée de tout le monde.

Voici mon Arme.cpppour comparer :

#include "Arme.h"
 
using namespace std;
 
Arme::Arme() : m_nom("Épée rouillée"), m_degats(10)
{
 
}
 
Arme::Arme(string nom, int degats) : m_nom(nom), m_degats(degats)
{
 
}
 
void Arme::changer(string nom, int degats)
{
    m_nom = nom;
    m_degats = degats;
}
 
void Arme::afficher() const
{
    cout << "Arme : " << m_nom << " (Dégâts : " << m_degats << ")" << endl;
}

N'oubliez pas d'inclure Arme.hsi vous voulez que cela fonctionne.

Et ensuite ?

Notre classe Armeest créée, de ce côté tout est bon. Mais maintenant, il faut adapter la classe Personnagepour qu'elle utilise non pas m_nomArmee tm_degatsArme, mais un objet de typeArme.
Et là… les choses se compliquent.

Adaptez la classe Personnagepour utiliser la classeArme

La classe Personnageva subir quelques modifications pour utiliser la classeArme. Restez attentifs car utiliser un objet dans un objet, c'est un peu particulier.

Personnage.h

Zou, direction le.h. On commence par enlever les deux attributs m_nomArmeet m_degatsArmequi ne servent plus à rien.

Les méthodes n'ont pas besoin d'être changées. En fait, il vaut mieux ne pas y toucher. Pourquoi ? Parce que les méthodes peuvent déjà être utilisées par quelqu'un (par exemple dans notre main()). Si on les renomme ou si on en supprime, le programme ne fonctionnera plus.

Ce n'est peut-être pas grave pour un si petit programme mais, dans le cas d'un gros programme, si on supprime une méthode, c'est la catastrophe assurée dans le reste du programme. Et je ne vous parle même pas de ceux qui écrivent des bibliothèques C++ : si, d'une version à l'autre des méthodes disparaissent, tous les programmes qui utilisent la bibliothèque ne fonctionneront plus !

Je vais peut-être vous surprendre en vous disant cela mais c'est là tout l'intérêt de la programmation orientée objet, et plus particulièrement de l'encapsulation : on peut changer les attributs comme on veut, vu qu'ils ne sont pas accessibles de l'extérieur ; on ne court pas le risque que quelqu'un les utilise déjà dans le programme.
En revanche, pour les méthodes, faites plus attention. Vous pouvez ajouter de nouvelles méthodes, modifier l'implémentation de celles existantes, mais pas en supprimer ou en renommer, sinon l'utilisateur risque d'avoir des problèmes.

Cette petite réflexion sur l'encapsulation étant faite (vous en comprendrez tout le sens avec la pratique), il faut ajouter un objet de type Armeà notre classePersonnage.

Voici mon nouveauPersonnage.h:

#ifndef DEF_PERSONNAGE
#define DEF_PERSONNAGE
 
#include <iostream>
#include <string>
#include "Arme.h" //Ne PAS oublier d'inclure Arme.h pour en avoir la définition
 
class Personnage
{
    public:
 
    Personnage();
    Personnage(std::string nomArme, int degatsArme);
    ~Personnage();
    void recevoirDegats(int nbDegats);
    void attaquer(Personnage &cible);
    void boirePotionDeVie(int quantitePotion);
    void changerArme(std::string nomNouvelleArme, int degatsNouvelleArme);
    bool estVivant() const;
 
 
    private:
 
    int m_vie;
    int m_mana;
    Arme m_arme; //Notre Personnage possède une Arme
};
 
#endif
Personnage.cpp

Nous n'avons besoin de changer que les méthodes qui utilisent l'arme, pour les adapter.
On commence par les constructeurs :

Personnage::Personnage() : m_vie(100), m_mana(100)
{
 
}
 
Personnage::Personnage(string nomArme, int degatsArme) : m_vie(100), m_mana(100),m_arme(nomArme, degatsArme)
{
 
}

Notre objet m_armeest ici initialisé avec les valeurs reçues en paramètre parPersonnage (nomArme, degatsArme). C'est là que la liste d'initialisation devient utile. En effet, on n'aurait pas pu initialiser m_armesans une liste d'initialisation !

Peut-être ne voyez-vous pas bien pourquoi. Si je peux vous donner un conseil, c'est de ne pas vous prendre la tête à essayer de comprendre ici le pourquoi du comment. Contentez-vous de toujours utiliser les listes d'initialisation avec vos constructeurs, cela vous évitera bien des problèmes.

Revenons au code.
Dans le premier constructeur, c'est le constructeur par défaut de la classe Armequi est appelé tandis que, dans le second, on appelle celui qui prend en paramètre un stringet unint.

La méthode recevoirDegatsn'a pas besoin de changer.
En revanche, la méthodeattaquerest délicate. En effet, on ne peut pas faire :

void Personnage::attaquer(Personnage &cible)
{
    cible.recevoirDegats(m_arme.m_degats);
}

Pourquoi est-ce interdit ? Parce que m_degatsest un attribut et que, comme tout attribut qui se respecte, il est privé ! Diantre… Nous sommes en train d'utiliser la classe Armeau sein de la classe Personnageet, comme nous sommes utilisateurs, nous ne pouvons pas accéder aux éléments privés.

Comment résoudre le problème ? Il n'y a pas 36 solutions. Cela va peut-être vous surprendre mais on doit créer une méthode pour récupérer la valeur de cet attribut. Cette méthode est appelée accesseur et commence généralement par le préfixe « get » (« récupérer », en anglais). Dans notre cas, notre méthode s'appelleraitgetDegats.

On conseille généralement de rajouter le mot-clé constaux accesseurs pour en faire des méthodes constantes, puisqu'elles ne modifient pas l'objet.

int Arme::getDegats() const
{
    return m_degats;
}

N'oubliez pas de mettre à jourArme.havec le prototype, qui sera le suivant :

int getDegats() const;

Voilà, cela peut paraître idiot et pourtant, c'est une sécurité nécessaire. On est parfois obligé de créer une méthode qui fait seulement unreturnpour accéder indirectement à un attribut.

Vous pouvez maintenant retourner dans Personnage.cppet écrire :

void Personnage::attaquer(Personnage &cible)
{
    cible.recevoirDegats(m_arme.getDegats());
}

getDegatsrenvoie le nombre de dégâts, qu'on envoie à la méthode recevoirDegatsde la cible. Pfiou !

Le reste des méthodes n'a pas besoin de changer, à part changerArmede la classePersonnage:

void Personnage::changerArme(string nomNouvelleArme, int degatsNouvelleArme)
{
    m_arme.changer(nomNouvelleArme, degatsNouvelleArme);
}

On appelle la méthode changerde m_arme.
LePersonnagerépercute donc la demande de changement d'arme à la méthode changerde son objetm_arme.

Comme vous pouvez le voir, on peut faire communiquer des objets entre eux, à condition d'être bien organisé et de se demander à chaque instant « est-ce que j'ai le droit d'accéder à cet élément ou pas ? ».
N'hésitez pas à créer des accesseurs si besoin est : même si cela peut paraître lourd, c'est la bonne méthode. En aucun cas vous ne devez mettre un attribut publicpour simplifier un problème. Vous perdriez tous les avantages et la sécurité de la POO (et vous n'auriez aucun intérêt à continuer le C++ dans ce cas).

Action !

Nos personnages combattent dans le main(), mais… nous ne voyons rien de ce qui se passe. Il serait bien d'afficher l'état de chacun des personnages pour savoir où ils en sont.

Je vous propose de créer une méthode afficherEtatdans Personnage. Cette méthode sera chargée de faire des coutpour afficher dans la console la vie, le mana et l'arme du personnage.

Prototype et include

On rajoute le prototype, tout bête, dans le.h:

void afficherEtat() const;

Implémentation

Implémentons ensuite la méthode. C'est simple, on a simplement à faire descout. Grâce aux attributs, on peut faire apparaître toutes les informations relatives au personnage :

void Personnage::afficherEtat() const
{
    cout << "Vie : " << m_vie << endl;
    cout << "Mana : " << m_mana << endl;
    m_arme.afficher();
}

Comme vous pouvez le voir, les informations sur l'arme sont demandées à l'objetm_armevia sa méthodeafficher(). Encore une fois, les objets communiquent entre eux pour récupérer les informations dont ils ont besoin.

Appel deafficherEtatdans lemain()

Bien, tout cela c'est bien beau mais, tant qu'on n'appelle pas la méthode, elle ne sert à rien
Je vous propose donc de compléter lemain()et de rajouter à la fin les appels de méthode :

int main()
{
    //Création des personnages
    Personnage david, goliath("Épée aiguisée", 20);
 
    //Au combat !
    goliath.attaquer(david);
    david.boirePotionDeVie(20);
    goliath.attaquer(david);
    david.attaquer(goliath);
    goliath.changerArme("Double hache tranchante vénéneuse de la mort", 40);
    goliath.attaquer(david);
 
    //Temps mort ! Voyons voir la vie de chacun…
    cout << "David" << endl;
    david.afficherEtat();
    cout << endl << "Goliath" << endl;
    goliath.afficherEtat();
 
    return 0;
}

On peut enfin exécuter le programme et voir quelque chose dans la console !

David
Vie : 40
Mana : 100
Arme : Épée rouillée (Degats : 10)
 
Goliath
Vie : 90
Mana : 100
Arme : Double hache tranchante vénéneuse de la mort (Degats : 40)

Pour que vous puissiez vous faire une bonne idée du projet dans son ensemble, je vous propose de télécharger un fichierzipcontenant :

  • main.cpp

  • Personnage.cpp

  • Personnage.h

  • Arme.cpp

  • Arme.h

Télécharger le projet RPG (3 Ko)

Je vous invite à faire des tests pour vous entraîner. Par exemple :

    • Continuez à faire combattredavidetgoliathdans lemain()en affichant leur état de temps en temps.

    • Introduisez un troisième personnage dans l'arène pour rendre le combat plus intéressant.

    • Rajoutez un attributm_nompour stocker le nom du personnage dans l'objet. Pour le moment, nos personnages ne savent même pas comment ils s'appellent, c'est un peu bête.

Du coup, je pense qu'il faudrait modifier les constructeurs et obliger l'utilisateur à indiquer un nom pour le personnage lors de sa création… à moins que vous ne donniez un nom par défaut si rien n'est précisé ? À vous de choisir !

  • Rajoutez des coutdans les autres méthodes dePersonnagepour indiquer à chaque fois ce qui est en train de se passer (« machin boit une potion qui lui redonne 20 points de vie »).

  • Rajoutez d'autres méthodes au gré de votre imagination… et pourquoi pas des attaques magiques qui utilisent du mana ?

  • Enfin, pour l'instant, le combat est écrit dans lemain()mais vous pourriez laisser le joueur choisir les attaques dans la console à l'aide decin.

Prenez cet exercice très au sérieux, ceci est peut-être la base de votre futur MMORPG (« Un jeu de rôle massivement multi-joueurs », si vous préférez) révolutionnaire !

Si vous commencez à voir des objets partout, c'est bon signe ! C'est ce que l'on appelle « penser objet ».

Méga schéma résumé

Croyez-moi si vous le voulez mais je ne vous demande même pas vraiment d'être capables de programmer tout ce qu'on vient de voir en C++. Je veux que vous reteniez le principe, le concept, comment tout cela est agencé.

Et pour retenir, rien de tel qu'un méga schéma bien mastoc, non ? Ouvrez grand vos yeux, je veux que vous soyez capables de reproduire la figure suivante les yeux fermés la tête en bas avec du poil à gratter dans le dos !

Résumé de la structure du code
Résumé de la structure du code

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.

  • Puisque le principe d'encapsulation impose de protéger les attributs, on crée des méthodes très simples appelées accesseurs qui renvoient la valeur d'un attribut. Par exemple,getDegats()renvoie la valeur de l'attributdegats.

  • Un objet peut contenir un autre objet au sein de ses attributs.

  • La programmation orientée objet impose un code très structuré. C'est ce qui rend le code souple, pérenne et réutilisable.

Exemple de certificat de réussite
Exemple de certificat de réussite