Cet article va, comme promis, présenter l'héritage en C++. L'héritage est un des piliers de la programmation orientée objet. Comme nous l'avons vu, en C++, quand on a besoin d'une structure de données, on définit une classe. Or, on a souvent besoin d'adapter une classe existante. Il ne sert à rien de réinventer la roue ... L'héritage à été créé pour celà. Il permet de "récupérer" les membres d'un objet, spécialiser ces derniers et d'en ajouter de nouveaux. La plupart des programmes en C++ sont constuits grâce à un assemblage de modélisations de différents concepts et de spécialisations et extensions de ces derniers. Nous verrons celà en quelques exemples. En fin d'article nous séparerons notre code en plusieurs fichiers et écrirons un Makefile pour le C++.
Rappelez vous de notre classe personne, vue dans le numéro précédent... Elle nous a causé du souci mais nous permet à présent d'identifier des personnes par leur sexe, leur âge, leur nom et leur prénom. Toutefois, nous allons avoir besoin d'une personne ayant un métier pour représenter plus avant les personnes qui nous entourent.
Nous pourrions recréer une classe "from scratch" (à partir de zéro) pour modéliser les travailleurs mais en y réfléchissant bien on se rend compte qu'un travailleur est avant tout une personne. Il est désigné par un nom, un prénom, son âge, son sexe, qui sont les champs qui définissent une personne, et son métier, qui est un champ non prévu dans la classe personne. On peut donc dire qu'un travailleur
est une personne
ayant un métier. Nous modélisons donc un travailleur ainsi :
class travailleur : public personne { string metier; public: travailleur (string m="Profession inconnue"); travailleur (string n, string p, int a, sexe_t s, string m); void set_metier(const char* m) { metier = m; } string get_representation() const { return get_identification()+" : "+metier; }; }; travailleur::travailleur (string n, string p, int a, sexe_t s, string m) : personne (p,n,a,s), metier(m) { } travailleur::travailleur (string m) : personne (), metier(m) { }
class travailleur : public personne
La déclaration de la classe travailleur
précise qu'elle est "dérivée" de personne
. On dit également que personne
est la classe de base, classe parente ou classe mère de travailleur
. De la même façon, travailleur
est une classe dérivée ou fille de personne
.
Le mot clé public
précise que les propriétés et méthodes publiques de la classe personne
deviennent des propriétés et méthodes publiques de travailleur
. On se sert d'ailleurs de la méthode get_identification
de la classe personne
pour construire get_representation
.
Les propriétés et méthodes déclarées private
dans personne
, en revanche, ne sont pas accessibles directement depuis travailleur
mais peuvent être modifiées ou affichées grâce aux méthodes publiques de personne
. On initialise donc les données privées en appelant le constructeur de la classe personne
dans la liste d'initialisations du constructeur de travailleur
.
Lors de l'instanciation d'un travailleur
le constructeur de la classe est exécuté. Ce constructeur doit lui même appeler le constructeur de la classe dont il est dérivé. Cet appel est toutefois facultatif si c'est le constructeur par défaut de la classe mère qui doit être utilisé. Le second constructeur de la classe travailleur
pourrait donc s'écrire :
travailleur::travailleur (string m) : metier(m) {}
Le constructeur personne()
est implicitement utilisé. En revanche, l'appel de personne(p,n,a,s)
dans le premier constructeur de travailleur
est nécessaire puisqu'il prend des arguments.
Voyons la classe travailleur
à l'oeuvre.
[...] /* includes et définitions de personne et travailleur */ [...] int main () { srand(time(NULL)); travailleur t("Xavier", "Garreau", 25, HOMME, "ingénieur"); travailleur t2; cout << t.get_identification() << endl; cout << t.get_representation() << endl; cout << t2.get_representation() << endl; t.set_metier("pigiste"); t2.set_metier("directeur"); cout << t.get_representation() << endl; cout << t2.get_representation() << endl; return 0; }
Ce programme compilé et exécuté par la méthode habituelle donne :
$ g++ -o ex_4_1 ex_4_1.c++ $ ./ex_4_1 Création d'un homme : Garreau Xavier * 25 ans Création d'un homme : Anonyme X * 0 ans Garreau Xavier Garreau Xavier : ingénieur Anonyme X : Profession inconnue Garreau Xavier : pigiste Anonyme X : directeur Destruction de Anonyme X * 0 ans Destruction de Garreau Xavier * 25 ans
On peut effectivement utiliser la méthode get_identification
de la classe personne
sur un travailleur
puisqu'elle est publique. Celà montre qu'un travailleur est avant tout une personne.
Notre classe se comporte de la façon attendue, c'est à dire que l'on peut affecter un métier à un travailleur.
On remarque enfin que le destructeur de personne
est automatiquement appelé lors de la destruction du travailleur. De façon générale, les destructeurs sont appelés "de bas en haut", c'est à dire que le destructeur de la classe dérivée est exécuté avant celui de ses classes de base. Vous pouvez, pour vous en convaincre, ajouter un destructeur tel que celui ci-dessous à la classe travailleur
~travailleur () { cout << "Destruction du travailleur" << endl; }
protected
On a vu qu'une classe dérivée comme travailleur
ne peut pas accéder aux membres privés de sa classe parente. Un travailleur
ne peut pas modifier son nom par exemple alors qu'une personne
le peut. Si c'est un choix délibéré de ne pas laisser cette possibilité, l'objectif est atteint. Toutefois, si on considère qu'un travailleur doit pouvoir modifier son nom, un problème se pose. Une solution est de définir des fonctions publiques dans personne
permettant de modifier les propriétés privées. Une autre solution est de déclarer la propriété publique. Ces deux méthodes ont l'inconvénient de permettre à n'importe qui de modifier librement le nom des objets des classes personne
et travailleur
.
Pour répondre à ce problème, il existe en C++ (ainsi qu'en d'autres langages orientés objet comme le java) la possibilité de déclarer des membres protected
qui sont considérés comme publics dans la classe à laquelle ils appartiennent et dans toutes ses classes dérivées mais private depuis l'extérieur de cette arborescence. Le mot clé protected
s'utilise exactement de la même manière que public
et private
.
On ajoute à notre classe personne une méthode sexe_t get_sexe()
, protégée. Son appel depuis les classes personne
et travailleur
renverra le sexe de la personne dont il est question. Si on tente de l'utiliser ailleurs, la compilation se terminera par une erreur.
La déclaration de la classe personne
sera complétée par :
protected: sexe_t get_sexe() const { return sexe; }
Un directeur est un travailleur auquel il faut dire monsieur ou madame, selon son sexe justement. Nous allons créer une nouvelle classe pour modéliser ce concept. Nous devons adapter la représentation d'un travailleur au cas spécifique du directeur.
class directeur : public travailleur { public: directeur (string n, string p, int a, sexe_t s) : travailleur(p,n,a,s,"Directeur") {}; string get_representation() const ; void set_metier(const char* m) const {}; }; string directeur::get_representation() const { switch (get_sexe()) { case HOMME: return "Monsieur " + travailleur::get_representation(); case FEMME: return "Madame " + travailleur::get_representation(); default: return "E.T. : " + travailleur::get_representation(); } }
On constate l'absence de constructeur par défaut et l'appel explicite du constructeur travailleur
prenant 5 paramètres, ce qui signifie qu'une déclaration directeur d;
provoquerait une erreur à la compilation, aucun constructeur par défaut n'étant défini.
La méthode get_representation
se charge de renvoyer une représentation du directeur, en utilisant la méthode protégée get_sexe
précédemment créée dans la classe personne
. On a du redéfinir get_representation
pour qu'elle soit adaptée au besoins du directeur
, c'est à dire qu'elle utilise la formule de politesse. Hormis la politesse, la representation est la même que celle d'un travailleur normal, on réutilise donc la méthode get_representation
de travailleur
. Pour éviter que la méthode de la classe directeur
ne soit appelée en boucle on doit utiliser l'opérateur de résolution de portée ::
pour spécifier que l'on désire utiliser la version de la méthode de la classe de base.
On redéfinit également la méthode set_metier
pour empêcher le changement de métier d'un directeur.
Voici un exemple d'utilisation :
int main () { srand(time(NULL)); directeur d("Toto", "Foobar", 46, SURPRISE); cout << d.get_representation() << endl; d.set_metier("chomeur"); cout << d.get_representation() << endl; return 0; }
Et la sortie générée à l'exécution :
$ ./ex_4_2 Création d'une femme : Toto Foobar * 46 ans Toto Foobar Madame Toto Foobar : Directeur Madame Toto Foobar : Directeur Destruction de Toto Foobar * 46 ans
Une fois assimilée cette notion de redéfinition, il apparaît évident qu'avoir deux méthodes get_identification
et get_representation
est inutile. Il vaut mieux n'avoir qu'une méthode get_identification
pour personne
et toutes les classes qui en sont dérivées. Donc, il nous faut modifier travailleur
et directeur
en conséquence pour nous affranchir de la méthode get_representation
.
Dans travailleur, on doit remplacer get_representation
par :
string get_identification() const { return personne::get_identification()+" : "+metier; }
Il faut faire de même pour la classe directeur
c'est à dire renommer la méthode et remplacer tous les appels à travailleur::get_representation
par travailleur::get_identification
.
Après ce morceau de bravoure, parlons un peu d'organisation. Si vous avez tout fait jusqu'à maintenant, vous avez trois classes et un programme dans le meme fichier. C'est un peu brouillon. Le C++ propose comme le C de répartir le code dans différents fichiers (dont les extensions vous sont proposées dans la page man de g++
). On peut en profiter pour répartir les différentes classes dans des fichiers séparés.
Le choix de ce que vous mettez dans les fichiers d'en-têtes et les fichiers sources vous appartient mais de façon générale, on place dans des fichiers .h les classes avec leur propriétés et les prototypes de leur méthodes ainsi que la définition des fonctions inline (comme travailleur::set_metier
ou travailleur::get_representation
). On y trouve aussi les autres définitions globales, espaces de noms, structures, énumérations.... On place ensuite dans des fichiers (.cc, .C, .cpp ou .c++) par exemple la définition des méthodes de classes, des fonctions globales (telles que main
). Généralement on créé un fichiers par unité logique, souvent un par classe, comme c'est souvent le cas en java.
On inclut les fichiers d'en-tête selon les mêmes règles que dans le cas du C traditionnel, grâce à la directive #include "chemin/fichier.h"
. On peut également utiliser les options habituelles de gcc telles que -I pour spécifier le chemin de recherche des fichiers d'en-tête.
Dans notre cas, nous répartissons le code en 7 fichiers. personne.h
contient les définitions de la classe personne
et de l'énumération sexe_t
. personne.c++
contient la définition complète de personne. On créé de façon équivalente les fichiers travailleur.h
, travailleur.c++
, directeur.h
et directeur.c++
. Enfin on place le code de l'application dans main.c++
. Bien entendu, directeur.h
devra inclure travailleur.h
qui lui même inclura personne.h
.
On peut comme en C entourer le contenu des fichiers d'en-tête par des tests de définition de symboles pour éviter les inclusions multiples de fichiers. Par exemple, directeur.h a un squelette semblable à :
#if !defined _DIRECTEUR_H #define _DIRECTEUR_H 1 /* Contenu */ #endif
main.c++
doit inclure les fichiers d'en-tête des classes qu'il utilise, c'est à dire dans notre exemple, directeur.h.
On obtient alors un programme dont le code source est extrêmement simple :
/* main.c++*/ #include <cstdlib> #include <iostream> #include <directeur.h> using namespace std; int main () { srand(time(NULL)); directeur d("Toto", "Foobar", 46, SURPRISE); cout << d.get_identification() << endl; return 0; }
La compilation/édition devient fastidieuse pour plusieurs fichiers, nous créons donc un fichier Makefile.
CXX=g++ CXXFLAGS=-I. OBJS=directeur.o travailleur.o personne.o main.o all : ex_4_4 ex_4_4: $(OBJS) @echo "Liaison de" $(OBJS) "en" $@ @$(CXX) $(CXXFLAGS) -o $@ $(OBJS) @echo "Fini, OK" @echo clean: @echo -n "Nettoyage en cours..." @-$(RM) *.o ex_4_4 @echo @echo "C'est propre" @echo
Utilisé comme tel, un make
ne produit rien, si ce n'est une erreur. En effet, un coup d'oeil dans la documentation de GNU make nous apprend que des règles implicites de créations d'objets sont bien définies pour le C++ mais à condition que l'extension utilisée soit .cc ou .C. A titre d'information, une règle implicite est ce qui permet à make de savoir que pour transformer fichier.c en fichier.o, il doit faire gcc -c fichier.c. Nous avons donc deux choix : soit nous renommons nos fichiers en .cc et le Makefile fonctionne alors sans problème, soit on crée une nouvelle règle apprenant à make comment créer des .o à partir de fichiers .c++. Ceci n'est pas un article sur make donc je ne m'étendrai pas sur le sujet. Le lecteur pourra se référer à la documentation adéquate. Il suffit de rajouter à notre Makefile la ligne suivante :
%.o: %.c++ @echo "Compilation de" $< @$(CXX) $(CXXFLAGS) -c $<
Avec ce nouveau Makefile nous pouvons tenter la compilation et vérifier le programme :
$ make clean Nettoyage en cours... C'est propre $ make Compilation de directeur.c++ Compilation de travailleur.c++ Compilation de personne.c++ Compilation de main.c++ Liaison de directeur.o travailleur.o personne.o main.o en ex_4_4 Fini, OK $ ./ex_4_4 Création d'un homme : Toto Foobar * 46 ans Monsieur Toto Foobar : Directeur Destruction de Toto Foobar * 46 ans
Expliquons les parties du Makefile spécifiques au C++ : CXX est une variable valant par défaut g++ (comme CC vaut par défaut cc, qui n'est qu'un lien vers gcc), CXXFLAGS sont les argument passés à $(CXX) pour la compilation. (comme CFLAGS pour $(CC)). On retrouve également le classique LDFLAGS pour les options de liaison, prise en charge par ld et CPPFLAGS pour les options passées au préprocesseur, lequel est $(CPP) et vaut $(CC) -E.
Je sais que c'est un peu vieux jeu de taper du Makefile mais la compréhension des Makefile permet de comprendre le fonctionnement d'autoconf/automake et surtout des Makefile qu'ils génèrent, assez touffus. En plus, faire un script configure pour un exemple aussi simple aurait été ridicule. Je n'exclus toutefois pas de le faire lorsque nous aborderons la construction d'interfaces en C++ à l'aide de gtkmm (pour Qt, se référer à la série d'article d'Yves Bailly dans le présent magazine).
Cet article est le plus court de la série sur le C++ pour vous permettre d'intégrer le concept d'héritage. La prochaine fois, nous verrons l'héritage multiple sauf si les courriers des lecteurs me poussent vers une autre direction. Nous aborderons alors les classes abstraites, les fonctions virtuelles et éventuellement l'utilité des membres statiques.
Les sources de cet article sont disponibles intégralement sur mon site, dans la section C++.
@+
Xavier GARREAU - http://www.xgarreau.org/ - <xavier@xgarreau.org>
Ingénieur de recherche PRIM'TIME TECHNOLOGY
http://www.prim-time.fr/
Membre fondateur du ROCHELUG
http://www.rochelug.org/
Précédent | Index | Suivant |
a+