LOCODUINO

Forum de discussion
Dépôt GIT Locoduino
Flux RSS

mardi 17 octobre 2017

45 visiteurs en ce moment

Le monde des objets (4)

. Par : Thierry

Avant d’aborder cet article, je vous propose de vous assurer que vous maîtrisez bien la notion de pointeur, essentielle ici. Si ce n’est pas le cas, prenez le temps de consulter les articles idoines : Les pointeurs (1) et Les pointeurs (2).

Cela étant dit, les bases sont posées, mais certains aspects importants de la programmation objet n’ont pas encore été expliqués…

’Vis’ ? Non ! ’this’ .

Reprenons ma version du constructeur Led() de Led pour illustrer un aspect parfois difficile à expliquer…

  1. Led(int pin)
  2. {
  3. pin = pin; // ???
  4. pinMode(pin, OUTPUT);
  5. etat = 0;
  6. }

On voit bien qu’il va y a avoir un problème avec l’argument pin qui porte le même nom que la donnée pin de la classe Led. Le C++ a prévu un mot clé permettant d’identifier à coup sûr l’objet que l’on est en train de traiter : this. C’est un pointeur sur le propriétaire de la méthode. On pourra ré-écrire la méthode ainsi :

  1. Led(int pin)
  2. {
  3. this->pin = pin;
  4. pinMode(this->pin, OUTPUT);
  5. etat = 0;
  6. }

this->pin (prononcer ’vis tou pine’ !) signifie clairement que le pin que l’on veut est celui de l’instance courante propriétaire de la méthode et pas l’argument. Il y a deux solutions pour venir à bout de ce problème récurrent : utiliser this partout où l’on parle des données de l’instance, et ainsi clairement montrer ce que l’on utilise même si il n’y a pas d’ambiguïté… C’est une façon de rendre le source plus compréhensible, même si c’est un peu plus lourd à écrire. L’autre façon consiste à différencier les arguments en les préfixant systématiquement. C’est ce que je fais dans tous les sources jusqu’à cet exemple. pin est toujours aPin s’il s’agit d’un argument. L’ambiguïté est levée puisque l’on peut écrire à nouveau pin = aPin; sans le this ! En réalité, j’emploie les deux modes, le this me servant à identifier les données de l’objet et les différencier des variables déclarées localement ou globalement :

  1. Led(int aPin)
  2. {
  3. this->pin = aPin;
  4. pinMode(this->pin, OUTPUT);
  5. etat = 0;
  6. }

Noter le changement d’accès aux méthodes et données d’un pointeur par -> au lieu du . classique lorsqu’il ne s’agit pas d’un pointeur.

Héritage épisode 2

Dans le troisième article, on a parlé de l’héritage (ou dérivation) qui consiste à dériver une classe existante pour en faire un objet légèrement différent. Grâce aux pointeurs, on va pouvoir faire faire des choses différentes à un même objet !
Un pointeur peut adresser indifféremment une instance d’une classe ou de n’importe laquelle de ses dérivées. Utilisons un new pour créer une instance d’une classe Led, et récupérons le pointeur pour le stocker dans une variable locale, puis faisons de même avec une instance de la nouvelle classe LedBicouleur :

  1. Led *pointeur = 0;
  2.  
  3. pointeur = new Led(10);
  4. pointeur->Allumer();
  5.  
  6. Led *pointeur2 = 0;
  7. pointeur2 = new LedBicouleur(11, 12);
  8. pointeur2->Allumer();

pointeur est le cas normal d’une adresse de classe Led stockée dans un pointeur de type Led *.
pointeur2 est la nouveauté. Toujours dans un pointeur de type Led *, on a posé une adresse d’une classe dérivée LedBicouleur. Lorsque la fonction Allumer() va être utilisée, c’est bien celle de LedBicouleur qui sera appelée. Par contre, comme pointeur2 est de type Led * il n’est pas possible d’appeler directement une fonction de LedBicouleur comme Allumer2()… Malgré tout, le langage a prévu de contourner ce problème en effectuant un cast. Le cast est une astuce de programmation qui n’a généralement pas de conséquence sur le code produit. On va forcer le compilateur à admettre que l’on connait mieux le type du contenu du pointeur que lui :

((LedBicouleur *) pointeur2)->Allumer2();

pointeur2 est brutalement et artificiellement converti en LedBicouleur* (attention aux parenthèses), et du coup devient capable d’accéder aux fonctions publiques de cette classe.
Un gros inconvénient est que parce que vous lui avez forcé la main, le compilateur ne vérifiera pas la justesse de la conversion. En supposant qu’une classe Aiguillage existe, j’aurais pu écrire

((Aiguillage *) pointeur2)->Droit();

et il n’y aurait pas eu de problème de compilation ! Par contre à l’exécution : plantage !

Tout cela n’est possible que parce que pointeur2 est un pointeur. Une instance simple comme Led led; ne peut pas contenir autre chose qu’un exemplaire de la classe Led.

Bien entendu ce que l’on vient de dire est applicable à des tableaux de pointeurs sur des objets de types identiques ou différents du moment qu’ils dérivent tous de la classe désignée dans la liste : Led * leds[10] peut contenir des pointeurs vers des Leds ou des dérivées comme LedBicouleur

Un peu de pureté

Lorsque l’on dérive plusieurs classes d’une même classe de base, il peut arriver que cette classe de base n’ai aucun rôle en tant que telle… On pourrait imaginer une classe ObjetRoulant dérivée en Loco, Wagon et Voiture. Mais il ne devrait pas être possible de créer une instance d’un ObjetRoulant, parce qu’en réalité il s’agit forcément d’un objet répondant à l’une des trois sous-classes (ou alors il faudra m’expliquer.)… Et si dans votre code vous n’avez pas considéré ce type d’objet, alors vous vous exposez à de potentiels problèmes d’exécution. Le langage a prévu cela avec les classes virtuelles pures, dites classes abstraites.
Une classe abstraite, comme son nom l’indique, n’a pas d’équivalent, d’instance possible dans la réalité. Elle n’est là que pour fournir un lien entre d’autres classes qui la dérivent. Pour obtenir ce type de classe, on peut passer par une fonction virtuelle pure :

  1. class ObjetRoulant
  2. {
  3. protected:
  4. byte nbEssieux;
  5.  
  6. public:
  7. byte GetNbEssieux(); // fonction normale
  8. virtual char *GetType() = 0; // fonction virtuelle pure !
  9. };

Dans le fichier cpp associé, il n’y aura pas du tout de GetType() ! Cette fonction n’a pas d’intérêt sur ObjetRoulant, elle est donc déclarée ici virtual (virtuelle) mais sans corps ( le = 0 !) pour obliger les classes dérivées à la définir. Ainsi la classe Loco :

  1. class Loco : public ObjetRoulant
  2. {
  3. public:
  4. char *GetType();
  5. };
  6.  
  7. char *Loco::GetType()
  8. {
  9. return "Loco";
  10. }

la dérivée de la classe abstraite doit absolument définir les fonctions virtuelles pures, sinon la compilation échouera.

Un autre moyen d’empêcher un utilisateur de créer une instance de votre classe est de déclarer ses constructeurs privés, ou au moins protégés si elle peut être dérivée.

  1. class ObjetRoulant
  2. {
  3. protected:
  4. byte nbEssieux;
  5. ObjetRoulant();
  6.  
  7. public:
  8. byte GetNbEssieux();
  9. virtual char *GetType();
  10. };

Statique, mais pas électrique…

Une classe est une définition, un canevas. Elle sert à définir un modèle qu’il sera ensuite possible de dériver, pour en changer -un peu- le comportement, ou d’instancier pour créer de véritables objets… Au delà de ces rôles classiques, les classes peuvent devenir des endroits de stockage pour des données et des méthodes qui ne sont pas instanciées, mais qui sont partagées par toutes les instances…
Imaginons que l’on veuille maintenir un compteur du nombre d’ObjetRoulants créés. Ce compteur fait indiscutablement partie de la classe ObjetRoulant, mais il est unique : il ne doit pas être répété pour chaque instance… De plus il va falloir trouver le moyen de l’incrémenter et de le décrémenter.

  1. class ObjetRoulant
  2. {
  3. public:
  4. static int NbObjets; // Donnée statique
  5.  
  6. protected:
  7. byte nbEssieux;
  8.  
  9. public:
  10. byte GetNbEssieux();
  11. virtual char *GetType() = 0;
  12. };

Dans cette déclaration simplifiée de la classe ObjetRoulant, est apparue la définition d’un entier, NbObjets. Il est static, ce qui signifie que cet entier n’existe qu’une et une seule fois en mémoire, il n’est pas répété dans les instances de la classe ObjetRoulant ou de ses dérivées.
Pour accéder à cet entier depuis n’importe où, il suffit de préciser le nom de sa classe propriétaire, comme on le fait pour les méthodes membres d’une classe :
ObjetRoulant::NbObjets .
Une donnée statique doit aussi être déclarée dans le fichier cpp, comme les méthodes :

  1. #include "ObjetRoulant.hpp"
  2.  
  3. int ObjetRoulant::NbObjets = 0; // déclaration
  4.  
  5. byte ObjetRoulant::GetNbEssieux()
  6. {
  7. return this->nbEssieux;
  8. }

Deux remarques sur cette déclaration : pas de mot clé static ici, et une initialisation qui est possible, par exemple à 0 dans notre cas.
Comme toutes les données d’une classe, celles ci peuvent être protected ou private. Dans ce cas, il faut éventuellement prévoir une fonction d’accès à la donnée par le monde extérieur.

  1. // HPP
  2. class ObjetRoulant
  3. {
  4. private:
  5. static int nbObjets; // Donnée statique, avec un 'n' minuscule pour le côté 'privé'.
  6.  
  7. public:
  8. int GetNbObjets();
  9. ...
  10. };
  11.  
  12. // CPP
  13. int ObjetRoulant::nbObjets = 0; // déclaration
  14.  
  15. int ObjetRoulant::GetNbObjets()
  16. {
  17. return nbObjets;
  18. }

Ce type d’écriture est possible, mais comme d’habitude l’appel à la méthode doit se faire via une instance de la classe (rappelons nous que Loco dérive de la classe ObjetRoulant, classe virtuelle pure que l’on ne peut instancier [1]) :

  1. Loco loco1;
  2. int nb = loco1.GetNbObjets();

On a dû créer une instance loco1 pour pouvoir accéder à nbObjets, et on voit bien qu’elle n’apporte rien à la fonction : this n’est utilisé nulle part…
Le C++ a prévu ce cas de figure en permettant de déclarer des fonctions static.

  1. // HPP
  2. class ObjetRoulant
  3. {
  4. private:
  5. static int nbObjets; // Donnée statique
  6.  
  7. public:
  8. static int GetNbObjets();
  9. ...
  10. };
  11.  
  12. // CPP
  13. int ObjetRoulant::nbObjets = 0; // déclaration
  14.  
  15. int ObjetRoulant::GetNbObjets()
  16. {
  17. return nbObjets;
  18. }

Rien ne change à part le mot clé static devant la méthode dans le hpp. Ce qui change, par contre, c’est le moyen d’accès à la donnée :
int nb = ObjetRoulant::GetNbObjets();

Plus d’instance créée pour l’occasion ! En contrepartie, l’écriture de fonction static obéit à une règle spécifique : puisqu’aucune instance n’est utilisée, l’utilisation du mot clé this y est strictement interdite !

Une fonction static est aussi une bonne solution pour ’ranger’ les méthodes fortement liées à une classe sans pour autant en être dépendantes. Dans quelques cas extrêmes, il m’est arrivé de coder une classe sans aucun contenu autre que des fonctions statiques regroupées sous le nom de la classe…

Enfin, pour incrémenter et décrémenter le nombre général d’objets roulants, quoi de mieux que le constructeur et le destructeur ?

  1. // HPP
  2. class ObjetRoulant
  3. {
  4. private:
  5. static int nbObjets; // Donnée statique
  6.  
  7. public:
  8. static int GetNbObjets();
  9. ObjetRoulant(); // constructeur
  10. ~ObjetRoulant(); // destructeur
  11. ...
  12. };
  13.  
  14. // CPP
  15. int ObjetRoulant::nbObjets = 0; // déclaration
  16.  
  17. ObjectRoulant::ObjetRoulant()
  18. {
  19. ObjetRoulant::nbObjets ++;
  20. }
  21.  
  22. ObjectRoulant::~ObjetRoulant()
  23. {
  24. ObjetRoulant::nbObjets --;
  25. }
  26.  
  27. int ObjetRoulant::GetNbObjets()
  28. {
  29. return nbObjets;
  30. }

Le constructeur, appelé à chaque création par new ou en direct (comme dans Loco loco1;), va augmenter le compteur d’objets. Le destructeur, appelé lui lors de chaque delete d’une instance ou par la disparition d’une instance directe, va le décrémenter.

A bientôt pour d’autres aventures objets !

[1Vous seriez vous cru capable de comprendre ce type de phrase il n’y a pas si longtemps ?

Réagissez à « Le monde des objets (4) »

Qui êtes-vous ?
Votre message

Pour créer des paragraphes, laissez simplement des lignes vides.

Lien hypertexte

(Si votre message se réfère à un article publié sur le Web, ou à une page fournissant plus d’informations, vous pouvez indiquer ci-après le titre de la page et son adresse.)

Rubrique « Programmation »

Comment gérer le temps dans un programme ?

La programmation, qu’est ce que c’est

Types, constantes et variables

Installation de l’IDE Arduino

Répéter des instructions : les boucles

Les interruptions (1)

Instructions conditionnelles : le if … else

Instructions conditionnelles : le switch … case

Comment gérer l’aléatoire ?

Calculer avec l’Arduino (1)

Calculer avec l’Arduino (2)

Les structures

Systèmes de numération

Les fonctions

Trois façons de déclarer des constantes

Transcription d’un programme simple en programmation objet

Les chaînes de caractères

Trucs, astuces et choses à ne pas faire !

Processing pour nos trains

Arduino : toute première fois !

Démarrer en Processing (1)

Les pointeurs (1)

Les pointeurs (2)

Les Timers (I)

Les Timers (II)

Les Timers (III)

Les Timers (IV)

Les Timers (V)

Bien utiliser l’IDE d’Arduino (1)

Bien utiliser l’IDE d’Arduino (2)

Piloter son Arduino avec son navigateur web et Node.js (1)

Piloter son Arduino avec son navigateur web et Node.js (2)

Piloter son Arduino avec son navigateur web et Node.js (3)

Les derniers articles

Processing pour nos trains


Pierre59

Piloter son Arduino avec son navigateur web et Node.js (3)


bobyAndCo

Piloter son Arduino avec son navigateur web et Node.js (2)


bobyAndCo

Démarrer en Processing (1)


DDEFF

Piloter son Arduino avec son navigateur web et Node.js (1)


bobyAndCo

Arduino : toute première fois !


Christian

Bien utiliser l’IDE d’Arduino (2)


Thierry

Bien utiliser l’IDE d’Arduino (1)


Christian, Dominique, Jean-Luc, Thierry

Les Timers (V)


Christian

Trucs, astuces et choses à ne pas faire !


Dominique

Les articles les plus lus

Les interruptions (1)

Les Timers (I)

Répéter des instructions : les boucles

Calculer avec l’Arduino (1)

Les Timers (II)

Les Timers (III)

Démarrer en Processing (1)

Piloter son Arduino avec son navigateur web et Node.js (1)

Les chaînes de caractères

Les pointeurs (1)