• <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    8.9. Pointeur this

    Nous allons à présent voir comment les fonctions membres, qui appartiennent à la classe, peuvent accéder aux données d'un objet, qui est une instance de cette classe. Cela est indispensable pour bien comprendre les paragraphes suivants.

    À chaque appel d'une fonction membre, le compilateur passe implicitement un pointeur sur les données de l'objet en paramètre. Ce paramètre est le premier paramètre de la fonction. Ce mécanisme est complètement invisible au programmeur, et nous ne nous attarderons pas dessus.

    En revanche, il faut savoir que le pointeur sur l'objet est accessible à l'intérieur de la fonction membre. Il porte le nom « this ». Par conséquent, *this représente l'objet lui-même. Nous verrons une utilisation de this dans le paragraphe suivant  (surcharge des operateurs).

    this est un pointeur constant, c'est-à-dire qu'on ne peut pas le modifier (il est donc impossible de faire des opérations arithmétiques dessus). Cela est tout à fait normal, puisque le faire reviendrait à sortir de l'objet en cours (celui pour lequel la méthode en cours d'exécution travaille).

    Il est possible de transformer ce pointeur constant en un pointeur constant sur des données constantes pour chaque fonction membre. Le pointeur ne peut toujours pas être modifié, et les données de l'objet ne peuvent pas être modifiées non plus. L'objet est donc considéré par la fonction membre concernée comme un objet constant. Cela revient à dire que la fonction membre s'interdit la modification des données de l'objet. On parvient à ce résultat en ajoutant le mot clé const à la suite de l'en-tête de la fonction membre. Par exemple :

    class Entier
    {
        int i;
    public:
        int lit(void) const;
    };
    
    int Entier::lit(void) const
    {
        return i;
    }

    Dans la fonction membre lit, il est impossible de modifier l'objet. On ne peut donc accéder qu'en lecture seule à i. Nous verrons une application de cette possibilité dans la section 8.15.

    Il est à noter qu'une méthode qui n'est pas déclarée comme étant const modifie a priori les données de l'objet sur lequel elle travaille. Donc, si elle est appelée sur un objet déclaré const, une erreur de compilation se produit. Ce comportement est normal. On devra donc toujours déclarer const une méthode qui ne modifie pas réellement l'objet, afin de laisser à l'utilisateur le choix de déclarer const ou non les objets de sa classe.

    Note : Le mot clé const n'intervient pas dans la signature des fonctions en général lorsqu'il s'applique aux paramètres (tout paramètre déclaré const perd sa qualification dans la signature). En revanche, il intervient dans la signature d'une fonction membre quand il s'applique à cette fonction (ou, plus précisément, à l'objet pointé par this). Il est donc possible de déclarer deux fonctions membres acceptant les mêmes paramètres, dont une seule est const. Lors de l'appel, la détermination de la fonction à utiliser dépendra de la nature de l'objet sur lequel elle doit s'appliquer. Si l'objet est const, la méthode appelée sera celle qui est const.


    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    8.8. Constructeurs et destructeurs

    Le constructeur et le destructeur sont deux méthodes particulières qui sont appelées respectivement à la création et à la destruction d'un objet. Toute classe a un constructeur et un destructeur par défaut, fournis par le compilateur. Ces constructeurs et destructeurs appellent les constructeurs par défaut et les destructeurs des classes de base et des données membres de la classe, mais en dehors de cela, ils ne font absolument rien. Il est donc souvent nécessaire de les redéfinir afin de gérer certaines actions qui doivent avoir lieu lors de la création d'un objet et de leur destruction. Par exemple, si l'objet doit contenir des variables allouées dynamiquement, il faut leur réserver de la mémoire à la création de l'objet ou au moins mettre les pointeurs correspondants à NULL. À la destruction de l'objet, il convient de restituer la mémoire allouée, s'il en a été alloué. On peut trouver bien d'autres situations où une phase d'initialisation et une phase de terminaison sont nécessaires.

    Dès qu'un constructeur ou un destructeur a été défini par l'utilisateur, le compilateur ne définit plus automatiquement le constructeur ou le destructeur par défaut correspondant. En particulier, si l'utilisateur définit un constructeur prenant des paramètres, il ne sera plus possible de construire un objet simplement, sans fournir les paramètres à ce constructeur, à moins bien entendu de définir également un constructeur qui ne prenne pas de paramètres.

    8.8.1. Définition des constructeurs et des destructeurs

    Le constructeur se définit comme une méthode normale. Cependant, pour que le compilateur puisse la reconnaître en tant que constructeur, les deux conditions suivantes doivent être vérifiées :

    • elle doit porter le même nom que la classe ;

    • elle ne doit avoir aucun type, pas même le type void.

    Le destructeur doit également respecter ces règles. Pour le différencier du constructeur, son nom sera toujours précédé du signe tilde ('~').

    Un constructeur est appelé automatiquement lors de l'instanciation de l'objet. Le destructeur est appelé automatiquement lors de sa destruction. Cette destruction a lieu lors de la sortie du bloc de portée courante pour les objets de classe de stockage auto. Pour les objets alloués dynamiquement, le constructeur et le destructeur sont appelés automatiquement par les expressions qui utilisent les opérateurs new, new[], delete et delete[]. C'est pour cela qu'il est recommandé de les utiliser à la place des fonctions malloc et free du C pour créer dynamiquement des objets. De plus, il ne faut pas utiliser delete ou delete[] sur des pointeurs de type void, car il n'existe pas d'objets de type void. Le compilateur ne peut donc pas déterminer quel est le destructeur à appeler avec ce type de pointeur.

    Le constructeur est appelé après l'allocation de la mémoire de l'objet et le destructeur est appelé avant la libération de cette mémoire. La gestion de l'allocation dynamique de mémoire avec les classes est ainsi simplifiée. Dans le cas des tableaux, l'ordre de construction est celui des adresses croissantes, et l'ordre de destruction est celui des adresses décroissantes. C'est dans cet ordre que les constructeurs et destructeurs de chaque élément du tableau sont appelés.

    Les constructeurs pourront avoir des paramètres. Ils peuvent donc être surchargés, mais pas les destructeurs. Cela est dû a fait qu'en général on connaît le contexte dans lequel un objet est créé, mais qu'on ne peut pas connaître le contexte dans lequel il est détruit : il ne peut donc y avoir qu'un seul destructeur. Les constructeurs qui ne prennent pas de paramètre ou dont tous les paramètres ont une valeur par défaut, remplacent automatiquement les constructeurs par défaut définis par le compilateur lorsqu'il n'y a aucun constructeur dans les classes. Cela signifie que ce sont ces constructeurs qui seront appelés automatiquement par les constructeurs par défaut des classes dérivées.

    Exemple 8-10. Constructeurs et destructeurs

    class chaine    // Implémente une chaîne de caractères.
    {
        char * s;   // Le pointeur sur la chaîne de caractères.
    
    public:
        chaine(void);           // Le constructeur par défaut.
        chaine(unsigned int);   // Le constructeur. Il n'a pas de type.
        ~chaine(void);          // Le destructeur.
    };
    
    chaine::chaine(void)
    {
        s=NULL;                 // La chaîne est initialisée avec
        return ;                // le pointeur nul.
    }
    
    chaine::chaine(unsigned int Taille)
    {
        s = new char[Taille+1]; // Alloue de la mémoire pour la chaîne.
        s[0]='�';              // Initialise la chaîne à "".
        return;
    }
    
    chaine::~chaine(void)
    {
        if (s!=NULL) delete[] s; // Restitue la mémoire utilisée si
                                 // nécessaire.
        return;
    }

    Pour passer les paramètres au constructeur, on donne la liste des paramètres entre parenthèses juste après le nom de l'objet lors de son instanciation :

    chaine s1;        // Instancie une chaîne de caractères
                      // non initialisée.
    chaine s2(200);   // Instancie une chaîne de caractères
                      // de 200 caractères.

    Les constructeurs devront parfois effectuer des tâches plus compliquées que celles données dans cet exemple. En général, ils peuvent faire toutes les opérations faisables dans une méthode normale, sauf utiliser les données non initialisées bien entendu. En particulier, les données des sous-objets d'un objet ne sont pas initialisées tant que les constructeurs des classes de base ne sont pas appelés. C'est pour cela qu'il faut toujours appeler les constructeurs des classes de base avant d'exécuter le constructeur de la classe en cours d'instanciation. Si les constructeurs des classes de base ne sont pas appelés explicitement, le compilateur appellera, par défaut, les constructeurs des classes mères qui ne prennent pas de paramètre ou dont tous les paramètres ont une valeur par défaut (et, si aucun constructeur n'est défini dans les classe mères, il appellera les constructeurs par défaut de ces classes).

    Comment appeler les constructeurs et les destructeurs des classes mères lors de l'instanciation et de la destruction d'une classe dérivée ? Le compilateur ne peut en effet pas savoir quel constructeur il faut appeler parmi les différents constructeurs surchargés potentiellement présents... Pour appeler un autre constructeur d'une classe de base que le constructeur ne prenant pas de paramètre, il faut spécifier explicitement ce constructeur avec ses paramètres après le nom du constructeur de la classe fille, en les séparant de deux points (':').

    En revanche, il est inutile de préciser le destructeur à appeler, puisque celui-ci est unique. Le programmeur ne doit donc pas appeler lui-même les destructeurs des classes mères, le langage s'en charge.

    Exemple 8-11. Appel du constructeur des classes de base

    /* Déclaration de la classe mère. */
    
    class Mere
    {
        int m_i;
    public:
        Mere(int);
        ~Mere(void);
    };
    
    /* Définition du constructeur de la classe mère. */
    
    Mere::Mere(int i)
    {
        m_i=i;
        printf("Exécution du constructeur de la classe mère.n");
        return;
    }
    
    /* Définition du destructeur de la classe mère. */
    
    Mere::~Mere(void)
    {
        printf("Exécution du destructeur de la classe mère.n");
        return;
    }
    
    /* Déclaration de la classe fille. */
    
    class Fille : public Mere
    {
    public:
        Fille(void);
        ~Fille(void);
    };
    
    /* Définition du constructeur de la classe fille
       avec appel du constructeur de la classe mère. */
    
    Fille::Fille(void) : Mere(2)
    {
        printf("Exécution du constructeur de la classe fille.n");
        return;
    }
    
    /* Définition du destructeur de la classe fille
       avec appel automatique du destructeur de la classe mère. */
    
    Fille::~Fille(void)
    {
        printf("Exécution du destructeur de la classe fille.n");
        return;
    }

    Lors de l'instanciation d'un objet de la classe fille, le programme affichera dans l'ordre les messages suivants :

    Exécution du constructeur de la classe mère.
    Exécution du constructeur de la classe fille.
    et lors de la destruction de l'objet :
    Exécution du destructeur de la classe fille.
    Exécution du destructeur de la classe mère.

    Si l'on n'avait pas précisé que le constructeur à appeler pour la classe Mere était le constructeur prenant un entier en paramètre, le compilateur aurait essayé d'appeler le constructeur par défaut de cette classe. Or, ce constructeur n'étant plus généré automatiquement par le compilateur (à cause de la définition d'un constructeur prenant un paramètre), il y aurait eu une erreur de compilation.

    Il est possible d'appeler plusieurs constructeurs si la classe dérive de plusieurs classes de base. Pour cela, il suffit de lister les constructeurs un à un, en séparant leurs appels par des virgules. On notera cependant que l'ordre dans lequel les constructeurs sont appelés n'est pas forcément l'ordre dans lequel ils sont listés dans la définition du constructeur de la classe fille. En effet, le C++ appelle toujours les constructeurs dans l'ordre d'apparition de leurs classes dans la liste des classes de base de la classe dérivée.

    Note : Afin d'éviter l'utilisation des données non initialisées de l'objet le plus dérivé dans une hiérarchie pendant la construction de ses sous-objets par l'intermédiaire des fonctions virtuelles, le mécanisme des fonctions virtuelles est désactivé dans les constructeurs (voyez la section 8.13 pour plus de détails sur les fonctions virtuelles). Ce problème survient parce que pendant l'exécution des constructeurs des classes de base, l'objet de la classe en cours d'instanciation n'a pas encore été initialisé, et malgré cela, une fonction virtuelle aurait pu utiliser une donnée de cet objet.

    Une fonction virtuelle peut donc toujours être appelée dans un constructeur, mais la fonction effectivement appelée est celle de la classe du sous-objet en cours de construction : pas celle de la classe de l'objet complet. Ainsi, si une classe A hérite d'une classe B et qu'elles ont toutes les deux une fonction virtuelle f, l'appel de f dans le constructeur de B utilisera la fonction f de B, pas celle de A (même si l'objet que l'on instancie est de classe A).

    La syntaxe utilisée pour appeler les constructeurs des classes de base peut également être utilisée pour initialiser les données membres de la classe. En particulier, cette syntaxe est obligatoire pour les données membres constantes et pour les références, car le C++ ne permet pas l'affectation d'une valeur à des variables de ce type. Encore une fois, l'ordre d'appel des constructeurs des données membres ainsi initialisées n'est pas forcément l'ordre dans lequel ils sont listés dans le constructeur de la classe. En effet, le C++ utilise cette fois l'ordre de déclaration de chaque donnée membre.

    Exemple 8-12. Initialisation de données membres constantes

    class tableau
    {
        const int m_iTailleMax;
        const int *m_pDonnees;
    public:
        tableau(int iTailleMax);
        ~tableau();
    };
    
    tableau::tableau(int iTailleMax) :
        m_iTailleMax(iTailleMax)    // Initialise la donnée membre constante.
    {
        // Allocation d'un tableau de m_iTailleMax entrées :
        m_pDonnees = new int[m_iTailleMax];
    }
    
    tableau::~tableau()
    {
        // Destruction des données :
        delete[] m_pDonnees;
    }

    Note : Les constructeurs des classes des bases virtuelles prenant des paramètres doivent être appelés par chaque classe qui en dérive, que cette dérivation soit directe ou indirecte. En effet, les classes de base virtuelles subissent un traitement particulier qui assure l'unicité de leurs données dans toutes leurs classes dérivées. Les classes dérivées ne peuvent donc pas se reposer sur leurs classes de base pour appeler le constructeur des classes virtuelles, car il peut y avoir plusieurs classes de bases qui dérivent d'une même classe virtuelle, et cela supposerait que le constructeur de cette dernière classe serait appelé plusieurs fois, éventuellement avec des valeurs de paramètres différentes. Chaque classe doit donc prendre en charge la construction des sous-objets des classes de base virtuelles dont il hérite dans ce cas.

    8.8.2. Constructeurs de copie

    Il faudra parfois créer un constructeur de copie. Le but de ce type de constructeur est d'initialiser un objet lors de son instanciation à partir d'un autre objet. Toute classe dispose d'un constructeur de copie par défaut généré automatiquement par le compilateur, dont le seul but est de recopier les champs de l'objet à recopier un à un dans les champs de l'objet à instancier. Toutefois, ce constructeur par défaut ne suffira pas toujours, et le programmeur devra parfois en fournir un explicitement.

    Ce sera notamment le cas lorsque certaines données des objets auront été allouées dynamiquement. Une copie brutale des champs d'un objet dans un autre ne ferait que recopier les pointeurs, pas les données pointées. Ainsi, la modification de ces données pour un objet entraînerait la modification des données de l'autre objet, ce qui ne serait sans doute pas l'effet désiré.

    La définition des constructeurs de copie se fait comme celle des constructeurs normaux. Le nom doit être celui de la classe, et il ne doit y avoir aucun type. Dans la liste des paramètres cependant, il devra toujours y avoir une référence sur l'objet à copier.

    Pour la classe chaine définie ci dessus, il faut un constructeur de copie. Celui-ci peut être déclaré de la façon suivante :

    chaine(const chaine &Source);
    Source est l'objet à copier.

    Si l'on rajoute la donnée membre Taille dans la déclaration de la classe, la définition de ce constructeur peut être :

    chaine::chaine(const chaine &Source)
    {
        int i = 0;                   // Compteur de caractères.
        Taille = Source.Taille;
        s = new char[Taille + 1];    // Effectue l'allocation.
        strcpy(s, Source.s);         // Recopie la chaîne de caractères source.
        return;
    }

    Le constructeur de copie est appelé dans toute instanciation avec initialisation, comme celles qui suivent :

    chaine s2(s1);
    chaine s2 = s1;

    Dans les deux exemples, c'est le constructeur de copie qui est appelé. En particulier, à la deuxième ligne, le constructeur normal n'est pas appelé et aucune affectation entre objets n'a lieu.

    Note : Le fait de définir un constructeur de copie pour une classe signifie généralement que le constructeur de copie, le destructeur et l'opérateur d'affectation fournis par défaut par le compilateur ne conviennent pas pour cette classe. Par conséquent, ces méthodes devront systématiquement être redéfinies toutes les trois dès que l'une d'entre elle le sera. Cette règle, que l'on appelle la règle des trois, vous permettra d'éviter des bogues facilement. Vous trouverez de plus amples détails sur la manière de redéfinir l'opérateur d'affectation dans la section 8.11.3.

    8.8.3. Utilisation des constructeurs dans les transtypages

    Les constructeurs sont utilisés dans les conversions de type dans lesquelles le type cible est celui de la classe du constructeur. Ces conversions peuvent être soit implicites (dans une expression), soit explicite (à l'aide d'un transtypage). Par défaut, les conversions implicites sont légales, pourvu qu'il existe un constructeur dont le premier paramètre a le même type que l'objet source. Par exemple, la classe Entier suivante :

    class Entier
    {
        int i;
    public:
        Entier(int j)
        {
            i=j;
            return ;
        }
    };

    dispose d'un constructeur de transtypage pour les entiers. Les expressions suivantes :

    int j=2;
    Entier e1, e2=j;
    e1=j;

    sont donc légales, la valeur entière située à la droite de l'expression étant convertie implicitement en un objet du type de la classe Entier.

    Si, pour une raison quelconque, ce comportement n'est pas souhaitable, on peut forcer le compilateur à n'accepter que les conversions explicites (à l'aide de transtypage). Pour cela, il suffit de placer le mot clé explicit avant la déclaration du constructeur. Par exemple, le constructeur de la classe chaine vue ci-dessus prenant un entier en paramètre risque d'être utilisé dans des conversions implicites. Or ce constructeur ne permet pas de construire une chaîne de caractères à partir d'un entier, et ne doit donc pas être utilisé dans les opérations de transtypage. Ce constructeur doit donc être déclaré explicit :

    class chaine
    {
        size_t Taille;
        char * s;
    
    public:
        chaine(void);
        // Ce constructeur permet de préciser la taille de la chaîne
    	// à sa création :
        explicit chaine(unsigned int);
        ~chaine(void);
    };

    Avec cette déclaration, l'expression suivante :

    int j=2;
    chaine s = j;

    n'est plus valide, alors qu'elle l'était lorsque le constructeur n'était pas déclaré explicit.

    Note : On prendra garde au fait que le mot clé explicit n'empêche l'utilisation du constructeur dans les opérations de transtypage que dans les conversions implicites. Si le transtypage est explicitement demandé, le constructeur sera malgré tout utilisé. Ainsi, le code suivant sera accepté :

    int j=2;
    chaine s = (chaine) j;

    Bien entendu, cela n'a pas beaucoup de signification et ne devrait jamais être effectué.


    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    8.7. Fonctions et classes amies

    Il est parfois nécessaire d'avoir des fonctions qui ont un accès illimité aux champs d'une classe. En général, l'emploi de telles fonctions traduit un manque d'analyse dans la hiérarchie des classes, mais pas toujours. Elles restent donc nécessaires malgré tout.

    De telles fonctions sont appelées des fonctions amies. Pour qu'une fonction soit amie d'une classe, il faut qu'elle soit déclarée dans la classe avec le mot clé friend.

    Il est également possible de faire une classe amie d'une autre classe, mais dans ce cas, cette classe devrait peut-être être une classe fille. L'utilisation des classes amies peut traduire un défaut de conception.

    8.7.1. Fonctions amies

    Les fonctions amies se déclarent en faisant précéder la déclaration classique de la fonction du mot clé friend à l'intérieur de la déclaration de la classe cible. Les fonctions amies ne sont pas des méthodes de la classe cependant (cela n'aurait pas de sens puisque les méthodes ont déjà accès aux membres de la classe).

    Exemple 8-8. Fonctions amies

    class A
    {
        int a;                        // Une donnée privée.
        friend void ecrit_a(int i);   // Une fonction amie.
    };
    
    A essai;
    
    void ecrit_a(int i)
    {
        essai.a=i;          // Initialise a.
        return;
    }

    Il est possible de déclarer amie une fonction d'une autre classe, en précisant son nom complet à l'aide de l'opérateur de résolution de portée.

    8.7.2. Classes amies

    Pour rendre toutes les méthodes d'une classe amies d'une autre classe, il suffit de déclarer la classe complète comme étant amie. Pour cela, il faut encore une fois utiliser le mot clé friend avant la déclaration de la classe, à l'intérieur de la classe cible. Cette fois encore, la classe amie déclarée ne sera pas une sous-classe de la classe cible, mais bien une classe de portée globale.

    Note : Le fait, pour une classe, d'appartenir à une autre classe lui donne le droit d'accéder aux membres de sa classe hôte. Il n'est donc pas nécessaire de déclarer amies d'une classe les classes définies au sein de celle-ci. Remarquez que cette règle a été récemment modifiée dans la norme C++, et que la plupart des compilateurs refuseront aux classes incluses d'accéder aux membres non publics de leur conteneur.

    Exemple 8-9. Classe amie

    #include <stdio.h>
    
    class Hote
    {
        friend class Amie;  // Toutes les méthodes de Amie sont amies.
    
        int i;              // Donnée privée de la classe Hote.
    
    public:
        Hote(void)
        {
            i=0;
            return ;
        }
    };
    
    Hote h;
    
    class Amie
    {
    public:
        void print_hote(void)
        {
            printf("%dn", h.i); // Accède à la donnée privée de h.
            return ;
        }
    };
    
    int main(void)
    {
        Amie a;
        a.print_hote();
        return 0;
    }

    On remarquera plusieurs choses importantes. Premièrement, l'amitié n'est pas transitive. Cela signifie que les amis des amis ne sont pas des amis. Une classe A amie d'une classe B, elle-même amie d'une classe C, n'est pas amie de la classe C par défaut. Il faut la déclarer amie explicitement si on désire qu'elle le soit. Deuxièmement, les amis ne sont pas hérités. Ainsi, si une classe A est amie d'une classe B et que la classe C est une classe fille de la classe B, alors A n'est pas amie de la classe C par défaut. Encore une fois, il faut la déclarer amie explicitement. Ces remarques s'appliquent également aux fonctions amies (une fonction amie d'une classe A amie d'une classe B n'est pas amie de la classe B, ni des classes dérivées de A).


    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    8.6. Classes virtuelles

    Supposons à présent qu'une classe D hérite de deux classes mères, les classes B et C. Supposons également que ces deux classes héritent d'une classe mère commune appelée classe A. On a l'arbre « généalogique » suivant :

    On sait que B et C héritent des données et des méthodes publiques et protégées de A. De même, D hérite des données de B et C, et par leur intermédiaire des données de A. Il se pose donc le problème suivant : quelles sont les données que l'on doit utiliser quand on référence les champs de A ? Celles de B ou celles de C ? On peut accéder aux deux sous-objets de classe A en spécifiant le chemin à suivre dans l'arbre généalogique à l'aide de l'opérateur de résolution de portée. Cependant, cela n'est ni pratique ni efficace, et en général, on s'attend à ce qu'une seule copie de A apparaisse dans D.

    Le problème est résolu en déclarant virtuelle la classe de base commune dans la spécification de l'héritage pour les classes filles. Les données de la classe de base ne seront alors plus dupliquées. Pour déclarer une classe mère comme une classe virtuelle, il faut faire précéder son nom du mot clé virtual dans l'héritage des classes filles.

    Exemple 8-7. Classes virtuelles

    class A
    {
    protected:
        int Donnee;        // La donnée de la classe de base.
    };
    
    // Héritage de la classe A, virtuelle :
    class B : virtual public A
    {
    protected:
        int Valeur_B;      // Autre donnée que "Donnee" (héritée).
    };
    
    // A est toujours virtuelle :
    class C : virtual public A
    {
    protected:
        int valeur_C;      // Autre donnée
                           // ("Donnee" est acquise par héritage).
    };
    
    class D : public B, public C   // Ici, Donnee n'est pas dupliqué.
    {
        /* Définition de la classe D. */
    };

    Note : Normalement, l'héritage est réalisé par le compilateur par aggrégation de la structure de données des classes de base dans la structure de données de la classe dérivée. Pour les classes virtuelles, ce n'est en général pas le cas, puisque le compilateur doit assurer l'unicité des données héritées de ces classes, même en cas d'héritage multiple. Par conséquent, certaines restrictions d'usage s'appliquent sur les classes virtuelles.

    Premièrement, il est impossible de transtyper directement un pointeur sur un objet d'une classe de base virtuelle en un pointeur sur un objet d'une de ses classes dérivées. Il faut impérativement utiliser l'opérateur de transtypage dynamique dynamic_cast. Cet opérateur sera décrit dans le chapitre 10.

    Deuxièmement, chaque classe dérivée directement ou indirectement d'une classe virtuelle doit en appeler le constructeur explicitement dans son constructeur si celui-ci prend des paramètres. En effet, elle ne peut pas se fier au fait qu'une autre de ses classes de base, elle-même dérivée de la classe de base virtuelle, appelle un constructeur spécifique, car il est possible que plusieurs classes de base cherchent à initialiser différemment chacune un objet commun hérité de la classe virtuelle. Pour reprendre l'exemple donné ci-dessus, si les classes B et C appellaient toutes les deux un constructeur non trivial de la classe virtuelle A, et que la classe D appellait elle-même les constructeurs de B et C, le sous-objet hérité de A serait construit plusieurs fois. Pour éviter cela, le compilateur ignore purement et simplement les appels au constructeur des classes de bases virtuelles dans les classes de base dérivées. Il faut donc systématiquement le spécifier, à chaque niveau de la hiérarchie de classe. La notion de constructeur sera vue dans la section 8.8


    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    8.5. Héritage

    L'héritage permet de donner à une classe toutes les caractéristiques d'une ou de plusieurs autres classes. Les classes dont elle hérite sont appelées classes mères, classes de base ou classes antécédentes. La classe elle-même est appelée classe fille, classe dérivée ou classe descendante.

    Les propriétés héritées sont les champs et les méthodes des classes de base.

    Pour faire un héritage en C++, il faut faire suivre le nom de la classe fille par la liste des classes mères dans la déclaration avec les restrictions d'accès aux données, chaque élément étant séparé des autres par une virgule. La syntaxe (donnée pour class, identique pour struct) est la suivante :

    class Classe_mere1
    {
        /* Contenu de la classe mère 1. */
    };
    
    [class Classe_mere2
    {
        /* Contenu de la classe mère 2. */
    };]
    
    [...]
    
    class Classe_fille : public|protected|private Classe_mere1
    [, public|protected|private Classe_mere2 [...]]
    {
        /* Définition de la classe fille. */
    };

    Dans cette syntaxe, Classe_fille hérite de la Classe_mere1, et des Classe_mere2, etc. si elles sont présentes.

    La signification des mots clés private, protected et public dans l'héritage est récapitulée dans le tableau suivant :

    Tableau 8-1. Droits d'accès sur les membres hérités

     

    mot clé utilisé pour l'héritage

    Accès aux données

    public

    protected

    private

    mot clé utilisé

    public

    public

    protected

    private

    pour les champs

    protected

    protected

    protected

    private

    et les méthodes

    private

    interdit

    interdit

    interdit

    Ainsi, les données publiques d'une classe mère deviennent soit publiques, soit protégées, soit privées selon que la classe fille hérite en public, protégé ou en privé. Les données privées de la classe mère sont toujours inaccessibles, et les données protégées deviennent soit protégées, soit privées.

    Il est possible d'omettre les mots clés public, protected et private dans la syntaxe de l'héritage. Le compilateur utilise un type d'héritage par défaut dans ce cas. Les classes de type struct utilisent l'héritage public par défaut et les classes de type class utilisent le mot clé private par défaut.

    Exemple 8-5. Héritage public, privé et protégé

    class Emplacement
    {
    protected:
        int x, y;              // Données ne pouvant être accédées
                               // que par les classes filles.
    
    public:
        void Change(int, int); // Méthode toujours accessible.
    };
    
    void Emplacement::Change(int i, int j)
    {
        x = i;
        y = j;
        return;
    }
    
    class Point : public Emplacement
    {
    protected:
        unsigned int couleur;  // Donnée accessible
                               // aux classes filles.
    
    public:
        void SetColor(unsigned int);
    };
    
    void Point::SetColor(unsigned int NewColor)
    {
        couleur = NewColor;    // Définit la couleur.
        return;
    }

    Si une classe Cercle doit hériter de deux classes mères, par exemple Emplacement et Forme, sa déclaration aura la forme suivante :

    class Cercle : public Emplacement, public Forme
    {
        /*
           Définition de la classe Cercle. Cette classe hérite
           des données publiques et protégées des classes Emplacement
           et Forme.
         */
    };

    Il est possible de redéfinir les fonctions et les données des classes de base dans une classe dérivée. Par exemple, si une classe B dérive de la classe A, et que toutes deux contiennent une donnée d, les instances de la classe B utiliseront la donnée d de la classe B et les instances de la classe A utiliseront la donnée d de la classe A. Cependant, les objets de classe B contiendront également un sous-objet, lui-même instance de la classe de base A. Par conséquent, ils contiendront la donnée d de la classe A, mais cette dernière sera cachée par la donnée d de la classe la plus dérivée, à savoir la classe B.

    Ce mécanisme est général : quand une classe dérivée redéfinit un membre d'une classe de base, ce membre est caché et on ne peut plus accéder directement qu'au membre redéfini (celui de la classe dérivée). Cependant, il est possible d'accéder aux données cachées si l'on connaît leur classe, pour cela, il faut nommer le membre complètement à l'aide de l'opérateur de résolution de portée (::). Le nom complet d'un membre est constitué du nom de sa classe suivi de l'opérateur de résolution de portée, suivis du nom du membre :

    classe::membre

    Exemple 8-6. Opérateur de résolution de portée et membre de classes de base

    struct Base
    {
        int i;
    };
    
    struct Derivee : public Base
    {
        int i;
        int LitBase(void);
    };
    
    int Derivee::LitBase(void)
    {
        return Base::i; // Renvoie la valeur i de la classe de base.
    }
    
    int main(void)
    {
        Derivee D;
        D.i=1;          // Accède à l'entier i de la classe Derivee.
        D.Base::i=2;    // Accède à l'entier i de la classe Base.
        return 0;

    votre commentaire