• Aucun résultat trouvé

En reprenant les classes définies ci-dessus, que se passe-t-il lorsqu'on écrit:

CafetiereSoluble ma_cafetiere;

Il se passe les choses suivantes: 1. Allocation de mémoire.

Autant d'octets que nécessaire compte-tenu des champs de CafetiereSoluble et de a (ou ses) classes de base.

2. Appel du constructeur par défaut de Cafetiere

3. Appel du constructeur par défaut de CafetiereSoluble

Autrement dit, tant qu'on travaille avec les constructeurs par défaut, tout se passe bien: le système se charge d'appeler les constructeur par défaut des classes de base, dans le bon ordre.

Constructeur de copie:

Le constructeur de copie doit être écrit de la manière suivante:

class CafetiereSoluble: public Cafetiere { public:

CafetiereSoluble(const CafetiereSoluble& m) : Cafetiere(m),cafe(m.cafe) {...}; }

en effet, il faut dire explicitement au compilateur quel constructeur il dit appeler pour la classe de base. Cela peut se faire dans la liste d'initialisation de CafetiereSoluble:

L'expression Cafetiere(m) est correcte puisque m étant une CafetiereSoluble, m est aussi une Cafetiere. Si on utilise les contructeurs de copie par défaut (c'est le cas, sauf lorsque l'on ne peut faire autrement), tout se passe correctement.

En fait, cela fonctionne parce que le paramètre m est passé par référence au constructeur de copie .

Autres constructeurs

rien n'empêche de définir sur Cafetiere un constructeur à qui on passe des paramètres, par exemple les quantités initiales d'eau, de sucre, ainsi que le nombre de gobelets et de cuillers:

class Cafetiere { public:

Cafetiere(float e, int g, int c,float s) : eau(e), gobelets(g), cuillers(c), sucre(s) {};

... }

De même, rien n'empêche de définir un constructeur pour CafetiereSoluble, à qui on donnera en plus la quantité de café soluble à incorporer dans le réservoir adhoc. Là encore, le système ne peut pas savoir quel constructeur de la classe de base doit être appelé: c'est donc de la responsabilité du constructeur de la classe dérivée d'appeler le

constructeur de sa classe de base, en utilisant la liste d'initialisation:

class CafetiereSoluble: public Cafetiere { public:

CafetiereSoluble(float e, int g, int c,float s, float f) : Cafetiere(e,g,c,s), cafe(f) {};

... }

class CafetierePoudre: public Cafetiere { public:

CafetierePoudre(float e, int g, int c,float s, float f) : Cafetiere(e,g,c,s), cafe(f) {};

... }

CafetierePoudre(float e, int g, int c, float s, float f) : eau(e),gobelets(g),cuillers(c),sucre(s),

cafe(f){};

ne compilera pas, car eau, gobelets, cuillers, sucre sont des membres de la classe Cafetiere, pas de CafetierePoudre. Il faut obligatoirement passer par un constructeur de Cafetiere.

.

...et destructeurs

Lorsqu'un objet de type CafetiereSoluble est détruit, il se passe la séquence suivante: 1. Le destructeur de CafetiereSoluble est appelé

2. Le destructeur de Cafetiere est appelé 3. La mémoire est rendue au système

Si on utilise avec des pointeurs ou des références, on peut avoir quelques surprises, qui nécessitent l'utilisation de destructeurs virtuels.

Le polymorphisme

Observons le code ci-dessous, dans lequel on gère trois machines à café, de deux types différents, par l'intermédiaire d'un tableau de pointeurs:

Cafetiere* machines[3];

machines[0] = new CafetiereSoluble(1.5,500,500,2,0.5); machines[1] = new CafetiereSoluble(1.5,500,500,2,0.5); machines[2] = new CafetierePoudre(1.5,500,500,2,0.9); for (int i=0; i<3; ++i) {

if (machines[i]->lire_etat() == 1) { machines[i]->faire_le_cafe(); machines[i]->encaisser_monnaie(); };

};

Le code ci-dessus déclare un tableau de trois objets de type pointeur sur Cafetiere, puis remplit le tableau avec deux machines à café soluble et une en poudre. Souvenez-vous que les fonctions lire_etat et faire_le_cafe ont été délarées avec le mot-clé virtual: ce mot signifie que le compilateur ne cherche pas à savoir exactement quelle fonction lire_etat sera appelée, ni quelle fonction faire_le_cafe() sera appelée, puisque l'allocation mémoire se fait de manière dynamique: il retient donc simplement qu'il devra appeler la version de la fonction

faire_le_cafe qui va bien, en fonction du type d'objet qui sera appelé à l'exécution du programme. Le mot

polymorphisme décrit la propriété de ces deux fonctions d'adopter "plusieurs formes", suivant le contexte du

programme. On parle aussi d'édition de liens dynamique. Remarquons qu'une fois de plus, on retrouve une manière de penser parfaitement naturelle: si je vous passe une casserole (classe de base) en vous demandant de la laver (fonction qui opère sur le type générique casserole), je vous passe en réalité une casserole bien particulière, et pas toujours la même (hier c'était le vieux chaudron à confiture qui me vient de ma grand-mère, aujourd'hui c'est une casserole en aluminium, dans les deux cas il s'agit d'un type de casserole particulier). Dans les deux cas vous allez la laver... mais suivant le type de casserole, vous vous y prendrez différemment. Par contre, la fonction encaisser_monnaie fait l'objet d'une édition de liens statiques (il n'y a pas le mot virtual devant, donc il n'y a pas ici de polymorphisme).

Lorsque vous redéfinissez une fonction virtuelle, la nouvelle définition doit avoir soit le même type de retour que la fonction originale, soit un type dérivé (on parle dans ce cas de "types de retour covariants"). Sinon, le compilateur refusera votre code. Bien entendu, elles devront avoir également les mêmes signatures, sinon il s'agit de deux fonctions différentes, et le mécanisme d'édition de liens dynamiques ne s'applique pas.

Pourquoi les fonctions ne sont-elles pas automatiquement virtuelles ? C'est le cas dans d'autres langages orientés objets, java par exemple. Le problème avec les fonctions virtuelles, c'est qu'elles sont moins performantes que les fonctions classiques. C'est normal: le mécanisme d'édition de liens dynamiques est très puissant, mais il a un coût. Donc, il est recommandé de ne les utiliser que lorsque c'est nécessaire, et pas lorsque la performance est rédhibitoire.

Si vous surchargez une fonction virtuelle, la fonction surchargée ne sera pas virtuelle automatiquement: il vous faut le spécifier explicitement.

Documents relatifs