Bonjour,
Apres plusieurs recherche, je ne parviens pas à comprendre le fonctionnement du mot clef "virtual". Quelqu'un aurait-il un exemple très simple (j'insiste vraiment je suis très novice) de son utilisation, s'il vous plait?
Merci par avance.
Version imprimable
Bonjour,
Apres plusieurs recherche, je ne parviens pas à comprendre le fonctionnement du mot clef "virtual". Quelqu'un aurait-il un exemple très simple (j'insiste vraiment je suis très novice) de son utilisation, s'il vous plait?
Merci par avance.
Ce qu'il permet, d'un point de vue syntaxe :
D'un point de vue utilité, il permet de gérer des objets différents par l'intermédiaire d'une interface commune, tout en permettant à ces objets d'effectuer des actions différentes pour réaliser cette interface.Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 class A { void f() {cout << "A::f" << endl;} virtual void g() {cout << "A::g" << endl;} }; class B : public A { void f() {cout << "B::f" << endl;} virtual void g() {cout << "B::g" << endl;} }; int main() { B b; A* pa = &b; B* pb = &b; pa->f(); pa->g(); pb->f(); pb->g(); }
j'obtiens comme résultats:
je m'attendais à obtenir:Citation:
A::f
B::g
B::f
B::g
Pourquoi c'estCitation:
A::f
A::g
B::f
B::g
qui intervient?Code:virtual void g() {cout << "B::g" << endl;}
Est-on obliger d'écrire 2 fois virtual? ou une seule devantaurait suffit?Code:virtual void g() {cout << "B::g" << endl;}
Hello,
Une méthode virtuelle peut être redéfinie dans les classes filles.
Code:
1
2
3
4
5 B b; A* pa = &b; B* pb = &b; pa->f(); // f() non virtuelle, donc appel de A::f(); pa->g(); // g() virtuelle, pa est de type A* (typage statique) mais pointe sur un objet de type B (typage dynamique), c'est donc B::g() qui est appelée.
Il faut bien l'écrire 2 fois.Citation:
Est-on obliger d'écrire 2 fois virtual? ou une seule devant
(Au minimum dans la classe de base (A ici), il me semble pas qu'indiquer "virtual" ou non dans les classes dérivées ne change quelque chose).
Parce que g est virtual :)
Quand tu considère pa, son type statique, la manière dont il est déclaré dans le programme, c'est pointeur sur un objet de type A. Mais en réalité, il pointe sur un objet de type B. Si on appelle une fonction classique sur pa, c'est son type statique qui va déterminer quelle fonction est vraiment appelée. Si la fonction est virtuelle, on va regarder le type de l'objet sur lequel on pointe réellement pour déterminer la fonction appelée.
Ni l'un, ni l'autre : Une seule fois est nécessaire, mais au niveau de la classe de base. Ceci dit, il est généralement utile quand on lit une classe dérivée de savoir si on redéfini une fonction de la classe de base ou pas. Il est donc préférable de re-préciser virtual dans la classe dérivée, ou en C++11 d'utiliser le mot clef override pour cela.
ok!! Je crois que je comprends ou vous voulez en venir.
Un typage dynaique est un typage qui se préoccupe du type réel de l'objet?
Je ne comprends pas...Citation:
Ceci dit, il est généralement utile quand on lit une classe dérivée de savoir si on redéfini une fonction de la classe de base ou pas. Il est donc préférable de re-préciser virtual dans la classe dérivée
Oui.
Quand dans une classe dérivée, on déclare une fonction ayant le même nom et les mêmes arguments qu'une fonction virtuelle de la classe de base, on dit qu'on redéfinit la fonction de la classe de base (puisque du point de vue de la classe dérivée, tout se passera comme si la fonction de la classe de base avait été remplacée par la nouvelle version). Dit plus simplement, virtual dans la classe dérivée n'est pas obligatorie, mais recommandé (ou alors, l'utilisation d'override).
Merci, cette fois j'ai compris.
Une petite precision tout de meme. Virtual permet de la flexibilité dans les cas ou on ne peut pas connaitre à l'avance le type réel de l'objet?
Dans notre exemple, au lieu d'appeler pa g(), on aurait pu directement appeler pbg()? Cela aurait été plus rigoureux?
virtual permet de trimballer un pointeur sur une classe de base, et lors de l'appel d'une méthode appliqué à ce pointeur, d'appeler en réalité la méthode implémentée dans l'objet dérivé.
C'est par exemple pratique quand tu as un vecteur de pointeurs vers une classe de base, et que tu souhaites appeler un même nom de méthode pour chacun des éléments.
Selon le type réel (déterminé dynamiquement), la bonne méthode (= la bonne définition) sera appelée.
Salut,
Le mot clé virtual indique au compilateur que l'on risque de redéfinir le comportement de la fonction dans les classes dérivées. Il a pour résultat de mettre en place toute une mécanique qui permet de le faire ;).
Dans une relation d'héritage (il n'a de sens qu'avec une telle relation), il permet d'obtenir ce que l'on appelle un comportement polymorphe, c'est à dire un comportement qui s'adaptera au type réel de la donnée manipulée même s'il s'agit d'une référence (ou un pointeur) sur la classe de base. C'est la base de la substituabilité, qui est l'ajout essentiel de la programmation orientée objet par rapport à la programmation purement procédurale ;)
Hormis pour jouer des conversions implicites, ça pourrait etre une bonne idée de virtualiser toutes ses fonctions? Ou ce n'est pas a faire pour eviter les erreurs et garder en lisibilité?
Salut,
tout virtualiser, c'est le parti pris de certains langages. Pour ma part, je suis contre.
Un virtual ça a un coût, pas forcément énorme, souvent négligeable, mais ça a un coût. Et ça reste une fonctionnalité qui modifie un peu ce que fait ton code.
La philosophie C++ est que tu ne payes que ce que tu écris/demandes/codes. Ce que je trouve vraiment bien AMHA.
Bousk a tout à fait raison. Certains langages ont -- par nature -- besoin que toutes les fonctions soient virtuelles. Mais pas C++.
Non seulement le fait de virtualiser une fonction a un cout en termes d'exécution, mais il faut te dire que la présence du mot clé virtual a une signification pour l'utilisateur de la classe, très semblable à la signification qu'il prend pour le compilateur : je peux, en cas de besoin, redéfinir le comportement de la fonction pour qu'il soit plus adapté au type réel de l'objet.
Pour certaines fonctions, cela a du sens de donner cette possibilité, pour d'autres, cela n'en a absolument pas ;)
Merci pour vos réponses.
On rentre ici dans un domaine que je n'est encore jamais abordé. Aurais tu un exemple très simple à comprendre?Citation:
je peux, en cas de besoin, redéfinir le comportement de la fonction pour qu'il soit plus adapté au type réel de l'objet.
Si tu n'as pas encore abordé le problème, il vaut peut etre mieux attendre que tu aies un peu avancé dans le cours/le tutoriel que tu suis.
Pour arriver à comprendre le principe, il faut que tu comprennes ce qu'est un héritage. Les choses deviendront claires d'elles meme une fois que tu auras abordé cette partie de ton cours / tutoriel. Mais, entre temps, cela ne ferait que te déconcerter d'avantage ;)
Déconcentré moi?:D
J'ai vu les héritages. une personne hors forum m'a répondu. apparement, il serait question de choisir la bonne fonction entre 2 fonctions virtualisées. C'est ça la modif de la fonction?
B.A.-BA des cours de polymorphisme, exemple typique
http://lmgtfy.com/?q=polymorphismeCode:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 struct Animal { virtual void cri() const=0; }; struct Chat : Animal { virtual void cri() const { std::cout<<"miaou"<<std::endl; } }; struct Chien : Animal { virtual void cri() const { std::cout<<"wouaf"<<std::endl; } }; int main() { Animal* p = new Chat; p->cri(); delete p; p = new Chien; p->cri(); delete p; return 0; }
Bon, l'héritage public est la relation la plus forte qui puisse exister entre deux classes. C'est une relation EST-UN, au sens sémantique du terme (une voiture ou un camion EST-UN véhicule, un rectangle, un cercle ou un triangle EST-UN(e) forme géométrique, ...).
Il y a certaines règles à respecter pour pouvoir utiliser l'héritage, ce qui fait que, contrairement à ce que l'on apprend à l'école, la classe Carré ne peut pas hériter de la classe Rectangle, mais nous y reviendrons un peu plus tard si besoin en est ;)
La classe de base (Vehicule ou FormeGeometrique, pour respecter les deux exemples que j'ai cités) présente un certain nombre de comportements communs à l'ensemble des classes qui peuvent en dériver.
Ainsi, la classe FormeGeometrique pourrait présenter des comportements comme print(), superficie() ou encore perimetre(). Mais tu t'attends à ce que ces comportements s'adaptent au type réel de la forme géométrique : tu t'attends à ce que la fonction print() affiche "rectangle" ou "cercle" ou "triangle" selon le cas, et tu t'attends à ce que les fonctions superficie et perimetre te renvoient une valeur correctement calculée. C'est ce que l'on appelle avoir un "comportement polymorphe" (un comportement qui s'adapte au type réel de l'objet manipulé).
Pour obtenir un tel comportement, on va déclarer les fonctions print, superficie et perimetre virtual, afin d'indiquer au compilateur que l'on risque de redéfinir le comportement de ces fonctions. Et comme il est impossible de calculer la superficie ou le périmètre d'une forme géométrique dont on ignore tout, nous déclarerons les fonctions perimetre et superficie comme étant virtuelles pures. Cela indique au compilateur que nous ne disposons, au niveau de la classe de base, pas des informations qui permettent de définir un comportement cohérent et que l'on ne définit donc pas le comportement pour ces fonctions.
Cela va transformer la classe de base en une classe abstraite, qui ne pourra pas être instanciée en tant que telle. Et, pour être en mesure d'instancier les classes dérivées, il faut que les deux fonctions soient correctement définies à chaque fois. au final, nous aurons quelque chose comme
Code:
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
53
54
55 class FormeGeometrique{ public: virtual ~FormeGeometrique(){} virtual void print() const{std::cout<<"Forme Geometrique, sans precision"<<std::endl;} /* il n'existe aucun comportement "cohérent" pour calculer la superficie d'une forme géométrique * dont on ignore tout */ virtual double superficie() const = 0; /* il n'existe aucun comportement "cohérent" pour calculer le périmètre d'une forme géométrique * dont on ignore tout */ virtual double perimetre() const = 0; }; class Cercle : public FormeGeometrique{ public: Cercle(double diametre):diametre_(diametre){} virtual void print() const{std::cout<<"Cercle"<<std::endl;} virtual double superficie() const{ return 3.1415926 * diametre_*diametre_/4; } virtual double perimetre() const{ return 3.1415926 *diametre_; } private: double diametre_; }; class Rectangle : public FormeGeometrique{ public: Cercle(double hauteur, double largeur):hauteur_(hauteur), largeur_(largeur){} virtual void print() const{std::cout<<"Rectangle"<<std::endl;} virtual double superficie() const{ return hauteur_* largeur_; } virtual double perimetre() const{ return (hauteur_+largeur_)*2; } private: double hauteur_; double largeur_; }; class Triangle : public FormeGeometrique{ public: Cercle(double base, double hauteur):base_(base),hauteur_(hauteur){} virtual void print() const{std::cout<<"Triangle"<<std::endl;} virtual double superficie() const{ return hauteur_* largeur_/2; } virtual double perimetre() const{ /* ... */ } private: double base_; double hauteur_; };
Super réponses merci!
Je ne connais pas encore les règles de l'héritage. Est-ce complexe?
Ce n'est pas complexe, mais elles doivent être appliquées avec la plus grande rigueur.
La première chose à voir est qu'il faut effectivement pouvoir dire, du pur point de vue du dictionnaire, qu'un objet du type dérivé est "une sorte" d'objet du type de base (ex: une voiture est une sorte de véhicule)
Une fois cette première étape franchie, il reste la plus importante : respecter les règles de programmation par contrat ;
- Les préconditions ne peuvent pas être renforcées dans le type dérivé
- les postconditions ne peuvent pas être assouplies dans le type dérivé
- Les invariants doivent être respectés
Il faut considérer tout membre et toute fonction membre (quelle que soit la visibilité) comme étant un invariant.
Pour ce qui est des pré et postconditions, c'est ce qui t'empêche de faire dériver ListeTriee de Liste ou Carre de Rectangle : il y a des conditions beaucoup plus fortes dans ListeTriee ou dans Carre que dans Liste ou Rectangle : la liste triée doit être triée avant insertion d'un nouvelle élément et doit être triée après, et un carré doit forcément avoir quatre cotés égaux. Liste et Rectangle ne sont pas soumis à ces restrictions ;)
Absolument pas :D
Les règles à appliquer pour l'héritage public sont relativement simples en soi.
Mais le fait est que ce sont des règles de conception et qu'aucun langage, aucun programme, aussi perfectionné soit-il ne pourra jamais déterminer si une décision conceptuelle est logique et cohérente dans un contexte donné.
Il faut donc, comme je le dis, les appliquer avec la plus grande rigueur, si on veut éviter les problèmes par la suite ;)
A ce jour, personne n'a encore pu trouver une situation dans laquelle l'héritage protégé se justifierait ;)Citation:
Tu omets de dire que l'héritage, c'est aussi l'héritage protégé
Parce que, sauf cas exceptionnel, je préfères de loin l'aggrégation à l'héritage privé qui est une relation EST-INMPLEMENTE-EN-TERMES-DECitation:
et l'héritage privé
C'est une technique propre à un autre paradigme, sans objet par rapport à l'héritage public (et qui, au passage, doit néanmoins respecter les règles de l'héritage public ;))Citation:
le CRTP,
Ce n'est qu'un cas particulier de l'héritage public, qui justifie encore d'avantage que l'on applique scrupuleusement les règles de conceptions liées à l'héritage publicCitation:
l'héritage multiple,
Ce n'est qu'un cas particulier du cas particulier ci-dessus. Je n'irai pas jusqu'à dire qu'il s'agit sans doute d'une erreur de conception si tu te retrouves dans une situation dans laquelle tu en as besoin, mais je suis néanmoins pas loin de le penser.Citation:
virtuel...
J'admets volontiers que l'héritage virtuel est la base de bibliothèques de qualité qui ont fait fureur il y a quelques années (Je pense à la VCL entre autres), mais cela ne change en rien mon avis sur la question: conceptuellement parlant, c'est qu'une décision a imposé un god objet quelque part, et je suis contre les god objets de manière générale :D
L'héritage privé / agrégation est souvent utilisé pour ajouter une fonctionnalité à une classe, dès que la classe contenant la fonctionnalité à ajouter ne doit pas être construite seule l'héritage privé est meilleur (ce qui arrive quand même la plupart du temps).VSCode:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 class SomeFunctionality { protected: SomeFunctionality(int i): m_i(i) { }; void functionality() { std::cout << m_i; } private: int m_i; }; class Foo : private SomeFunctionality { public: Foo(): SomeFunctionality(42) { } using SomeFunctionality::functionality; };
Dans le cas de l'agrégation, soit on utilise une amitié (et tous les champs privés sont visible) soit la fonctionnalité peut être construite seule. (A moins que quelque chose m'échappe :)).Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 template <class F> class SomeOtherFunctionality { SomeOtherFunctionality(int i): m_i(i) { }; void functionality() { std::cout << m_i; } int m_i; friend F; }; class Bar { public: Bar(): m_functionality(42) { } void functionality() { m_functionality.functionality(); } private: SomeOtherFunctionality<Bar> m_functionality; };
Attention, je n'ai jamais dit que je refusais systématiquement l'héritage privé :D
J'aurais sans doute du dire "sauf cas particulier" au lieu de "sauf cas exceptionnel", tant il est vrai que l'on ne peut pas considérer les cas dans lesquels l'héritage privé est nécessaire comme "exceptionnels" ;).
Mais, de prime abord, j'envisagerai "par défaut" de préférence une relation "plus souple" d’agrégation qu'un héritage privé, que je réserve pour les cas où "l'agrégation ne représente définitivement pas une alternative viable (ou efficace)".
L'héritage privé est une technique merveilleuse, mais comme bon nombre de techniques de cet acabit, il faut veiller à ne l'utiliser que quand c'est réellement nécessaire. Trop souvent, les gens ont sans doute tendance à l'utiliser à tord et à travers, ce qui nuit à la qualité de leur développement en général ;)
Je te parle du type de relation qui existe entre une classe qui hérite (que ce soit de manière publique ou privée) d'une autre par rapport à celle qui existe entre une classe qui agrège une autre.
Ou, si tu préfères, des implications qu'il faudrait prendre en compte pour arriver à "faire autrement" si la fantaisie (ou le besoin) s'en faisait sentir.
Avec une agrégation, tu peux envisager de changer le type de l'objet agrégé sans rien avoir à modifié si le nouveau type de l'objet présente la même interface que le type d'origine. Dans le pire des cas, ce ne seront que quelques fonctions qui devront être corrigées pour s'accorder avec le nouveau type.
Alors qu'avec un héritage -- quel qu'il soit -- ce n'est pas seulement ce qui utilise l'interface publique de la classe de base que tu devras modifier mais aussi l'ensemble de ce qui utilise l'interface protégée de la classe de base, au stricte minimum (sans même compter une éventuelle amitié).
Pour faire une analogie simple : il existe très vraisemblablement une relation beaucoup plus forte entre toi et tes parents ou entre toi et tes enfants que la relation qui pourrait exister entre toi et tes voisins. Et ne vient pas me poser la question de savoir ce qui se passe si tes voisins sont justement tes parents ou tes enfants, ce serait faire preuve de mauvaise foi ;)!
En outre, s'il est vrai que l'héritage -- quel qu'il soit -- permet énormément de choses, il ne faut pas oublier qu'il est soumis à des restrictions importantes. A tel point qu'il est beaucoup plus facile de se passer de l'héritage -- moyennant certaines adaptations du code ou du schéma de pensée -- que de l'agrégation, qui est une relation qui, rappelons le, existe dans tout langage permettant la programmation structurée ;)
Effectivement, ma phrase n'est pas forcément claire et manque de ponctuation, dsl.
"dès que la classe contenant la fonctionnalité à ajouter ne doit pas être construite seule, alors l'héritage privé est meilleur"
-> Dans mon exemple, si on souhaite interdire la création de SomeFunctionality ou de SomeOtherFunctionality quand elle sont utilisées seules (sans agrégation/héritage privée), l'héritage privé est meilleur.
ex:En espérant que se soit plus clair. :)Code:SomeFunctionality f; // non autorisé car n'a pas de sens, la fonctionnalité n'a un sens que lorsqu'elle complète une autre classe
OK, il s'agit donc d'un mixin.
L'argument est également valable pour l'héritage protégé Il est souvent utilisé avec les classes de politique.
ok ça je comprends.Citation:
Les préconditions ne peuvent pas être renforcées dans le type dérivé
les postconditions ne peuvent pas être assouplies dans le type dérivé
Les invariants doivent être respectés
Il faut considérer tout membre et toute fonction membre (quelle que soit la visibilité) comme étant un invariant.
Par contre, ce qui suit dans votre discussion...:aie:
Quelqu'un peut me traduire?:?
Non. Le C++, c'est comme le sexe : il ne faut pas brûler les étapes.
Tu découvriras tout ça par toi-même. :)