I. Les fonctions membres en C++▲
Trois types de fonctions peuvent être définis dans une classe (1) en C++ :
- les fonctions membres statiques ;
- les fonctions membres normales ;
- les fonctions membres virtuelles.
I-A. Les fonctions membres statiques▲
Le mot-clé static utilisé en début du prototype de la fonction permet de déclarer une fonction membre statique ou encore fonction de classe :
struct
my_type
{
static
void
s_function(); // fonction statique
}
;
Une fonction membre statique n'est pas liée à un objet. Elle n'a donc pas de paramètre implicite this et ne peut donc accéder à d'autres membres d'instances de la classe sinon les membres statiques :
#include
<iostream>
struct
my_type
{
static
void
s_function(); // fonction membre statique
static
int
mi_class_variable;
int
mi_member_variable;
}
;
int
my_type::
mi_class_variable=
0
;
void
my_type::
s_function()
{
std::
cout<<
"je suis une fonction membre statique !
\n
"
;
mi_class_variable =
5
; // On peut référencer un membre statique de la classe
/*
mi_member_variable = 2;// Erreur, une fonction membre statique ne peut utiliser
// un membre non statique de la classe dépendant
// d'une instance
this->mi_member_variable = 2; // Erreur, une fonction membre statique
// ne peut utiliser this ; elle n'est pas
// liée à une instance de la classe
*/
}
int
main()
{
my_type::
s_function(); // appel d'une fonction membre statique
my_type var;
var.s_function(); // bien qu'un objet soit utilisé, cet appel est équivalent
// au précédent. var n'est pas pris en compte dans l'appel
return
0
;
}
L'appel d'une fonction membre statique d'une classe ne dépend pas d'une instance de ce type.
Par conséquent, les fonctions membres statiques ne nous intéresseront pas par la suite, car elles ne dépendent nullement d'un objet.
I-B. Les fonctions normales▲
Les fonctions normales n'ont pas de mot-clé spécifique pour leur déclaration : c'est le comportement par défaut d'une fonction membre d'une classe.
#include
<iostream>
struct
my_type
{
void
a_function(); // fonction membre normale
}
;
void
my_type::
a_function()
{
std::
cout<<
"je suis une fonction normale !
\n
"
;
}
int
main()
{
/*
my_type::a_function(); // Erreur : nécessite un objet pour être appelée
*/
my_type var;
var.a_function(); // nécessite une instance du type pour être invoquée.
return
0
;
}
Ces fonctions membres ont un pointeur this désignant l'objet au départ duquel elles ont été invoquées et peuvent accéder aux membres de cet objet :
#include
<iostream>
struct
my_type
{
void
a_function(); // fonction membre normale
int
mi_member;
}
;
void
my_type::
a_function()
{
std::
cout<<
"je suis une fonction normale !
\n
"
;
this
->
mi_member =
41
; // OK
mi_member++
; // OK : équivalent à (this->mi_member)++;
}
int
main()
{
my_type var;
var.a_function();
std::
cout<<
"valeur de mi_member de var : "
<<
var.mi_member<<
"
\n
"
;
// C'est bien l'objet var qui a été associé à l'appel de la fonction
return
0
;
}
I-C. Les fonctions virtuelles▲
Les fonctions virtuelles précèdent leur déclaration du mot-clé virtual(2) :
#include
<iostream>
struct
my_type
{
virtual
void
a_function(); // fonction virtuelle
}
;
void
my_type::
a_function() // dans la définition de la fonction, il ne faut
// pas répéter le mot-clé 'virtual'
{
std::
cout<<
"je suis une fonction virtuelle !
\n
"
;
}
int
main()
{
/*
my_type::a_function(); // Erreur : nécessite un objet pour être appelée
*/
my_type var;
var.a_function(); // nécessite une instance du type pour être invoquée.
return
0
;
}
Comme les fonctions membres normales, les fonctions virtuelles ont un pointeur this et peuvent accéder aux membres de cet objet :
#include
<iostream>
struct
my_type
{
virtual
void
a_function(); // fonction virtuelle
int
mi_member;
}
;
void
my_type::
a_function()
{
std::
cout<<
"je suis une fonction normale !
\n
"
;
this
->
mi_member =
41
; // OK
mi_member++
; // OK : équivalent à (this->mi_member)++;
}
int
main()
{
my_type var;
var.a_function();
std::
cout<<
"valeur de mi_member de var : "
<<
var.mi_member<<
"
\n
"
;
// C'est bien l'objet var qui a été associé à l'appel de la fonction
return
0
;
}
Soit en résumé :
L'appel d'une fonction membre non statique d'une classe nécessite une instance du type.
Par défaut, en C++, les fonctions membres ne sont pas virtuelles. Le mot-clé virtual est nécessaire pour définir une fonction virtuelle.
En C++, les fonctions virtuelles doivent être membres d'une classe.
Anticipons en signalant l'existence d'une catégorie particulière de fonctions virtuelles en C++ : les fonctions virtuelles pures. Une fonction virtuelle pure est une fonction virtuelle à laquelle est rajoutée =0 à la fin de sa déclaration :
struct
my_type
{
virtual
void
a_function()=
0
; // fonction virtuelle pure
}
;
Nous reviendrons un peu plus loin sur les fonctions virtuelles pures, pour l'instant il suffit de savoir que ça existe et qu'avant d'être « pures », ce sont avant tout des fonctions virtuelles.
Toutes les fonctions d'une classe peuvent-elles être virtuelles ? Oui, feinte presque : seuls les constructeurs (et les fonctions statiques) ne peuvent pas être virtuels. Toutes les autres fonctions le peuvent : que ce soit le destructeur, les opérateurs, ou des fonctions quelconques. Nous verrons par la suite ce que cela signifie et l'intérêt dans chaque cas.
struct
my_type
{
// virtual my_type(); // erreur : un constructeur ne peut être virtuel
// virtual static void s_function(); // erreur : une fonction ne peut être ET statique ET virtuelle
virtual
~
my_type();// OK
virtual
void
function(); // OK
virtual
my_type&
operator
+
(my_type const
&
); // OK
}
;
La virtualité s'hérite : une fonction virtuelle dans la classe de base reste virtuelle dans la classe dérivée même si le mot-clé virtual n'est pas accolé :
struct
base
{
void
function_1();
virtual
void
function_2();
void
function_3();
}
;
struct
derived : public
base
{
void
function_1();
void
function_2();
virtual
void
function_3();
}
;
Nous avons avec cet exemple :
base |
derived |
|
---|---|---|
function_1 |
non virtuelle |
non virtuelle |
function_2 |
virtuelle |
virtuelle |
function_3 |
non virtuelle |
virtuelle |
Autant comme le montre la troisième ligne, il est possible de masquer une fonction non virtuelle d'une classe de base par une fonction virtuelle dans une classe dérivée, autant il est impossible de s'en débarrasser. Une fonction est et sera virtuelle pour toutes les classes dérivant de la classe l'ayant définie comme telle. Cependant, afin d'éviter toute confusion, il est fortement recommandé d'utiliser le mot-clé virtual dans les classes dérivées :
Les classes dérivées devraient utiliser le mot-clé virtual pour les fonctions définies comme virtuelles dans la classe de base.
I-D. La surcharge de fonction▲
Introduisons un dernier point pour poser notre problématique : la surcharge de fonction. Il est aussi possible de surcharger une fonction dans une classe, c'est-à-dire définir plusieurs fonctions avec le même nom, à condition qu'elles diffèrent par :
- leur nombre d'arguments ;
- et/ou le type d'au moins un des arguments ;
- et/ou leur constance.
La signature d'une fonction en C++ désigne son nom, le nombre de ses arguments, leur type et la constance de la fonction. Surcharger une fonction F1 revient alors à proposer une nouvelle fonction F2, telle que la signature de F1 est différente de celle de F2 autrement que par le nom qu'elles partagent.
Par exemple, le code suivant présente différentes surcharges d'une fonction membre :
struct
my_type
{
void
function(double
,double
){}
void
function(double
){}
// le nombre d'arguments est différent
// de la précédente définition
void
function(int
){}
// le type de l'argument est différent
// de la précédente définition
void
function(int
) const
{}
// la constance est différente
// de la précédente définition
void
function(char
&
){}
void
function(char
const
&
){}
// char& et char const & sont deux types différents
}
;
La surcharge est un cas de polymorphisme en C++ (3). Cela permet d'adapter la fonction à appeler selon les arguments en paramètre :
#include
<iostream>
struct
my_type
{
void
function(double
,double
) // (1)
{
std::
cout<<
"(1)
\n
"
;
}
void
function(double
) // (2)
{
std::
cout<<
"(2)
\n
"
;
}
void
function(int
) // (3)
{
std::
cout<<
"(3)
\n
"
;
}
void
function(int
) const
// (4)
{
std::
cout<<
"(4)
\n
"
;
}
void
function(char
&
) // (5)
{
std::
cout<<
"(5)
\n
"
;
}
void
function(char
const
&
) // (6)
{
std::
cout<<
"(6)
\n
"
;
}
}
;
int
main()
{
my_type var;
var.function(1.
,1.
); // (1)
var.function(1.
); // (2)
var.function(1
); // (3)
my_type const
c_var=
my_type();
c_var.function(1
); // (4)
char
c('a'
);
var.function(c); // (5)
char
const
&
rc =
c;
var.function(rc); // (6)
return
0
;
}
La surcharge a ses limites. En particulier, une même classe ne peut pas redéfinir une fonction avec le même nom d'une fonction existante si :
- elles ne diffèrent que par leur type retour ;
- elles ont les mêmes arguments, le même type retour, mais l'une d'elles est statique ;
- elles ont les mêmes arguments, le même type retour, la même constance, mais l'une d'elles est virtuelle (ou virtuelle pure) et l'autre normale ;
- elles ont les mêmes arguments, le même type retour, la même constance, mais l'une d'elles est virtuelle et l'autre virtuelle pure ;
- un argument ne diffère qu'à cause d'un typedef ;
- elles ne diffèrent que par un const non significatif sur le type d'un argument.
- elles ne diffèrent que par les valeurs par défaut de leur(s) argument(s).
Ainsi les surcharges suivantes sont interdites dans une même classe :
struct
my_type
{
void
function_1();
int
function_1(); // Erreur : seul le type retour est différent
static
void
function_2();
void
function_2(); // Erreur : function_2 est déjà définie et statique
void
function_2()const
; // const ne suffit pas pour différencier une
// fonction non statique et une fonction statique
virtual
void
function_3();
void
function_3(); // Erreur : function_3 est déjà définie et virtuelle
virtual
void
function_3_bis();
void
function_3_bis()const
; // OK : const suffit pour différencier des
// fonctions membres non statiques
virtual
void
function_4();
virtual
void
function_4()=
0
; // Erreur : la fonction a déjà été déclarée
// virtuelle, mais non pure.
// L'erreur aurait été la même si on avait
// d'abord déclaré la fonction virtuelle pure
// puis la fonction virtuelle (non pure)
void
function_5(int
);
typedef
int
t_int;
void
function_5(t_int); // Erreur : le typedef n'est qu'un synonyme
void
function_6(char
);
void
function_6(char
const
); // Erreur : le const n'est pas significatif
// pour différencier les deux fonctions
void
function_6_bis(char
*
);
void
function_6_bis(char
*
const
); // Erreur : le const n'est pas significatif
// pour différencier les deux fonctions
void
function_6_ter(char
*
);
void
function_6_ter(char
const
*
); // OK : char const * et char * sont bien
// deux types différents
void
function_7(int
);
void
function_7(int
=
42
); // Erreur : les définitions sont équivalentes
}
;
I-E. Quand l'héritage chamboule tout !▲
L'héritage importe dans la classe dérivée toutes les déclarations des classes de base (4) :
struct
base
{
void
function(){}
}
;
struct
derived : base
{
}
;
int
main()
{
derived d;
d.function(); // on récupère l'interface de base
return
0
;
}
Cependant, une fonction déclarée dans une classe dérivée ayant le même nom qu'une fonction de la classe de base, mais avec une signature différente masque la fonction de la classe de base dans la classe dérivée :
struct
base
{
void
function(){}
}
;
struct
derived : base
{
void
function(int
){}
void
call_a_function()
{
function(); // Erreur base::function est masquée par derived::function
base::
function(); // OK : on indique explicitement la fonction à appeler
function(1
); // appel de derived::function(int)
}
}
;
int
main()
{
derived d;
d.function(); // Erreur base::function est masquée par derived::function
d.base::
function(); // OK : on indique explicitement la fonction à appeler
d.function(1
); // appel de derived::function(int)
return
0
;
}
La surcharge dans une classe dérivée d'une fonction définie dans une classe de base avec une signature différente masque la fonction de la classe de base dans et pour la classe dérivée.
Nous avons vu à la section précédente qu'il n'était pas possible dans une même classe de redéfinir une nouvelle fonction avec la même signature qu'une fonction existante (même nom, mêmes paramètres, même constance). Et, nous en arrivons au point qui va nous intéresser, à savoir :
Une classe dérivée peut redéfinir une fonction d'une classe de base ayant la même signature !
Ainsi, en reprenant dans la section précédente l'exemple des surcharges interdites et en recopiant les surcharges interdites vers la classe dérivée, nous obtenons le code valide suivant :
struct
base
{
void
function_1();
static
void
function_2();
virtual
void
function_3();
virtual
void
function_4();
void
function_5(int
);
void
function_6(char
);
void
function_7(int
);
}
;
struct
derived : public
base
{
int
function_1(); // OK
void
function_2(); // OK
void
function_2()const
; // OK
void
function_3(); // OK
virtual
void
function_4()=
0
; // OK
typedef
int
t_int; // OK
void
function_5(t_int); // OK
void
function_6(char
const
); // OK
void
function_7(int
=
42
); // OK
}
;
L'objectif est maintenant de savoir quelles fonctions sont appelées lorsqu'une même signature est disponible dans une classe dérivée et une classe de base selon l'expression utilisée pour l'appel :
struct
base
{
void
function_1()
{
}
virtual
void
function_2()
{
}
void
call_function_1()
{
function_1(); // Quelle est la fonction appelée ?
}
void
call_function_2()
{
function_2(); // Quelle est la fonction appelée ?
}
}
;
struct
derived : public
base
{
void
function_1()
{
}
virtual
void
function_2()
{
}
void
call_function_1()
{
function_1(); // Quelle est la fonction appelée ?
}
void
call_function_2()
{
function_2(); // Quelle est la fonction appelée ?
}
}
;
int
main()
{
base b;
b.function_1(); // Quelle est la fonction appelée ?
b.function_2(); // Quelle est la fonction appelée ?
derived d;
d.function_1(); // Quelle est la fonction appelée ?
d.function_2(); // Quelle est la fonction appelée ?
base &
rb =
b;
rb.function_1(); // Quelle est la fonction appelée ?
rb.function_2(); // Quelle est la fonction appelée ?
base &
rd =
d;
rd.function_1(); // Quelle est la fonction appelée ?
rd.function_2(); // Quelle est la fonction appelée ?
return
0
;
}
Pour arriver à répondre à toutes ces questions, il nous faut introduire une nouvelle notion : le type statique et le type dynamique d'une variable.
II. Type statique et type dynamique▲
Une variable possède deux types : un type statique et un type dynamique.
Le type statique d'une variable est celui déterminé à la compilation. Le type statique est le plus évident : c'est celui avec lequel vous avez déclaré votre variable. Il est sous votre nez lorsque vous regardez le code.
Le type dynamique d'une variable est celui déterminé à l'exécution. Le type dynamique quant à lui n'est pas immédiat en regardant le code. En effet, il va pouvoir varier à l'exécution selon ce que la variable va effectivement désigner pendant le déroulement du programme.
Le type dynamique et le type statique coïncident pour les variables utilisées par valeur :
int
a;
char
c;
std::
string s;
class
a_class{
/*[...]*/
}
;
a_class an_object;
enum
E_enumeration{
/*[...]*/
}
;
E_enumeration e;
Avec cet exemple, on a :
variable |
type statique |
type dynamique |
---|---|---|
a |
int |
int |
c |
char |
char |
s |
std::string |
std::string |
an_object |
a_class |
a_class |
e |
E_enumeration |
E_enumeration |
Encore une fois, l'héritage introduit une différence entre un type dynamique et un type statique. Cette différence apparaît avec les pointeurs et les références quand le type déclaré du pointeur ou de la référence n'est pas le type de l'objet effectivement pointé (resp. référencé) à l'exécution. Le type statique est celui défini dans le code, le type dynamique d'une référence ou d'un pointeur est celui de l'objet référencé (resp. pointé) :
struct
base {}
;
struct
derived : public
base {}
;
int
main()
{
base b;
derived d;
base &
rb =
b;
base &
rd =
d;
base *
pb =
&
b;
base *
pd =
&
d;
return
0
;
}
Avec l'exemple, ci-dessus, les types des variables (5) sont :
variable |
type statique |
type dynamique |
---|---|---|
base b |
base |
base |
derived d |
derived |
derived |
base &rb = b |
base |
base |
base &rd = d |
base |
derived |
base *pb = &b |
base |
base |
base *pd = &d |
base |
derived |
Seul les pointeurs et les références vers des instances de classe ou de structure ont des types dynamiques et des types statiques pouvant diverger.
Le type statique d'un objet dans une fonction membre est celui où se déroule la fonction. Le type dynamique, this étant un pointeur, dépend de l'objet effectif sur lequel s'applique la fonction. Type statique et type dynamique peuvent alors être différents :
class
base
{
public
:
void
function()
{
// Quel est le type static de this ?
// Quel est le type dynamique de this ?
}
}
;
class
derived : public
base
{
public
:
void
function_2()
{
// Quel est le type static de this ?
// Quel est le type dynamique de this ?
}
}
;
int
main()
{
base b;
b.function();
derived d;
d.function();
d.function_2();
return
0
;
}
fonction |
type statique de this |
type dynamique de this |
---|---|---|
Pour l'appel b.fonction, dans la fonction base::fonction |
base |
base |
Pour l'appel de d.fonction, dans la fonction base::fonction |
base |
derived |
Pour l'appel de d.fonction_2 dans la fonction derived::fonction_2 |
derived |
derived |
Le type statique d'une variable est soit le type dynamique de cette variable soit une classe de base directe ou indirecte du type dynamique.
Toute autre combinaison est une erreur pouvant aboutir à un plantage ou un comportement indéterminé.
Attention, si nous avons dit que le pointeur a un type statique différent de son type dynamique, cela s'applique aussi bien au pointeur non déréférencé qu'au pointeur déréférencé :
struct
base {}
;
struct
derived : public
base {}
;
int
main()
{
base b;
derived d;
base *
pd =
&
d;
return
0
;
}
variable |
type statique |
type dynamique |
---|---|---|
pd |
base* |
derived* |
*pd |
base |
derived |
III. Types et appel de fonction▲
Lorsqu'une expression contient un appel d'une fonction sur un objet donné, la fonction effectivement appelée dépend de plusieurs paramètres :
- la fonction est-elle virtuelle ou non ?
- la fonction est-elle appelée sur un objet par valeur ou sur une référence/pointeur ?
Selon la réponse, la résolution de l'appel est faite à la compilation ou à l'exécution :
fonction non virtuelle |
fonction virtuelle |
|
---|---|---|
appel sur un objet |
COMPILATION |
COMPILATION |
appel sur une référence ou un pointeur |
COMPILATION |
EXÉCUTION |
La résolution à la compilation utilise le type statique, car c'est le seul connu à ce moment.
La résolution à l'exécution se base sur le type dynamique.
Ce qui donne :
fonction non virtuelle |
fonction virtuelle |
|
---|---|---|
appel sur un objet |
type statique |
type statique |
appel sur une référence ou un pointeur |
type statique |
type dynamique |
Un peu de code pour illustrer tout cela :
#include
<iostream>
struct
base
{
void
function_1()
{
std::
cout<<
"base::function_1
\n
"
;
}
virtual
void
function_2()
{
std::
cout<<
"base::function_2
\n
"
;
}
}
;
struct
derived :public
base
{
void
function_1()
{
std::
cout<<
"derived::function_1
\n
"
;
}
virtual
void
function_2()
{
std::
cout<<
"derived::function_2
\n
"
;
}
}
;
int
main()
{
base b;
std::
cout<<
"
\n
appels pour base b; :
\n
"
;
b.function_1();
b.function_2();
derived d;
std::
cout<<
"
\n
appels pour derived d; :
\n
"
;
d.function_1();
d.function_2();
base &
rb =
b;
std::
cout<<
"
\n
appels pour base &rb = b; :
\n
"
;
rb.function_1();
rb.function_2();
base &
rd =
d;
std::
cout<<
"
\n
appels pour base &rd = d; :
\n
"
;
rd.function_1();
rd.function_2();
return
0
;
}
Ce code produit comme sortie :
appels pour base b; :
base::function_1
base::function_2
appels pour derived d; :
derived::function_1
derived::function_2
appels pour base &rb = b; :
base::function_1
base::function_2
appels pour base &rd = d; :
base::function_1
derived::function_2
Les types statiques et dynamiques sont :
variable |
type statique |
type dynamique |
---|---|---|
base b |
base |
base |
derived d |
derived |
derived |
base &rb = b |
base |
base |
base &rd = d |
base |
derived |
fonction_1 est une fonction non virtuelle et fonction_2 est une fonction virtuelle. Soit en reprenant le tableau précédent précisant la fonction appelée :
Appels sur b |
fonction non virtuelle ( fonction_1 ) |
fonction virtuelle ( fonction_2 ) |
---|---|---|
appel sur un objet |
type statique : base::fonction_1 |
type statique : base::fonction_2 |
Appels sur d |
||
appel sur un objet |
type statique : derived::fonction_1 |
type statique : derived::fonction_2 |
Appels sur rb |
||
appel sur une référence |
type statique : base::fonction_1 |
type dynamique : base::fonction_2 |
Appels sur rd |
||
appel sur une référence |
type statique : base::fonction_1 |
type dynamique : derived::fonction_2 |
Cette résolution dynamique présentée avec des références fonctionne de la même façon avec un pointeur qu'il soit utilisé en tant que tel ou déréférencé :
#include
<iostream>
struct
base
{
virtual
void
function()
{
std::
cout<<
"base::function
\n
"
;
}
}
;
struct
derived :public
base
{
virtual
void
function()
{
std::
cout<<
"derived::function
\n
"
;
}
}
;
int
main()
{
derived d;
base &
rd =
d;
base *
pd =
&
d;
// 3 appels équivalents :
pd->
function();
(*
pd).function();
rd.function();
return
0
;
}
Le comportement est identique si l'expression utilise un pointeur de fonction :
#include
<iostream>
struct
base
{
virtual
~
base(){}
virtual
void
function_1()
{
std::
cout<<
"base::function_1
\n
"
;
}
void
function_2()
{
std::
cout<<
"base::function_2
\n
"
;
}
}
;
struct
derived : public
base
{
virtual
void
function_1()
{
std::
cout<<
"derived::function_1
\n
"
;
}
void
function_2()
{
std::
cout<<
"base::function_2
\n
"
;
}
}
;
void
call_a_function(void
(base::
*
pf_)())
{
base b;
(b.*
pf_)();
derived d;
(d.*
pf_)();
base &
rd =
d;
(rd.*
pf_)();
}
int
main()
{
std::
cout<<
"function_1 :
\n
"
;
call_a_function(&
base::
function_1);
std::
cout<<
"function_2 :
\n
"
;
call_a_function(&
base::
function_2);
return
0
;
}
La sortie produite est bien :
function_1 :
base::function_1
derived::function_1
derived::function_1
function_2 :
base::function_2
base::function_2
base::function_2
La liaison tardive prenant appui sur le type dynamique telle que nous la décrivons ici est valable presque tout le temps. Comme nous allons le voir par la suite, ce mécanisme présente quelque subtilité en particulier lors des phases sensibles que sont la construction ou la destruction d'un objet.
IV. À quoi servent les fonctions virtuelles ?▲
L'utilisation du type dynamique pour résoudre l'appel d'une fonction virtuelle est une des grandes forces de la programmation orientée objet (POO). Elle permet d'adapter et de faire évoluer un comportement défini dans une classe de base en spécialisant les fonctions virtuelles dans les classes dérivées. La substitution d'un objet de type dynamique dérivant du type statique présent dans l'expression contenant l'appel vers une fonction virtuelle s'inscrit dans le cadre du polymorphisme d'inclusionQu'est-ce que le polymorphisme d'inclusion ?.
Ce polymorphisme d'inclusion permet l'abstraction dans un logiciel orienté objet. Ainsi, un objet peut être manipulé à partir d'un pointeur ou d'une référence vers la classe de base. Les membres publics de la classe de base déterminent les services proposés et la mise en œuvre est déléguée aux classes dérivées apportant des points de variations ou spécialisant des comportements dans les fonctions virtuelles :
#include
<iostream>
struct
shape
{
virtual
void
draw() const
{
std::
cout<<
"une forme amorphe
\n
"
;
}
}
;
void
draw_a_shape(shape const
&
rs)
{
// ne connaît que shape. Pas ses classes dérivées
rs.draw();
}
struct
square : public
shape
{
virtual
void
draw() const
{
std::
cout<<
"un carre
\n
"
;
}
}
;
struct
circle : public
shape
{
virtual
void
draw() const
{
std::
cout<<
"un cercle
\n
"
;
}
}
;
int
main()
{
shape sh;
draw_a_shape(sh);
square sq;
draw_a_shape(sq);
circle c;
draw_a_shape(c);
return
0
;
}
L'abstraction permet de mieux isoler les différents composants les uns des autres en ne les rendant dépendants que des services dont ils ont vraiment besoin. Elle favorise la modularité et l'évolutivité des architectures logicielles. Notre fonction draw_a_shape peut dessiner tout type d'objet non prévu lors de son écriture du moment que ces objets sont d'un type dynamique héritant de shape et spécialisant ses fonctions virtuelles. Il devient possible de rajouter de nouveaux types et de pouvoir les dessiner sans avoir à réécrire la fonction draw_a_shape.
Les fonctions virtuelles réduisent le couplage entre une classe ou une fonction cliente et une classe fournisseur en déléguant aux classes dérivées la réalisation d'une partie des services proposés par la classe de base.
Les fonctions virtuelles sont un mécanisme pour la mise en œuvre du principe ouvert/fermé (6) en permettant de faire évoluer une application par l'ajout de nouvelle classe dérivée (ouvert) sans avoir à toucher le code existant utilisant l'interface de la classe de base (fermé).
Les fonctions virtuelles favorisent la réutilisation. Toutes les fonctions ou les classes s'appuyant sur des références ou des pointeurs de la classe de base peuvent être directement utilisées avec des objets d'un nouveau type dérivé.
Si le terme complet est polymorphisme d'inclusion, beaucoup de documents en C++ (articles - et celui-ci n'échappe pas à la règle - , cours, livres, etc.) omettent de préciser d'inclusion. Polymorphe, polymorphique, polymorphisme, polymorphiquement s'emploient souvent seuls dès qu'il s'agit de parler d'héritage et donc de la manipulation d'un objet d'une classe dérivée à partir d'une référence ou d'un pointeur d'une de ses classes de base avec en arrière plan le mécanisme des fonctions virtuelles. C'est un raccourci qui peut faire oublier les autres formes de polymorphisme qui ne s'appuient pas sur les fonctions virtuelles et le mécanisme d'héritage. Il faut juste se souvenir que le polymorphisme ne se réduit pas au polymorphisme d'inclusion.