LOCODUINO

Le monde des objets

Le monde des objets (4)

.
Par : Thierry

DIFFICULTÉ :

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...

Led(int pin)
{
  pin = pin; // ???
  pinMode(pin, OUTPUT);
  etat = 0;
}

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 :

Led(int pin)
{
  this->pin = pin;
  pinMode(this->pin, OUTPUT);
  etat = 0;
}

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 :

Led(int aPin)
{
  this->pin = aPin;
  pinMode(this->pin, OUTPUT);
  this->etat = 0;
}

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 :

Led *pointeur = 0;

pointeur = new Led(10);
pointeur->Allumer();

Led *pointeur2 = 0;
pointeur2 = new LedBicouleur(11, 12);
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 :

class ObjetRoulant
{
protected:
  byte nbEssieux;

public:
  byte GetNbEssieux(); // fonction normale
  virtual char *GetType() = 0;  // fonction virtuelle pure !
};

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 :

class Loco : public ObjetRoulant
{
public:
  char *GetType();
};

char *Loco::GetType()
{
  return "Loco";
}

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.

class ObjetRoulant
{
protected:
  byte nbEssieux;
  ObjetRoulant();

public:
  byte GetNbEssieux();
  virtual char *GetType();
};

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.

class ObjetRoulant
{
public:
  static int NbObjets;  // Donnée statique

protected:
  byte nbEssieux;

public:
  byte GetNbEssieux();
  virtual char *GetType() = 0;
};

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 :

#include "ObjetRoulant.hpp"

int ObjetRoulant::NbObjets = 0;  // déclaration

byte ObjetRoulant::GetNbEssieux()
{
  return this->nbEssieux;
}

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.

// HPP
class ObjetRoulant
{
private:
  static int nbObjets;  // Donnée statique, avec un 'n' minuscule pour le côté 'privé'.

public:
  int GetNbObjets();
...
};

// CPP
int ObjetRoulant::nbObjets = 0;  // déclaration

int ObjetRoulant::GetNbObjets()
{
  return nbObjets;
}

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]) :

Loco loco1;
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.

// HPP
class ObjetRoulant
{
private:
  static int nbObjets;  // Donnée statique

public:
  static int GetNbObjets();
...
};

// CPP
int ObjetRoulant::nbObjets = 0;  // déclaration

int ObjetRoulant::GetNbObjets()
{
  return nbObjets;
}

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 ?

// HPP
class ObjetRoulant
{
private:
  static int nbObjets;  // Donnée statique

public:
  static int GetNbObjets();
  ObjetRoulant();  // constructeur
  ~ObjetRoulant();  // destructeur
  ...
};

// CPP
int ObjetRoulant::nbObjets = 0;  // déclaration

ObjectRoulant::ObjetRoulant()
{
  ObjetRoulant::nbObjets ++;
}

ObjectRoulant::~ObjetRoulant()
{
  ObjetRoulant::nbObjets --;
}

int ObjetRoulant::GetNbObjets()
{
  return nbObjets;
}

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. On maintient ainsi le nombre total d’objets roulant juste en créant et en détruisant ces objets !

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 ?

4 Messages

  • Le monde des objets (4) 27 avril 2018 16:17, par Lk

    Bonjours, merci pour votre article.
    J’aimerais pouvoir créer une classe dont le constructeur prennent en paramètre un objet pour pouvoir récupérer les variables qui lui sont définies. J’ai essayé avec les pointeurs, sans réussite.

    //Le fichier .cpp

    #include "CaptPos.h"
    #include "Moteur.h"

    CaptPos : : CaptPos (int a0, int captA, Moteur *motA) //avec et sans *

    Et j’obtiens
    “error : no matching function for call to ’Moteur::Moteur()’”

    Merci de m’aider à résoudre mon problème.

    Répondre

  • Le monde des objets (4) 27 avril 2018 16:33, par Thierry

    Bonjour

    Le mieux est de poser la question sur le forum en mettant en fichier attaché le source complet, tel que vous l’imaginez. Difficile de savoir ce que vous voulez réellement faire avec trois lignes...

    Répondre

    • Le monde des objets (4) 28 avril 2018 15:00, par Lk

      Ce n’est pas vraiment un projet de modélisme ferroviaire, et j’ai pu voir que ce n’était pas vraiment recommander de s’inscrire sinon.

      Voici Moteur.h

      class Moteur
      {
        public:
          Moteur(int E, int M);
          void reculer();
          void avancer();
          void arreter();
        private:
          int _E;
          int _M;
      };

      et j’aimerais pouvoir utiliser dans la classe CaptPos un objet Moteur (pour l’initialisation et le retour a (0 ;0) de mon repère) qu’on lui mettrai en paramètre :

      CaptPos.h

      class CaptPos
      {
        public:
          CaptPos (int a0, int captA, Moteur motA);
          void initialiser();
          void Pos();
        private:
          int _a0, _captA;
          Moteur _motA;
      };

      Merci si vous pouvez m’aider.
      PS : je vois que les accolades ne s’affichent pas mais elles y sont.

      Répondre

  • Le monde des objets (4) 1er mai 2018 14:07, par Thierry

    Je ne devrais pas répondre puisqu’il ne s’agit pas de petits trains, mais je vais le faire quand même parce que la réponse peut aider d’autres programmeurs débutants. Accessoirement, il faudra que j’ajoute un petit chapitre sur le sujet dans mon article sur les pointeurs (http://www.locoduino.org/spip.php?a...)...

    Une classe comme Moteur n’est pas un type simple comme un entier ou un byte. Copier le contenu d’un exemplaire d’une classe dans un autre n’est pas trivial. La première méthode consiste à utiliser un constructeur particulier, dit constructeur par copie, qui reçoit l’original en argument et copie le contenu dans ’this’.

    Moteur(const Moteur &original)
    {
    this->_E = original._E;
    this->_M = original._M;
    }

    Le ’const’ dit que personne ne modifiera le contenu de ’original’ dans le corps du constructeur, et le ’&’ permet à la fois de passer un pointeur plutôt qu’une classe entière (gain de mémoire sur la pile), et de manipuler l’objet comme s’il n’était pas un pointeur ! Notez le ’original.’ au lieu de ’original->’ dans le code.

    Mais l’usage d’un constructeur suppose l’utilisation d’une allocation dynamique ’new’, ce qui signifie des pointeurs...

    L’autre possibilité est de définir ce que signifie un ’=’ pour cette classe. On le fait ainsi :

    class Moteur
    {
      public:
        ...
        Moteur &operator=(const Moteur &original);

    On déclare un ’operator=’ dans la classe Moteur, qui prend en argument un original et qui renvoie un pointeur sur la version modifiée. En réalité, la valeur de retour n’a pas trop d’importance... Dans le cpp correspondant, on va ajouter la fonction :

    Moteur &Moteur::operator=(const Moteur &original)
    {
        this->_E = original._E;
        this->_M = original._M;
        return *this;
    }

    Une fois cela écrit, il faut déclarer correctement le constructeur de CaptPos en utilisant la même syntaxe :

    CaptPos (int a0, int captA, const Moteur &motA);

    et de coder correctement ce constructeur :

    CaptPos::CaptPos(int a0, int captA, const Moteur &motA)
    {
      this->_a0 = a0;
      this->_captA = captA;
      this->_motA = motA;
    }

    A la faute de frappe près (je n’ai pas compilé pour vérifier ma syntaxe), ça devrait le faire !

    Répondre

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 »

Les derniers articles

Les articles les plus lus