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 : 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 :
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.

ANNEXE 1 : Et le polymorphisme dans tout ca ?

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.

ANNEXE 2 : Séparation de l'interface et de l'implantation

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 : 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.