Il est né ?
Vous êtes ici : www.xgarreau.org >> aide >> devel >> cpp : C héréditaire le C++ ?
Version imprimable

C héréditaire le C++ ?

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

L'héritage

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; }

Redéfinition de méthodes

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.

Programme multi-fichiers

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;
}

Makefile

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/

Références :


Précédent Index Suivant

a+

Auteur : Xavier GARREAU
Modifié le 10.09.2004

Rechercher :

Google
 
Web www.xgarreau.org