Вы находитесь на странице: 1из 13

Chapitre 5 : Polymorphisme, mthodes virtuelles et

abstraction

Introduction :

Nous savons quil est possible de redfinir les mthodes dune classe mre dans une classe fille.
Lors de lappel dune fonction ainsi redfinie, la fonction appele est la dernire fonction
dfinie dans la hirarchie de classe. Pour appeler la fonction de la classe mre alors quelle a
t redfinie, il faut prciser le nom de la classe laquelle elle appartient. Bien que simple, cette
utilisation de la redfinition des mthodes peut poser des problmes.
Supposons quune classe B hrite de sa classe mre A. Si A possde une mthode f() appelant
une autre mthode g() redfinie dans la classe fille B, que se passe-t-il lorsquun objet de
classe B appelle la mthode f ? La mthode f appele tant celle de la classe A, elle appellera
la mthode g de la classe A. Par consquent, la redfinition de g ne sert rien ds quon lappelle
partir dune des fonctions dune des classes mres.

Une premire solution consisterait redfinir la mthode f dans la classe B. Mais ce nest ni
lgant, ni efficace. Il faut en fait forcer le compilateur ne pas faire le lien dans la fonction f

50
de la classe A avec la fonction g de la classe A. Il faut que f appelle soit la fonction g de la
classe A si elle est appele par un objet de la classe A, soit la fonction g de la classe B si elle est
appele pour un objet de la classe B. Le lien avec lune des mthodes g ne doit tre fait quau
moment de lexcution, cest--dire quon doit faire une dition de liens dynamique. Le C++
permet de faire cela. Pour cela, il suffit de dclarer virtuelle la fonction de la classe de base qui
est redfinie dans la classe fille, cest--dire la fonction g.

1. Mthodes virtuelles

Une mthode virtuelle est une mthode prcde par le mot-cl virtual dans la classe de
base.
Les mthodes virtuelles sont des mthodes qui sont appeles selon la vraie classe de lobjet qui
lappelle. Les objets qui contiennent des mthodes virtuelles peuvent tre manipuls en tant
quobjets des classes de base, tout en effectuant les bonnes oprations en fonction de leur type.
Ils apparaissent donc comme tant des objets de la classe de base et des objets de leur classe
complte indiffremment, et on peut les considrer soit comme les uns, soit comme les autres.
Un tel comportement est appel polymorphisme (cest--dire qui peut avoir plusieurs aspects
diffrents).
Une classe possdant des fonctions virtuelles est dite classe polymorphe. La qualification
virtual devant la redfinition d'une fonction virtuelle est facultative : les redfinitions d'une
fonction virtuelle sont virtuelles d'office.

Exemple :
Class Polygone {
...
void toto(double a) {
double b=f(a);
.....}
virtual double f(double a);
...
Class Triangle:public Polygone{
.....
double f(double a);
}

51
Si dans le programme principal, on crit :
Polygone p;
...
p.toto();
La mthode toto appellera la mthode f de Polygone car p est de type Polygone.
Si par contre, on crit :
Triangle t;
...
t.toto();
t tant de type Triangle (qui drive de Polygone), on peut appeler la fonction toto de la classe
Polygone. Mais, ici, la mthode toto appellera la fonction f de la classe Triangle. Cela est d
au fait que la fonction f de la classe Polygone est virtuelle. Si elle ntait pas virtuelle la fonction
toto appellerait systmatiquement la fonction f de la classe Polygone.

2. Mthodes virtuelles pures - classes abstraites

Une mthode virtuelle pure est une mthode qui est dclare mais non dfinie dans une classe.
Elle est dfinie dans une des classes drives de cette classe.
Une classe abstraite est une classe comportant au moins une mthode virtuelle pure.
tant donn que les classes abstraites ont des mthodes non dfinies, il est impossible
dinstancier des objets pour ces classes. En revanche, on pourra les rfrencer avec des
pointeurs.
Pour dclarer une mthode virtuelle pure dans une classe, il suffit de faire suivre sa dclaration
de = 0 . La fonction doit galement tre dclare virtuelle :
virtual type nom (paramtres) = 0 ;
= 0 signifie ici simplement quil ny a pas dinstance de cette mthode dans cette classe.
Exemple :
Class Polygone {
...
virtual void CalcAire()=0;
...
}
Utilisation :
Polygone p; // ERREUR : cration dune instance de la classe abstraite Polygone
Polygone *pt; // Oui : cest un pointeur

52
pt = new Polygone; // ERREUR : cration dune instance de la classe abstraite Polygone
Triangle t; // Oui : la classe Triangle nest pas abstraite
pt = new Triangle; // Oui

Le mcanisme des mthodes virtuelles pures et des classes abstraites permet de crer des classes
de base contenant toutes les caractristiques dun ensemble de classes drives, pour pouvoir
les manipuler avec un unique type de pointeurs. En effet, les pointeurs des classes drives sont
compatibles avec les pointeurs des classes de base, on pourra donc rfrencer les classes
drives avec des pointeurs sur les classes de base, donc avec un unique type sous-jacent : celui
de la classe de base. Cependant, les mthodes des classes drives doivent exister dans la classe
de base pour pouvoir tre accessibles travers le pointeur sur la classe de base. Cest ici que
les mthodes virtuelles pures apparaissent. Elles forment un moule pour les mthodes des
classes drives, qui les dfinissent. Bien entendu, il faut que ces mthodes soient dclares
virtuelles, puisque laccs se fait avec un pointeur de classe de base et quil faut que ce soit la
mthode de la classe relle de lobjet (cest--dire la classe drive) qui soit appele.

3. Polymorphisme et identification dynamique du type

Le C++ est un langage fortement typ. Malgr cela, il se peut que le type exact dun objet soit
inconnu cause de lhritage. Par exemple, si un objet est considr comme un objet dune
classe de base de sa vritable classe, on ne peut pas dterminer a priori quelle est sa vritable
nature. Cependant, les objets polymorphiques (qui, rappelons-le, sont des objets disposant de
mthodes virtuelles) conservent des informations sur leur type dynamique, savoir leur
vritable nature. En effet, lors de lappel des mthodes virtuelles, la mthode appele est la
mthode de la vritable classe de lobjet.

a. Conversion standard vers une classe de base :


Si la classe D drive publiquement de la classe B alors les membres de B sont membres de D.
Autrement dit, les membres publics de B peuvent tre atteints travers un objet D. Ou encore :
tous les services offerts par un B sont offerts par un D.
Par consquent, l o un B est prvu, on doit pouvoir mettre un D. C'est la raison pour laquelle
la conversion (explicite ou implicite) d'un objet de type D vers le type B, une classe de base
accessible de D, est dfinie, et a le statut de conversion standard.
Exemple :
53
class Point { // point gomtrique
int x, y;
public:
Point(int, int);
...
};
class Pixel : public Point { // pixel = point color
char *couleur;
public:
Pixel(int, int, char *);
...
};

Utilisation :
Pixel px(1, 2, "rouge");
Point pt = px; // un pixel a t mis l o un point tait
// attendu : il y a conversion implicite
De manire interne, la conversion d'un D vers un B est traite comme l'appel d'une fonction
membre de D qui serait publique, protge ou prive selon le mode (ici : public) dont D drive
de B.
Ainsi, la conversion d'une classe drive vers une classe de base prive ou protge existe mais
n'est pas utilisable ailleurs que depuis l'intrieur de la classe drive. Bien entendu, le cas le
plus intressant est celui de la drivation publique.
Selon qu'elle s'applique des objets ou des pointeurs (ou des rfrences) sur des objets, la
conversion d'un objet de la classe drive vers la classe de base recouvre deux ralits trs
diffrentes :
1. Convertir un objet D vers le type B c'est lui enlever tous les membres qui ne font pas
partie de B (les membres spcifiques et ceux hrits d'autres classes). Dans cette
conversion il y a perte effective d'information :

Figure : Conversion de oD, un objet D, en un objet B

2. Au contraire, convertir un D* en un B* (c'est--dire un pointeur sur D en un pointeur


sur B) ne fait perdre aucune information. Point travers une expression de type B*,
l'objet n'offre que les services de la classe B, mais il ne cesse pas d'tre un D avec tous

54
ses membres :

Figure : Conversion de pD, un pointeur sur un D, en un pointeur sur un B

Par exemple, supposons possder deux fonctions de prototypes :


void fon1(Point pt);
void fon2(Point *pt);
et supposons qu'elles sont appeles de la manire suivante :
Pixel pix = Pixel(2, 3, "rouge")
...
fon1(pix);
fon2(&pix);
Le statut de l'objet pix en tant que paramtre de ces deux fonctions est tout fait diffrent.
L'objet pass fon1 comme argument est une version tronque de pix, alors que le pointeur
pass fon2 est l'adresse de l'objet pix tout entier :
void fon1(Point pt) {
La valeur de pt est un Point ; il n'a pas de couleur. Il y avait peut-tre l'origine un
Pixel mais, ici, aucune conversion (autre que dfinie par l'utilisateur) ne peut faire
un Pixel partir de pt.
}
void fon2(Point *pt) {
Il n'est rien arriv l'objet dont l'adresse a servi initialiser pt lors de l'appel de cette
fonction. Si on peut garantir que cet objet est un Pixel, l'expression suivante (place
sous la responsabilit du programmeur) a un sens :
((Pixel *) pt)->couleur ...
}
Pour ce qui nous occupe ici, les rfrences (pointeurs gres de manire interne par le
compilateur) doivent tre considres comme des pointeurs. Nous pouvons donc ajouter un
troisime cas :

55
void fon3(Point &rf) {
Il n'est rien arriv l'objet qui a servi initialiser rf lors de l'appel de cette fonction.
Si on peut garantir que cet objet est un Pixel, l'expression suivante (place sous la
responsabilit du programmeur) a un sens :
((Pixel &) rf).couleur ...
}

b. Type statique, type dynamique, gnralisation :


Considrons la situation suivante :
class B {
...
};
class D : public B {
...
};
D unD;
...
B unB = unD;
B *ptrB = &unD;
B &refB = unD;
Les trois affectations ci-dessus sont lgitimes ; elles font jouer la conversion standard d'une
classe vers une classe de base. Le type de unB ne soulve aucune question (c'est un B), mais
les choses sont moins claires pour les types de ptrB et refB. Ainsi ptrB, par exemple, pointe-t-
il un B ou un D ?
o on dit que le type statique de *ptrB (ce que ptB pointe) et de refB est B, car c'est ainsi
que ptB et refB ont t dclares ;
o on dit que le type dynamique de *ptrB et de refB est D, car tel est le type des valeurs
effectives de ces variables.
Le type statique d'une expression dcoul de l'analyse du texte du programme ; il est connu
la compilation.
Le type dynamique, au contraire, est dtermin par la valeur courante de l'expression, il peut
changer durant l'excution du programme.

c. Identification dynamique du type

56
Le cot du polymorphisme, en espace mmoire, est d'un pointeur par objet, quel que soit le
nombre de fonctions virtuelles de la classe. Chaque objet d'une classe polymorphe comporte un
membre de plus que ceux que le programmeur voit : un pointeur vers une table qui donne les
adresses qu'ont les fonctions virtuelles pour les objets de la classe en question.
D'o lide de profiter de l'existence de ce pointeur pour ajouter au langage, sans cot
supplmentaire, une gestion des types dynamiques qu'il faudrait sinon crire au coup par coup
(probablement a l'aide de fonctions virtuelles). Fondamentalement, ce mcanisme se compose
des deux oprateurs dynamic_cast et typeid.

L'oprateur dynamic_cast :
Le transtypage dynamique permet de convertir une expression en un pointeur ou une rfrence
dune classe, ou un pointeur sur void. Il est ralis laide de loprateur dynamic_cast.
Cet oprateur impose des restrictions lors des transtypages afin de garantir une plus grande
fiabilit :
o il effectue une vrification de la validit du transtypage ;
o il nest pas possible dliminer les qualifications de constance (pour cela, il faut utiliser
loprateur const_cast).
Syntaxe : dynamic_cast<type> (expression)
o type dsigne le type cible du transtypage, et expression lexpression transtyper.

Exemple :
class Animal {
...
virtual void uneFonction() { // il faut au moins une fonction virtuelle
... // (car il faut des classes polymorphes)
}
};
class Mammifere : public Animal {
...
};
class Chien : public Mammifere {
...
};

57
class Caniche : public Chien {
...
};
class Chat : public Mammifere {
...
};
class Reverbere {
...
};
Chien medor;
Animal *ptr = &medor;
...
Mammifere *p0 = ptr;
// ERREUR ( la compilation) : un Animal n'est pas forcment un Mammifre
Mammifere *p1 = dynamic_cast<Mammifere *>(ptr);
// OK : p1 reoit une bonne adresse, car Mdor est un mammifre
Caniche *p2 = dynamic_cast<Caniche *>(ptr);
// OK, mais p2 reoit 0, car Mdor n'est pas un caniche
Chat *p3 = dynamic_cast<Chat *>(ptr);
// OK, mais p3 reoit 0, car Mdor n'est pas un chat non plus
Reverbere *p4 = dynamic_cast<Reverbere *>(ptr);
// OK, mais p4 reoit 0, car Mdor n'est pas un rverbre

L'oprateur dynamic_cast s'applique galement aux rfrences. L'explication est la mme,


sauf qu'en cas d'impossibilit d'effectuer la conversion, cet oprateur lance l'exception bad
cast :
Animal &ref = medor;
Mammifere &r1 = dynamic_cast<Mammifere &>(ref); // Oui
Caniche &r2 = dynamic_cast<Caniche &>(ref); // exception bad cast lance
// non attrape, elle est fatale
L'oprateur static_cast :
Contrairement au transtypage dynamique, le transtypage statique neffectue aucune vrification
des types dynamiques lors du transtypage. Il est donc nettement plus dangereux que le
transtypage dynamique.
58
Cependant, contrairement au transtypage C classique, il ne permet toujours pas de supprimer
les qualifications de constance.
Le transtypage statique seffectue laide de loprateur static_cast, dont la syntaxe est
exactement la mme que celle de loprateur dynamic_cast :
static_cast <type> (expression)
o type et expression ont les mmes signification que pour loprateur dynamic_cast.
Contrairement loprateur dynamic_cast, loprateur static_cast permet donc
deffectuer les conversions entre les types autres que les classes dfinies par lutilisateur.
Aucune vrification de la validit de la conversion na lieu cependant (comme pour le
transtypage C classique).
Exemple :
int sum = 1000;
int count = 21;
double average1 = sum/count;
cout<<"Before conversion = "<<average1<<endl;
double average2 = static_cast<double>(sum)/count;
cout<<"After conversion = "<<average2<<endl;

L'oprateur const_cast :
La suppression des attributs de constance et de volatilit peut tre ralise grce loprateur
const_cast. Cet oprateur suit exactement la mme syntaxe que les oprateurs
dynamic_cast et static_cast :
const_cast <type> (expression)
Loprateur const_cast peut travailler essentiellement avec des rfrences et des pointeurs.
Il permet de raliser les transtypages dont le type destination est moins contraint que le type
source vis--vis des mots-cls const et volatile.
En revanche, loprateur const_cast ne permet pas deffectuer dautres conversions que les
autres oprateurs de transtypage (ou simplement les transtypages C classiques) peuvent raliser.
Par exemple, il est impossible de lutiliser pour convertir un flottant en entier. Lorsquil travaille
avec des rfrences, loprateur const_cast vrifie que le transtypage est lgal en
convertissant les rfrences en pointeurs et en regardant si le transtypage nimplique que les
attributs const et volatile. const_cast ne permet pas de convertir les pointeurs de
fonctions.

59
Exemple :
class CCTest {
public:
void setNumber( int );
void printNumber() const;
private:
int number;
};

void CCTest::setNumber( int num ) { number = num; }

void CCTest::printNumber() const {


cout << "\nBefore: " << number;
const_cast< CCTest * >( this )->number--;
cout << "\nAfter: " << number;
}

int main() {
CCTest X;
X.setNumber( 8 );
X.printNumber();
}

L'oprateur reinterpret_cast :

Loprateur de transtypage le plus dangereux est reinterpret_cast. Sa syntaxe est la


mme que celle des autres oprateurs de transtypage dynamic_cast, static_cast et
const_cast :
reinterpret_cast <type> (expression)
Cet oprateur permet de rinterprter les donnes dun type en un autre type. Aucune
vrification de la validit de cette opration nest faite.
Exemple :
#include <cstdint>
#include <cassert>
#include <iostream>
int f() { return 42; }
int main()
{
int i = 7;

// pointer to integer and back


uintptr_t v1 = reinterpret_cast<uintptr_t>(&i); // static_cast is an
error
std::cout << "The value of &i is 0x" << std::hex << v1 << '\n';
int* p1 = reinterpret_cast<int*>(v1);
assert(p1 == &i);

// pointer to function to another and back

60
void(*fp1)() = reinterpret_cast<void(*)()>(f);
// fp1(); undefined behavior
int(*fp2)() = reinterpret_cast<int(*)()>(fp1);
std::cout << std::dec << fp2() << '\n'; // safe

// type aliasing through pointer


char* p2 = reinterpret_cast<char*>(&i);
if(p2[0] == '\x7')
std::cout << "This system is little-endian\n";
else
std::cout << "This system is big-endian\n";

// type aliasing through reference


reinterpret_cast<unsigned int&>(i) = 42;
std::cout << i << '\n';
}

L'oprateur typeid :

Le C++ fournit loprateur typeid afin de rcuprer les informations de type des expressions.
Sa syntaxe est la suivante :
Typeid (expression)
o expression est lexpression dont il faut dterminer le type.
Le rsultat de loprateur typeid est une rfrence sur un objet constant de classe
type_info.
Les informations de type rcupres sont les informations de type statique pour les types non
polymorphiques. Cela signifie que lobjet renvoy par typeid caractrisera le type de
lexpression fournie en paramtre, que cette expression soit un sous-objet dun objet plus driv
ou non.
Exemple :
Animal *ptr = new Caniche;
cout << typeid(ptr).name() << '\n';
cout << typeid(*ptr).name() << '\n';
cout << "L'animal point par ptr "
<< (typeid(*ptr) == typeid(Chien) ? "est" : "n'est pas")
<< " un chien\n";
cout << "L'animal point par ptr est un "
<< typeid(*ptr).name() << "\n";
Affichage obtenu :
Animal *
Chien

61
L'animal point par ptr n'est pas un chien
L'animal point par ptr est un Caniche

62

Вам также может понравиться