JN
Accueil
> Cours
> Programmez en orienté objet avec C++
> Gérez l'accès et l'encapsulation
Programmez en orienté objet avec C++
10 heures Difficile
Mis à jour le 10/02/2022
Gérez l'accès et l'encapsulation
Tout d'abord, un petit rappel. En POO, il y a deux parties bien distinctes :
1. On crée des classes pour définir le fonctionnement des objets. C'est ce qu'on fait dans ce
chapitre.
2. On utilise des objets. C'est ce qu'on a fait au chapitre précédent.
Il faut bien distinguer ces deux parties car cela devient ici très important.
1. Création de la classe :
cpp
1 class Personnage
2 {
3 // Méthodes
4 void recevoirDegats(int nbDegats)
5 {
7 }
9 void attaquer(Personnage &cible)
10 {
11
12 }
13
14 void boirePotionDeVie(int quantitePotion)
15 {
16
17 }
18
19 void changerArme(string nomNouvelleArme, int degatsNouvelleArme)
20 {
21
22 }
23
24 bool estVivant()
25 {
26
27 }
28
29 // Attributs
30 int m_vie;
31 int m_mana;
32 string m_nomArme;
33 int m_degatsArme;
34 };
2. Utilisation de l'objet :
c_cpp
1 int main()
2 {
3 Personnage david, goliath;
4 //Création de 2 objets de type Personnage : david et goliath
6 goliath.attaquer(david); //goliath attaque david
7 david.boirePotionDeVie(20); //david récupère 20 de vie en buvant une potion
8 goliath.attaquer(david); //goliath attaque david
9 david.attaquer(goliath); //david contre-attaque... c'est assez clair non ?
10
11 goliath.changerArme("Double hache tranchante veneneuse de la mort", 40);
12 goliath.attaquer(david);
13
14
15 return 0;
16 }
Tenez, pourquoi n'essaierait-on pas ce code ? Allez, on met tout dans un même fichier, en prenant soin
de définir la classe avant le main() :
c_cpp
1 #include <iostream>
2 #include <string>
4 using namespace std;
6 class Personnage
7 {
8 // Méthodes
9 void recevoirDegats(int nbDegats)
10 {
11
12 }
13
14 void attaquer(Personnage &cible)
15 {
16
17 }
18
19 void boirePotionDeVie(int quantitePotion)
20 {
21
22 }
23
24 void changerArme(string nomNouvelleArme, int degatsNouvelleArme)
25 {
26
27 }
28
29 bool estVivant()
30 {
31
32 }
33
34 // Attributs
35 int m_vie;
36 int m_mana;
37 string m_nomArme;
38 int m_degatsArme;
39 };
40
41 int main()
42 {
43 Personnage david, goliath;
44 //Création de 2 objets de type Personnage : david et goliath
45
46 goliath.attaquer(david); //goliath attaque david
47 david.boirePotionDeVie(20); //david récupère 20 de vie en buvant une potion
48 goliath.attaquer(david); //goliath attaque david
49 david.attaquer(goliath); //david contre-attaque... c'est assez clair non ?
50
51 goliath.changerArme("Double hache tranchante veneneuse de la mort", 40);
52 goliath.attaquer(david);
53
54
55 return 0;
56 }
Compilez et admirez... la belle erreur de compilation !
Error : void Personnage::attaquer(Personnage&) is private within this context
Encore une insulte de la part du compilateur !
On en arrive justement au problème qui nous intéresse : celui des droits d'accès (oui, j'ai fait exprès de
provoquer cette erreur de compilation ; vous ne pensiez tout de même pas que ce n'était pas prévu ?).
Gérez les droits d'accès
Chaque attribut et chaque méthode d'une classe peut posséder son propre droit d'accès.
Il existe grosso modo deux droits d'accès différents :
1. public : l'attribut ou la méthode peut être appelé depuis l'extérieur de l'objet.
2. private : l'attribut ou la méthode ne peut pas être appelé depuis l'extérieur de l'objet. Par
défaut, tous les éléments d'une classe sont private .
Il existe d'autres droits d'accès, plus complexes. Nous les verrons plus tard.
Concrètement, qu'est-ce que cela signifie ? Qu'est-ce que "l'extérieur" de l'objet ?
Eh bien, dans notre exemple, "l'extérieur" c'est le main() , là où on utilise l'objet.
On fait appel à des méthodes mais, comme elles sont par défaut privées, on ne peut pas les appeler
depuis le main() !
Pour modifier les droits d'accès et mettre par exemple public , il faut taper "public" suivi du
symbole : . Tout ce qui se trouvera à la suite sera public .
On va mettre en public toutes les méthodes, et en privé tous les attributs :
c_cpp
1 class Personnage
2 {
3 // Tout ce qui suit est public (accessible depuis l'extérieur)
4 public:
6 void recevoirDegats(int nbDegats)
7 {
9 }
10
11 void attaquer(Personnage &cible)
12 {
13
14 }
15
16 void boirePotionDeVie(int quantitePotion)
17 {
18
19 }
20
21 void changerArme(string nomNouvelleArme, int degatsNouvelleArme)
22 {
23
24 }
25
26 bool estVivant()
27 {
28
29 }
30
31 // Tout ce qui suit est prive (inaccessible depuis l'extérieur)
32 private:
33
34 int m_vie;
35 int m_mana;
36 string m_nomArme;
37 int m_degatsArme;
38 };
Tout ce qui suit le mot-clé public: est public, donc toutes nos méthodes sont publiques.
Tout ce qui suit le mot-clé private: est privé, donc tous nos attributs sont privés.
Voilà, vous pouvez maintenant compiler ce code, et vous verrez qu'il n'y a pas de problème (même si le
code ne fait rien pour l'instant).
On appelle des méthodes depuis le main() : comme elles sont publiques, on a le droit de le faire.
En revanche, nos attributs sont privés, ce qui veut dire qu'on n'a pas le droit de les modifier depuis le
main() . En clair, on ne peut pas écrire dans le main() :
cpp
1 goliath.m_vie = 90;
Essayez, vous verrez que le compilateur vous ressort la même erreur que tout à l'heure.
Mais alors, on ne peut pas modifier la vie du personnage depuis le main() ?
C'est ce qu'on appelle l'encapsulation.
Respectez le principe d'encapsulation
Si on mettait tout en public ? Les méthodes et les attributs, comme cela on peut tout modifier
depuis le main() et plus aucun problème ! Non ? Quoi j'ai dit une bêtise ?
Oh, trois fois rien. Vous venez juste de vous faire autant d'ennemis qu'il y a de programmeurs qui font de
la POO dans le monde.
Il y a une règle d'or en POO, et tout découle de là. S'il vous plaît, imprimez ceci en gros sur une
feuille et placardez cette feuille sur un mur de votre chambre :
Encapsulation : tous les attributs d'une classe doivent toujours être privés.
Je ne veux pas en voir un seul mettre un attribut en public !
Voilà qui explique pourquoi j'ai fait exprès, dès le début, de mettre les attributs en privé. Ainsi, on
ne peut pas les modifier depuis l'extérieur de la classe, et cela respecte le principe
d'encapsulation.
Vous vous souvenez de la métaphore du cube pour un objet ?
Le code à l'intérieur, ce sont les attributs.
Les boutons sur la façade avant, ce sont les méthodes.
Et là, pif paf pouf, vous devriez avoir tout compris d'un coup. En effet, le but du modèle objet est
justement de masquer à l'utilisateur les informations complexes (les attributs) pour éviter qu'il ne fasse
des bêtises avec.
Imaginez par exemple que l'utilisateur puisse modifier la vie... qu'est-ce qui l'empêcherait de mettre 150
de vie alors que la limite maximale est 100 ?
C'est pour cela qu'il faut toujours passer par des méthodes (des fonctions) qui vont d'abord vérifier qu'on
fait les choses correctement avant de modifier les attributs. Cela garantit que le contenu de l'objet reste
une "boîte noire". On ne sait pas comment cela fonctionne à l'intérieur quand on l'utilise, et c'est très bien
ainsi. C'est une sécurité, cela permet d'éviter de faire péter tout le bazar à l'intérieur.
Si vous avez fait du C, vous connaissez le mot-clé struct . On peut aussi l'utiliser en C++
pour créer des classes.
La seule différence avec le mot-clé class est que, par défaut, les méthodes et attributs sont
publics au lieu de privés.
Séparez les prototypes et les définitions
Bon, on avance mais on n'a pas fini ! Voici ce que je voudrais qu'on fasse :
1. Séparer les méthodes en prototypes et définitions dans deux fichiers différents, pour avoir un
code plus modulaire.
2. Implémenter les méthodes de la classe Personnage (c'est-à-dire écrire le code à l'intérieur
parce que, pour le moment, il n'y a rien).
À ce stade, notre classe figure dans le fichier main.cpp , juste au-dessus du main() . Et les
méthodes sont directement écrites dans la définition de la classe. Cela fonctionne, mais c'est un peu
bourrin.
Pour améliorer cela, il faut tout d'abord clairement séparer le main() (qui se trouve dans
main.cpp ) des classes.
Pour chaque classe, on va créer :
1. Un "header" (fichier *.hpp ) qui contiendra les attributs et les prototypes de la classe.
2. Un fichier source (fichier *.cpp ) qui contiendra la définition des méthodes et leur
implémentation.
Je vous propose d'ajouter à votre projet deux fichiers nommés très exactement :
1. Personnage.hpp .
2. Personnage.cpp .
Vous noterez que je mets aussi une majuscule à la première lettre du nom du fichier, histoire
d'être cohérent jusqu'au bout.
Vous devriez être capable de faire cela tout seul avec votre IDE. Sous Code::Blocks, il faut passer
par les menus File > New File ; je saisis par exemple le nom Personnage.hpp avec
son extension.
1. Le fichier Personnage.hpp
Le fichier .hpp va donc contenir la déclaration de la classe avec les attributs et les prototypes des
méthodes. Dans notre cas, pour la classe Personnage , nous obtenons :
c_cpp
1 #ifndef DEF_PERSONNAGE
2 #define DEF_PERSONNAGE
4 #include <string>
6 class Personnage
7 {
8 public:
10 void recevoirDegats(int nbDegats);
11 void attaquer(Personnage &cible);
12 void boirePotionDeVie(int quantitePotion);
13 void changerArme(std::string nomNouvelleArme, int degatsNouvelleArme);
14 bool estVivant();
15
16 private:
17
18 int m_vie;
19 int m_mana;
20 std::string m_nomArme; //Pas de using namespace std, il faut donc mettre std::
devant string
21 int m_degatsArme;
22 };
23
24 #endif
Seuls les prototypes des méthodes figurent dans le .hpp . C'est déjà beaucoup plus clair.
Dans les .hpp , il est recommandé de ne jamais mettre la directive
using namespace std; , car cela pourrait avoir des effets néfastes, par la suite, lorsque
vous utiliserez la classe.
Par conséquent, il faut rajouter le préfixe std:: devant chaque string du .hpp .
Sinon, le compilateur vous sortira une erreur du type string does not name a type .
2. Le fichier Personnage.cpp
C'est là qu'on va écrire le code de nos méthodes (on dit qu'on implémente ou qu'on définit les
méthodes).
Nous allons voir dans ce screencast comment définir l’ensemble des méthodes de la classe en
respectant toutes les bonnes pratiques de développement. Vous êtes prêt ?
En résumé, voici le code complet de Personnage.cpp :
c_cpp
1 #include "Personnage.hpp"
3 using namespace std;
5 void Personnage::recevoirDegats(int nbDegats)
6 {
7 m_vie -= nbDegats;
8 //On enlève le nombre de dégâts reçus à la vie du personnage
10 if (m_vie < 0) //Pour éviter d'avoir une vie négative
11 {
12 m_vie = 0; //On met la vie à 0 (cela veut dire mort)
13 }
14 }
15
16 void Personnage::attaquer(Personnage &cible)
17 {
18 cible.recevoirDegats(m_degatsArme);
19 //On inflige à la cible les dégâts que cause notre arme
20 }
21
22 void Personnage::boirePotionDeVie(int quantitePotion)
23 {
24 m_vie += quantitePotion;
25
26 if (m_vie > 100) //Interdiction de dépasser 100 de vie
27 {
28 m_vie = 100;
29 }
30 }
31
32 void Personnage::changerArme(string nomNouvelleArme, int degatsNouvelleArme)
33 {
34 m_nomArme = nomNouvelleArme;
35 m_degatsArme = degatsNouvelleArme;
36 }
37
38 bool Personnage::estVivant()
39 {
40 return m_vie > 0;
41 }
3. Le fichier main.cpp
Retour au main() . Première chose à ne pas oublier : inclure Personnage.hpp pour pouvoir
créer des objets de type Personnage .
c_cpp
1 #include "Personnage.hpp" //Ne pas oublier
Le main() reste le même que tout à l'heure, on n'a pas besoin de le modifier. Au final, le code est
donc très court et le fichier main.cpp ne fait qu'utiliser les objets :
c_cpp
1 #include <iostream>
2 #include "Personnage.hpp" //Ne pas oublier
4 using namespace std;
6 int main()
7 {
8 Personnage david, goliath;
9 //Création de 2 objets de type Personnage : david et goliath
10
11 goliath.attaquer(david); //goliath attaque david
12 david.boirePotionDeVie(20); //david récupère 20 de vie en buvant une potion
13 goliath.attaquer(david); //goliath attaque david
14 david.attaquer(goliath); //david contre-attaque... c'est assez clair non ?
15 goliath.changerArme("Double hache tranchante veneneuse de la mort", 40);
16 goliath.attaquer(david);
17
18 return 0;
19 }
N'exécutez pas le programme pour le moment. En effet, nous n'avons toujours pas vu comment
faire pour initialiser les attributs, ce qui rend notre programme inutilisable.
Nous verrons comment le rendre pleinement fonctionnel très bientôt, et vous pourrez alors (enfin
!) l'exécuter.
Pour le moment, il faudra donc vous contenter de votre imagination. Essayez d'imaginer que David et
Goliath sont bien en train de combattre (je ne veux pas vous gâcher la chute mais, normalement, c'est
David qui gagne à la fin) !
En résumé
Les éléments qui constituent la classe peuvent être publics ou privés. S'ils sont publics, tout le
monde peut les utiliser n'importe où dans le code. S'ils sont privés, seule la classe peut les utiliser.
En programmation orientée objet, on suit la règle d'encapsulation : on rend les attributs privés, afin
d'obliger les autres développeurs à utiliser uniquement les méthodes.
Pourquoi initialiser un objet ? C’est justement ce qu’on va voir dans le chapitre suivant : les constructeurs
et les destructeurs.
J'ai terminé ce chapitre et je passe au suivant
Et si vous obteniez un diplôme OpenClassrooms ?
Formations jusqu’à 100 % financées
Date de début flexible
Projets professionnalisants
Mentorat individuel
Trouvez la formation et le financement faits pour vous
Être orienté Comparez nos types de formation
Créez une classe Initialisez un objet
Les professeurs
Mathieu Nebra
Entrepreneur à plein temps, auteur à plein temps et co-fondateur d'OpenClassrooms :o)
Ranga Gonnage
Développeur logiciel, mentor et enseignant.
OPENCLASSROOMS
OPPORTUNITÉS
AIDE
POUR LES ENTREPRISES
EN PLUS
Français