Travaux Pratiques C++/Objet

Eric Lecolinet - Télécom ParisTech - Dept. INFRES

Liens utiles

Exercices

Le but de ces travaux pratiques est de créer un logiciel permettant de traiter l'ensemble des personnes étudiant ou travaillant à Telecom ParisTech. Ce logiciel sera realisé par étapes, en se limitant à la déclaration et l'implémentation de quelques classes et fonctionnalités typiques que l'on complétera progressivement. Noter qu'il est utile de lire chaque "étape" en entier (en particulier les "remarques") avant de la traiter.

1e Etape: Démarrage

2e Etape: Classe de base

Ecrire la déclaration (fichier header .h) et l'implementation (fichier source .cpp) de la classe de base de l'arbre d'héritage des classes (que l'on pourra par exemple appeler Personne). Pour créer ces 2 fichiers, dans QtCreator cliquer : Fichier > Nouveau fichier ou projet... > Classe C++ (le nom du .h et du .cpp sera forgé à partir de celui de la classe). Noter aussi que, par convention, les noms de vos classes devront commencer par une majuscule et ceux des variables et des fonctions par une minuscule.

Pour simplifier, la classe de base n'aura que 3 attributs : l'age, qui devra être un entier, et le prénom et le nom, qui devront être des string du C++ (pas des char* du langage C !) Déclarer et implémenter 2 constructeurs (un sans argument, un avec arguments), le destructeur, ainsi que les "accesseurs" et "modifieurs" pour pouvoir lire ou modifier les attributs. Définir également une méthode permettant d'afficher le contenu de l'objet (cad. la valeur de ses attributs) sur le Terminal.

Pour compiler le fichier et corriger les erreurs

3e Etape: Programme de test

Un programme exécutable nécessite la présence d'une fonction main(). Cette fonction ne doit pas se trouver dans l'implémentation de la classe Personne car ceci interdirait sa réutilisation ultérieure. On va donc créer un autre fichier, appelé par exemple main.cpp qui va implémenter la fonction main().

A titre de test, faire en sorte que main() crée une instance de la classe Personne (avec new) en initilisant ses attributs, puis affiche son contenu sur le Terminal. Cette fonction, comme toutes celles qui suivront, devra respecter le principe d'encapsulation (pas d'accès aux attributs des objets autrement que par des méthodes).

Pour compiler et générer l'exécutable : pensez à rajouter main.cpp dans SOURCES dans le Makefile puis faites comme précédemment en utilisant QtCreator.

Pour exécuter le programme: le plus simple est de taper son nom précédé de ./ dans le Terminal, par exemple : ./myprog

4e Etape: Classes Etudiant et Prof

On va maintenant créer deux classes typiques des personnes présentes à Télécom : la classe Etudiant et la classe Prof. Ces deux classes héritent de la précédente et, pour simplifier, n'ont qu'un attribut supplémentaire : l'année de promotion (un entier positif) dans le premier cas et le bureau (string) dans le second. Bien sûr dans la réalité il faudrait aussi créer d'autres classes et l'arbre d'héritage serait plus complexe !

Déclarer et implémenter ces classes, chacune dans des fichiers qui lui sont propres pour une plus grande modularité et faciliter la réutilisation. Ces classes étant simples, on pourra les implémenter entièrement dans le .h si on le souhaite. Ne pas oublier les constructeurs, les accesseurs, les modifieurs et la méthode d'affichage. Modifier le Makefile si nécessaire, compiler, corriger les éventuelles erreurs et tester le programme.

5e Etape: Traitement générique (en utilisant le polymorphisme)

On veut maintenant pouvoir traiter génériquement une liste comprenant indifféremment des Etudiants et des Profs. Pour ce faire créer dans main.cpp un tableau de pointeurs dont les élements pointent tantôt vers un Etudiant tantôt vers un Prof (crées avec new).

Ecrire ensuite une boucle permettant d'afficher le contenu de tous les objets pointés par le tableau. Noter que cette boucle n'a pas besoin de connaître le type précis des objets pointés par le tableau (ca peut être n'importe quelle sous-classe de Personne). Comment s'appelle la propriété caractéristique de l'Orienté Objet qui permet cela et que faut-il faire pour qu'elle s'applique dans ce cas précis ?

Compiler, exécuter, et vérifier que le résultat est correct.

6e étape. Déclaration et implémentation de la classe Eleve

On veut maintenant spécialiser la classe Etudiant en définissant une sous-classe Eleve. Une caractérisque des élèves est qu'ils ont des notes. Ecrire cette classe avec les méthodes adéquates pour que l'on puisse affecter un tableau de notes à un élève ou récupérer ce tableau en faisant attention à ce qui suit :

Compiler, exécuter, et vérifier que le résultat est correct avant de passer à la suite.

Les autres sous-classes d'Etudiant (pas déclaréees pour l'instant) auront également des notes mais celles-ci ne sont pas calculées de la même manière (ils ne suivront pas forcement des UEs, etc.).

Rajouter à la classe Etudiant une méthode qui retourne la note globale. Son calcul étant indéfini au niveau de cette classe, utiliser une méthode abstraite. Qu'est-il alors nécessaire de faire dans les sous-classes d'Etudiant ? Le faire dans le cas de la classe Eleve (en retournant la moyenne des notes du tableau).

Enfin, faire en sorte que la méthode d'affichage affiche la moyenne pour n'importe quel type d'Etudiant ainsi que le tableau de notes dans le cas des Eleves.

7e étape. Copie et destruction des objets

Contrairement à Java, C++ ne propose pas de ramasse miettes en standard pour récupérer la mémoire dynamique qui n'est plus utilisée (= qui n'est plus référencée par aucun pointeur). Parmi les classes précédemment écrites quelles sont celles qu'il faut modifier et comment afin qu'il n'y ait pas de fuite mémoire si on détruit leurs instances ?

De même, la copie d'objets peut poser problème dans certains cas. Quelle(s) classe(s) sont concernées parmi celles déjà écrites et que convient-il de faire ?

Rajoutez le code nécessaire, et faites quelques tests dans main.cpp en mélangeant des constructions, destructions, tentatives de copies de Profs et d'Eleves et vérifiez que tout se passe comme souhaité !

8e étape. Listes de la STL

On va maintenant créer une nouvelle classe Groupe qui va servir à contenir une liste de Personnes au moyen d'une list de la STL.

Deux stratégies sont ici possibles :

La première stratégie nécessite de définir des méthodes dans la classe Groupe pour gérer la liste. La seconde stratégie évite ce travail car elle permet d'hériter des méthodes de list. Elle est donc plus rapide mais offre moins de contrôle (on ne choisit pas les noms de méthodes comme on veut, on hérite de toutes les méthodes y compris certaines qui sont peut-être inutile ou pas souhaitables, etc.)

Pour aller plus vite, écrivez la classe Groupe en utilisant la seconde stratégie (si vous avez le temps vous pourrez la réécrire ensuite en optant pour la première stratégie pour comparer). Comme d'habitude, n'oubliez pas le constructeur et autres méthodes utiles (en particulier pour afficher les personnes). Pour l'instant on ne s'interesse pas à la destruction des objets.

Pour tester, créez ensuite 2 groupes dans main() : par exemple un groupe Inf224 et un groupe CineClub que vous peuplerez de quelques personnes (en mélangeant des Profs et des Eleves), certaines d'entre-elles appartenant aux 2 groupes. Appelez la fonction d'affichage pour chacun des groupes pour vérifier que tout est OK.

9e étape. Gestion mémoire

L'étape précédente pose un problème de gestion mémoire : quand et comment détruire les objets plus utilisés ? Cette question est non triviale dans le cas général : comme les objets peuvent appartenir à plusieurs groupes, on ne peut pas systématiquement les détruire quand on les enlève d'un groupe, ni quand on détruit le groupe.

Comme déjà expliqué, Java ou C# résolvent ce problème par le ramasse miettes. En C ou C++ il faudrait explicitement vérifier chaque fois qu'on enlève un objet d'un groupe s'il est encore pointé par un autre groupe pour déterminer si on peut le détruire. Une autre statégie consisterait à avoir un groupe "maître" responsable de la destruction des objets (typiquement un groupe contenant toutes les personnes actuellement à l'Ecole). Néanmoins cette solution est loin d'être parfaite : imaginons par exemple qu'un ancien élève ou un ancien prof - donc plus à l'Ecole - participe toujours au groupe "CineClub"...

En l'absence de ramasse miette, la solution générale à ce problème est le comptage de références, qui peut s'implémenter facilement en C++ grâce aux smart pointers. Copier tout d'abord le fichier intrusive_ptr.h dans votre répértoire. Cette implémentation dérivée des smart pointers intrusifs de Boost définit le template intrusive_ptr qui permet de pointer des classes possédant un compteur de réferences. De plus, ce fichier définit aussi une classe de base générique Pointable possédant un compteur compatible avec intrusive_ptr.

En utilisant cette implémentation des smart pointers modifiez la classe Personne et la classe Groupe de manière à ce que les objets soient automatiquement détruits lorsqu'ils ne figurent plus dans aucun groupe. Pour tester que votre code marche bien, modifiez le destructeur de Personne pour qu'il affiche le nom de la personne "détruite" puis enlevez des personnes de vos 2 groupes (y compris celles qui sont dans les 2 groupes) pour vérifier que les objets sont effectivement détruits. Compilez et testez. Pour comprendre ce qui se passe, refaites de même après avoir défini (par #define) les macros SMART_PTR_DEBUG et SMART_PTR_DEBUG_MESSAGES avant l'inclusion de intrusive_ptr.h.

Essayez maintenant de remplacer les intrusive_ptr par des shared_ptr. Avantage: les shared_ptr ne nécessitent pas que les objets aient un compteur (la classe Pointable est donc inutile dans ce cas). Inconvénient: ils sont fortement incompatibles † avec les pointeurs ordinaires (raw pointers) du C++. Pour cette raison le compilateur interdit d'affecter un raw pointer vers un shared_ptr (et inversement). Pour les utiliser avec cette version de g++ il suffit de rajouter :

#include <tr1/memory>
using std::tr1::shared_ptr;

† Note: la raison est que les shared_ptr ne pointent pas seulement l'objet mais aussi son compteur de référence (qui n'est pas inclus dans l'objet contrairement au cas des intrusive_ptr). De ce fait des affectations entre des shared_ptr et des raw pointers auraient pour effet de perdre le compteur !

10e étape. Constance et passage par référence

Modifiez le code que vous avez déjà écrit de telle sorte que les arguments soient passés par réference ou const réference et que les méthodes soient const partout où c'est souhaitable.

11e étape. Sauvegarder des objets sur fichier

Rajouter aux classes précédentes les méthodes adéquates pour sauvegarder le contenu des objets dans un fichier (cf. ofstream). Dans ces méthodes on pourra utiliser l'opérateur << comme pour l'affichage sur le terminal mais en remplacant cout par la variable identifiant ce fichier.

Pour simplifier on supposera que les string ne contiennent pas d'espaces. Noter qu'il faudra également sauver le nom des classes dans le fichier afin de pouvoir relire les objets (question suivante).

Rajouter dans main() le code necessaire pour ouvrir un fichier puis sauver le contenu d'un Group en appelant la méthode adéquate (pour simplifier on supposera qu'un fichier ne contient qu'un seul groupe). Remarque: pensez à fermer le fichier après l'écriture.

12e étape. Lire des objets depuis un fichier

Rajouter aux classes les méthodes adéquates pour lire les objets depuis un fichier (cf. ifstream). La encore on supposera qu'un fichier correspond à un groupe. On créera les personnes de type adéquat à partir du nom de classe précédemment sauvegardé dans le fichier. Ceci pourra typiquement se faire dans une méthode de Group. Notez que vous aurez probablement besoin de la méthode good() des ifstream.

Rajouter dans main() le code necessaire pour vérifier que tout marche bien.

Remarque: pour simplifier on a supposé que les strings ne contenaient pas d'espaces. De plus, la solution précédente suppose que le fichier est parfaitement formaté (s'il manque un champ la lecture du fichier s'interrompra sans pouvoir lire la suite). Une solution plus générale consiste à sauvegarder les données en ne mettant qu'un seul champ par ligne. Il faut alors utiliser la méthode getline() pour la relecture ainsi que les stringstream pour effectuer des conversions des strings vers les entiers, flottants, etc.

Etape finale. Doxygen

Commenter votre code source (en particulier les headers) et générer la documentation en utilisant Doxygen. On pourra se limiter aux fonctionnalités de base de Doxygen vues en cours. Pour plus d'infos, voir par exemple ce tutoriel.

Pour rendre le TP

1) Verifier que votre code compile et s'exécute correctement depuis une salle de TP Unix de l'Ecole. La compilation doit s'effectuer en tapant make dans le Terminal (notez que, tel qu'on l'a utilisé dans ce TP, Qt Creator fait appel à make). Attention: un programme sans Makefile ou qui ne compile pas ou qui plante sur une machine de l'Ecole ne sera pas pas examiné et sera considéré comme non rendu.

2) Ecrire un bref fichier README indiquant les questions traitées et contenant les réponses aux questions et tous les commentaires que vous jugerez utiles. Ce fichier doit être en PDF ou en HTML (en UTF-8) ou au format texte (en UTF-8 également). N'oubliez pas de mettre votre nom (au moins dans le README et de préférence aussi dans les autres fichiers)

3) Créer un fichier zip ou tar.gz (pas d'autre format!) contenant votre README et le code source (ne mettez pas l'executable ni les .o)

4) Aller à l'URL http://services.infres.enst.fr/rendutp/, cliquer (le bon ;-) lien puis entrer votre nom et télécharger le fichier zip ou tar.gz

NB: seule la dernière version téléchargée est prise en compte



Eric Lecolinet - http://www.telecom-paristech.fr/~elc - Telecom ParisTech