Série 2 : héritage et polymorphisme
25 avril 2001
Introduction
Dans cette série on va reprendre le programme de la série précédente
dans lequel on avait défini une classe Compte représentant un type de
compte très simple, et utiliser les mécanismes de l'héritage et du
polymorphisme pour définir et utiliser deux types de comptes plus
évolués :
-
les comptes avec découvert, permettant d'avoir un
certain découvert, et
- les comptes avec journal, permettant d'enregistrer les
opérations effectuées sur le compte dans un fichier.
Une banque sera l'entité qui contiendra la liste des comptes
des clients et leur permettra de créer ou consulter un compte de
manière interactive.
A chaque étape il y aura un petit travail à faire. De plus pour mieux
comprendre, il y a deux annexes à la fin de la page sur le
polymorphisme et la
séparation de l'interface et de l'implantation.
1 Point de départ
Avant toute chose il faut recopier le répertoire de la série sur votre
compte (en tapant cp -R ~cremet/serie2 .) et vous placer dans ce
répertoire (cd serie2). Vous travaillerez ensuite directement
sur les fichiers présents dans ce répertoire.
Le point de départ de cette série est grosso modo le corrigé de la
série dernière.
Il y a cependant quelques différences à bien remarquer :
-
on a séparé l'en-tête de la classe et son implantation en deux
fichiers Compte.H et
Compte.C. Cela participe d'une meilleure
organisation logique qui facilite la lecture du code (voir l'annexe
séparation de l'interface et de l'implantation
pour plus d'explications),
- on a modifié le fichier Compte.H pour permettre à la classe
Compte d'être étendue par des sous-classes. Plus précisément on a
remplacé le mot clé private par protected pour rendre l'attribut
solde accessible depuis les classes dérivées et on a placé le mot
clé virtual devant les méthodes de la classe pour leur
permettre d'être redéfinies dans une sous-classe.
A faire :
Constater ces modifications.
2 La classe CompteAvecDecouvert
Un compte avec découvert se comporte comme un compte de base sauf
qu'il a un attribut en plus, decouvert_maximum de type int, et que sa
méthode retrait(...) permet de retirer plus d'argent qu'il n'y en a
sur le compte.
On pourrait recopier le code de la classe Compte, l'appeler
CompteAvecDecouvert, rajouter un attribut decouvert_maximum et réécrire la
méthode retrait(...). Cette manière de procéder est en général assez
fastidieuse, surtout pour les grosses classes, et souvent impossible
car on ne dispose pas du code source mais seulement du code
machine. En plus on aurait une redondance d'information pour les
attributs de la classe qui ne changent pas.
On va donc utiliser le mécanisme de l'héritage et définir une nouvelle
classe CompteAvecDecouvert qui hérite de la classe Compte (et donc
automatiquement de tous ses attributs et méthodes). On définira
simplement dans cette classe un nouvel attribut decouvert_maximum et on
redéfinira la méthode retrait(...).
A faire :
Regarder le fichier d'en-tête de la classe
CompteAvecDecouvert (CompteAvecDecouvert.H) et
compléter son fichier d'implantation
(CompteAvecDecouvert.C).
3 La classe CompteAvecJournal
Un compte avec journal est un compte de base qui comporte en plus un
attribut journal de type ofstream permettant d'enregistrer les
opérations effectuées sur le compte dans un fichier (vous pouvez
relire la section 15.1 de QC pour vous remémorer comme travailler avec
les fichiers).
Comme précédemment, on va utiliser le mécanisme de l'héritage pour
définir une nouvelle classe CompteAvecJournal qui hérite de la classe
Compte. En plus de rajouter l'attribut cité plus haut, il faudra
aussi redéfinir toutes les méthodes pour qu'elles écrivent aussi les
résultats des opérations dans un fichier. Remarquez que ces nouvelles
méthodes devront appeler les méthodes de la super-classe Compte.
A faire :
Ecrire totalement les fichiers d'en-tête et
d'implantation de la classe CompteAvecJournal, c'est-à-dire un fichier
CompteAvecJournal.H et un fichier
CompteAvecJournal.C, en s'aidant
de l'exemple de la classe CompteAvecDecouvert.
On aura ainsi défini deux sous-classes, CompteAvecDecouvert et
CompteAvecJournal, de la super-classe Compte.
4 La classe Banque
Pour gérer la liste des comptes et permettre à l'utilisateur de créer
et consulter des comptes de manière interactive, on va définir une
classe Banque dont voici le fichier d'en-tête :
Banque.H et le squelette du fichier d'implantation
correspondant à compléter : Banque.C.
A faire :
Compléter le fichier Banque.C.
5 La fonction principale
A faire :
Ecrire un fichier main.C dans lequel est définie
la fonction principale int main() ... qui crée un objet de type
Banque et appelle sa méthode demarrer() pour le lancer.
6 Compilation
g++ -Wall Compte.C CompteAvecDecouvert.C CompteAvecJournal.C Banque.C main.C -o banque
L'option -Wall demande au compilateur d'afficher tous les
warnings, c'est-à-dire des messages d'erreurs qui ne font pas
échouer la compilation mais qui amènent la plupart du temps à des
erreurs lors de l'exécution du programme.
A faire :
Compiler le programme (vous pouvez faire un
copier-coller avec le bouton gauche de la souris pour sélectionner,
suivi du bouton du milieu pour coller)
7 Test du programme
A faire :
Lancez maintenant le programme et créez de facon interactive un compte
avec découvert et un compte avec journal.
Ensuite consultez ces deux comptes et vérifiez que dans un cas, pour
le compte avec découvert, il est réellement possible d'avoir un
découvert, et que dans le second cas, pour le compte avec journal, les
opérations sont enregistrées dans un fichier du même nom que le
propriétaire du compte.
Commencons par une petite question. Dans le code suivant :
Compte* c = new CompteAvecDecouvert("Durant",2000);
Reponse r = c->retrait(1000);
Quel code sera exécuté lors de l'appel de la fonction
c->retrait(1000) ?
La méthode de la classe Compte qui répondra rep_Solde_insuffisant, car
il n'y a pas encore d'argent deposé sur le compte, ou la méthode de la
classe CompteAvecDecouvert qui répondra rep_Ok, car on n'aura pas dépasser
le découvert maximum ?
La bonne réponse est la deuxième. Bien que c soit déclaré de type
Compte, il est en fait de type CompteAvecDecouvert, et à l'exécution du
programme, c'est la bonne méthode qui sera appelée. C'est ce qu'on
appelle le polymorphisme.
Ici, l'intéret de l'exemple est limité car dès la compilation, on sait
que c est en fait de type CompteAvecDecouvert (il suffit de lire le code).
Par contre, parfois le type ``véritable'' d'une variable peut dépendre
de l'exécution du programme, par exemple selon ce que rentre
l'utilisateur du programme, et le mécanisme du polymorphisme est alors
indispensable.
C'est exactement ce qu'il se passe dans cette série
dans laquelle on a modélisé une banque qui permet de créer
dynamiquement des comptes d'un type ou de l'autre, puis de les
consulter.
Ces comptes sont placés dans un tableau d'éléments de type
Compte. Sans le polymorphisme on perdrait en fait l'information de
leur type véritable.
La dernière fois on a écrit la déclaration de la classe Compte
(l'interface) et la définition de ses méthodes (l'implantation), ainsi
que la fonction main(), dans un même fichier compte.C.
Pour des raisons de meilleure organisation logique du code source et
de rapidité de la compilation, on sépare généralement ces trois
composantes du programme en trois fichiers :
-
un fichier Compte.H, qui contient l'interface de la
classe,
- un fichier Compte.C, qui commence par #include "Compte.H", suivi
par l'implantation des différentes méthodes de la classe Compte, et
- un fichier main.C, qui contient la définition de la
fonction principale main()
La directive #include opère en fait une copie textuelle du
fichier dont le nom est placé juste après. Si le nom est entre
" alors le fichier est recherché dans le répertoire courant,
sinon il est recherché dans d'autres répertoires qui sont standards,
mais qui sont aussi configurables à travers l'option -I du
compilateur (taper man g++ pour plus de détails).
On compile ensuite le programme en tapant :
g++ -Wall Compte.C main.C -o compte
Remarquez qu'on ne doit pas inclure le fichier Compte.H dans la
ligne de compilation.
Remarque :
Comme les fichiers .H (fichiers d'en-têtes) sont
destinés uniquement à être inclus dans d'autres fichiers (et pas
compilés), on doit faire en sorte qu'ils ne soient inclus qu'une seule
fois. On utilise pour cela l'inclusion conditionnelle.
On rajoute simplement un test dans chaque fichier d'en-tête (.H). Par
exemple un fichier MaClasse.H aura la forme suivante :
#ifndef MA_CLASSE_H
#define MA_CLASSE_H
class MaClasse {
...
...
...
};
#endif
Cette construction signifie simplement : ``Si MA_CLASSE_H n'a pas été
défini alors on le définit maintenant''.
This document was translated from LATEX by
HEVEA.