IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

 C++ Discussion :

Méthode virtuelle ou abstraite ?


Sujet :

C++

  1. #1
    Membre régulier
    Inscrit en
    Novembre 2008
    Messages
    308
    Détails du profil
    Informations forums :
    Inscription : Novembre 2008
    Messages : 308
    Points : 90
    Points
    90
    Par défaut Méthode virtuelle ou abstraite ?
    Bonjour,
    Je voudrais savoir quelle est la différence entre l'utilité d'une méthode virtuelle et celle d'une méthode abstraite.
    Si on ne veut pas instancier la classe mère, ne suffirait-il pas de créer des méthodes abstraites qui seront définies dans les classes filles au lieu de mettre des méthodes virtuelles?
    Qu'est-ce que signifie mettre une méthode virtuelle et abstraite en même temps dans une classe mère?

  2. #2
    Expert confirmé
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Décembre 2003
    Messages
    3 549
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Décembre 2003
    Messages : 3 549
    Points : 4 625
    Points
    4 625
    Par défaut
    En C++, une méthode abstraite c'est une méthode virtuelle qui est également pure.

  3. #3
    Membre régulier
    Inscrit en
    Novembre 2008
    Messages
    308
    Détails du profil
    Informations forums :
    Inscription : Novembre 2008
    Messages : 308
    Points : 90
    Points
    90
    Par défaut
    D'acord. Et quand est-ce qu'on préfère l'une à l'autre?

  4. #4
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 627
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 627
    Points : 30 692
    Points
    30 692
    Par défaut
    Salut,

    Je crois qu'il est nécessaire de commencer par expliquer clairement ce qu'est quoi...

    Une fonction virtuelle est une fonction pour laquelle tu préviens le compilateur que le comportement risque d'être modifié par une classe dérivée.

    Une fonction virtuelle pure (abstraite) est une fonction qui signale au compilateur que tu ne dispose pas des informations nécessaires pour implémenter un comportement "de base" et donc dont le comportement devra être précisé pour les classes dérivées que tu souhaite pouvoir instancier.

    Il n'est donc pas question de "préférer" l'un par rapport à l'autre, mais bel et bien de savoir si la classe dispose de suffisamment d'information pour permettre de définir le comportement d'une fonction virtuelle ou non:

    Si la classe dispose des informations nécessaires à la définition d'un comportement cohérent pour la classe de base, il n'y a aucune raison de transformer une fonction virtuelle en une fonction virtuelle pure.

    Par contre, si tu ne dispose pas des informations nécessaires pour définir un comportement cohérent, tu n'auras pas vraiment le choix: tu devra déclarer une fonction virtuelle pure.

    De plus, l'utilisation de fonctions virtuelles n'est absolument pas la seule solution pour empêcher l'instanciation d'une classe:

    Si un héritage est prévu, le simple fait de rendre le constructeur protégé (pour que les classes dérivées puissent y accéder) peut suffire, sans que cela ne rende la classe abstraite, par la présence d'une fonction virutelle pure quelconque (il sera cependant souvent intéressant de penser à rendre également le constructeur par copie protégé )

    De plus, il n'y a aucune incompatibilité à déclarer des fonctions non virtuelles,
    des fonctions virtuelles et des fonctions virtuelles pures au sein d'une seule et même structure.

    Au contraire, toute fonction pour laquelle il n'est pas question de redéfinir un comportement devrait définitivement rester non virtuelle

    Par contre, quel que soit le "niveau d'héritage", si tu déclares une ou plusieurs fonctions virtuelles pures, toutes les classes dérivées pour lesquelles elles n'auront pas été redéfinies seront également des classes abstraites, et donc non instanciable.

    Par exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class NonInstanciable
    {
        public:
            virtual ~NonInstanciable();
            /*...*/
        private:
            NonInstanciable(){}
    };
    class DeriveeInstanciable : public NonInstanciable
    {
        public:
            DeriveeInstanciable (){}
            virtual DeriveeInstanciable();
            /*...*/
    };
    (nous considérons que les fonctions virtuelles sont définies dans les fichiers *.cpp correspondant)

    te placera dans la situation suivante:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int main()
    {
        NonInstanciable ni; // ERREUR: le constructeur est protégé 
                            // dans ce contexte
        NonInstanciable * ptrni=new NonInstanciable // IDEM
        DeriveeInstanciable  i; // OK: le constructeur est public
        DeriveeInstanciable * ptri= new DeriveeInstanciable; //IDEM
        NonInstanciable polymorph = new DeriveeInstanciable; // OK (*)
        /*...*/
    }
    (*)Bien que polymorph se fasse passer pour un pointeur de type NonInstanciable (dont le constructeur n'est pas accessible), c'est bel et bien le constructeur de DeriveeInstanciable qui sera appelé.

    Et, pour, pour comprendre le principe des fonctions virtuelles pures, si tu as les classes
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    class Base  /* ( 1 ) */
    {
        pubic:
            Base(){}
            virtual ~Base(); // nous considérons qu'il est défini dans le fichier
                             //  *.cpp correspondant
            virtual void foo() = 0; // une fonction virtuelle pure (*)
    };
    class Derivee : public Base /* ( 2 ) */
    {
        public:
            Derivee();
            virtual ~Derivee(); // nous considérons qu'il est défini dans le
                                // fichier *.cpp correspondant
    };
    class AutreDerivee : public Derivee /* ( 3 ) */
    {
        public:
            AutreDerivee ();
            virtual ~AutreDerivee(); // nous considérons qu'il est défini dans
                                     //  le fichier *.cpp correspondant
            virtual void foo(); // nous considérons qu'il est défini dans le 
                                // fichier *.cpp correspondant 
    };
    class TroisiemeDerivee : public Base/* ( 4 ) */
    {
        public:
            TroisiemeDerivee ();
            virtual ~TroisiemeDerivee(); // nous considérons qu'il est défini
                                         //  dans le fichier *.cpp correspondant
            virtual void foo(); // nous considérons qu'il est défini dans le 
                                // fichier *.cpp correspondant 
    };
    La classe Base ( 1 ) est abstraite parce que foo() est une fonction virtuelle pure, et ne peut pas être instanciée

    La classe Derivee ( 2 ) est également abstraite parce que la fonction foo n'a pas été redéfinie, et que c'est donc toujours une fonction virtuelle pure

    Les classes AutreDerivee ( 3 ) et TroisiemeDerivee ( 4 ) sont des classes concrètes car la fonction foo a été redéfinie pour chacune de ces classes...

    Elles peuvent donc être instanciée, y compris sous la forme de pointeur sur Base (ca concerne les deux) ou de pointeur sur Derivee (uniquement AutreDerivee, car TroisiemeDerivee ne dérive pas de... Derivee )

  5. #5
    Membre régulier
    Inscrit en
    Novembre 2008
    Messages
    308
    Détails du profil
    Informations forums :
    Inscription : Novembre 2008
    Messages : 308
    Points : 90
    Points
    90
    Par défaut
    Merci beaucoup pour ta précieuse explication!
    Citation Envoyé par koala01 Voir le message
    Elles peuvent donc être instanciée, y compris sous la forme de pointeur sur Base (ca concerne les deux) ou de pointeur sur Derivee (uniquement AutreDerivee, car TroisiemeDerivee ne dérive pas de... Derivee )
    Mais TroisiemeDerivee dérive de AutreDerivee qui dérive de Derivee. Pourquoi TroisiemeDerivee ne peut être considérée comme fille de Derivee et donc être instanciée sous forme de pointeur sur cette dernière comme il est possible d'en faire pour Base?

    Encore trois questions:

    1. Est-ce que c'est obligatoire d'écrire "virtual" avant d'écrire le prototype de la méthode virtuelle dans toutes les classes filles qui utilisent cette méthode ou bien il suffit d'en faire dans la classe mère (c'est ce que je croyait qu'on fait)?

    2. Est-ce qu'on peut redéfinir dans une classe fille une fonction qui n'est pas virtuelle ?

    3. Quand est-ce qu'on utilise les destructeurs virtuels? Si on ne les met pas virtuels qu'est-ce que ça change?

  6. #6
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 627
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 627
    Points : 30 692
    Points
    30 692
    Par défaut
    Citation Envoyé par yo_haha Voir le message
    Merci beaucoup pour ta précieuse explication!


    Mais TroisiemeDerivee dérive de AutreDerivee qui dérive de Derivee. Pourquoi TroisiemeDerivee ne peut être considérée comme fille de Derivee et donc être instanciée sous forme de pointeur sur cette dernière comme il est possible d'en faire pour Base?
    Vérifie le code... TroisiemeDerivee dérive de... Base uniquement...

    Tu peux donc l'instancier sous la forme d'un pointeur sur Base, mais pas sous la forme d'un pointeur sur Derivee
    1. Est-ce que c'est obligatoire d'écrire "virtual" avant d'écrire le prototype de la méthode virtuelle dans toutes les classes filles qui utilisent cette méthode ou bien il suffit d'en faire dans la classe mère (c'est ce que je croyait qu'on fait)?
    Ce n'est normalement pas obligatoire... Mais je préfères, à titre strictement personnel rajouter le mot clé pour dans la définition de toutes les classes dérivées...

    La raison est bien simple: cela permet à celui qui a le fichier d'en-tête d'une classe dérivée de se rendre compte que la fonction dont il voit la signature est prévue pour avoir un comportement polymorphe
    2. Est-ce qu'on peut redéfinir dans une classe fille une fonction qui n'est pas virtuelle ?
    Pour qu'une fonction ait un comportement polymorphe, il faut impérativement qu'elle soit déclarée virtuelle.

    Par contre, il peut arriver que tu ne souhaites pas que le comportement soit polymorphe, et tu peux alors "cacher" le comportement induis par la classe de base en redéfinissant une fonction non virtuelle dans la classe dérivée...

    Mais cela implique un comportement assez surprenant:

    soit les classes
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Base
    {
        public:
            Base(){}
            virtual ~Base(); /* considéré comme défini dans le 
                              * fichier *.cpp ad hoc 
                              */
            void foo(){ std::cout<<"Base::foo()"<<std::endl;}
    };
    class Derivee : public Base
    {
        public:
            Derivee (){}
            virtual ~Derivee(); /* considéré comme défini dans le 
                                 * fichier *.cpp ad hoc 
                                 */
            void foo(){ std::cout<<"Derivee::foo()"<<std::endl;}
    };
    Tant que ton instance de type Derivee sera effectivement considérée comme étant de type... Derivee, l'invocation de la fonction membre foo affichera bel et bien "Derivee::foo()"

    Par contre, dés que ton instance de type Derivee sera considérée comme étant de type... Base, l'invocation de la fonction membre foo affichera... "Base::foo()", parce que, la fonction n'étant pas déclarée virtuelle, elle n'est pas reprise dans la "vtable" (pour faire simple, et donc, forcément de manière incomplète et pas tout à fait juste, il s'agit d'un tableau qui liste les fonctions dont le comportement doit être adapté en fonction du type réel manipulé (polymorphisme) ), et donc le compilateur considère que... c'est le comportement du type Base qui doit être appliqué:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    int main()
    {
        /* si l'instance de type Derivee est considérée comme étant
         * de type Derivee, il n'y a pas de problème
         */
        Derivee d;
        d.foo(); // affiche Derivee::foo()
        /* Ca fonctionne aussi avec un pointeur */
        Derivee *ptrd= new Derivee;
        ptrd->foo(); // affiche aussi Derivee::foo()
        /* par contre, dés que l'objet est considéré comme étant du type
         * Base, c'est le comportement de Base::foo() qui est pris en compte
         */
        /* Qu'il s'agisse d'une référence sur le type Base */
        Base & refb=d;
        /* ou d'un pointeur */
        Base * ptrb= &d;
        Base * ptrb2= new Derivee;
        refb.foo(); // affiche... Base::foo()
        ptrb->foo(); // idem
        ptrb2->foo(); // toujour pareil
        /*...*/
        return 0;
    }
    alors que, si la fonction avait été déclarée virtuelle, le fait de présenter une instance (ou un pointeur sur instance) de type Derivee aurait fait jouer le polymorphisme et fait afficher Derivee::foo() même dans les cas de refb, ptrb et ptrb2 (selon exemple)
    3. Quand est-ce qu'on utilise les destructeurs virtuels?
    On utilise les destructeurs virtuels dés que:
    1. la classe sert de base à une classe dérivée
    2. le destructeur est public
    3. on prévois de détruire une instance du type de la classe dérivée alors qu'on la connait comme étant du type de base
    (les trois conditions doivent être remplies)

    En effet, il ne sert à rien de déclarer un destructeur virtuel si la classe n'entre pas dans une hiérarchie d'héritage

    De même, tu ne pourra pas envisager d'invoquer un delete ou un delete[] sur un pointeur du type de base si il est déclaré protégé:
    soit les classes
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class Base
    {
        public:
            Base(){}
        protected: 
            /* il faut qu'il soit quand meme accessible aux classes dérivées ;)
             */
            virtual ~Base(); /* considéré comme défini dans le 
                             * fichier *.cpp ad hoc 
                             */
    };
    class Derivee : public Base
    {
        public:
            Derivee (){}
            virtual Derivee(); 
            void foo(){ std::cout<<"Derivee::foo()"<<std::endl;}
    };
    int main
    {
        Base* ptr=new Derivee; //OK le constructeur de Derivee est public
        delete ptr; // ERREUR ~Base est protégé dans ce contexte
        /*...*/
    }
    Enfin, il ne sert à rien de déclarer le destructeur virtuel si tu n'essaye jamais de détruire une instance du type dérivé en la faisant passer pour le type de base (que tu n'utilise jamais le polymorphisme du destructeur)
    Si on ne les met pas virtuels qu'est-ce que ça change?
    Ca change que, si le destructeur n'est pas virtuel alors que tu te trouve dans une situation dans laquelle il aurait du l'être, le polymorhisme ne sera pas appliqué à la destruction, et que seule le destructeur de la classe de base sera effectivement appelé, avec tous les risques liés à d'éventuelles fuites de mémoire... voire pire...

    Compare le résultat avec ces deux hiérarchies:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    /* une hiérarchie avec le destructeur virtuel */
    class BaseVirt
    {
        public:
            BaseVirt(){}
            /* Normalement implémenté dans un fichier *.cpp... mais
             * pour te montrer le code, et par flegme, je le place ici
             */
           virtual ~BaseVirt(){std::cout<<"Destruteur de BaseVirt"<<std::endl;}
    }
    class DeriveeVirt : public BaseVirt
    {
        public:
            DeriveeVirt (){}
            /* Normalement implémenté dans un fichier *.cpp... mais
             * pour te montrer le code, et par flegme, je le place ici
             */
           virtual ~DeriveeVirt (){std::cout<<"Destruteur de DeriveeVirt "
                                          <<std::endl;}
    };
    /* et une autre sans déclarer le destructeur virtuel */
    class Base
    {
        public:
            Base(){}
            /* comme il n'est pas virtuel, il peut etre  implémenté dans le
             * fichier d'en-tête
             */
           ~Base(){std::cout<<"Destruteur de Base"<<std::endl;}
    };
    class Derivee : public Base
    {
        public:
            Derivee (){}
            /* comme il n'est pas virtuel, il peut etre  implémenté dans le
             * fichier d'en-tête
             */
           ~Derivee (){std::cout<<"Destruteur de Derivee "<<std::endl;}
    };
    int main()
    {
        /* créons une instance de chaque type dérivé, et faisons la passer
         * pour le type de base correspondant
         */
        Base* bNonVirtuelle = new Derivee;
        BaseVirt  bVirtuelle = new DeriveeVirt;
        /*...*/
        /* et detruisons les */
        delete bVirtuelle; /* ( 1 ) */
        delete bNonBivrutelle;  /* ( 2 ) */
        return 0;
    }
    La destruction de bVirtuelle ( 1 ) fait afficher les deux messages dans l'ordre correcte:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    Destruteur de DeriveeVirt
    Destruteur de BaseVirt
    ce qui indique que le destructeur de DeriveeVirt est bel et bien appelé avant celui de BaseVirt...

    S'il y a quelque chose à faire dans le destructeur de DeriveeVirt (par exemple, une libération de ressources quelconque), ce sera fait " en temps et en heure"

    Par contre, la destruction de bNonVirtuelle ( 2 ) ne fait afficher qu'un seul message:
    ce qui indique que le destructeur de Derivee n'est jamais appelé...

    La conséquence est funeste: les ressources propres au type Derivee (et ne faisant pas partie du type Base) ne sont jamais libérées ... Et on perd toute possibilité d'y accéder (re)

    Je te laisse imaginer le résultat si les ressources sont des fichiers, ou des tableaux dynamiques de 10.000 élément...

    Et ce serait encore pire si tu venais à souffrir 10.000 fois du problème (re re )

  7. #7
    Rédacteur
    Avatar de 3DArchi
    Profil pro
    Inscrit en
    Juin 2008
    Messages
    7 634
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Juin 2008
    Messages : 7 634
    Points : 13 017
    Points
    13 017
    Par défaut
    Quelques remarques complémentaires à la bonne et longue explication de Koala :
    1/ Pour les raisons qu'il t'a expliqué, il est recommandé que le destructeur soit (virtuel et public) ou (non virtuel et protégé) (Guideline #4: A base class destructor should be either public and virtual, or protected and nonvirtual.)
    2/ Si ton destructeur n'est pas virtuel mais publique et que tu essaies de détruire un objet à partir de la variable de base, en fait, il semblerait que le comportement soit indéterminé :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
     
    class Base
    {
        public:
            Base(){}
            /* comme il n'est pas virtuel, il peut etre  implémenté dans le
             * fichier d'en-tête
             */
           ~Base(){std::cout<<"Destruteur de Base"<<std::endl;}
    };
    class Derivee : public Base
    {
        public:
            Derivee (){}
            /* comme il n'est pas virtuel, il peut etre  implémenté dans le
             * fichier d'en-tête
             */
           ~Derivee (){std::cout<<"Destruteur de Derivee "<<std::endl;}
    };
    int main()
    {
        /* créons une instance de chaque type dérivé, et faisons la passer
         * pour le type de base correspondant
         */
        Base* bNonVirtuelle = new Derivee;
        /*...*/
        /* et detruisons les */
        delete bNonBivrutelle;  /* ( 2 ) */
        return 0;
    }
    (2) est indéterminé : tu peux voir afficher "Destruteur de Base" ou ... "42"...

  8. #8
    Membre régulier
    Inscrit en
    Novembre 2008
    Messages
    308
    Détails du profil
    Informations forums :
    Inscription : Novembre 2008
    Messages : 308
    Points : 90
    Points
    90
    Par défaut
    Merci infiniment koala01 de ta patiente explication
    Aussi merci à 3DArchi de ses remarques

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Réponses: 5
    Dernier message: 14/10/2012, 19h25
  2. classe abstraite & méthodes virtuelles
    Par Tho123 dans le forum C++
    Réponses: 4
    Dernier message: 23/04/2012, 23h53
  3. Classe abstraite et méthodes virtuelles
    Par Daikyo dans le forum Langage
    Réponses: 10
    Dernier message: 16/11/2010, 15h50
  4. Réponses: 15
    Dernier message: 05/07/2007, 01h29
  5. Réponses: 23
    Dernier message: 16/03/2007, 20h21

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo