Nous considérons une classe Color
qui fournit deux méthodes, une méthode name()
qui retourne le nom de la couleur et une méthode rgbValue()
retourne les composants RGB de la couleur (Rouge, Vert & Bleue) au format 24 bits.
class Color { public: std::string name() const { return ""; } unsigned rgbValue24() const { return 0; } };
Nous souhaitons hériter que les objets Blue
, Red
et Green
qui héritent de cette classe redéfinisse les fonctions name
et rgbValue24
. L'objectif et que ces fonctions fournissent la bonne couleur.
class Red: public Color { public: std::string name() const { return "red"; } unsigned rgbValue24() const { return 0xFF0000; } }; class Green: public Color { public: std::string name() const { return "green"; } unsigned rgbValue24() const { return 0x00FF00; } }; class Blue: public Color { public: std::string name() const { return "blue"; } unsigned rgbValue24() const { return 0x0000FF; } };
Nous souhaitons désormais pouvoir utiliser ces couleurs pour remplir une surface:
void fillrect(const Color& theColor, int x, int y, int width, int height) { std::cout << "Fill the area [" << x << ", " << y << ", " << x + width << ", " << y + height << "] with color " << theColor.name() << "." << std::endl; }
Cependant, si nous exécutons le code suivant :
fillreact(Red(), 0, 0, 10, 10);
La sortie sera:
Fill the area [0, 0, 10, 10] with color .
Ce qui est parfaitement normal puisque lors de l'appel de la fonction name()
:
std::cout << "Fill the area [" << x << ", " << y << ", " << x + width << ", " << y + height << "] with color " << theColor.name() << "." << std::endl;
le type de theColor
est Color
. Donc le compilateur va appelé la méthode Color::name()
et non pas la méthode Red::name()
bien que l'objet a pour type effectif Red
.
Nous voyons une différence entre deux notions de type:
Red
dans le cas précédent;Color
dans le cas précédent.
Par défaut, la méthode appellée est celle qui est définie dans le type de manipulation et non pas le type de l'objet lors de la création. Cependant, cela n'est pas satisfaisant. Nous aimerions pouvoir remplacer dans le type Color
la méthode name()
par la méthode name()
définie dans le type Red
.
En reprenant l'exemple précédent, nous pouvons introduire le mot clé virtual
devant chacune des deux méthodes qui ont été définies :
class Color { public: virtual std::string name() const { return ""; } virtual unsigned rgbValue24() const { return 0; } };
Le fait de mettre le mot-clé virtual
devant chacune des méthodes indique que ces méthodes sont substituables. Ceci signifie que désormais l'exécution de :
fillreact(Red(), 0, 0, 10, 10);
produira la sortie:
Fill the area [0, 0, 10, 10] with color Red.
Ajouter la mot clé virtual
indique au compilateur que la méthode qui doit être appelée est la méthode qui est définie par le type effectif (celui lors de la création) de l'objet.
Ainsi dans la fonction, nous avons l'appel theColor.name()
:
void fillrect(const Color& theColor, int x, int y, int width, int height) { std::cout << "Fill the area [" << x << ", " << y << ", " << x + width << ", " << y + height << "] with color " << theColor.name() << "." << std::endl; }
Normalement, sans le mot clé virtual
, le compilateur aurait fait les opérations suivantes :
theColor
. C'est Color
.Color::name()
.
Avec le mot clé virtual
, la compilateur fait les opérations suivantes :
theColor
au moment de sa création. C'est Red
.Red::name()
.Quand on ajouter une méthode virtuelle à une classe, on ajoute le stockage d'informations complémentaires. Ces informations complémentaires sont :
name()
, on appelle la méthode Red::name()
et non pas la méthode Color::name()
.
Comment s'effectue donc l'appel d'une méthode method()
en C++ :
object
de type Object
ne contient aucune méthode virtuelle. Alors on appelle la méthode Object::method()
. object
de type VirtualObject
contient une ou plusieurs méthodes virtuelles. On regarde si la méthode est une méthode virtuelle.VirtualObject::method()
. DerivedObject
, dans ce cas, le compilateur recherche dans la table des méthodes virtuelles la méthode qui s'appelle method()
à appeller. Supposons que la méthode method()
a été redéfinie dans la classe DerivedObject
, alors le compilateur va trouver dans la table des méthodes virtuelles la méthode pour l'entrée method()
, la méthode DerivedObject::method()
. Si la méthode n'a pas été redéfinie, alors le compilateur va trouver dans la table des méthodes virtuelles pour l'entrée method()
, la méthode Object::method()
.
Regardons les méthodes de la classe Color
:
class Color { public: std::string name() const { return ""; } unsigned rgbValue24() const { return 0; } };
Les méthodes name()
et rgbValue24()
retourne des valeurs par défaut. Ce qui n'est d'ailleurs pas souhaitable, puisque quelque part, ces valeurs n'ont pas de sens. Il est possible de définir une classe squelette, appellée aussi classe asbtraite dans laquelle les méthodes sont déclarées mais ne sont pas implantées. Pour la classe Color
, cela revient à l'écriture suivante :
class Color { public: std::string name() const = 0; unsigned rgbValue24() const = 0; };
La classe Color
expose deux méthodes name()
et rgbValue()
qui sont deux méthodes virtuelles mais n'ayant aucun code. Cela impose aux classes qui héritent de la classe Color
de devoir implanter les méthodes virtuelles.
De plus, une classe qui possède des méthodes virtuelles non définies (purement abstraite) ne peut pas être crée. En conséquence :
Color basicColor;
génère une erreur à la compilation. En effet, vous essayez de créer une classe dont tous les éléments (ici en l'espèce ce sont les méthodes) ne sont pas définis. Ce qui est impossible. Le compilateur vous protège contre ce type d'erreur.