Documente Academic
Documente Profesional
Documente Cultură
LHERITAGE EN C++
Plan du chapitre :
I. Introduction .
.. 64
II. Hritage simple .
66
III. Hritage multiple
...
... 69
IV. Hritage et constructeur......
constructeur...... 70
V. Compatibilit entre classe de base et classe drive ..... 72
VI. Le constructeur
ucteur de recopie et lhritage .... 74
VII. Le polymorphisme ...... 76
VIII.Lhritage et la surcharge des oprateurs .
... 83
Objectifs du chapitre :
Comprendre le mcanisme dhritage en programmation oriente objets et lappliquer en C++.
Dcouvrir la technique de polymorphisme en langage C++.
C++
I. Introduction:
I.1 Exemple :
Imaginons que nous devions fabriquer un logiciel qui permet de grer une bibliothque. Cette
bibliothque comporte plusieurs types de documents ; des livres, des CDs, ou des DVDs. Une
premire tude nous amne mettre en uvre les classes suivantes :
Nous remarquons que dans les trois types de documents, un certain nombre de caractristiques
se retrouvent systmatiquement.
Afin d'viter la rptition des lments constituant chacune des classes, il est prfrable de
factoriser toutes ces caractristiques communes pour en faire une nouvelle classe plus gnraliste.
En effet, nous pouvons dire que, d'une faon gnrale, et quelque soit le type de document, il
comporte au moins un titre, un auteur, etc. il semble allait de soi, que le nom de cette nouvelle
classe gnrale s'appelle justement Document.
La gnralisation se reprsente par une flche qui part de la classe fille vers la classe mre. Par
exemple, Un Livre possde, certes un nombre de page, mais en suivant la flche indique par la
relation de gnralisation, elle comporte galement un nom d'auteur, un titre, une rfrence. En
fait, la classe Livre hrite de tout ce que possde la classe Document, les attributs comme les
mthodes.
Dans cet exemple, nous avons un seul niveau d'hritage, mais il est bien entendu possible d'avoir
une hirarchie beaucoup plus dveloppe. D'ailleurs, si nous regardons de plus prt, nous
remarquons que nous pouvons appliquer une nouvelle fois la gnralisation en factorisant la dure
du support CD et du support DVD. En fait, il s'agit dans les deux cas d'un support commun appel
Multimdia.
I.2 Dfinitions :
Lhritage est une technique permettant de construire une classe partir dune ou des plusieurs
autres classes dites : classe mre ou superclasse ou classe de base.
La classe drive est appele: Classe fille ou sous-classe.
II.1 Syntaxe :
class C_mere
{
//...
};
class C_derivee: <mode> C_mere
{
//...
};
II.2 Exemple :
Point couleur
Classe drive
class Point
{
int x;
int y;
public:
void initialise (int , int) ;
void afficher() ;
};
class Pointcol: public Point
{
int couleur;
public:
void setcol(int c)
{
couleur=c ;
}
};
void main()
{
Pointcol p ;
p.initialiser (10,20) ;
p.setcol(5) ;
p.afficher() ;// (10, 20)
}
II.3 Utilisation des membres de la classe de base dans une classe drive :
Lexemple prcdent, destin montrer comment sexprime lhritage en C++, ne cherchait pas
explorer toutes les possibilits.
Or la classe pointcol telle que nous lavons dfinie prsente des lacunes. Par exemple, lorsque
nous appelons affiche pour un objet de type pointcol nous nobtenons aucune information sur sa
couleur.
Une premire faon damliorer cette situation consiste crire une nouvelle fonction membre
public :
void afficherC()
{
cout << "("<<x<<","<<y<<")"<<endl ;
cout << "couleur="<< couleur<<endl ;
}
Mais alors cela signifierait que la fonction afficherC membre de la classe Pointcol aurait un accs aux
membres privs de point ce qui serait contraire au principe dencapsulation.
En revanche, rien nempche une classe drive daccder nimporte quel membre public de sa
classe de base. Dou une dfinition possible de afficherC :
void afficherC()
{
afficher() ;
cout << "couleur="<< couleur<<endl ;
}
Dune manire analogue, nous pouvons dfinir dans pointcol une fonction dinitialisation comme
initialiserC :
void initialiserC(int a, int b, int c)
{
initialiser(a,b) ;
couleur=c ;
}
Drivation publique:
Drivation publique:
Drivation protge:
Exemple :
Remarque :
Lhritage multiple est possible en C++ mais permet de poser quelques problmes :
Si les deux classes mres ont des attributs ou des mthodes de mme nom (collision des noms
lors de la propagation).
La classe D hrite deux fois les attributs de A, une fois travers la classe B et lautre fois
travers C.
class C_mere1
{
//...
};
class C_mere2
{
//...
};
class C_derivee: <mode> C_mere1, <mode> C_mere2
{
//...
};
public: public:
A(...) ; B(...) ;
~A() ; ~B() ;
.... ....
}; };
Pour crer un objet de type B, il faut tout dabord crer un objet de type A, donc faire appel au
constructeur de A, puis le complter par ce qui est spcifique B et faire appel au constructeur de
B. ce mcanisme est pris en charge par C++ : il ny aura pas prvoir dans le constructeur de B
lappel du constructeur de A.
La mme application sapplique aux destructeur : lors de la destruction dun objet de type B, il y
aura automatiquement appel du destructeur de B, puis appel de celui de A(les destructeurs sont
appels dans lordre inverse de lappel des destructeurs).
Exemple:
class Point
{
int x;
int y;
public:
Point(int abs=0, int ord=0)
{
cout <<"++constr. point: "<<abs<<, <<ord<<endl ;
x=abs ; y=ord ;
}
~Point()
{
cout <<"desctr. point: "<<x<<","<<y<<endl ;
}
};
class Pointcol: public Point
{
int couleur;
public:
Pointcol(int , int , int) ;
~Pointcol ()
{
cout<<"destr. pointcol -couleur: "<<couleur<<endl ;
}
};
Pointcol::Pointcol(int abs=0,int ord=0,int c=1):Point(abs,ord)
{
couleur=c;
cout <<"++constr. pointcol: "<<abs<<, <<ord<<","<<couleur;
}
void main()
{
Pointcol a(10,15,3) ;
Pointcol b(2,3) ;
Pointcol c(12) ;
Pointcol *adr;
adr = new Pointcol(12,25);
delete adr ;
}
Remarque :
Quelque soit les situations, nous disposons toujours des quatre mmes phases pour la cration de
l'objet et toujours dans le mme ordre.
1. Allocation mmoire ncessaire pour contenir tous les attributs que comporte l'objet.
2. Appel du constructeur de la classe drive. Ce constructeur est appel mais pas encore
excut. Appel du constructeur de la classe de base spcifi par la liste d'initialisation en
rcuprant les bons arguments pour les attributs de la classe de base.
3. Appel et excution du constructeur de la classe de base. A moins que la classe de base soit
elle-mme une classe drive d'une autre classe de base, les instructions qui constituent le
corps du constructeur sont excutes.
4. Excution du constructeur de la classe drive. Puisque la partie gnrale est bien initialise,
nous pouvons nous occuper de la partie spcifique la classe drive. Les instructions du
corps du constructeur sont donc excutes.
Exemple:
class Forme
{
int x;
int y;
public:
Forme(int x, int y)
{ this->x=x ; this->y=y ;}
void deplacer(int dx, int dy)
{ x+=dx;y+=dy ; }
};
class Cercle: public Forme
{
int rayon;
public:
Cercle(int x, int y, int r):Forme(x, y)
{ rayon=r ; }
void agrandir(int a)
{ rayon+=a; }
};
void main()
{
Forme f1(2,3); //1//
Cercle c1(15, -1, 50) ; //2//
c1.deplacer(3, 4) ; //3//
c1.agrandir(10) ; //4//
f1=c1 ; //5//
f1.deplacer(5, -8) ; //6//
c1=f1 ; //7//
}
5. droite et gauche de l'oprateur d'affectation, les types sont diffrents. C'est toujours le
type qui est droite qui est transform vers le type de gauche. Ici, le cercle c1 est aussi une
forme, donc la conversion implicite est lance. Cette dmarche parat normale puisqu' l'issue
de cette opration, les attributs de l'objet f1 sont parfaitement dfinis.
6. Dans ce contexte, il est galement possible de changer de position puisque, de toute faon, la
mthode associe a t dfinie dans la classe Forme.
7. Tentative daffectation d'une forme dans un cercle. Nous obtenons galement une erreur de
compilation. Il s'agit galement d'un changement de type. Si ce casting tait tolr, cela
voudrait dire que nous autoriserions d'avoir des attributs avec des valeurs alatoires !.
Effectivement f1 ne dispose pas de rayon, ainsi l'attribut rayon de c2 se retrouverait sans
aucune valeur bien prcise. Cette dmarche n'est pas tolre par le compilateur et nous le
comprenons.
!
BEN ROMDHAN Mourad 73
LHERITAGE EN C++
class A {...} ;
class B : public A {...};
void fct(B); //function recevant un argument de type B
...
B b1(...);
fct(b1);// appel de fct qui on doit transmettre b1
//par valeur ce qui implique lappel dun constructeur
//par de recopie de la classe B
Bien entendu, tout ce que nous allons dire sappliquerait galement aux autres situations
dinitialisation par recopie, c'est--dire o une fonction renverrait par valeur un rsultat de type B
ou encore celui o lon initialiserait un objet de type B avec un autre objet de type B comme
dans B b2=b1 ou encore B b2(b1).
Ici, cela signifie que la partie de b1 appartenant la classe A sera traite comme un membre de
type A. Il y aura donc appel du constructeur de recopie de A pour les membres donnes
correpondants. Notez bien que si A a defini un tel constructeur, il sera appel ; dans le cas
contraire, on se servira du constructeur de recopie par dfaut de A.
Exemple:
Voici un programme illustrant cette possibilit. Nous les deux classes point et pointcol en les
munisant toutes les deux dun constructeur de recopie:
class Point
{
int x;
int y;
public:
Point(int abs=0, int ord=0)
{
cout <<"constr. point: "<<abs<<, <<ord<<endl;
x=abs; y=ord ;
}
Point(Point &p)
{
x=p.x; y=p.y;
cout <<"constr. par recopie point:"<<x<<","<<y<<endl;
}
};
class Pointcol: public Point
{
int couleur;
public:
Pointcol(Pointcol &p):Point(p) // conversion implicite de p
{ // dans le type point
couleur=p.couleur;
cout <<"constr. par recopie pointcol:"<<couleur<<endl;
}
Pointcol(int abs=0,int ord=0,int c=1):Point(abs,ord)
{
couleur=c;
cout <<"++constr. pointcol:"<<abs<<","<<ord<<","<<couleur;
}
};
void fct(Pointcol p)
{
cout << " *** entree dans fct ***\n";
}
void main()
{
Pointcol a(2,3,4);
Pointcol b(a);
fct(a); //appel de fct, qui on transmet a par valeur
}
VII. Le polymorphisme:
VII.1 Dfinition:
Mcanisme qui consiste dfinir des fonctions de mme nom dans les classes de bases et les
classes drives.
Les classes drives hritent tous les membres publics et protgs de la classe mre.
2. Une redfinition permet de fournir une nouvelle dfinition d'une mthode d'une classe
ascendante et ainsi de substituer la description qui en t faite. Nous avons galement le
mme nom que la mthode parente mais surtout avec une signature rigoureusement identique.
La redfinition constitue la base du polymorphisme.
Voici comment nous pouvons transformer lexemple du paragraphe prcdent en nommant affiche
et initialise les nouvelles fonctions membres de pointcol :
#include "point.h"
class Pointcol: public Point
{
int couleur;
public:
void afficher()
{ Point::afficher() ;
cout << "couleur="<<couleur<<endl ;
}
void initialiser(int a, int b, int c)
{
Point::initialiser(a,b) ;
couleur=c ;
}
};
Remarquer que dans chaque classe les mthodes afficher() et initialiser() portent la mme
signature mais contiennent un code diffrent propre la spcificit de la classe.
Premire utilisation :
void main()
{
Pointcol *a= new Pointcol(2,3,4);
Point *b=new Point(a);
b->afficher();
}
Commentaire: Cest bien dutiliser la mme signature mais o est la notion du polymorphisme ?
Lhritage et le polymorphisme sont des mcanismes trs puissants mais jusque l les mthodes
ont une liaison statique la compilation.
Question : comment faut-il faire pour appliquer le corps spcifique chaque classe dynamiquement.
Cest dire qu lexcution, le compilateur applique la mthode afficher() pour chaque objet
selon sa propre dfinition.
class Point
{ int couleur;
public:
virtual void afficher() ;
virtual initialiser(int , int , int );
};
Deuxime utilisation pointeur de la classe mre qui pointe sur un objet de la classe drive:
void main()
{ Pointcol a(2,3,4);
Point *b=&a;
b->afficher();
}
Resultat de lexcution : (2,3) couleur=4
appel de la mthode afficher() de la classe drive
Rfrence de la classe mre sur un objet de la classe drive:
void main()
{ Pointcol a(2,3,4);
Point &p=a;
p.afficher();
}
Resultat de lexcution : (2,3) couleur=4
appel de la mthode afficher() de la classe drive
Remarque:
A lexcution du programme principal le compilateur affecte suivant le type dobjet la
mthode afficher() adquate de faon dynamique.
La redfinition dune fonction virtuelle nest pas obligatoire, do si la classe drive na pas
redfinie la mthode virtuelle, alors pendant "lappel dynamique" cest la fonction de la classe
mre (virtuelle) qui va tre excute.
Le mot virtual se place uniquement dans la dclaration de la classe. Lorsque vous dfinissez
la mthode l'extrieur de la classe, vous ne devez plus re-spcifier le mot virtual devant la
signature de la mthode
Lorsque nous avons une redfinition des destructeurs dans une hirarchie de classe qui
comporte des mthodes virtuelles, il est gnralement prfrable que les destructeurs fassent
galement partie des tables des adresses des mthodes virtuelles. En effet, lorsque nous
ralisons un delete sur un pointeur d'une classe de base, le bon destructeur est alors pris en
compte. Vous obtenez ce comportement en dclarant virtual le destructeur de la classe de base.
Lorsque qu'une mthode virtuelle est invoque l'intrieur d'un des constructeurs de la
hirarchie, c'est toujours la mthode virtuelle de la classe de base qui est sollicit. En effet,
puisque nous sommes en phase de cration, les tables des adresses des mthodes virtuelles
n'existent pas encore. Nous ne pouvons donc pas intgrer le polymorphisme sur un
constructeur, il faut que l'objet soit d'abord cr. Du coup, un constructeur ne peut jamais tre
virtuel.
VII.4 Les classes abstraites :
En POO, nous pouvons dfinir des classes destines non pas instancier des objets, mais
simplement donner naissance dautres classes par hritage. On dit quon a affaire des
classes abstraites.
En C++, nous pouvons toujours dfinir de telles classes, en dclarant des fonctions membres
virtuelles dont on ne prcise pas le contenu dans la classe de base. Seules les classes de base
possdent alors, ventuellement une description du corps de ces fonctions virtuelles. On les
appelle des fonctions virtuelles pures.
Une fonction virtuelle pure se dclare en remplaant le corps de la fonction par les symboles = 0.
Syntaxe :
class NomClasse
{
//
virtual TypeRetout nomFonction (liste des arguments)=0;
//
}
Remarque:
Une classe comportant au moins une fonction virtuelle pure est considre comme abstraite et
il nest plus possible de dclarer des objets de son type.
Une fonction dclare virtuelle pure dans une classe de base doit obligatoirement tre
redfinie dans une classe drive ou dclare nouveau virtuelle pure ; dans ce dernier cas, la
classe drive est aussi abstraite.
Exemple : Figure
class Figure
{
public:
virtual float perimetre()=0;
virtual char * getNom()=0;
};
class UnRectangle:public Figure
{
float longueur;
float largeur;
public :
UnRectangle(float longueur,float largeur)
{
this->longueur=longueur;
this->largeur=largeur;
}
float perimetre()
{
return 2*(longueur+largeur);
}
char* getNom()
{
return "RECTANGLE";
}
};
class UnCarre:public UnRectangle
{
float cote;
public:
UnCarre(float cote):UnRectangle(cote,cote)
{
this->cote=cote;
}
float perimetre()
{
return cote*4;
}
char* getNom()
{
return "CARRE";
}
};
void main()
{
UnRectangle *Rect=new UnRectangle(10,5);
UnCarre *Carre=new UnCarre(10);
Figure *T[2];
T[0]=Rect;
T[1]=Carre;
for (int i=0;i<=1;i++)
{
cout<<"Le PERIMETRE DU "<<T[i]->getNom()<<" = "<<
T[i]->perimetre()<<endl;
}
}
Informations 1 Informations 2 Informations 3
tete
Mais ici lon souhaite que les diffrentes informations puissent tre de types diffrents. Aussi
cherchons-nous isoler dans une classe (nomm liste) toutes les fonctionnalits de gestion de la
liste elle-mme sans entrer dans les dtails spcifiques aux objets concerns. Nous appliquerons
alors ce schma :
tete
struct element
{
element *suivant ;
mere *contenu ;
} ;
class Liste{
element *tete ;
public:
Liste() ;
~Liste() ;
void ajouter(mere *) ;
void afficher() ;
.....
};
Exemple :
class Article
{
long code;
char nom[100];
double quantite;
double prix;
public:
virtual void afficher()
{
cout <<"code="<<code<<endl ;
cout <<"Nom="<<nom<<endl ;
cout <<"Quantit="<<quantite<<endl ;
cout <<"Prix unitaire="<<prix<<endl ;
}
virtual void saisir()
{
cout <<"code="<<endl; cin>>code ;
cout <<"Nom="<< endl ; cin >> nom ;
cout <<"Quantit="<< endl ; cin >> quantite ;
cout <<"Prix unitaire="<< endl ;cin>> prix ;
}
};
class Boissons:public Article
{
public:
double volume;
void afficher()
{
cout << "cest un boissons"<<endl
Article ::afficher() ;
cout << "volume="<<volume<<endl ;
}
void saisir()
{
cout << "saisie dun boissons"<<endl
Article ::saisir() ;
cout << "Volume= "<<endl ; cin>> volume ;
}
} ;
class Confiture: public Article{
public:
double poids;
void afficher()
{
cout << "cest un confiture"<<endl
Article ::afficher() ;
cout << "Poids="<<poids<<endl ;
}
void saisir()
{
cout << "cest un confiture"<<endl
Article ::saisir() ;
cout << "Poids= "<<endl ; cin>> poids ;
}
} ;
Or dans ces conditions, si s est de type pointcol, une banale affectation telle que s=a+b ; sera
rjete par le compilateur, faute de disposer de la conversion de point en pointcol.
Si on souhaite dfinir la somme de deux points colors, il faudra redefinir loprateur + au sein de
pointcol.
Nous avons expliqu comment C++ dfinit laffectation par dfaut entre deux objets de meme
type. Dautre part, nous avons montr quil tait possible de surdfinir cet oprateur daffectation.
Supposons que la classe B hrite de la classe A
Laffectation de deux objets de type B se droule membre membre en considrant que la "partie
hrit de A" constitue un membre. Ainsi, les membres propre B sont traits par laffectation
prvue pour leur type. La partie hrit de A est traite par laffectation prvue dans la classe A,
cest--dire par loprateur= surdfini dans A sil existe, par laffectation par defaut sinon.
On retrouve un comportement tout fait analogue celui dcrit dans le cas du constructeur de
recopie.
Laffectation de deux objets de type B fera alors ncessairement appel loprateur= dfini dans
B. celui de A ne sera pas appel, mme sil a t surdfini. Il faudra donc que loprateur= de B
prenne en charge tout ce qui concerne laffectation dobjets de type B, y compris pour ce qui est
des membres hrits de A.
class Point
{
protected:
int x;
int y;
public:
Point &operator = (const Point &p)
{ x=p.x;
y=p.y;
cout << oprateur = de point\n;
return *this;
}
};
Dans cet exemple loprateur = dfini dans la classe point na pas t appel lors dune affectation
entre objets de type poincol.
Le problme est voisin de celui rencontr propos du constructeur de recopie. Donc si lon veut
pouvoir profiter de loprateur = dfini dans la classe point, il faudra lappeler explicitement :
Nous convertissons les pointeurs (this et &p) sur des objets de pointcol en des pointeurs de type
point. Il suffit ensuite de raliser une affectation entre les nouveaux objets points (*p1 et *p2)
pour entraner lappel de loprateur = de la classe Point.