Une fonction virtuelle permet aux classes dérivées de remplacer l'implémentation fournie par la classe de base. Le compilateur s'assure que la méthode remplacée est bien appelée quand l'objet en question est bien du type de la classe dérivée, même si l'objet est accédé par un pointeur de base plutôt qu'un pointeur dérivé. Cela permet aux algorithmes de la classe de base d'être remplacé dans la classe dérivée, meme si les utilisateurs ne connaissent pas la classe dérivée.
La classe dérivée peut complétement remplacer ("override") les fonctions membres de la classe de base, ou la classe derivée peut partiellement remplacer ("augment") les fonctions membres de la classe de base. Cette dernière action, si elle est désirée, est obtenue lorsque les fonctions membres de la classe dérivée appellent les fonctions membres de la classe de base.
[ Haut | Bas | Rechercher ]
Lorsque vous avez un pointeur sur un objet, l'objet peut trés bien être une classe qui est dérivée de la classe du pointeur (e.g. un Vehicle* qui pointe sur un objet Car; cela est appelé le "polymorphisme"). Il y a donc deux types: le type (statique) du pointeur (ici Vehicle), et le type (dynamique) de l'objet qui est pointé (ici Car).
Le typage statique (static typing) signifie que la légalité de l'appel d'une fonction membre est vérifiée le plus tôt possible: lors de la compilation. Le compilateur utilise le type statique du pointeur pour déterminer si l'appel de la fonction membre est légal ou non. Si le type du pointeur permet de peut gérer la fonction membre, alors l'objet pointé peut certainement le gérer également. E.g. si Vehicle a un certain nombre de fonctions membres, alors Car a également ces fonctionnalités car c'est une sorte-de Vehicle.
le lien dynamique (dynamic binding) signifie que l'adresse du code lors de l'appel d'une fonction membre est déterminé au dernier moment possible: basé sur le type dynamic de l'objet lors de l'exécution. On appelle cela le lien dynamique (dynamic binding) car le lien au code qui est appelé est accompli dynamiquement (lors de l'exécution). Le lien dynamique est un résultat des fonctions virtuelles.
[ Haut | Bas | Rechercher ]
Par contraste, les fonctions membres virtual sont résolues dynamiquement (durant l'exécution). C'est à dire que la fonction membre est sélectionnée dynamiquement (durant l'exécution) suivant le type de l'objet et non suivant le type du pointeur ou de la référence sur cet objet. On appelle cela le "dynamic binding". La plupart des compilateurs utilise une variante de la technique suivante: si l'objet a une ou plusieurs fonctions virtual, le compilateur rajoute un pointeur caché dans l'objet appelé "virtual pointer" ou "v-pointer". Ce "v-pointer" pointe sur un table globale appelée "table virtuelle" (virtual table) ou "v-table".
Le compilateur crée une v-table pour chaque classe ayant au moins une fonction virtual. Par exemple si la classe Circle a des fonctions virtual pour draw(), move() et resize()il y aura exactement une v-table associée à la classe Circle, cela quelque soit le nombre d'objets Circle, et le v-pointer de ces objets Circle pointera sur la v-table de Circle. La v-table elle-même à des pointeurs sur chaque fonction virtuelles de la classe. Par exemple, la v-table de Circle aura trois pointeurs: un pointeur sur Circle::draw(), un pointeur sur Circle::move(), et un pointeur sur Circle::resize().
Pendant le dispatch d'une fonction virtuelle, le système récupère la v-table de la classe pointée par le v-pointer de l'objet et récupère ensuite l'emplacement de la méthode donnée par la v-table.
L'overhead taille de la technique ci dessus est nominal: un pointeur additionnel par objet (mais seulement pour les objets qui ont besoin du dynamic binding), ainsi qu'un pointeur par méthode virtuelle. L'overhead performance est également nominal comparé à l'appel d'un fonction normale, une fonction virtual nécessite deux appels supplémentaires (un pour obtenir la valeur du v-pointer, un deuxième pour obtenir l'adresse de la méthode). Aucun de ces overheads n'existe pour les fonctions non-virtual car le compilateur fait la résolution des fonctions non-virtual à la compilation basé sur le type du pointeur.
Note: La discussion ci-dessus est considérablement simplifiée, car il n'est pas tenu compte de la structure obtenue par l'héritage multiple, l'héritage virtuel, le RTTI, etc., ni ne tient compte des pages faults, des appels de fonctions par un pointeur sur fonction, etc. Si vous souhaitez davantage d'information envoyez un message sur comp.lang.c++; S'IL VOUS PLAIT NE M'ENVOYEZ PAS DE MAILS !
[ Haut | Bas | Rechercher ]
It's surprisingly easy.
Suppose there is a base class Vehicle with derived classes Car and "Truck". The code traverses a list of Vehicle objects and does different things depending on the type of Vehicle. For example it might weigh the "Truck" objects (to make sure they're not carrying too heavy of a load) but it might do something different with a Car object check the registration, for example.
The initial solution for this, at least with most people, is to use an if statement. E.g., "if the object is a "Truck", do this, else if it is a Car, do that, else do a third thing":
typedef std::vector<Vehicle*> VehicleList; void myCode(VehicleList& v) { for (VehicleList::iterator p = v.begin(); p != v.end(); ++p) { Vehicle& v = **p; // just for shorthand // generic code that works for any vehicle... ... // perform the "foo-bar" operation. // note: the details of the "foo-bar" operation depend // on whether we're working with a car or a truck. if (v is a Car) { // car-specific code that does "foo-bar" on car v ... } else if (v is a Truck) { // truck-specific code that does "foo-bar" on truck v ... } else { // semi-generic code that does "foo-bar" on something else ... } // generic code that works for any vehicle... ... } }
The problem with this is what I call "else-if-heimer's disease" (say it fast
and you'll understand). The above code gives you else-if-heimer's disease
because eventually you'll forget to add an
The solution is to use dynamic binding rather than dynamic typing. Instead of having (what I call) the live-code dead-data metaphor (where the code is alive and the car/truck objects are relatively dead), we move the code into the data. This is a slight variation of Bertrand Meyer's Inversion Principle.
The idea is simple: use the description of the code within the
class Vehicle { public: // performs the "foo-bar" operation virtual void fooBar() = 0; };Then you remove the whole if...else if... block and replace it with a simple call to this virtual function:
typedef std::vector<Vehicle*> VehicleList; void myCode(VehicleList& v) { for (VehicleList::iterator p = v.begin(); p != v.end(); ++p) { Vehicle& v = **p; // just for shorthand // generic code that works for any vehicle... ... // perform the "foo-bar" operation. v.fooBar(); // generic code that works for any vehicle... ... } }Finally you move the code that used to be in the {...} block of each if into the fooBar() member function of the appropriate derived class:
class Car : public Vehicle { public: virtual void fooBar(); }; void Car::fooBar() { // car-specific code that does "foo-bar" on 'this' ... this is the code that was in {...} of if (v is a Car) } class Truck : public Vehicle { public: virtual void fooBar(); }; void Truck::fooBar() { // truck-specific code that does "foo-bar" on 'this' ... this is the code that was in {...} of if (v is a Truck) }If you actually have an else block in the original
class Vehicle { public: // performs the "foo-bar" operation virtual void fooBar(); }; void Vehicle::fooBar() { // semi-generic code that does "foo-bar" on something else ... this is the code that was in {...} of the else // you can think of this as "default" code... }
That's it!
The point, of course, is that we try to avoid decision logic with decisions
based on the kind-of derived class you're dealing with. In other words,
you're trying to avoid
[ Haut | Bas | Rechercher ]
Les fonctions virtual sont liées au code associé à la classe de l'objet, plutôt que à la classe du pointeur ou de la référence.Lorsque vous écrivez delete basePtr, et que la classe de base à un destructeur virtual, le destructeur appelée est celui associé à au type de l'objet *basePtr, plutôt que celui associé au type du pointeur. Il s'agit généralement d'une bonne chose.
ATTENTION; POUR PERSONNES AVERTIES.
Techniquement parlant, vous avez besoin de déclarer
le destructeur de la classe de base virtuel si et seulement si vous pensez
permettez à quelqu'un d'appeler le destructeur de l'objet par un
pointeur de la classe de base (cela est normalement réalisé
implicitement par un delete), et si l'objet à détruire et
d'une classe dérivée qui a un destructeur non trivial. Une
classe a un destructeur non trivial si elle a ou un destructeur explicitement
défini, ou si elle a un obket member ou une classe de base qui
a un destructeur non trivial (notez bien qu'il s'agit d'une définition
récursive (e.g. une classe a un destructeur non trivial si elle
a un objet membre(qui a une classe de base (qui a un objet membre(qui
a une classe de base (qui a un destructeur explicitement défini)))))
FIN ATTENTION; POUR PERSONNES AVERTIES
Si vous avez du mal a comprendre la règle précédente, essayez celle ci (ultra)simplifiée en longueur. Une classe doit avoir un destructeur virtuel a moins que cette classe n'ait pas de fonctions virtuelles. Donc: si vous avez des fonctions virtuelles, vous allez certainement effectuer des opérations sur les objets dérivés via le pointeur de base, et certaines de ces opérations peuvent appeler le destructeur (normalement appelé implicitement par delete). De plus, une fois que vous avez mis votre première fonction virtuelle au sein de la classe, vous avez déjà réservé l'espace de stockage supplémentaire par objet (un pointeur par objet, notez que cela est théoriquement spécifique au compilteur; en pratique, tout le monde fait plus ou moins la même chose), donc mettre le destructeur virtuel ne vous coûtera généralement rien de plus.
[ Haut | Bas | Rechercher ]
Vous pouvez obtenir l'effet d'un constructeur virtuel par l'utilisation d'une fonction membre virtual clone() (construction par copie), ou une fonction membre virtual create() (pour le constructeur par défaut).
class Shape {
public:
virtual ~Shape() { }
// Un destructeur virtual
virtual void draw()
= 0;
// Une fonction virtuelle
pure
virtual
void move() = 0;
// ...
virtual
Shape* clone() const = 0; // Utilises la construction
par copie
virtual
Shape* create() const = 0; // Utilises le constructeur par défaut
};
class Circle
: public Shape {
public:
Circle* clone()
const { return new Circle(*this); }
Circle* create() const { return new Circle();
}
// ...
};
Dans la fonction membre clone(), le code new Circle(*this) appelle le constructeur de copie de Circle pour copier l'état de this dans le nouvel objet Circle. (C'est un clone). Dans la fonction membre create(), le code new Circle() appelle le constructeur par défaut de Circle..
Les clients les utilisent comme s'il s'agissait de "constructeurs virtual":
void userCode(Shape&
s)
{
Shape* s2 = s.clone();
Shape* s3 = s.create();
// ...
delete s2; //
Vous avez probablement besoin d'un destructeur
virtuel ici.
delete s3;
}
Cette méthode fonctionnera correctement que Shape soit un Circle, Square, ou une autre généralisation (kind-of) de Shape qui n'existe pas encore. Il permet donc la création d'un objet du même type que celui que vous traitez sans que vous en ayez connaissance puisque vous travaillez avec le type de base.
Note: Le type de retour de la fonction membre clone() de Circle est intentionnellement différente du type de retour de la fonction membre clone()de Shape. Cela s'appelle un type de retour covariant (Covariant Return Types), une fonctionnalité qui n'était originellement pas partie intégrante du langage. Si votre compilateur se plaint de la déclaration Circle* clone() const dans la classe Circle (e.g., avec un message du genre "The return type is different" ou "The member function's type differs from the base class virtual function by return type alone"), vous avez un vieux compilateur et vous avez besoin de changer le type de retour en Shape*.
[ Haut | Bas | Rechercher ]
Ecrire à l'auteur,
au traducteur,
ou en savoir plus sur la traduction.
C++ FAQ Lite fr |
Table des matières |
Index |
A propos de l'auteur |
© |
Téléchargez votre propre copie ]
Dernière révision Sun Apr 13 23:54:40 PDT 2003