Solutions du TD1 POO
Table des matières
1. Section 3 Ma première classe
1.1. Question 3.3
Déclaration de la classe Personnage
. Cette déclaration est à include dans le fichier Personnage.h
.
Normalement, Netbeans a déjà créé automatiquement les macros pour la non-double inclusion des fichiers
de spécification (c.à.d #ifndef... #define ... #endif
). Les attributs de la classe sont à accès privé,
donc ils sont déclarés dans un bloc qui débute par le mot clé private
. Deux attributs sont de classe
std::string
, on doit donc inclure le fichier #include<string>
. Afin d'éviter d'écrire std::string
à chaque fois, on demande au compilateur d'aller systématiquement regarder le nom des fonctions/classes
de la bibliothèque standard (std
) pour compiler avec l'instruction using namespace std;
(std
est
l'espace de nommage de la bibliothèque C++). Ainsi on peut se contenter d'écrire string
au lieu de
std::string
.
#ifndef PERSONNAGE_H #define PERSONNAGE_H #include<string> using namespace std; class Personnage { private: string _nom; string _prenom; unsigned _age; }; #endif
1.2. Question 3.4
Le fichier Personnage.cpp
est le fichier qui contiendra la mise en oeuvre des constructeurs et des méthodes de la classe Personnage
. Il démarrera par l'inclusion du fichier de spécification Personnage.h
#include"Personnage.h"
1.3. Question 3.5
Il faut d'abord spécifier le constructeur dans la classe Personnage
dans le fichier Personnage.h
. Le constructeur
est à accès public, il doit donc être déclaré dans un bloc qui démarre avec le mot-clé public
. Tout constructeur
porte le nom de la classe (donc ici c'est Personnage
) et le constructeur par défaut n'aucun paramètre. Notez qu'un
constructeur n'a pas de type de retour.
... class Personnage { ... public: Personnage(); ... }; ...
Ensuite on le met en oeuvre dans le fichier Personnage.cpp
. L'initialisation des attributs doit respecter l'ordre
des attributs qui ont été déclaré dans Personnage.h
. Donc ici on initialise d'abord _nom
puis _prenom
puis _age
.
Dans le fichier Personnage.cpp
, il faut nécessairement faire précéder le nom des méthodes/constructeurs du préfixe Personnage::
.
Personnage::Personnage():_nom("Doe"),_prenom("John"),_age(20){}
1.4. Question 3.6
Dans cette question on rajoute un message dans le corps du constructeur par défaut dans le fichier Personnage.cpp
. À chaque fois que l'on appelle ce constructeur, le message s'affichera à l'écran, pratique pour comprendre ce qui se passe. Pour afficher à l'écran
on utilise des opérateurs de flux <<
. La sortie standard d'un programme (qui est l'écran) est appelée cout
et
est définie dans le fichier d'entête iostream
de la bibliothèque standard.
#include<iostream> ... using namespace std; Personnage::Personnage():_nom("Doe"),_prenom("John"),_age(20) { cout << "Le personnage " << _prenom << " " << _nom << " est né et il a déjà " << _age << " ans.\n"; }
1.5. Question 3.7
Le destructeur est unique et il a toujours la même forme: le nom de la classe précédé d'un tilde ~
On le déclare dans le fichier Personnage.h
class Personnage { ... public: ... ~Personnage(); ... };
Et on le met en œuvre dans Personnage.cpp
. Dans cet exercice, on demande à ce que le destructeur
affiche un message. C'est juste pour faire comprendre quand le programme utilise le destructeur
d'un objet. En pratique, ce destructeur n'affiche jamais de message. On peut également ne pas
le définir. Dans ce cas, le compilateur définira automatiquement un destructeur par défaut. La
mise en œuvre du destructeur est obligatoire dès lors que la destruction de l'objet nécessite
la destruction d'attributs alloués dynamiquement (il faut alors appeler delete
explicitement
sur ces attributs dans le corps du destructeur). Dans la classe Personnage
, ce n'est pas le cas.
Personnage::~Personnage() { cout << "RIP " << _prenom << " " << _nom << '\n'; }
1.6. Question 3.8
On demande de créer un objet dans le programme principal (dans la fonction main()
).
Par exemple:
#include "Personnage.h" int main() { Personnage johnDoe; return 0; }
1.7. Question 3.9
Dans ce programme, le constructeur par défaut de la classe Personnage
est appelé lorsque l'on déclare
l'objet johnDoe
. Au moment où la fonction main()
exécute return 0
, cette instruction fait se terminer la
fonction main()
qui commande la destruction automatique de l'objet johnDoe
(appel du destructeur). L'objet
johnDoe
est détruit automatiquement car il a été alloué de façon statique ici (pas de new
).
2. Section 4 Accesseurs et Modificateurs
2.1. Question 4.1
L'instruction demandée ne peut pas compiler. L'attribut _age
est à accès privé dans
la classe Personnage
donc seule des méthodes/constructeurs/destructeurs de la
classe Personnage
y ont accès. Or on demande ici à la fonction main()
d'y accéder, d'où
l'erreur de compilation. Il nous définir un accesseur pour cet attribut.
2.2. Question 4.2
1- Déclaration de l'accesseur unsigned int age()
dans Personnage.h
. Il doit être en
accès public:
class Personnage { ... public: unsigned int age(); };
2- Définition de l'accesseur dans Personnage.cpp
.
L'accesseur est une fonction qui doit retourner la valeur de l'attribut privé _age
de l'objet courant.
Cette méthode appartient à la classe Personnage
donc a effectivement accès à tous les attributs
privé de l'objet courant.
unsigned int Personnage::age() { return _age; }
3- En remplaçant dans le main()
, l'expression johnDoe._age
par johnDoe.age()
, cela compile.
En effet age()
est une méthode à accès publique, elle peut donc être utilisée dans toute fonction, et
donc dans le main()
.
2.3. Question 4.3
Pour les mêmes raisons qu'en 4.1, cela ne compile pas. _age
est à accès privé.
2.4. Question 4.4
1- Déclaration du mutateur à accès publique dans Personnage.h
class Personnage { ... public: ... void changeAge(unsigned int nouvelAge); };
2- Définition du mutateur dans Personnage.cpp
void Personnage::changeAge(unsigned int nouvelAge) { _age = nouvelAge; }
3- Utilisation du mutateur dans le programme principal
#include"Personnage.h" int main() { Personnage johnDoe; johnDoe.changeAge(40); std::cout << "age du personnage: " << johnDoe.age() << '\n'; return 0; }
Le programme appelle la méthode publique changeAge
sur l'objet johnDoe
avec le paramètre 40
. Donc la variable paramètre nouvelAge
de la méthode
changeAge
a pour valeur
40 dans cet appel. Et l'attribut _age
de l'objet johnDoe
reçoit
cette valeur lors de l'exécution de changeAge(40)
.
2.5. Question 4.5
1- Déclaration des mutateurs à accès publique dans Personnage.h
class Personnage { ... public: ... void changeNom(string nouveauNom); void changePrenom(string nouveauPrenom); };
2- Définition des mutateurs dans Personnage.cpp
void Personnage::changeNom(string nouveauNom) { _nom = nouvauNom; } void Personnage::changePrenom(string nouveauPrenom) { _prenom = nouvauPrenom; }
3. Section 5 Surcharge de constructeur
3.1. Question 5.1
1- On déclare le nouveau constructeur dans Personnage.h
. On conserve le constructeur par défaut.
On peut avoir autant de constructeur que l'on veut. Ils portent tous le même nom, qui est le nom
de la classe. Ce qui va les différencier la liste de leur paramètres.
class Personnage { ... public: Personnage(); // constructeur par defaut Personnage(string prenom, string nom); // deuxième constructeur ... }
2- Puis on rajoute sa définition dans Personnage.cpp
Personnage::Personnage(string prenom, string nom):_nom(nom),_prenom(prenom),_age(20) {}
Ce constructeur initialise l'attribut _nom
avec la valeur contenue dans le paramètre nom
.
Il fait de même pour _prenom
. L'attribut _age
est quant à lui initialisé à 20.
L'ordre des initialisations est important, il doit être celui des déclarations des
attributs dans la classe Personnage
.
3- Dans le main()
, on peut l'utiliser comme suit:
int main() { Personnage ironMan("Tony","Stark"); }
3.2. Question 5.2
1- On déclare le nouveau constructeur dans Personnage.h
. On conserve les deux autres constructeurs.
class Personnage { ... public: Personnage(); // constructeur par defaut Personnage(string prenom, string nom); // deuxième constructeur Personnage(string prenom, string nom, int age); }
2- Puis on rajoute sa définition dans Personnage.cpp
Personnage::Personnage(string prenom, string nom, int age):_nom(nom),_prenom(prenom),_age(age) {}
3- Dans le main()
, on peut l'utiliser comme suit:
int main() { Personnage ironMan("Tony","Stark",40); }
3.3. Question 5.3
1- Le constructeur par copie est unique. On déclare le constructeur dans Personnage.h
. On conserve les trois autres constructeurs.
class Personnage { ... public: Personnage(); // constructeur par defaut Personnage(string prenom, string nom); Personnage(string prenom, string nom, int age); Personnage(const Personnage & pers); // constructeur par copie }
Ce constructeur a un seul paramètre particulier c'est une référence C++ (noté &
) sur un objet pers
de type
Personnage
qui est constant (const
). Les références C++ sont difficiles à utiliser mais elles ressemblent
à des pointeurs en moins flexibles, elles sont hors programme POO. Le mot-clé const
est également hors programme,
mais sachez qu'il est très important en pratique. Si vous souhaitez étendre vos connaissances en C++ après ce module
POO, ces deux notions sont obligatoires à connaître.
2- Le constructeur par copie est défini dans Personnage.cpp
Personnage::Personnage(const Personnage & pers):_nom(pers._nom),_prenom(pers._prenom),_age(pers._age) {}
Chaque attribut de l'objet courant est initialisé avec l'attribut respectif de l'objet pers
. Notez que
le constructeur Personnage
faisant partie de la classe Personnage
a accès aux attributs privés de l'objet
courant mais aussi de l'objet pers
.
3- Son utilisation dans le programme principal
int main() { Personnage georgeEn1985("Georges","MacFly",47); // constructeur paramétré Personnage georgesEn1955(georgesEn1985); // constructeur par copie // 2 objets sont créés, le deuxième est une copie du premier }
4. Section 6 Allocation dynamique d’objets Personnage
4.1. Question 6.1
L'allocation dynamique permet de créer un objet dans une fonction, objet qui est accédé par un pointeur et qui permet de pouvoir utiliser cet objet même si la fonction qu'il l'a créé s'est terminée. On dit qu'un objet alloué dynamiquement est persistent en mémoire (pas automatiquement détruit à la fin de la fonction). Mais du fait qu'il est persistent en mémoire, il faut nécessairement le détruire quand cet objet n'est plus utile.
Allocation dynamique d'un objet Personnage. L'objet est créé avec l'operateur new
qui appelle un des constructeurs
de la classe Personnage
. L'opérateur new
retourne l'adresse mémoire de l'objet ainsi créé, adresse qui est
stockée dans un pointeur Personnage *
dont le nom est ww
.
int main() { Personnage * ww = new Personnage("Wonder","Woman", 230); }
4.2. Question 6.2
Maintenant on change son nom et on l'affiche. Pour cela, on exploite la notation ->
sur le pointeur ww
.
int main() { Personnage * ww = new Personnage("Wonder","Woman", 230); ww->changeNom("Man"); cout << ww->nom(); }
4.3. Question 6.3
L'objet doit être explicitement détruit à l'aide de delete
, qui va appeler implicitement
le destructeur de Personnage
. Attention delete
ne modifie pas la valeur contenue dans ww
qui devient donc une adresse invalide car on vient de détruire l'objet qui y était. Le point
ww
est donc invalide. Pour exprimer que ww
est invalide, il est préférable alors de le mettre
à la valeur nullptr
. Il est ainsi facile de tester plus tard que ww
est invalide par un test
d'égalité avec nullptr
.
int main() { Personnage * ww = new Personnage("Wonder","Woman", 230); ww->changeNom("Man"); cout << ww->nom(); delete ww; ww =nullptr; return 0; }
5. Section 7 Enumérations
5.1. Question 7.1
Comme stipulé dans le sujet de TP, on peut définir un énumération "à la C" dans Personnage.h
au dessus de la spécification de la classe Personnage
.
enum Genre { MASCULIN, FEMININ };
Mais il est préférable en C++ d'utiliser une classe énumérée en insérant le mot-clé class
.
enum class Genre { MASCULIN, FEMININ };
Dans la suite des questions on considère que Genre
est une classe énumérée de C++.
5.2. Question 7.2
Dans la classe Personnage
on rajoute l'attribut suivant.
class Personnage { private: ... Genre _genre; ... };
5.3. Question 7.3
On modifie les constructeurs.
1- modification des spécifications, on rajoute un paramètre Genre genre
aux constructeurs paramétrés.
class Personnage { ... public: Personnage(); // constructeur par defaut Personnage(string prenom, string nom, Genre genre); Personnage(string prenom, string nom, int age, Genre genre); Personnage(const Personnage & pers); // constructeur par copie };
2- modification des mises en œuvre, on doit initialiser l'attribut _genre
dans tous les constructeurs
Personnage::Personnage():_nom("Doe"), _prenom("John"), _age(20), _genre(Genre::MASCULIN) {} Personnage::Personnage(string prenom, string nom, Genre genre):_nom(nom), _prenom(prenom), _age(age), _genre(genre) {} Personnage::Personnage(string prenom, string nom, int age, Genre genre): _nom(nom), _prenom(prenom), _age(age), _genre(genre) {} Personnage::Personnage(const Personnage & pers):_nom(pers._nom), _prenom(pers._prenom), _age(pers._age), _genre(pers._genre) {}
3- message fonction du genre dans les constructeurs en utilisant une structure de contrôle conditionnelle
cout << "Le personnage " << _prenom << " " << _nom << " est "; if(_genre == GENRE::MASCULIN) { cout << " né "; } else { cout << " née "; } cout << " et il a déjà " << _age << "ans.\n";
6. Section 8 Mise en œuvre de méthodes
6.1. Question 8.1
On ajoute une simple méthode pour l'affichage. Cette méthode retourne une chaine de caractères
1- Spécification
#include<string> using namespace std; class Personnage { ... public: ... string identite(); };
2- Mise en œuvre. La concaténation de chaines de caractères se fait avec l'opérateur +
et la conversion
d'un entier en chaîne de caractères avec to_string(int)
(C++ supérieur à C++11, vous travaillez en C++14)
string Personnage::identite() { return "Salut, je m'appelle " + _prenom + " " + _nom + " et j'ai " + tostring(_age) + " ans."; }
6.2. Question 8.2
Dans le main()
, on peut l'utiliser comme cela.
int main() { Personnage tony("Tony","Stark",40,Genre::MASCULIN); cout << tony.identite() << '\n'; Personnage leia("Leia","Skywalker",25,Genre::FEMININ); string leiaIdentite = leia.identite(); // stocke l'identite dans une variable; cout << leiaIdentite << '\n'; }
6.3. Question 8.3
En C++ on peut utiliser des booléens bool
en opposition à C où les booléens n'existent pas.
1- Spécification
class Personnage { ... public: ... bool estVieux(); };
2- Mise en œuvre.
bool Personnage::estVieux() { return _age >= 80; }
6.4. Question 8.4
1- Spécification
class Personnage { ... public: ... bool estPlusVieuxQue(Personnage * pers); };
2- Mise en œuvre.
bool Personnage::estPlusVieuxQue(Personnage * pers) { return _age > pers->age(); // on peut également écrire _age > pers->_age; }
6.5. Question 8.5
Dans le main, exemple 1 (alloc statique)
int main() { Personnage yoda("Yoda","Maitre",800,Genre::MASCULIN); Personnage leia("Leia","Skywalker",25,Genre::FEMININ); cout << yoda.prenom() << " est-il vieux ?" ; if(yoda.estVieux()) { cout << "oui."; } else { cout << "non."; } cout << leia.prenom() << " est-elle plus vieille que " << yoda.prenom() << "? "; // attention estPlusVieuxQue attend un paramètre pointeur // contenant l'adresse de yoda, donc on passe en paramètre &yoda et non pas yoda if(leia.estPlusVieuxQue(&yoda)) { cout << "oui."; } else { cout << "non."; } }
Dans le main, exemple 2 (alloc dynamique)
int main() { Personnage * yoda = new Personnage("Yoda","Maitre",800,Genre::MASCULIN); Personnage * leia = new Personnage("Leia","Skywalker",25,Genre::FEMININ); cout << yoda->prenom() << " est-il vieux ?" ; if(yoda->estVieux()) { cout << "oui."; } else { cout << "non."; } cout << leia->prenom() << " est-elle plus vieille que " << yoda->prenom() << "? "; if(leia->estPlusVieuxQue(yoda)) { cout << "oui."; } else { cout << "non."; } // on oublie pas de detruire les objets delete leia; leia=nullptr; delete yoda; yoda=nullptr; }
7. Section 9 La classe Histoire
7.1. Question 9.1
Pensez à utiliser les menus de NetBeans pour sélectionner New C++ Class
, NetBeans génèrera les deux
fichiers Histoire.h
et Histoire.cpp
7.2. Question 9.2
#include<vector> #include<string> #include"Personnage.h" using namespace std; class Histoire { private: string _titre; vector<Personnage *> _personnages; };
7.3. Question 9.3
C'est la première fois que nous voyons une méthode privée. Cette méthode ne peut donc être utilisée qu'au
sein de la classe Histoire
. Elle réalise une opération intermédiaire, elle ne peut être appelée que
dans une autre méthode de Histoire
.
1- Spécification
class Histoire { private: string _titre; vector<Personnage *> _personnages; void stockePersonnage(Personnage * p); };
2- Mise en œuvre dans Histoire.cpp
On créé une place de plus à la fin du vecteur pour insérer le nouveau Personnage
.
void Histoire::stockePersonnage(Personnage * p) { _personnages.push_back(p); }
7.4. Question 9.4
1- Spécification
class Histoire { private: string _titre; vector<Personnage *> _personnages; void stockePersonnage(Personnage * p); public: Personnage * creerPersonnage(string nom, string prenom,int age, Genre genre); };
2- Mise en œuvre
Ici on voit tout l'intérêt de l'allocation dynamique. creerPersonnage
va créer un Personnage
et retourner
son adresse afin que l'objet puisse être utilisé ultérieurement une fois stocké.
Avec une allocation statique, c'est tout bonnement
impossible. Pour stocker le personnage, j'utilise la méthode stockePersonnage
dont c'est l'objectif.
Attention, la classe Histoire
créé ainsi des objets en allocation dynamique. Toujours pensez
qu'il faudra trouver un moyen par la suite de détruire ces objets (voir Questions 9.9 et 9.10).
Personnage * Histoire::creerPersonnage(string nom, string prenom,int age, Genre genre) { Personnage * nouveauPersonnage = new Personnage(nom,prenom,age,genre); stockePersonnage(nouveauPersonnage); return nouveauPersonnage; }
7.5. Question 9.5
1- Spécification
class Histoire { public: Histoire(); };
2- Mise en œuvre
On appelle successivement et dans le bon ordre les constructeurs par défauts de _titre
et de _personnages
. Par défaut ces deux constructeurs répondent à la question
(titre vide, vecteur vide).
Histoire::Histoire(): _titre(), _personnages() {}
7.6. Question 9.6
1- Spécification
class Histoire { public: long nombreDePersonnages(); };
2- Mise en œuvre
On retourne le nombre de personnages dans le vecteur _personnages
. C'est donné par
la méthode size()
appliquée au vecteur _personnages
.
long Histoire::nombreDePersonnages() { return _personnages.size(); }
7.7. Question 9.7
1- Spécification
class Histoire { private: string _titre; public: string titre(); // accesseur/getter void changeTitre(string nouveauTitre); //mutateur/setter };
2- Mise en œuvre
string Histoire::titre() { return _titre; } void Histoire::changeTitre(string nouveauTitre) { _titre = nouveauTitre; }
7.8. Question 9.8
1- Spécification
class Histoire { public: void afficherPersonnages(int age); // methode qui ne retourne rien: void };
2- Mise en œuvre
L'idée est d'itérer sur les éléments du vecteur _personnages
et de vérifier
l'âge du personnage pour savoir s'il faut l'afficher ou pas.
void Histoire::afficherPersonnages(int age) { for(size_t i = 0; i < _personnages.size(); ++i) { if(_personnages.at(i)->age() <= age) // on peut utiliser _personnages[i] mais c'est moins sauf { cout << _personnages.at(i)->prenom() << " " << _personnages.at(i)->nom() << '\n'; } } }
7.9. Question 9.9
1- Spécification
class Histoire { private: void effacerTousLesPersonnages(); };
2- Mise en œuvre
On doit effacer les personnages qui sont stockés dans le vecteur Personnage
. Il ne suffit pas d'effacer
le vecteur (avec la méthode clear()
) car on n'aurait effacé que les pointeurs et les objets pointés
seraient toujours présents en mémoire mais non accessible (fuite de mémoire). Il nous faut donc appeler sur
chaque élément du vecteur l'opérateur delete
. Ici on voit typiquement la différence entre C++ et un
langage type Python ou Java. Ces deux derniers ne nécessitent pas l'utilisation explicite de delete
.
Dans ces langages on aurait vider le vecteur uniquement. Ensuite un système propre à Java et Python
détecte que des objets sont inaccessibles dans le programme et les détruit automatiquement. Ce système s'appelle un ramasse miette
(Eng: Garbage collector). Très pratique pour le programmeur, il a néanmoins l'inconvénient de rendre Java et Python moins performants (entre autres choses). En C++, pas de ramasse-miette, le programmeur doit ramasser ses propres miettes !!
void Histoire::effacerTousLesPersonnages() { for(size_t i = 0; i < _personnages.size(); ++i) { if(_personnages.at(i) != nullptr) // je verifie que le pointeur n'est pas nul, au cas où... { delete _personnages.at(i); } } // je vide le vecteur de pointeur, une fois tous // les objets pointés détruits _personnages.clear(); }
7.10. Question 9.10
1- Spécification
class Histoire { public: ~Histoire(); };
2- Mise en œuvre
La classe Histoire
a deux attributs. Le premier est alloué statiquement, rien à faire. Le deuxième,
lui, stocke des objets dynamiquement alloués par la méthode creerPersonnage
. Il faut explicitement
les effacer. Cela tombe bien la méthode effacerTousLesPersonnages
fait exactement cela.
Histoire::~Histoire() { effacerTousLesPersonnages(); }
7.11. Question 9.11
Remarquez que le choix des noms est important. En lisant le code suivant, vous comprenez aisément ce qui se passe dans le programme principal.
#include"Personnage.h" #include"Histoire.h" int main() { Histoire monHistoire; monHistoire.changeTitre("Retour vers le futur"); monHistoire.creerPersonnage("Marty","McFly",17,Genre::MASCULIN); monHistoire.creerPersonnage("Emmett","Brown",77,Genre::MASCULIN); monHistoire.creerPersonnage("Georges","McFly",47,Genre::MASCULIN); monHistoire.afficherPersonnages(1000); return 0; // ici le destructeur ~Histoire est appelé, qui va détruire les personnages }
7.12. Question 9.12
1- Spécification
class Histoire { public: Histoire(const Histoire & hist); };
2- Mise en œuvre
Quand on fait un constructeur par copie qui stocke des pointeurs on se pose toujours la
question de savoir s'il faut copier uniquement les pointeurs (clone) ou si on doit
également copier les objets pointés (deep clone). La réponse est que cela dépend du contexte.
Dans le cadre la classe Histoire
, elle est en charge de créer ses propres personnages
avec creerPersonnage
donc il est cohérent de faire une copie profonde. En fait, on est
en train de dire que la classe Histoire
est composée d'objets Personnage
(c'est
une relation de Composition en UML, voir le TD2).
Histoire::Histoire(const Histoire & hist):_titre(hist._titre),_personnages() { for(size_t i = 0; i < hist.nombreDePersonnages(); ++i) { stockePersonnage(new Personnage(hist._personnages.at(i))); // ici je passe par le constructeur par copie de Personnage, plus élégant à écrire // autre facon: creerPersonnage(hist._personnages.at(i).prenom(), // hist._personnages.at(i).nom(), // hist._personnages.at(i).age(), // hist._personnages.at(i).genre()); // conceptuellement plus propre } }