Openframeworks . Qu'est-ce qu'un pointeur ?

2008.10.21

Douglas Edric Stanley

Prenons un cas de figure relativement typique : nous voulons faire apparaître des centaines d'objets graphiques sur l'écran et nous voulons qu'ils apparaissent et disparaissent de manière fluide et sans accrocs.

Disons aussi que chacun de ces objets sera une représentation tirée de la même image ou aléatoirement choisie parmi une liste relativement courte d'images. Si nous chargeons chacune de ces images, on risque de faire une longue pause lorsque la machine cherche le fichier et l'importe dans notre programme. Notre but sera donc de charger une fois le(s) image(s) et que tous les objets se servent de ce(s) même(s) image(s) sans les recharger dans la mémoire. Comme cela, on ne ralentira pas la machine chaque fois qu'une nouvelle image sera créée, et qu'en plus, on n'alourdira pas inutilement la mémoire en stockant plusieurs fois la même image.

Importer une image dans votre programme

Commençons par l'importation des images. Voici une image (teapot.png) tirée d'une édition de 1916 d'Alice au pays des merveilles :

Pour l'utiliser, il nous faut un dossier data dans lequel nous allons mettre le fichier .jpg, .gif ou .png. Ici il s'agit de teapot.png. Nous avons utilisé le format png (png-24 précisément) pour pouvoir inclure une couche alpha (une transparence progressive à 8 octets). Créez donc ce dossier dans votre projet, et placez ce fichier dedans.

Si cela vous rappelle quelque chose, c'est que vous avez lu le chapitre sur l'utilisation des images photographiques dans Processing. L'écriture du code sera légèrement différent dans OpenFrameworks, mais le procédé est essentiellement le même : importer le fichier d'une image photographique dans le dossier data, importer cette photo dans une variable, utiliser cette variable dans votre programme pour afficher la photo où on veut sur l'écran.

Ajouter au header testApp.h de votre projet une déclaration d'une variable de type ofimage qui contiendra votre image :


ofimage image;

Ensuite, aller dans votre fichier d'implémentation testApp.cpp, importez l'image dans le setup() et affichez l'image dans le draw(). Si vous avez une image avec une couche alpha (comme la notre), activez également au début du programme l'utilisation des couches alpha (cf. ofEnableAlphaBlending()) :


void testApp::setup(){
    ofEnableAlphaBlending();
    image.loadImage("teapot.png");
}

void testApp::draw(){
    ofBackground(255,255,255);
    image.draw(50,100);
}

Executez votre programme en tapant cmd-r (xCode) ou F9 (Code::Blocks). Vous devriez voir quelque chose qui ressemble à ceci:

Créer une classe TeaTime

Nous allons créer plusieurs objets qui vont chacun afficher cette image. Commençons par créer les fichiers nécessaires. On va appeler cette classe TeaTime. Créez donc les fichiers TeaTime.h et TeaTime.cpp et remplissez-les par le code suivant (que nous allons expliquer dans un instant, rassurez-vous).

Voici les déclarations de la classe TeaTime :


#ifndef _TEATImE
#define _TEATImE

#include "ofmain.h"

class TeaTime {

public:
    TeaTime(ofimage*, float, float);
    void update();
    void draw();

private:
    ofimage *pointeur;
    float x,y;

};

#endif

Voici l'implémentation de la classe TeaTime :


#include "TeaTime.h"

TeaTime::TeaTime(ofimage* adresse, float nx, float ny) {
    // stocker l'adresse de l'image dans un "pointeur"
    pointeur = adresse;
    x = nx; // stocker la nouvelle position x
    y = ny; // stocker la nouvelle position x
}

void TeaTime::update() {
    // bouger un peu
    x += ofRandom(-1,1);
    y += ofRandom(-1,1);
}

void TeaTime::draw() {
    // aller dans l'objet pointé par le pointeur
    // et activer la méthode draw()
    pointeur->draw(x,y);
}

Notez bien que nous avons utilisé deux nouveaux signes, * et ->. Comme vous avez probablement deviné, ces signes introduisent l'usage des pointeurs dans notre programme. Encore une fois, ne vous inquiétez pas -- nous allons les expliquer dans un instant.

maintenant, pour créer des objets de notre classe TeaTime, retournez au testApp et modifiez ses deux fichiers.

Commençons par testApp.h. Connectez les deux fichiers ensemble en ajoutant une instruction #include :


#include "TeaTime.h"

Ensuite, ajoutez un vecteur d'objets à partir de la classe TeaTime. On va appeler ce vecteur d'objets vaisselle :


ofimage image;
vector<TeaTime> vaisselle;

Enfin, modifiez votre testApp.cpp en ajoutant l'animation, l'affichage et l'augmentation des objets de votre classe TeaTime. Notez également l'usage d'un nouveau signe, le signe & qui indique également l'usage d'un pointeur dans notre programme :


#include "testApp.h"

void testApp::setup(){
    ofEnableAlphaBlending();
    image.loadImage("teapot.png");
}

void testApp::update(){
    for(int i=0; i<vaisselle.size(); i++)
        vaisselle[i].update();
}

void testApp::draw(){
    ofBackground(255, 255, 255);
    for(int i=0; i<vaisselle.size(); i++)
        vaisselle[i].draw();
}

void testApp::mousePressed(int x, int y, int button){
    // ajouter un nouveau objet en passant l'adresse
    // de l'image, suivi de la position x,y du clic
    vaisselle.push_back( TeaTime(&image, x, y) );
}

Executez votre programme en appuyant sur F9 (Code::Blocks) ou Cmd-R (xCode). Observez ce qui se passe quand vous cliquez sur l'écran : un nouvel objet se créé dans le vecteur vaisselle, et s'anime. Sachez que chacun de ces objets partage la même image et ne charge pas inutilement ce fichier plusieurs fois. En réalité, chacun des objets « pointe » vers la même image dans la mémoire de la machine. D'où l'expression pointeur.

& | * | ->

Parlons de nos trois nouvelles signes : &, * et ->. Et rappelons-nous surtout notre objectif : a) charger une image dans la mémoire de notre programme, b) partager cette image entre tous les objets en faisant de sorte que chaque objet « pointe » vers l'image d'origine.

Le premier signe, & signifie « l'adresse de... ». En informatique toute valeur existe quelque part dans la mémoire de la machine. Jusqu'ici nous manipulions ces valeurs via leur noms : x, y, largeur, hauteur, image. Il est aussi possible de demander à la machine, « au fait, où est-ce que vous avez stocké mon image dans la mémoire ? ». C'est le rôle de & : il donne une valeur qui correspond à l'adresse numérique de cette variable. A noter : dans le cas d'une valeur complexe, comme une image qui contient plusieurs valeurs pour chacun des pixels, cette adresse indique le début* de ces pixels dans la mémoire.

Le deuxième signe, * signifie « pointeur de... » ou « la valeur qui se trouve à... » selon le contexte. Pour l'instant, nous parlerons de la première signification, « pointeur de... ». En gros, le signe * indique « attention, cette variable n'est pas une variable normale », d'où l'usage d'un astérisque dans l'écriture. Quand on déclare une variable avec un astérisque, nous indiquons que cette variable ne contiendra pas une valeur, mais l'adresse d'une autre variable qui contient la valeur. Évidement, ces deux signes font une paire : une variable avec un astérisque peut contenir l'adresse d'une autre variable. Du coup si j'écris ceci :


int x = 5;
int *ptr_x = &x;

Dans ce cas, la variable x contiendra la valeur 5, alors que ptr_x contiendra l'adresse de la mémoire où réside la variable x.

Toute variable occupe de la mémoire. Si j'écris int x = 5, l'ordinateur créera assez de mémoire pour conserver une valeur de type int (entier). Voici une image très approximative de cette idée.

Comme on voit dans l'illustration, x contient une valeur, 5, stockée dans la barrette de RAm à l'adresse 660. Ensuite le pointeur de x, ptr_x contient la valeur 660. Il existe donc des variables qui contiennent des valeurs, puis des variables qui contiennent des « pointeurs » vers l'adresse d'autres variables.

Dans l'illustration, on voit également le pointeur ptr_image qui pointe à son tour vers l'adresse 663. Celle-ci concerne le début l'image nommée img. Avec ce numéro, nous pouvons copier plusieurs fois cette adresse à tous les objets qui en auraient besoin.

On est prêt maintenant à comprendre notre code de la classe TeaTime. Regardez la partie du code dans testApp.cpp qui construit chaque objet :


void testApp::mousePressed(int x, int y, int button){
    vaisselle.push_back( TeaTime(&image, x, y) );
}

A chaque clic de la souris, nous appelons le constructeur TeaTime() en lui passant trois valeurs : l'adresse de l'image, la position x, et la position y. Ce qui veut dire, « créez-moi un nouvel objet de type TeaTime, voici l'adresse de son image, puis les valeurs x et y de la souris ».

Pour voir du côté du récepteur de cette instruction, regardez le constructeur de la classe TeaTime.cpp :


TeaTime::TeaTime(ofimage* adresse, float nx, float ny) {
    pointeur = adresse;
    x = nx;
    y = ny;
}

Ici, le constructeur de TeaTime reçoit cette adresse et le stocke temporairement dans une variable appelé adresse. Cette adresse est ensuite stocké de mannière permanente dans pointeur.

Au lieu de passer à chaque objet une copie de l'image lors de sa construction, nous passons en réalité l'adresse d'une image, que nous stockons dans un pointeur, nommé ici pointeur. Comme ça, nous ne dupliquons pas l'image dans la mémoire : elle est chargée une fois et utilisée autant de fois que nous le voulons.

Il reste enfin le signe ->. Celle-ci veut dire « aller dans la chose pointé par ce pointeur, où vous trouverez ceci... ». Pour comprendre cette idée, regardez la méthode TeaTime::draw() :


void TeaTime::draw() {
    pointeur->draw(x,y);
}

Souvenez vous, pointeur n'est pas l'image, mais un pointeur d'une image. Vous ne pouvez pas écrire pointeur.draw(x,y); parce que pointeur ne contient pas de méthode draw(). Vous devez dire, aller là-bas, où vous trouverez une méthode draw() (d'où le signe ->).

En résumé :

Ces idées peuvent semblé un peu compliqués au début, mais au bout d'un certain temps vous comprendrez non seulement leur fonctionnement, mais surtout leur intérêt. Vous aurez souvent besoin de pointeurs, surtout dans le cas que nous venons d'explorer : le chargement d'images.

Multiplions les images

Finissons avec un vecteur de plusieurs images, dans lequel on peut choisir différentes images et non pas toujours la même.

Voici six images que vous aller charger dans votre dossier data.

Faites un clic-droit sur chacune de ces images et chargez-les dans votre dossier data. Ce dossier devrait ressembler parfaitement à celui-ci :

Si vous avez du mal, vous pouvez également charger un fichier six_images.zip à la fin de cet article.

Il faut maintenant transformer notre variable qui contient une seule image en un tableau de plusieurs images. Aller dans la définition testApp.h et changer la définition de votre variable ofimage image :


ofimage images[6];

Comme nous avons 6 images, nous avons créé six emplacements pour de futures images. Notez que nous n'utilisons pas ici des vecteurs, parce que le nombre d'images ne risque pas d'augementer : nous savons au début combien d'images nous allons utiliser. Ce n'est pas la peine de créer une liste dynamique de type vector<ofimage> bien que celui-ci soit parfaitement possible (mais un peu plus longue à écrire).

Ensuite, chargez toutes les images dans les emplacements :


void testApp::setup(){
    ofEnableAlphaBlending();

    images[0].loadImage("mad_hat.png");
    images[1].loadImage("pitcher.png");
    images[2].loadImage("small_teacup.png");
    images[3].loadImage("sugar_bowl.png");
    images[4].loadImage("teacup.png");
    images[5].loadImage("teapot.png");
}

Enfin, changez le code de construction, pour qu'une image aléatoire apparaisse lors de chaque clic:


void testApp::mousePressed(int x, int y, int button){
    int quelle_image = (int)ofRandom(0,6);
    vaisselle.push_back( TeaTime(&images[quelle_image], x, y) );
}

Executez votre programme en appuyant sur F9 (Code::Blocks) ou Cmd-R (xCode). Votre programme devrez ressemblez à quelque chose comme ceci :