Nous avons introduit dans l’article précédent la programmation « objet » dans l’IDE Arduino, les principes du C++ et ses avantages. Continuons d’en explorer les affriolantes capacités.
Le monde des objets (2)
Des objets partout !
. Par :
, . URL : https://www.locoduino.org/spip.php?article87Visibilité, encapsulation
Nous allons maintenant développer une petite classe qui va nous servir d’exemple très simple à comprendre, mais qui fera partie d’un ensemble plus ambitieux dans les chapitres suivants...
Imaginons une Led, qui doit être initialisée avec un numéro de broche et qui peut s’allumer et s’éteindre, donc avec une variable qui stockera son état. Le clignotement est un développement ultérieur possible...
La déclaration pourrait ressembler à ça :
class Led
{
// Données
byte pin;
byte etat;
// Méthodes
void Setup(byte aPin); // initialisation de la broche pour la led.
void Allumer();
void Eteindre();
};
pin
stocke le numéro de broche dont on aura besoin à chaque changement d’état, lequel est mémorisé dans etat
. Les trois méthodes Setup()
Allumer()
et Eteindre()
permettent respectivement d’initialiser la classe et ses données, d’allumer ou d’éteindre la diode.
Petite remarque syntaxique : le ’;
’ est vraiment important à la fin de la définition de la classe. Il est obligatoire, tout comme pour un enum
ou un struct
en C.
Comme nous l’avons vu dans le premier chapitre, l’encapsulation consiste à regrouper sous un même nom tout ce qui concerne un type d’objet, données et méthodes. Il permet également de ne laisser accessible que ce qui doit l’être vu de l’extérieur. Il y a trois niveau de protection du contenu d’une classe : privé, protégé et public.
- Si rien n’est précisé comme dans notre exemple Led, tout est privé. Ce qui est privé n’est visible que des objets de la classe elle même. Cela veut dire par exemple que cette première version de la classe ne peut pas fonctionner. Tout est privé, ce qui empêche quiconque d’accéder aux méthodes
Setup()
Allumer()
etEteindre()
! Voilà une classe qui perd un peu de son intérêt. - Ce qui est déclaré protected (protégé) est visible par la classe elle même, mais aussi par les classes dérivées, concept que nous aborderons un peu plus tard.
- Enfin les éléments publics sont visibles par tous.
Ce sont les rôles de deux nouveaux mot-clés private
et public
illustrés dans la classe Led
.
class Led
{
private:
byte pin;
byte etat;
public:
void Setup(byte aPin); // initialisation de la broche pour la led.
void Allumer();
void Eteindre();
};
Leur sens est évident : private
cache les données et les méthodes aux yeux du monde, mis à part les objets de la classe Led
, et public
les rend visibles et modifiables par tout le monde.
Par exemple si on utilise la classe Led ci-dessus, le code suivant est incorrect :
setup() // setup général du .ino
{
Led maled;
maled.pin = 10; // Erreur de compilation, pin n'est pas accessible.
}
Ce qui est logique ici, puisque l’on veut que l’utilisateur de la classe passe forcément par Setup()
pour être sûr de bien initialiser la broche... Notez l’utilisation du ’.’ pour accéder aux membres d’un exemplaire de la classe comme ’maled’ ici. C’est la même syntaxe, le même principe que pour une structure.
Allons plus loin. Pour une DEL, selon la façon dont elle est câblée, un état haut (HIGH) de la broche peut l’allumer ou l’éteindre ! Voir notamment à la fin de l’article consacré aux leds.
Pour que le classe Led fonctionne dans les deux cas, il faut définir le type de montage de la led : normal (broche reliée à l’anode et fournissant le courant) ou inversé (broche reliée à la cathode et absorbant le courant), afin d’être sûr que lorsque l’on appelle la méthode Allumer()
, le résultat est bien celui espéré... Pour simplifier le codage, passons alors par une fonction privée Rafraichir
qui fera l’interprétation voulue.
/// Déclaration de la classe Led
class Led
{
private:
byte pin;
byte etat; // HIGH pour allumé, LOW pour éteint,
// quelque soit la valeur de montageInverse
bool montageInverse; // true si il faut LOW pour allumer !
void Rafraichir();
public:
void Setup(byte aPin, bool aMontageInverse = false); // initialisation de la broche
// pour la led.
void Allumer();
void Eteindre();
};
// Définition des méthodes.
void Led::Setup(byte aPin, bool aMontageInverse)
{
montageInverse = aMontageInverse;
pin = aPin;
pinMode(aPin, OUTPUT);
// Commençons diode éteinte...
Eteindre();
}
void Led::Allumer()
{
etat = HIGH; // Allumé
Rafraichir();
}
void Led::Eteindre()
{
etat = LOW; // Eteint
Rafraichir();
}
void Led::Rafraichir()
{
byte vraiEtat = etat; // on prend l'état demandé
if (montageInverse == true) // si c'est un montage inversé
vraiEtat = ! vraiEtat; // on inverse
digitalWrite(pin, vraiEtat);
}
// Programme Arduino
Led maled;
void setup()
{
maled.Setup(10, false);
}
Petite remarque syntaxique : la compilation du source commence en haut, et va vers le bas. Il est donc important que la déclaration précède la définition. En clair, la définition des méthodes doit se faire après la déclaration de la classe !
Les méthodes Allumer()
et Eteindre()
se contentent de mettre à jour etat
, puis la méthode privée Rafraichir()
est appelée et s’occupe de modifier l’état matériel de la broche en fonction de l’état demandé et du type de montage. Noter la valeur par défaut de l’argument dans la déclaration de la méthode Setup :
void Setup(byte aPin, bool aMontageInverse = false);
Le fait de donner une valeur initiale à aMontageInverse
permet d’omettre cet argument en appelant Setup()
quand sa valeur est false
. Cela permet d’écrire soit maled.Setup(10);
et aMontageInverse
est initialisé automatiquement à false
, soit maled.Setup(10, false);
, sinon il faut spécifier un état inverse avec maled.Setup(10, true);
.
Résultat de tout cela, on a ajouté une fonctionnalité à la classe sans toucher au reste du programme, et avec l’assurance d’un minimum de perturbations. Personne n’a besoin de savoir comment se fait le changement d’état, ni quelle méthode a été appelée. Rendre privé une partie du code et des données rend l’objet plus simple vu de l’extérieur, malgré une complexité intérieure qui peut être très importante. L’interface avec le reste du code reste humainement compréhensible...
On pourrait écrire la méthode privée Rafraichir()
de cette autre façon :
void Led::Rafraichir()
{
digitalWrite(pin, etat ^ montageInverse);
}
^
est l’opérateur du OU Exclusif qui donne les résultats suivants :
etat | montageInverse | résultat |
1 (HIGH) | 1 (true) | 0 (LOW) |
1 (HIGH) | 0 (false) | 1 (HIGH) |
0 (LOW) | 1 (true) | 1 (HIGH) |
0 (LOW) | 0 (false) | 0 (LOW) |
Reportez-vous à « Calculer avec l’Arduino (2) » pour des informations complémentaires sur le OU Exclusif.
Constructeurs...
Une classe n’est pas un objet. Ce serait un peu comme dire que la table des matières est le livre ! Une classe, c’est la description de ce qu’un objet de ce type doit contenir et comment il doit se comporter. A un moment, il faut donc créer l’objet proprement dit, c’est à dire mettre en place en mémoire les variables de l’objet et les initialiser. On parle d’instanciation.
Ainsi lorsque l’on écrit
Led maled;
on fait appel implicitement à une fonction très particulière : le constructeur. C’est la première méthode de l’objet à être exécutée. La définition de cette méthode spéciale n’est pas obligatoire (si vous ne souhaitez pas initialiser les données par exemple) dans la mesure où une version par défaut de ce constructeur sans arguments existe toujours, automatiquement créée par le compilateur si elle n’est pas fournie par le programmeur, mais dans ce cas le contenu des données membres de l’objet créé est indéterminé. Typiquement, les entiers ne contiendront pas forcément 0 après la création de l’objet, mais plutôt n’importe quoi !
Cette méthode particulière est facilement identifiée par le fait que son nom est celui de la classe, et qu’elle n’accepte pas de valeur de retour :
class Led
{
private:
byte pin;
byte etat;
bool montageInverse;
void Rafraichir();
public:
Led(); // Constructeur
void Setup(byte aPin, bool aMontageInverse = false); // initialisation de la broche
// pour la led.
void Allumer();
void Eteindre();
};
Led::Led()
{
pin = 0;
Eteindre();
}
Plutôt que d’avoir un constructeur qui va tout mettre à 0, puis un Setup qui va vraiment faire l’initialisation, répartissons mieux les rôles. On peut ajouter à la classe un constructeur avec des arguments :
Led::Led(int aPin, bool aMontageInverse = false)
{
pin = aPin;
montageInverse = aMontageInverse;
}
void Led::Setup()
{
pinMode(pin, OUTPUT);
Eteindre();
}
L’initialisation d’une nouvelle instance est ainsi modifiée :
Led maled(10);
maled.Setup();
Les rôles sont mieux répartis entre le constructeur qui remplit les variables locales de la classe, et le Setup()
qui les utilise pour initialiser l’objet au bon moment.
... et destructeurs
Par symétrie avec le constructeur, il y a aussi un destructeur optionnel, dont le nom commence par un caractère ~
(ça se prononce ’tilde’ en français). Il suffit de savoir que c’est possible en C++, même si son usage est assez limité sur Arduino.
Code complet
Avant de clore ce chapitre, voici le code complet de la petite classe Led :
/// Déclaration de la classe Led
class Led
{
private:
byte pin;
byte etat; // HIGH pour allumé, LOW pour éteint,
// quelque soit la valeur de montageInverse
bool montageInverse; // true si il faut LOW pour allumer !
void Rafraichir();
public:
Led(byte aPin, bool aMontageInverse = false); // constructeur complet
void Setup();
void Allumer();
void Eteindre();
};
// Définition des méthodes. Par convention, on commence par le constructeur
Led::Led(byte aPin, bool aMontageInverse)
{
pin = aPin;
montageInverse = aMontageInverse;
}
// Et le reste, dans l'ordre de définition de la classe pour s'y retrouver...
void Led::Setup()
{
pinMode(pin, OUTPUT);
// Je décide de commencer dans un état éteint...
Eteindre();
}
void Led::Allumer()
{
etat = HIGH; // Allumé
Rafraichir();
}
void Led::Eteindre()
{
etat = LOW; // Eteint
Rafraichir();
}
void Led::Rafraichir()
{
byte vraiEtat = etat; // on prend l'état demandé
if (montageInverse == true) // si c'est un montage inversé
vraiEtat = ! vraiEtat; // on inverse
digitalWrite(pin, vraiEtat);
// ou la ligne unique suivante :
// digitalWrite(pin, etat ^ montageInverse);
}
/// Fin de la classe Led
/// Partie classique du Sketch
// On crée 2 leds
Led rouge(10); // pin 10
Led verte(11); // pin 11
void setup()
{
rouge.Setup();
verte.Setup();
}
void loop()
{
rouge.Allumer();
delay(1000);
rouge.Eteindre();
delay(1000);
verte.Allumer();
delay(1000);
verte.Eteindre();
delay(1000);
}
Le premier gain de ce type d’écriture C++, c’est la lisibilité du code. La partie croquis classique setup+loop est vraiment réduite à l’essentiel et se concentre sur le comportement général. Le vieux proverbe ’Diviser pour régner’ est très utilisé en informatique et permet de simplifier les problèmes complexes en les réduisant en somme de problèmes simples. C’est exactement ce que permet l’objet et réduisant les méthodes à des rôles très simples. L’assemblage dans le croquis devient limpide !
Un autre avantage est la réutilisation. Au delà de la création de bibliothèque qui pourrait reprendre la classe, il est très simple de transférer Led
dans un autre croquis. On est sûr de ne pas en emmener trop, et au pire le nettoyage (enlever le traitement du montage inverse, par exemple) n’est pas compliqué.
Enfin, et contrairement à une idée répandue, un gain de mémoire programme est très probable. Le principe de dérivation dont il sera question dans le troisième volet pousse à une réutilisation du code existant et à une rationalisation des données. Il en résulte souvent une économie notable pour la mémoire programme. Par contre, la mémoire vive, la SRAM, peut être impactée par les fonctions virtuelles, mais ça c’est une autre histoire !
Si c’est toujours clair, nous pourrons passer au chapitre suivant. Sinon, reprenez depuis le début !