Suite à la demande de Zarb94 et leSept et vu le temps pluvieux cette après midi, je me suis dit que j'allais essayer de documenter un peu ces histories de pointeurs en C et C++ sur nos petits Arduinos.
Il existe le document de 52 pages rédigé par @osaka que vous pouvez lire mais il semble qu'une "courte" démystification sur la mémoire et les pointeurs pourrait être utile pour les débutants et moins débutants.
Voici donc ma contribution qui se veut plus simpliste sur le sujet.
On ne peut pas parler de pointeurs sans parler de mémoire dans les ordinateurs.
La mémoire vous permet d'organiser l'information, en la rangeant dans des cases successives.
Le nombre de cases dont vous disposez définit la capacité de stockage de votre ordinateur. Comme dans un système de mesures où vous allez avoir des grammes, des kilogrammes, des tonnes etc, les informaticiens ont aussi défini une nomenclature pour mesure l'espace disponible.
En simplifiant la plus petite information représentable est le bit - qui stocke une valeur 0 ou 1.
Les informaticiens aiment les puissances de 2 et comme jouer avec juste des 0 ou des 1 était limité, les informaticiens ont décidé que l'unité courante (ce n'est pas tout à fait exact car il existe des système différents) serait un groupe de 8 bits (8 = 23). En groupant 8 bits ensemble on définit un octet ou byte en anglais. On utilise en français le symbole o
minuscule (octet) pour dire 1 octet. (les anglophones utiliseront la notation b
pour bit et B
pour Byte=octet).
Bien sûr un système avec un seul octet ne servirait pas à grand chose et donc pour représenter plus d'octets d'un coup, ils ont défini le kilo octet comme étant 210 octets, soit 1024 octets. On le notera ko (et donc les anglophones noteront KB ou s’ils utilisent le symbole international kB)
Si vous avez beaucoup de kilo-octets, les informaticiens ont défini le méga octet comme état 210 kilo-octets, soit 1024 x 1024 octets = 1048576 octets.
Cette notation a déchainé les passions... Dans les unités du standard international, on parle en puissance de 10 pour le kilo = 103, le méga = 106, le giga = 109... Une nouvelle norme a donc été créée en 1998 pour noter les multiples de 210: les kibi (kilo binaire), mébi, gibi etc. Je vous laisse lire plus d'information sur wikipedia mais il faut savoir que ce n'est pas utilisé, c'est juste si vous voulez frimer lors de votre prochain dîner mondain
Bon, OK, c'est bien beau tout cela mais donc j'ai potentiellement des dizaines de milliers de cases mémoires. Comment je m'y retrouve dans toutes ces cases ?
Comme dans une rue où les maisons sont numérotées, pour repérer une case on utilisera une adresse. La première case mémoire a l'adresse 0, la deuxième l'adresse 1, la troisième l'adresse 2 etc... c'est simple non ? et les micro-processeurs ont des instructions spécifique pour dire "va chercher l'octet situé à l'adresse 227" par exemple.
Dans notre monde Arduino et des micro-processeur AVR, sur un Arduino UNO à base de ATmega328P on a 3 types de mémoire:
- Une mémoire pour les programmes, connue sous le nom de mémoire Flash, qui a 32 Ko
- Une mémoire pour les données, connue sous le nom de SRAM, qui a 2 Ko
- Une mémoire pour les données, connue sous le nom de mémoire EEPROM, qui a 1 Ko
La mémoire Flash et l'EEPROM disposent de propretés physiques qui font que même si vous coupez le courant les valeurs des bits (0 ou 1) resteront mémorisées alors que la SRAM sera perdue. Pour que les données soit persistantes dans ces mémoires, il faut appliquer des champs électriques forts pour forcer des électrons à aller à un endroit ou à un autre ce qui fait que ces opérations ne sont pas rapides et au final abiment un peu à chaque fois la case mémoire.
La mémoire Flash pourra supporter environ 10,000 écritures et l'EEPROM environ 100,000 cycles d'écritures. Si vous programmez souvent votre arduino - genre chargez un programme toutes les 5 minutes et ce pendant 8 heures (soit 8x12=96 fois par jour) --> vous aurez usé votre Arduino en 10000 / 96 = 104 jours. si vous faites cela que le week end comme passe-temps, ça donne quand même 2 ans de vie à votre Arduino... (bien sûr dans la vraie vie on ne charge pas un programme toutes les 5 minutes pendant 8h et donc votre arduino durera bien plus longtemps).
La mémoire SRAM elle s'efface quand on coupe le courant, pour mettre un bit à 0 ou 1 on aura à appliquer moins de courant et donc la durée de vie de cette mémoire est super longue, on n'a pas à s'en faire. Comme en plus il n'y a pas de courant fort à créer, c'est une opération rapide et donc c'est pratique de stocker dans cet espace des choses dont on a besoin souvent avec un accès en lecture ou écriture rapide --> c'est pour cela qu'on y met les données de notre programme.
Bon, OK, c'est bien beau tout cela, j'ai des octets qui ont une adresse en mémoire. OK. et j'en fais quoi ?
8 bits peuvent coder 256 possibilités (chacune des 8 cases pouvant prendre la valeur 0 ou 1 on a 28 possibilités).
Comme ce serait limité si on ne pouvait travailler qu'avec 256 possibilités, les langages de programmation définissent des types de valeurs plus complexes, tenant sur plusieurs octets.
Sur nos Arduinos genre UNO ou MEGA, le type int
par exemple va être stocké sur 2 octets. on a donc 16 bits qui peuvent prendre la valeur 0 ou 1, soit 216 possibilités, donc 65536 valeurs représentables. Comme l'indique la documentation, les programmeurs ont décidé d'une représentation pour les nombres positifs et négatifs et donc nos 65536 valeurs représentables vont se répartir entre -32768 et 32767.
Vous pouvez aussi avoir le type entier non signé, unsigned int qui est aussi stocké sur les mêmes 2 octets mais comme on ne représente pas les nombres négatifs, on pourra y représenter une valeur entre 0 et 65535 (toujours 65536 valeurs).
En disant au compilateur le type de la variable, on fige donc les valeurs représentables dans le programme.
Bon, OK, c'est bien beau tout cela, j'ai des octets qui ont une adresse en mémoire et que je peux grouper pour représenter des valeurs plus grandes. dois-je savoir autre chose ?
Oui, il y a encore une chose à savoir - connue sous les doux noms des petits et grands indiens
Quand vous représentez un nombre sur 2 octets, donc 65536 valeurs possibles, qu'on représente en base deux sous la forme 0000 0000 0000 0000
2 à 1111 1111 1111 1111
2 il va falloir définir quels sont les 8 bits qui vont dans quel octet. Met-on les 8 bits de droite (ce que l'on appelle les poids faible) dans le premier octet ou le second en mémoire ? Grave question... et les informaticiens n'ont pas réussi à se mettre d'accord (en fonction de ce qui était plus efficace pour certains types de micro-processeurs). On s'est retrouvé avec un camp qui mettait l'octet de poids faible dans l'adresse mémoire de la premiere case et l'octet de poids fort dans la seconde et d'autres qui faisaient l'inverse. Je vous laisse en lire plus sur tout cela sur wikipedia, c'est ce qu'on appelle le boutisme ou endianisme et vous aurez des petits-boutistes et des grands-boutistes (big-endian et little-endian pour les anglophones).
Il faut savoir que sur un Arduino avec notre compilateur, le choix qui est fait est le little-endian, les octets de poids faible sont mis en premier dans les cases mémoire d'adresse la plus faible.
Donc si vous avez l'entier sur 2 octets 1111 0000 0101 01012 à stocker en mémoire à l'adresse 543 par exemple, on sait qu'il faudra 2 octets et donc on aura 0101 01012 rangé en case 543 et 1111 00002 rangé en case 544. ça peut sembler un peu contre nature, car on a l'habitude de lire les chiffres de gauche à droite et là c'est comme si les bits étaient inversés par paquets de 8... c'est comme cela, il faut le savoir - mais la bonne nouvelle c'est que le compilateur gère tout cela pour vous et donc dans la majorité des cas on ne s'en souciera pas.