Jeux Libres
       
           

» Les Tutoriels » Apprenez à programmer en C++ ! » Les classes (Partie 2/2)

Note : Vous vous apprêtez à lire un tutoriel de M@teo21 et Nanoc initialement publié à cette adresse sous la licence Creative Commons BY-NC-SA 2.0.

Les classes (Partie 2/2)


Allez, hop hop hop, on enchaîne ! Pas question de s'endormir, on est en plein dans la POO là. ^^

Dans le 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 (suuuper important l'encapsulation, c'est la base de tout je le rappelle).

Dans cette seconde partie du chapitre, nous allons découvrir comment initialiser nos attributs à l'aide d'un constructeur, un élément indispensable à toute classe qui se respecte. Puisqu'on parlera de constructeur, on parlera aussi de destructeur, ça va de paire vous verrez.
Nous complèterons notre classe Personnage et nous l'associerons avec une nouvelle classe Arme que nous allons créer. Nous découvrirons alors tout le pouvoir qu'il y a de combiner des classes entre elles, et vous devriez normalement commencer à imaginer pas mal de possibilités à partir de là. ;)





Chapitre précédent     Sommaire     Chapitre suivant


Constructeur et destructeur


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 :
1
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 :
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 :
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 :
1
2
3
4
5
6
7
Personnage::Personnage()
{
   m_vie = 100;
   m_mana = 100;
   m_nomArme = "Epe rouille";
   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 :
1
Personnage david, goliath; // Les constructeurs de david et goliath sont appels.

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 :
1
2
3
4
5
6
7
Personnage::Personnage()
{
   m_vie = 100;
   m_mana = 100;
   m_nomArme = "Epe rouille";
   m_degatsArme = 10;
}

Le code que vous allez voir ci-dessous produit le même effet :
1
2
3
4
Personnage::Personnage() : m_vie(100), m_mana(100), m_nomArme("Epe rouille"), m_degatsArme(10)
{
   // Rien mettre dans le corps du constructeur, tout a dj 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 :
1
Personnage();

On appelle ça : le constructeur par défaut (il fallait bien lui donner un nom le pauvre :p ).

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 :
1
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 :
1
2
3
4
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 ? o_O


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 :
1
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 :
1
Personnage david, goliath("Epe aiguise", 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 :p ), 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 :
1
~Personnage();

Dans Personnage.cpp, l'implémentation sera :
1
2
3
4
5
6
7
8
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 vrifications si ncessaire 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 !


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 des informations à l'écran 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 ;)

Ça s'utilise comme ceci :
1
2
3
4
5
6
7
8
9
// Prototype de la mthode (dans le .h) :
void maMethode(int parametre) const;
 
 
// Dclaration de la mthode (dans le .cpp) :
void MaClasse::maMethode(int parametre) const
{
 
}

On utilisera le mot-clé const sur des méthodes qui se content 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 se contente juste de vérifier le niveau de vie.
1
2
3
4
5
6
7
8
9
10
11
bool Personnage::estVivant() const
{
   if (m_vie > 0)
   {
       return true;
   }
   else
   {
       return false;
   }
}

En revanche, une méthode comme recevoirDegats() ne peut pas être déclarée constante ! En effet, elle modifie le niveau de vie du Personnage puisque celui-ci reçoit des dégâts.


On pourrait trouver d'autres exemples de méthodes qui seraient 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, ça sert à quoi de créer des méthodes constantes ?


Ca sert à 3 choses principalement :

  • 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 en modifiez, le compilateur plantera en vous disant que vous ne respectez pas la règle que vous vous êtes fixée. Et ça c'est bien.
  • Pour les utilisateurs de votre classe : c'est très important aussi pour eux, ça leur indique que la méthode ne fait que renvoyer un résultat mais qu'elle ne modifie pas l'objet. Dans une documentation, le mot-clé const apparaît dans le prototype de la méthode et est un excellent indicateur de ce qu'elle fait, ou plutôt de ce qu'elle ne peut pas faire (ça pourrait se traduire par : "cette méthode ne modifiera pas votre objet").
  • Pour le compilateur : si vous vous rappelez du chapitre sur les variables, je vous conseillai de toujours déclarer const ce qui peut l'être. On est dans le même cas ici. On offre des garanties aux utilisateurs de la classe et on aide le compilateur à générer du meilleur code binaire.


Associer 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 encore plus à manipuler des objets. :)

La classe Arme



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

Souvenez-vous ce que je vous ai dit au début : il y a 100 façons différentes de concevoir un même programme en POO. Tout est dans l'organisation des classes entre elles, comment elles communiquent, etc.
Ce que nous avons fait jusqu'ici était pas mal, mais je veux vous montrer ici qu'on peut faire autrement, un peu plus dans l'esprit objet, donc... mieux. ;)


Qui dit nouvelle classe dit 2 nouveaux fichiers :

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

On n'est pas obligé de procéder ainsi. On pourrait tout mettre dans un seul fichier. On pourrait même mettre plusieurs classes par fichier, rien ne l'interdit en C++. Cependant, pour des raisons d'organisation, je vous recommande de faire comme moi.


Arme.h



Voici ce que je propose de mettre dans Arme.h :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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 includes qu'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 soient bien privés (encapsulation). Vous remarquerez qu'au lieu de m_nomArme et m_degatsArme, j'ai choisi de nommer mes attributs m_nom et m_degats tout simplement. C'est plus logique en effet : vu qu'on est déjà dans l'Arme, ce n'est pas la peine de repréciser dans les attributs qu'il s'agit de l'arme, on le sait déjà, on est dedans. ;)

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

Reste à implémenter toutes ces méthodes dans Arme.cpp. Pfeuh, fastoche ! :D

Arme.cpp



Entraînez-vous à écrire Arme.cpp, c'est tout bête, les méthodes font maxi 2 lignes, bref c'est à la portée de tout le monde. ^^

Voici mon Arme.cpp pour comparer :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "Arme.h"
 
using namespace std;
 
Arme::Arme() : m_nom("Epe rouille"), 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 << " (Dgts : " << m_degats << ")" << endl;
}

N'oubliez pas d'inclure "Arme.h" si vous voulez que ça marche. ^^

Et ensuite ?



Bon, notre classe Arme est créée, c'est bon pour ça. Mais maintenant, il va falloir adapter la classe Personnage pour qu'elle utilise non pas m_nomArme et m_degatsArme, mais un objet de type Arme.
Et là... c'est là que ça se complique. :D

Adapter la classe Personnage pour utiliser une Arme



La classe Personnage va subir quelques modifications pour utiliser la classe Arme. Restez attentifs, car utiliser un objet DANS un objet, c'est un peu particulier.

Personnage.h



Zou, direction le .h. On commence par virer nos 2 attributs m_nomArme et m_degatsArme qui ne servent plus à rien.

Les méthodes n'ont pas besoin d'être changées. En fait, il ne vaut mieux pas les changer. Pourquoi ? Parce que les méthodes sont déjà potentiellement utilisées par quelqu'un (par exemple dans notre main). Si on les renomme ou si on en supprime, notre 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 cata assurée dans le reste du programme. Et je 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 librairie ne fonctionneront plus !

Je vais peut-être vous surprendre en vous disant ça, 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 nos attributs comme on veut, vu qu'ils ne sont pas accessibles de l'extérieur, on ne prend 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 des méthodes 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 va falloir ajouter un objet de type Arme à notre Personnage.

Il faut penser à ajouter un include de "Arme.h" si on veut pouvoir utiliser un objet de type Arme.


Voici mon nouveau Personnage.h :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#ifndef DEF_PERSONNAGE
#define DEF_PERSONNAGE
 
#include <iostream>
#include <string>
#include "Arme.h" // Ne PAS oublier d'inclure Arme.h pour en avoir la dfinition
 
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 possde 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 :
1
2
3
4
5
6
7
8
9
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_arme est ici initialisé avec les valeurs reçues en paramètre par Personnage (nomArme, degatsArme). C'est là que la liste d'initialisation devient utile. En effet, on n'aurait pas pu initialiser m_arme sans une liste d'initialisation !

Peut-être ne voyez-vous pas bien pourquoi. Conseil perso : ne vous prenez pas la tête à essayer de comprendre le pourquoi du comment ici, et contentez-vous de toujours utiliser les listes d'initialisation avec vos constructeurs, ça vous évitera bien des problèmes.

Revenons au code.
Dans le premier constructeur, c'est le constructeur par défaut de la classe Arme qui est appelé, tandis que dans le second c'est celui qui prend en paramètre un string et un int qui est appelé.

La méthode recevoirDegats n'a pas besoin de changer.
En revanche, la méthode attaquer est délicate. En effet, on ne peut pas faire :
1
2
3
4
void Personnage::attaquer(Personnage &cible)
{
   cible.recevoirDegats(m_arme.m_degats);
}

Pourquoi est-ce interdit ? Parce que m_degats est un attribut, et que comme tout bon attribut qui se respecte, il est privé ! Diantre... On est en train d'utiliser la classe Arme au sein de la classe Personnage, et comme on est utilisateurs, on ne peut pas accéder aux éléments privés. o_O

(La POO, ça peut parfois donner mal à la tête j'avais oublié de vous prévenir :p )

Bon, comment résoudre le problème ? Il n'y a pas 36 solutions. Ca 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'appellerait getDegats.

On conseille généralement de rajouter le mot-clé const aux accesseurs pour en faire des méthodes constantes, puisqu'elles ne modifient pas l'objet.
1
2
3
4
int Arme::getDegats() const
{
   return m_degats;
}

N'oubliez pas de mettre à jour Arme.h avec le prototype aussi, qui sera le suivant :
1
int getDegats() const;

Voilà, ça 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 juste un return pour accéder indirectement à un attribut.

De même, on crée parfois des accesseurs permettant de modifier des attributs. Ces accesseurs sont généralement précédés du préfixe set (mettre, en anglais).
Vous avez peut-être l'impression qu'on viole la règle d'encapsulation ? Eh bien non. Car la méthode nous permet de faire des tests pour vérifier qu'on ne met pas n'importe quoi dans l'attribut, donc ça reste une façon sécurisée de modifier un attribut.

Vous pouvez maintenant retourner dans Personnage.cpp et écrire :
1
2
3
4
void Personnage::attaquer(Personnage &cible)
{
   cible.recevoirDegats(m_arme.getDegats());
}

getDegats renvoie le nombre de dégâts, qu'on envoie à la méthode recevoirDegats de la cible. Pfiou ! ^^

Le reste des méthodes n'a pas besoin de changer, à part changerArme de la classe Personnage :
1
2
3
4
void Personnage::changerArme(string nomNouvelleArme, int degatsNouvelleArme)
{
   m_arme.changer(nomNouvelleArme, degatsNouvelleArme);
}

On appelle la méthode changer de m_arme.
Le Personnage répercute donc la demande de changement d'arme à la méthode changer de son objet m_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 ça peut paraître lourd c'est la bonne méthode. En aucun cas vous ne devez mettre un attribut public pour 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 :p ).


Action !


Nos personnages combattent dans le main, mais... on ne voit 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 afficherEtat dans Personnage. Cette méthode sera chargée de faire des cout pour afficher dans la console la vie, la mana et l'arme du personnage.

Prototype et include



On va rajouter le prototype, tout bête, dans le .h :
1
void afficherEtat() const;

Implémentation



Implémentons ensuite la méthode. C'est simple, on a juste des cout à faire. Grâce aux attributs, on peut indiquer toutes les infos sur le personnage :
1
2
3
4
5
6
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'objet m_arme via sa méthode afficher(). Encore une fois, les objets communiquent entre eux pour récupérer les informations dont ils ont besoin.

Appel de afficherEtat dans le main



Bien, tout ça c'est bien beau, mais tant qu'on n'appelle pas la méthode, elle ne sert à rien :p
Je vous propose donc de compléter le main et de rajouter à la fin les appels de méthode :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main()
{
   // Cration des personnages
   Personnage david, goliath("Epe aiguise", 20);
 
   // Au combat !
   goliath.attaquer(david);
   david.boirePotionDeVie(20);
   goliath.attaquer(david);
   david.attaquer(goliath);
   goliath.changerArme("Double hache tranchante vnneuse 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 :D
David
Vie : 40
Mana : 100
Arme : Epée rouillée (Degats : 10)

Goliath
Vie : 90
Mana : 100
Arme : Double hache tranchante vénéneuse de la mort (Degats : 40)

Si vous êtes sous Windows, vous aurez probablement un bug avec les accents dans la console. Ignorez-le, ne vous en préoccupez pas, ce qui nous intéresse c'est le fonctionnement de la POO ici. Et puis de toute manière, dans la prochaine partie du cours on travaillera avec de vraies fenêtres, donc la console c'est temporaire pour nous. :-°


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

  • main.cpp
  • Personnage.cpp
  • Personnage.h
  • Arme.cpp
  • Arme.h

... bref, c'est-à-dire tout le projet tel qu'il est sur mon ordinateur à l'heure actuelle.


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

  • Continuez à faire combattre david et goliath dans le main en affichant leur état de temps en temps.
  • Introduisez un troisième personnage dans l'arène pour rendre le combat plus brutal intéressant. :-°
  • Rajoutez un attribut m_nom pour 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é ? A vous de choisir !
  • Rajoutez des cout dans les autres méthodes de Personnage pour 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 de la mana ?
  • Enfin, pour l'instant le combat est écrit dans le main, mais vous pourriez laisser le joueur choisir les attaques dans la console à l'aide de cin. Vous savez le faire, allez allez !

Prenez cet exercice très au sérieux, ceci est peut-être la base de votre futur MMORPG révolutionnaire !

Précision utile : la phrase ci-dessus était une boutade. :-°
Ce cours ne vous apprendra pas à créer un MMORPG, vu le travail phénoménal que cela représente. Mieux vaut commencer par se concentrer sur de plus petits projets réalistes, et notre RPG en est un. Ce qui est intéressant ici, c'est de voir comment est conçu un jeu orienté objet (comme c'est le cas de la plupart des jeux aujourd'hui). Si vous avez bien compris le principe, vous devriez commencer à voir des objets dans tous les jeux que vous connaissez ! Par exemple, un bâtiment dans Starcraft 2 est un objet qui a un niveau de vie, un nom, il peut produire des unités (via une méthode), etc.


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 vous demande même pas vraiment d'être capable 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 capable de le reproduire les yeux fermés la tête en bas avec du poil à gratter dans le dos !





Si vous avez dû retenir une bonne chose de ce second chapitre, c'est cet échange, cette communication constante entre les objets. Et encore ! On n'avait ici que 2 classes, Personnage et Arme. Je vous laisse imaginer dans un vrai projet ce que ça donne. ;)
L'intérêt de la POO est là : une organisation précise, chaque objet fait ce qu'il a à faire et délègue certaines parties de son travail à d'autres objets (ici, Personnage déléguait la gestion de l'arme à un objet de type Arme).

On ne peut pas dire "Je fais de la POO" du jour au lendemain, c'est clair. C'est un travail qui demande de l'organisation, de la méthode. Il faut toujours bien réfléchir avant de se lancer dans un projet, si simple soit-il.
Mais réfléchir un peu avant de programmer, est-ce un mal ? Je ne crois pas. ;)

Concentrez-vous sur le fichier zip que je vous ai donné et essayez de vous familiariser avec, en faisant par exemple les améliorations proposées. Il ne faut surtout pas que vous soyez perdus.



Chapitre précédent     Sommaire     Chapitre suivant



Distribué et adapté par David
Consulté 6542 fois



Hébergeur du site : David
Version PHP : 5.4.45-0+deb7u2
Uptime : 240 jours 11 heures 31 minutes
Espace libre : 1570 Mo
Dernière sauvegarde : inconnue
Taille de la sauvegarde : 1112 Mo


5357119 pages ont été consultées sur le site !
Dont 2665 pages pendant les 24 dernières heures.

Page générée en 0.6 secondes


Nos sites préférés
- Création d'un jeu de plateforme de A à Z avec SDL
- Zelda ROTH : Jeux amateurs sur le thème de Zelda
- Zeste de Savoir : la connaissance pour tous et sans pépins
- YunoHost : s'héberger soi-même en toute simplicité
- Site de Fvirtman : recueil de projets et de codes en C et C++
- Par ici la sortie : le site des idées de sorties


  © 2005-2019 linor.fr - Toute reproduction totale ou partielle du contenu de ce site est strictement interdite.