Programmation objet ou fonctionnelle

Salut,

@Vincent-Arnaud_Chapp voici la discussion sur la POO / programmation fonctionnelle.

La programmation orientée objet et la programmation fonctionnelle sont deux façons d’envisager le développement d’un programme, on parle de paradigme.

En fonctionnelle tu aura simplement des fonctions, variables, et probablement des structures quelque chose comme ça :

// DĂ©clarations
uint8_t compteur = 0;
void incrementCompteur(uint8_t & unCompteur) {
  unCompteur++;
}

// Programme principale
incrementCompteur(compteur);

Le même code avec le paradigme objet ressemblerait à ceci (avec des classes et des méthodes) :

// DĂ©claration
class Compteur {
  private:
    uint8_t valeur;
  public:
    Compteur();
    void increment();
};

Compteur::Compteur() : valeur(0) {
  // Rien Ă  faire ici
}

Compteur::increment() {
  this->valeur++;
}

// Programme principale
Compteur * compteur = new Compteur();
compteur->increment();

N’hésite pas si tu as d’autres questions :wink:

super merci ! pour l’instant c’est assez flou, mais il faut que je m’y plonge … et donc la programmation orientée objet est beaucoup plus lourde en termes de ressources ? quel est son avantage alors ?

Bonnes questions.

J’ai écris plusieurs jeux (Rubik’s Cube et Tours de Hanoï) en orienté objet et la META le supporte très bien.

Les avantages ?

  • Tu peux utiliser des concepts comme le modèle de conception MVC, pour modèle, vue et contrĂ´leur, qui te permettront de rĂ©partir les responsabilitĂ©s dans diffĂ©rentes classes. Par exemple, le modèle pourrait ĂŞtre un simple compteur, et tu dĂ©lègue Ă  la vue tout ce qui est affichage de celui-ci, etc.
  • Si tu veux porter ton projet sur un autre outil (ordinateur ou autre console programmable), tu aura surtout la couche vue Ă  modifier, si le reste n’est pas liĂ© Ă  l’API de ton outil.
  • Pour la rĂ©solution de bugs c’est un rĂ©gal, car les responsabilitĂ©s Ă©tant bien rĂ©partit : si par exemple tu as un problème d’affichage tu n’a qu’à aller voir le code de la vue concernĂ©e.

Il y a sûrement d’autres avantages à utiliser la programmation orientée objet, mais là sur le moment ma mémoire me fait défaut.

Si tu apprends à développer commence par faire des projets en fonctionnelle en suivant les workshops de l’académie. Quand tu sera à l’aise avec certains concepts avancés comme les références et les pointeurs, tu pourra alors envisager de passer à la POO :wink:

un grand merci !

1 Like

Bonjour Vincent,

Si je peux me permettre de m’immiscer dans votre discussion, je vais essayer de reformuler les choses sur un plan un peu plus conceptuel…

La Programmation Orientée Objet (POO) est un paradigme, c’est-à-dire une représentation du « monde », une manière de voir les choses, un modèle cohérent qui repose sur un fondement défini et universellement reconnu par la communauté informatique. Ce modèle de programmation a été élaboré au début des années 1960 par les norvégiens Ole-Johan Dahl et Kristen Nygaard, puis approfondi par l’américain Alan Kay dans les années 1970, alors qu’il travaillait sur le langage Smalltalk au Xeros PARC.

Pour mieux comprendre ce qu’est la POO et ce qu’elle nous apporte, je te propose de revenir un peu sur les paradigmes « classiques » de programmation qui l’ont précédée.

Programmation séquentielle

Dans ce modèle, l’ordinateur déroule une liste d’instructions et les exécute pas à pas, de façon linéaire. C’est typiquement ce qu’on fait quand on programme en assembleur par exemple. On peut tout à fait faire de la programmation séquentielle en C ou en C++, mais ça devient très vite laborieux. En effet, réutiliser une portion de code revient à faire du copier-coller de blocs d’instructions, et d’y changer éventuellement la valeur de quelques variables… On n’a pas la notion de fonction en programmation séquentielle !

Programmation procédurale

Dans ce modèle, on va justement chercher à construire une abstraction de certaines portions du code pour en faire des procédures, qu’on pourra ensuite réutiliser en les appelant par des enchaînements contrôlés dans le code. C’est exactement ce que l’on fait quand on définit une fonction en C/C++ ou en Python : on définit une routine qui réunit et factorise un bloc d’instructions au sein d’une même entité (la fonction). Et on appelle / invoque ensuite cette fonction, au moment opportun pour en dérouler la procédure, c’est-à-dire exécuter le bloc d’instructions qui la composent. Une fonction peut être appelée et rappelée autant de fois que nécessaire. Une fonction peut même s’appeler elle-même (on parle alors de fonction récursive).

Dans un programme écrit selon le paradigme procédural, on définit donc un certain nombre de variables (des booléens, des entiers, des tableaux, etc.) qu’on manipule ensuite avec des fonctions : les variables peuvent en effet être transmises aux fonctions sous la forme de paramètres (qui deviennent alors les arguments de la fonction). Mais à aucun moment on n’associe véritablement ces variables, de manière explicite, aux procédures qui les manipulent. Intrinsèquement, cela veut dire que les données et les procédures sont traitées indépendamment les unes des autres, sans tenir compte des relations étroites qui les unissent. C’est précisément ce que permet l’approche orientée objet.

Programmation orientée objet

Dans ce paradigme, on va justement associer les données et les procédures qui sont « faîtes pour aller ensemble » au sein d’une même entité : un objet. Les données vont permettre de définir la structure de l’objet (de quoi est-il fait). Ce sont elles qui vont caractériser son état. Tandis que les procédures vont permettre de définir le comportement de l’objet (quelles sont ses capacités à agir), c’est-à-dire son aptitude à entrer en interaction avec les autres objets du « monde », ou avec lui-même. La notion de monde doit ici être comprise comme une nouvelle manière d’organiser son code.

Le langage C n’est pas un langage orienté-objet.
C’est le cas, par contre, du langage C++ ou de Python.

Pour prendre un exemple en C++, d’un point de vue purement structurel, la notion d’objet est assez proche de la notion de structure de données qu’on a déjà en C :

struct pixel {
    int16_t  x;     // coordonnée horizontale
    int16_t  y;     // coordonnée verticale
    uint16_t color; // couleur exprimée dans le référentiel RGB565 utilisé sur la META
}

Les variables qui composent la structure sont appelées les membres de la structure. Cette forme déclarative nous permet de définir un modèle pour décrire de quoi est composé un pixel dans le « monde » qu’on s’apprête à créer.

En programmation orientée-objet, on va faire exactement la même chose… sauf qu’au lieu d’employer le mot-clef struct, on définira notre modèle avec le mot-clé class et on le nommera avec une majuscule :

class Pixel {
    int16_t  x;
    int16_t  y;
    uint16_t color;
}

Mais alors quelle est la différence ? On utilise juste un mot-clef différent ?…

Assurément… NON ! Une classe est bien plus qu’une simple structure de données.

Concept de Classe

J’écrivais plus haut qu’un objet est défini par :

  • ses propriĂ©tĂ©s structurelles, qui dĂ©finissent son Ă©tat,
  • et ses propriĂ©tĂ©s comportementales, qui dĂ©finissent sa capacitĂ© Ă  (inter)agir.

En POO, on va définir toutes ces propriétés au sein d’un modèle abstrait de l’objet qu’on appelle une classe. On pourrait l’assimiler à un plan de construction, ou un patron. Ce modèle va donc nous permettre de construire des objets à son image. Les objets seront en quelque sorte des concrétisations du modèle. Tous les objets construits à l’image de leur classe sont nommés les instances de la classe. Par analogie, selon une métaphore architecturale, les instances seraient toutes les maisons bâties selon le même plan de construction.

Propriétés structurelles : les attributs

Si toutes les instances d’une classe se ressemblent, puisqu’elles sont construites d’après le même patron, elles ont néanmoins leur identité propre. Toutes les maisons construites d’après le même plan se ressemblent. Elles ont toutes des murs de façade… mais si certaines ont leurs murs peints en blanc, d’autres peuvent avoir des murs jaunes, ou bleus… Les propriétés des objets définies par leur classe peuvent s’exprimer différemment d’un objet à l’autre.

Dans l’exemple que j’ai défini plus haut, les membres x, y et color de la classe Pixel représentent les propriétés structurelles de tous les objets construits selon le même modèle de Pixel. Dans la terminologie objet on appelle ces propriétés les attributs de l’objet.

Le domaine de définition de l’attribut color est caractérisé par le type uint16_t. Autrement dit, chaque instance de Pixel est caractérisé par une couleur qui peut être codée par n’importe quel entier 16-bit non-signé compris entre 0x0000 et 0xffff. On peut donc engendrer des objets pixels et leur affecter une couleur différente. Ils disposent tous de la même propriété, mais elle pourra s’exprimer différemment chez chacun d’entre eux.

Tous ces attributs sont finalement des variables dont la valeur est propre à chaque instance. On parlera aussi de variables d’instance pour les désigner.

Chaque objet possède donc sa propre identité. Une zone mémoire distincte est attribuée à chacun d’entre eux pour héberger les valeurs qui sont affectées à chacun de leurs attributs.

Propriétés comportementales : les méthodes

Bien, maintenant intéressons-nous au comportement de chaque pixel. On va le préciser au sein du modèle en définissant par exemple une fonction draw() par le biais de laquelle le pixel va pouvoir se dessiner sur l’écran de la console, ainsi qu’une fonction move() qui va nous permettre de déplacer le pixel en modifiant ses coordonnées :

class Pixel {

    // propriétés structurelles

    int16_t  x;     // coordonnée horizontale du pixel
    int16_t  y;     // coordonnée verticale du pixel
    uint16_t color; // couleur du pixel

    // propriétés comportementales

    void draw();                       // dessine le pixel Ă  l'Ă©cran
    void move(int16_t dx, int16_t dy); // déplace le pixel
};

Ces fonctions vont nous permettre d’interagir avec chaque pixel et, par conséquent, de modifier éventuellement leur état, indépendamment les uns des autres, puisque leur état leur est propre. Dans la terminologie objet, ces fonctions sont appelées des méthodes. Et comme chacune de ces méthodes peut être invoquée sur les instances, on parlera aussi de méthodes d’instance pour les désigner.

Quand on invoque la méthode d’une instance, dans la terminologie objet on dira aussi qu’on lui envoie un message. Ce message peut être porteur de paramètres : les méthodes (qui sont finalement des fonctions) peuvent en effet accepter des arguments, comme n’importe quelle fonction.

Tu remarqueras qu’ici on n’a fait que déclarer les méthodes d’instances. On n’a pas encore explicité ce que font effectivement ces méthodes. On n’a fait que spécifier les prototypes de chacune des méthodes, c’est-à-dire le nom de la méthode, la liste éventuelle des arguments qu’elle accepte (avec leurs types) et le type de l’entité éventuellement retournée par la méthode. Il va donc falloir définir chaque méthode, pour détailler leur implémentation, c’est-à-dire le bloc d’instructions qu’elles sont censées exécuter.

Déclaration et définition d’une classe

En C++, il est recommandé de séparer la déclaration et la définition d’une classe dans deux fichiers distincts :

  • Un fichier d’en-tĂŞte qui permet de dĂ©clarer la classe. Il est recommandĂ© de le nommer comme la classe, avec l’extension .h (comme header).

  • Un fichier d’implĂ©mentation qui permet de dĂ©finir la classe. Il est aussi recommandĂ© de le nommer comme la classe, avec l’extension .cpp.

On a déjà détaillé la déclaration de la classe Pixel, qu’on va maintenant reporter dans son fichier d’en-tête Pixel.h. Ce fichier d’en-tête permettra d’inclure la déclaration de la classe partout où on en aura besoin dans le code. Le compilateur aura en effet besoin de connaître les déclarations relatives à la classe lorsqu’elle sera utilisée quelque-part dans le code. Il ne faut donc pas oublier de poser un garde-fou pour éviter les inclusions multiples :

#ifndef PIXEL // garde-fou permettant de se protéger
#define PIXEL // des inclusions multiples

class Pixel {

    // propriétés structurelles

    int16_t  x;     // coordonnée horizontale du pixel
    int16_t  y;     // coordonnée verticale du pixel
    uint16_t color; // couleur du pixel

    // propriétés comportementales

    void draw();                       // dessine le pixel Ă  l'Ă©cran
    void move(int16_t dx, int16_t dy); // déplace le pixel
};

#endif

On peut maintenant définir la classe dans le fichier Pixel.cpp :

#include "Pixel.h"
#include <Gamebuino-Meta.h>

void Pixel::draw() {
    gb.display.drawPixel(this->x, this->y, this->color);
}

void Pixel::move(int16_t dx, int16_t dy) {
    this->x += dx;
    this->y += dy;
}

Quelques remarques sur l’implémentation de la classe Pixel :

  • Il faut impĂ©rativement inclure le fichier d’en-tĂŞte de la classe pour le compilateur, car il aura besoin de connaĂ®tre les dĂ©clarations des prototypes des mĂ©thodes qui sont dĂ©finies ici.

  • On inclue Ă©galement ici le fichier d’en-tĂŞte de la bibliothèque Gamebuino-Meta puisqu’on utilise la fonction gb.display.drawPixel() et que le compilateur aura besoin de connaĂ®tre la dĂ©claration de son prototype pour pouvoir compiler le fichier Pixel.cpp.

  • La dĂ©finition d’une mĂ©thode s’effectue simplement en prĂ©fixant le nom de la mĂ©thode par le nom de la classe, suivie de l’opĂ©rateur de rĂ©solution de portĂ©e ::.

  • Tu as certainement remarquĂ© l’usage d’un mot-clef particulier (this), ainsi que la curieuse notation this-> … en fait, this est un pointeur vers l’objet lui-mĂŞme. C’est ce qu’on appelle une auto-rĂ©fĂ©rence. Et l’usage veut que l’objet utilise cette auto-rĂ©fĂ©rence lorsqu’il a besoin d’accĂ©der Ă  l’une de ses propriĂ©tĂ©s (attributs ou mĂ©thodes). Par exemple, this->x lui retourne la valeur de son attribut x. Il pourrait y accĂ©der directement, car l’auto-rĂ©fĂ©rence n’est pas obligatoire, mais elle est recommandĂ©e pour mieux identifier quand l’objet fait rĂ©fĂ©rence Ă  l’une de ses propriĂ©tĂ©s dans le code. L’auto-rĂ©fĂ©rence this permet Ă©galement Ă  l’objet de communiquer un pointeur qui le rĂ©fĂ©rence Ă  un autre objet.

Maintenant qu’on a le modèle… comment fait-on pour créer des objets d’après ce modèle ?

Constructeurs

Une classe définit le plan de construction de ses instances, on vient de le voir, mais son rôle ne s’arrête pas là. Elle est aussi responsable de leur construction. Et pour cela elle doit exposer au reste du monde une méthode particulière, qu’on appelle un constructeur. De cette façon lorsqu’un objet quelconque du monde aura besoin de créer un nouveau pixel, il suffira qu’il envoie un message à la classe Pixel pour lui demander de construire une nouvelle instance.

Le constructeur est donc une méthode qui relève de la classe, on parlera aussi de méthode de classe. Et il porte nécessairement le nom de sa classe. Par exemple le constructeur de la classe Pixel sera noté Pixel(). Et comme n’importe quelle fonction, un constructeur peut aussi accepter des arguments. Par exemple, il peut être utile de préciser au constructeur de la classe Pixel les coordonnées du pixel que l’on souhaite instancier :

#ifndef PIXEL
#define PIXEL

class Pixel {

    // attributs

    int16_t  x;     // coordonnée horizontale du pixel
    int16_t  y;     // coordonnée verticale du pixel
    uint16_t color; // couleur du pixel

    // méthodes

    void draw();                       // dessine le pixel Ă  l'Ă©cran
    void move(int16_t dx, int16_t dy); // déplace le pixel

    // déclaration du constructeur

    Pixel(int16_t x, int16_t y);
};

#endif

Note bien ici qu’on n’a pas précisé le type de retour du constructeur… et c’est normal, puisque son type est nécessairement Pixel.

Voyons maintenant comment définir ce constructeur :

Pixel::Pixel(int16_t x, int16_t y) {
    this->x = x;
    this->y = y;
    this>color = 0xffff;
}

Une chose importante à noter ici : les arguments de la fonction portent le même nom que les attributs de l’objet… on aurait pu faire autrement, en nommant les arguments i et j au lieu de x et y. Il n’y aurait pas eu d’ambiguité pour distinguer les attributs des arguments. Mais je les ai volontairement nommés de la même manière pour mettre le doigt sur la notion de masquage des variables.

Ă€ retenir

Lorsqu’une variable est déclarée dans un bloc imbriqué avec le même nom qu’une autre variable déclarée dans le bloc supérieur (c’est précisément le cas ici), chaque nouvelle déclaration dans un sous-bloc d’une variable de même nom masque la précédente. Autrement dit, dans le constructeur (le sous-bloc), les arguments x et y masquent les attributs, qui sont déclarés au niveau de la classe (le bloc supérieur).

Mais on lève cette ambiguité en utilisant l’auto-référence this pour expliciter le fait que l’on souhaite accéder aux attributs pour leur affecter la valeur des arguments passés au constructeur.

Tu vois que, dans le cas de ce constructeur, on passe les arguments x et y qui seront directement affectés aux attributs correspondants de l’objet. Mais on fixe également des valeurs par défaut à tous les autres attributs. Le rôle d’un constructeur est, non seulement de créer l’objet en mémoire, mais également de l’initialiser.

Il existe en C++ une autre forme d’écriture, plus concise, avec une liste d’initialisation. On appelle ça un sucre syntaxique, mais il exprime exactement la même chose :

// définition du constructeur avec une liste d'initialisation

Pixel::Pixel(int16_t x, int16_t y) : x(x), y(y), color(0xffff)) {}

Il est également possible de définir plusieurs constructeurs au sein d’une même classe :

Pixel::Pixel() {
    this->x = 0;
    this->y = 0;
    this>color = 0xffff;
}

Pixel::Pixel(int16_t x, int16_t y) {
    this->x = x;
    this->y = y;
    this>color = 0xffff;
}

Pixel::Pixel(int16_t x, int16_t y, uint16_t color) {
    this->x = x;
    this->y = y;
    this>color = color;
}

C’est tout à fait possible dans la mesure où les constructeurs sont déclarés avec un nombre et/ou des types d’arguments différents. On appelle ça la surcharge de constructeurs.

Pour intégrer tous ces constructeurs en une seule et même définition, on peut aussi faire de la surcharge automatique en définissant des valeurs par défaut dans le prototype du constructeur :

// déclaration d'un constructeur avec des valeurs par défaut

Pixel::Pixel(int16_t x=0, int16_t y=0, uint16_t color=0xffff);

Puis vient la définition simplifiée du constructeur (avec une liste d’initialisation) :

Pixel::Pixel(int16_t x, int16_t y, uint16_t color) : x(x), y(y), color(color) {}

Pour éviter d’avoir à écrire du code redondant, c’est-à-dire du code qui est dupliqué d’un constructeur à l’autre, on peut aussi utiliser un mécanisme de délégation entre constructeurs :

// déclaration d'un constructeur avec des valeurs par défaut

Pixel::Pixel(int16_t x=0, int16_t y=0, uint16_t color=0xffff);

// déclaration d'une surchage du constructeur

Pixel::Pixel(uint16_t color);

Cette forme d’écriture permet de passer au constructeur une liste réduite d’arguments lorsque tous les autres peuvent être définis par défaut. Et voilà comment les définir en écrivant un code réduit à un minimum d’instructions :

// définition du constructeur délégué par défaut

Pixel::Pixel(int16_t x, int16_t y, uint16_t color) : x(x), y(y), color(color) {}

// définition de la surcharge du constructeur par délégation

Pixel::Pixel(uint16_t color) : Pixel(0, 0, color) {}

Un constructeur qui n’a pas d’argument ou dont tous les arguments ont des valeurs par défaut est ce qu’on appelle un constructeur par défaut.

Si tu ne définis aucun constructeur pour une classe, le compilateur déclarera implicitement un constructeur par défaut, sans arguments et qui ne fait rien. Autrement dit qui n’initialise rien ! Il faut donc t’assurer qu’il n’y a pas un risque de corruption de l’intégrité de tes objets si tu procèdes ainsi.

Maintenant qu’on dispose d’une panoplie de constructeurs, voyons comment on peut instancier des objets Pixel.

Notions fondamentales liées à l’instanciation des objets

En C++, il y a deux façons de créer un objet :

  • Soit de manière directe : dans ce cas, la durĂ©e de vie de l’objet est limitĂ©e Ă  la portĂ©e du bloc dans lequel il a Ă©tĂ© dĂ©clarĂ©. Si on crĂ©e un objet Ă  l’intĂ©rieur d’une fonction par exemple, la durĂ©e de vie de l’objet est limitĂ©e Ă  la portĂ©e de la fonction. SitĂ´t qu’on ressort de la fonction, l’objet est tout simplement dĂ©truit et la mĂ©moire qui Ă©tait occupĂ©e par cet objet est immĂ©diatement libĂ©rĂ©e. Cette forme d’instanciation est souvent dĂ©signĂ©e comme une instanciation statique… mais ce terme est mal choisi et souvent mal employĂ© car il peut prĂŞter Ă  confusion. Le terme static a en effet un autre sens en C++… mais lĂ  on sort du cadre :slight_smile:

  • Soit de manière indirecte, en utilisant un pointeur : dans ce cas, il faut ĂŞtre extrĂŞmement prudent, car la mĂŞme règle s’applique… mais uniquement au pointeur de l’objet, et pas Ă  l’objet lui-mĂŞme ! C’est-Ă -dire que lorsque le pointeur est dĂ©truit (quand on sort de la fonction par exemple), l’objet qu’il pointait ne l’est pas ! Autrement dit, il continue Ă  vivre quelque-part en mĂ©moire, sauf que le seul lien qui permettait d’y accĂ©der (le pointeur), lui, a Ă©tĂ© dĂ©truit… C’est le phĂ©nomène qui est Ă  l’origine de ce que l’on appelle des fuites de mĂ©moire. En effet, une zone mĂ©moire est allouĂ©e au moment de la crĂ©ation de l’objet… mais elle n’est jamais libĂ©rĂ©e lors de la destruction de son pointeur. Il va falloir procĂ©der explicitement Ă  la destruction de l’objet, avant de dĂ©truire son pointeur. Dans ce cas, on parle d’instanciation par allocation dynamique.

Pour mieux fixer ces notions, considérons ce nouvel exemple :

#include "Pixel.h"

void spawnPixel() {
    uint8_t  x = random(80);
    uint8_t  y = random(64);
    uint16_t c = random(0xffff);

    Pixel p(x, y, c);
    p.draw();
}

void setup() {

}

void loop() {
    spawnPixel();
}

Ici, les choses se déroulent simplement. À chaque fois qu’on exécute la fonction spawnPixel(), une variable p de type Pixel est créée, et on affecte à cette variable une instance de la classe Pixel. La variable p occupe alors une zone en mémoire (dans la RAM).

Puis on envoie le message draw() à p pour lui demander de se dessiner à l’écran. Note ici que l’envoi du message est effectué avec la notation pointée p.draw().

Lorsque la fonction se termine, la variable p est détruite, et l’espace mémoire qu’elle occupait est automatiquement libéré.

Dans le cas de l’instanciation par allocation dynamique, les choses sont beaucoup plus subtiles :

#include "Pixel.h"

void spawnPixel() {
    uint8_t  x = random(80);
    uint8_t  y = random(64);
    uint16_t c = random(0xffff);

    Pixel* p = new PIxel(x, y, c);
    p->draw();
}

void setup() {

}

void loop() {
    spawnPixel();
}

Ici, on déclare une variable p qui est un pointeur sur un objet de type Pixel. Remarque bien la notation qui est employée, on utilise une étoile pour déclarer le pointeur : Pixel* p. Et la création de l’objet est effectuée à l’aide de l’opérateur new. À partir de là, l’objet existe quelque-part en mémoire (il occupe une zone de la RAM), et on dispose de son pointeur p pour y accéder et interagir avec lui. Pour envoyer un message à l’objet en passant par son pointeur, on utilise la notation p->draw().

Mais lorsque la fonction se termine, c’est le pointeur p qui est détruit… pas l’objet lui-même !!! Grave ça dans ta mémoire à coups de burin, c’est une nuance fondamentale que tu dois absolument retenir.

Après l’exécution de la fonction spawnPixel(), l’objet existe toujours quelque part en mémoire, et on n’a plus aucun moyen d’y accéder, puisque son pointeur a été détruit. Cet exemple illustre donc la mauvaise façon de faire ! Il manque un élément crucial : l’appel explicite à la procédure de destruction de l’objet :

void spawnPixel() {
    uint8_t  x = random(80);
    uint8_t  y = random(64);
    uint16_t c = random(0xffff);

    Pixel* p = new PIxel(x, y, c);
    p->draw();

    // destruction explicite de l'objet désigné par le pointeur
    // pour garantir la libération de la mémoire qu'il occupait

    delete p;

    // ensuite le pointeur est détruit à son tour
    // en sortant de la fonction
}

Ici, la destruction est exprimée de manière explicite avec l’opérateur delete. L’objet pointé par p est donc effectivement détruit, et la zone mémoire qu’il occupait est libérée. Ceci garantit la récupération de la mémoire. Il n’y a plus de risque de fuite. Le pointeur p est ensuite détruit à son tour, de manière automatique, lorsque l’exécution de la fonction est terminée.

Encore faut-il que la classe procède correctement à la destruction de l’objet et de toutes les ressources qu’il possédait de son côté… Pour détruire complètement un objet, et du même coup résoudre la problématique des fuites de mémoire potentielles, la classe doit exposer au reste du monde une méthode spéciale, qu’on appelle un destructeur.

Destructeurs

Un destructeur est une méthode de classe qui est exécutée quand une de ses instances est détruite. Que ce soit dans le cas où l’objet a été instancié de manière directe, ou dans le cas d’une instanciation par allocation dynamique de la mémoire avec l’opérateur new, le destructeur de la classe est automatiquement appelé pour passer un coup de balai et effectuer le nettoyage nécessaire avant que l’objet soit supprimé de la mémoire.

Pour des classes simples, c’est-à-dire celles qui définissent leurs attributs en leur affectant des valeurs simples (comme des entiers ou des booléens par exemple), ou en leur affectant des objets instanciés de manière directe, un destructeur n’est pas nécessaire. Le compilateur fera le ménage automatiquement et libèrera la mémoire pour toi.

Par contre, si ton objet possède des ressources allouées dynamiquement, ou si tu as besoin d’effectuer une maintenance quelconque avant que l’objet ne soit détruit, le destructeur est l’endroit parfait pour le faire, car c’est généralement la dernière chose à faire avant que l’objet ne disparaisse définitivement.

Un destructeur est une méthode qui porte le nom de la classe (comme le constructeur), mais son nom doit être précédé du symbole ~ (tilde) :

#ifndef PIXEL
#define PIXEL

class Pixel {

    // attributs

    int16_t  x;     // coordonnée horizontale du pixel
    int16_t  y;     // coordonnée verticale du pixel
    uint16_t color; // couleur du pixel

    // méthodes

    void draw();                       // dessine le pixel Ă  l'Ă©cran
    void move(int16_t dx, int16_t dy); // déplace le pixel

    // déclaration du constructeur

    Pixel(int16_t x, int16_t y);

    // déclaration du destructeur

    ~Pixel();
};

#endif

Si aucun destructeur n’est défini explicitement, le compilateur en ajoute un par défaut… qui ne fait rien.

Dans le cas précis de la classe Pixel, les attributs sont définis par des types simples, ils seront donc automatiquement détruits par le compilateur. Il n’y rien de particulier à faire au niveau du destructeur :

Pixel::~Pixel() {

    // il n'y a rien de particulier Ă  faire ici
    // puisqu'aucune ressource n'a été allouée
    // de manière dynamique par la classe Pixel

}

La définition d’un tel destructeur est donc inutile ici, puisqu’elle se résume à ce que ferait le destructeur par défaut. Tu peux donc te permettre de ne pas le déclarer explicitement au niveau de ta classe. Le compilateur le fera de manière automatique. Mais tu rencontreras probablement bien des cas ou un destructeur est nécessaire…

Bien… mon intention n’était pas de te faire un cours complet sur la programmation orientée-objet. Il y a encore énormément de choses à dire sur le sujet, et des notions fondamentales à détailler comme :

  • l’encapsulation,
  • les accesseurs et les mutateurs,
  • l’hĂ©ritage,
  • l’hĂ©ritage multiple,
  • le polymorphisme
  • …
  • et j’en passe…

Tu trouveras sans difficultés de nombreux cours détaillés et pertinents sur le Net.
Je ne vois pas l’intérêt de m’y substituer ici…

Je voulais juste te donner quelques clés pour mieux appréhender le concept général de la POO. J’espère que j’y suis un peu parvenu…

J’ajouterai également que j’ai volontairement proposé des illustrations des quelques concepts que je t’ai présentés en m’appuyant sur le langage C++ car, malheureusement, la META n’est pas du tout la bonne plateforme pour expérimenter la programmation orientée-objet avec Python. Elle ne dispose pas d’assez de mémoire (RAM) pour supporter l’implémentation qui est proposée par Gamebuino en s’appuyant sur CircuitPython. Si tu veux te frotter à la POO avec Python sur une console de jeux programmable, je te suggère plutôt d’aller voir ce que l’on peut faire avec le PyGamer d’Adafruit qui a été mieux dimensionné pour cela. Adafruit est d’ailleurs le créateur du projet CircuitPython qui a été, par la suite, adapté à la META… mais avec de nombreuses limitations, eu égard à l’espace mémoire restreint dont dispose la META et la volonté de Gamebuino d’avoir conservé le lien avec sa bibliothèque native <Gamebuino-Meta.h> écrite en C++ comme une sous-couche nécessaire à son adaptation de CircuitPython.

Voilà, au plaisir de découvrir tes créations à venir,
Amicalement,
Steph

3 Likes

c’est passionnant, merci ! et c’est marrant, je suis sociologue de métier, et je trouve des parallèles intéressants entre “notre” représentation du monde et celle qui est portée par la POO …
Je n’ai pas encore tout lu par contre, c’est compliqué pour moi et il faut d’abord que je m’approprie mieux les bases de la programmation C++, mais je mets ce post précieusement de côté pour y revenir régulièrement au fur et à mesure que ma compréhension s’affine.
A bientĂ´t !

Avec plaisir Vincent !
Oui, l’informatique s’est beaucoup nourrie de ce que l’on observe dans le monde du vivant ou dans les sociétés. Des tas de modèles sont apparus et reflètent cette empreinte quand on sait la lire :wink:
N’hésite pas à revenir poser des questions plus précises si tu en ressens le besoin.
Je suppose qu’il y’aura toujours une bonne âme pour venir t’aider ici.
Ă€ bientĂ´t !