Types et variables
Un programme manipule des nombres : un score, une position, un nombre de vies. Ce chapitre montre comment les ranger dans des variables, choisir leur type, calculer avec, et passer d'un type à l'autre.
Les types
Chaque variable a un type, qui fixe sa taille en mémoire et les valeurs qu'elle peut prendre. Simple8 a six types de nombres entiers :
| Type | Taille | Valeurs | Pour… |
|---|---|---|---|
ubyte | 8 bits (1 octet) | 0 à 255 | un compteur, une couleur, une coordonnée d'écran |
byte | 8 bits (1 octet) | -128 à 127 | une petite vitesse, positive ou négative |
uint | 16 bits (2 octets) | 0 à 65 535 | un score, une adresse en mémoire |
int | 16 bits (2 octets) | -32 768 à 32 767 | une position qui peut sortir de l'écran |
ulong | 32 bits (4 octets) | 0 à 4 294 967 295 | un très grand nombre |
long | 32 bits (4 octets) | environ ± 2 milliards | un très grand nombre, signé |
Il n'y a ni char ni bool : un caractère ou un « vrai / faux » se
range dans un ubyte. Il existe aussi un type string pour les
chaînes de caractères ; les tableaux et les chaînes ont leur propre chapitre, le
chapitre 6.
Huit, seize ou trente-deux bits
Les processeurs des machines rétro calculent huit bits à la fois. Un
ubyte se traite en une seule opération ; un uint en demande deux,
un ulong quatre. Voici ce que donne la même ligne, + 10, sur trois
variables de types différents, dans l'assembleur généré pour le C64 :
.petit = .petit + 10; ubyte : 4 instructions
LDA Main_petit
CLC
ADC #<10
STA Main_petit.grand = .grand + 10; uint : 7 instructions
LDA Main_grand
CLC
ADC #<10
STA Main_grand
LDA Main_grand+1
ADC #>10
STA Main_grand+1.enorme = .enorme + 10; ulong : 13 instructions
LDA Main_enorme
CLC
ADC #<10
STA Main_enorme
LDA Main_enorme+1
ADC #>10
STA Main_enorme+1
LDA Main_enorme+2
ADC #0
STA Main_enorme+2
LDA Main_enorme+3
ADC #0
STA Main_enorme+3La règle d'or : le plus petit type qui suffit. Un ubyte
chaque fois que la valeur tient entre 0 et 255, un uint ou un
int au-delà, un long seulement quand c'est indispensable.
Signé ou non signé
Les types sans u (byte, int, long) sont
signés : ils acceptent les nombres négatifs, mais montent deux fois moins haut.
Les types en u (unsigned) ne vont que de 0 vers le haut.
Une donnée de la machine (un octet de graphisme, une couleur, une adresse) n'est jamais
négative : c'est un ubyte ou un uint. Le compilateur y veille, et
refuse une valeur qui ne tient pas dans le type :
byte x = 200; // erreur : un byte s'arrête à 127
Value out of range for x (-128..127) -> 200: use ubyte for 0..255
Écrire un nombre
| Forme | Exemple | Valeur |
|---|---|---|
| Décimal | 250, -5 | 250, -5 |
| Hexadécimal | 0xD020 | 53 280 |
| Binaire | 0b10100000 | 160 |
| Caractère | 'A', '0' | 65, 48 (son code ASCII) |
La notation $D020 des assembleurs n'est acceptée que dans un bloc
_asm. Dans le code Simple8, on écrit 0xD020.
Déclarer une variable
Une déclaration donne le type, puis le nom, et éventuellement une valeur de départ. Elle se termine par un point-virgule, et ne déclare qu'une variable à la fois :
ubyte vies = 3; // un type, un nom, une valeur de départ
uint score; // sans valeur : elle vaut 0 au démarrage
int vx;
int vy; // int vx, vy; n'est pas accepté
Il n'y a pas de variable globale : une variable appartient toujours à une unit ou à une fonction.
Les variables de la unit
Déclarées en tête de la unit, avant main(), elles existent pendant tout le
programme. Dans une fonction, on les écrit avec un point devant :
.score.
unit {
uint score; // variable de la unit
main() {
.score = 100; // le point : la variable de la unit
}
}
Sans le point, le compilateur cherche une variable locale du même nom, ne la trouve pas,
et s'arrête : Variable 'score' not found !
Les variables locales
Déclarées dans une fonction, elles n'appartiennent qu'à elle et s'écrivent sans point. Aucune autre fonction ne peut les lire.
main() {
ubyte gemmes = 7; // locale à main()
.score = .score + gemmes;
}
La valeur de départ
Au chapitre précédent, boot.asm préparait la machine avant d'appeler
main(). Il s'occupe aussi des variables :
- une variable sans valeur de départ est mise à 0 ;
- une variable avec une valeur de départ la reçoit : la valeur est
rangée dans le programme, et
boot.asmla recopie dans la mémoire vive avantmain().
Les constantes
Une valeur qui ne change jamais se déclare const. Elle se place en tête de la
unit et s'écrit, elle aussi, avec un point :
unit {
const uint POINTS_PAR_GEMME = 250;
const uint BONUS = POINTS_PAR_GEMME * 4; // calculée à partir d'une autre : 1000
main() {
.score = .POINTS_PAR_GEMME * 7;
}
}
- Une constante ne coûte rien : le compilateur remplace
.POINTS_PAR_GEMMEpar 250 dans le code, sans aller la lire en mémoire. - Elle peut se calculer à partir d'autres constantes déclarées avant elle. Dans la déclaration d'une constante, le point est facultatif.
- Elle ne se déclare qu'en tête de unit, pas dans une fonction.
RGB(0, 0, 7), vu au chapitre 2, est aussi une valeur constante : elle est calculée à la compilation.
Par convention, les constantes s'écrivent en majuscules, les variables en minuscules.
Les variables rapides : fast
Sur les processeurs 6502 (C64, NES, PC Engine), les 256 premiers octets de la mémoire,
la page zéro, s'atteignent plus vite que le reste. Le mot fast y place
une variable de la unit :
fast ubyte image; // lue à chaque image : en page zéro
Cette zone est petite et partagée : 206 octets sur les machines 6502, 127 sur la Game Boy
(où fast utilise la mémoire haute). Les bibliothèques y prennent leur part,
puis l'optimiseur y range lui-même les variables locales les plus utilisées dans les
boucles.
Utiliser fast avec parcimonie. Chaque octet déclaré
fast est un octet que l'optimiseur ne pourra plus donner à une variable de
boucle. Pour débuter, on peut s'en passer : le compilateur fait déjà ce choix au mieux.
Si la zone déborde, la compilation s'arrête sur Zero page overflow.
Calculer
Une affectation, =, range le résultat d'un calcul dans une variable :
.score = .POINTS_PAR_GEMME * gemmes;
Les opérations de calcul
Les quatre opérations, plus le reste de la division. Avec a qui vaut 17 et
b qui vaut 5 :
| Opérateur | Opération | Exemple | Résultat |
|---|---|---|---|
+ | addition | a + b | 22 |
- | soustraction | a - b | 12 |
* | multiplication | a * b | 85 |
/ | division entière : on garde la partie entière | a / b | 3 |
% | reste de la division entière (on dit « modulo ») | a % b | 2 |
17 divisé par 5 donne 3, et il reste 2 : 3 × 5 + 2 = 17. Le reste sert
souvent à faire tourner une valeur en boucle : .image % 4 passe par 0, 1, 2,
3, puis revient à 0.
Comparer
Une comparaison répond par vrai ou faux :
| Opérateur | Se lit | Exemple, avec a = 17 et b = 5 |
|---|---|---|
== | est égal à | a == b : faux |
!= | est différent de | a != b : vrai |
< | est plus petit que | a < b : faux |
> | est plus grand que | a > b : vrai |
<= | est plus petit ou égal à | a <= 17 : vrai |
>= | est plus grand ou égal à | b >= 6 : faux |
Attention à ne pas confondre ==, qui compare, et =, qui range
une valeur. Les comparaisons servent à prendre des décisions et à répéter des
instructions ; elles seront détaillées dans le chapitre sur les conditions et les
boucles, avec les opérateurs logiques && (et), || (ou)
et ! (non), qui les combinent.
Les opérations bit à bit
Dans la mémoire, un nombre est une suite de bits, des 0 et des 1. Un
ubyte en a 8. On les numérote de droite à gauche : le bit 0 à droite vaut 1,
le bit 1 vaut 2, le bit 2 vaut 4… jusqu'au bit 7, qui vaut 128. Le nombre est la somme
des bits à 1 :
| bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| vaut | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
| 12 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
12 s'écrit 0b00001100 : les bits 2 et 3 sont à 1, et 4 + 8 = 12.
Les opérateurs bit à bit travaillent sur chaque bit séparément. Ils servent surtout à
commander le matériel, dont les registres rangent plusieurs réglages dans un seul octet,
un par bit. Avec p = 12 et q = 10 :
& et : 1 seulement si les deux bits sont à 1
p 0 0 0 0 1 1 0 0 12
q 0 0 0 0 1 0 1 0 10
p & q 0 0 0 0 1 0 0 0 8
| ou : 1 si l'un des deux bits au moins est à 1
p 0 0 0 0 1 1 0 0 12
q 0 0 0 0 1 0 1 0 10
p | q 0 0 0 0 1 1 1 0 14
^ ou exclusif : 1 si les deux bits sont différents
p 0 0 0 0 1 1 0 0 12
q 0 0 0 0 1 0 1 0 10
p ^ q 0 0 0 0 0 1 1 0 6
~ non : chaque bit est inversé
p 0 0 0 0 1 1 0 0 12
~p 1 1 1 1 0 0 1 1 243
<< décalage à gauche : les bits avancent vers la gauche
3 0 0 0 0 0 0 1 1 3
3 << 2 0 0 0 0 1 1 0 0 12
>> décalage à droite : les bits reculent vers la droite
12 0 0 0 0 1 1 0 0 12
12 >> 2 0 0 0 0 0 0 1 1 3
Décaler d'un bit vers la gauche multiplie par 2, vers la droite divise par 2 :
x << 3 multiplie par 8. Le processeur le fait bien plus vite qu'une
multiplication.
Les usages les plus courants :
| Pour… | On écrit | Exemple |
|---|---|---|
| garder les 4 bits de droite | x & 0x0F | 0b10110110 & 0x0F donne 0b00000110 (6) |
| mettre le bit 7 à 1 | x | 0x80 | 0b00000011 | 0x80 donne 0b10000011 (131) |
| inverser le bit 0 | x ^ 1 | 0b10110110 ^ 1 donne 0b10110111 (183) |
| multiplier par 8 | x << 3 | 3 << 3 donne 24 |
Modifier une variable
Plutôt que de répéter son nom, on peut modifier une variable directement :
.vies--; // .vies = .vies - 1
.image++; // .image = .image + 1
.score += 250; // .score = .score + 250
.couleur <<= 1; // .couleur = .couleur << 1
Tous les opérateurs ont leur forme composée : += -=
*= /= %= &= |=
^= <<= >>=.
Le débordement
Une variable ne peut pas dépasser la valeur maximale de son type. Si un calcul la dépasse, elle repart de l'autre bout, comme un compteur kilométrique :
ubyte x = 250;
x = x + 10; // 4, et non 260 : 255 est le maximum, on repart de 0
int y = 30000;
y = y + 10000; // -25 536 : le maximum d'un int est 32 767
Aucune erreur ne le signale pendant l'exécution : c'est au développeur de choisir un type assez grand. C'est parfois voulu, pour un compteur d'images qui tourne en boucle par exemple.
Les conversions
Vers un type plus grand
Ranger une valeur dans un type plus grand se fait tout seul, sans perte. Un nombre négatif reste négatif :
byte b = -3;
int a = b; // -3
Vers un type plus petit : le cast
Dans l'autre sens, une partie de la valeur peut se perdre. Le compilateur demande donc de l'écrire explicitement, avec le type voulu entre parenthèses : c'est un cast.
int a = 300;
byte c = a; // erreur
byte d = (byte)a; // accepté : d vaut 44
Cannot assign int to byte without explicit cast. Use: c = (byte)expression;
Le cast garde les bits du bas et jette les autres : 300 s'écrit
0x012C sur deux octets, il ne reste que l'octet bas, 0x2C, soit
44.
Les calculs se font en int
Dans un calcul, les byte et les ubyte sont d'abord convertis en
int. Le résultat d'un calcul sur des petits types est donc un
int, et peut être négatif :
ubyte u1 = 1;
ubyte u2 = 2;
int d = u1 - u2; // -1
C'est pour cela qu'il faut un cast pour ranger un tel calcul dans un petit type, comme
dans l'exemple ci-dessous : (ubyte)('0' + .vies).
La division est entière. 7 / 2 vaut 3 et
7 % 2 vaut 1. Avec un nombre négatif, le résultat est arrondi vers zéro :
-7 / 2 vaut -3, et -7 % 2 vaut -1.
Un exemple : le tableau de score
Ce programme calcule un score et un nombre de vies, puis les affiche. Il n'existe pas encore de fonction pour écrire un nombre : on l'écrit chiffre par chiffre, avec la division et le reste.
// Types et variables : un tableau de score (Commodore 64)
architecture commodore.c64;
addlib vic.base;
addlib vic.screentext;
unit {
const uint POINTS_PAR_GEMME = 250; // une constante, dans la ROM
ubyte vies = 3; // une variable de la unit, avec sa valeur de départ
uint score; // une variable de la unit, à 0 au démarrage
main() {
ubyte gemmes = 7; // une variable locale de main()
// les calculs
.score = .POINTS_PAR_GEMME * gemmes; // 1750
.vies--; // 2
// l'écran
vic.setBorderColor(0);
vic.setBackgroundColor(6);
vic.initScreen();
vic.clearScreen();
vic.setFontColor(1);
// le score, chiffre par chiffre
vic.setCursor(14, 10);
vic.print("SCORE ");
vic.putChar((ubyte)('0' + .score / 10000));
vic.putChar((ubyte)('0' + (.score / 1000) % 10));
vic.putChar((ubyte)('0' + (.score / 100) % 10));
vic.putChar((ubyte)('0' + (.score / 10) % 10));
vic.putChar((ubyte)('0' + .score % 10));
// les vies
vic.setCursor(14, 12);
vic.print("VIES ");
vic.putChar((ubyte)('0' + .vies));
}
}
.score / 1000donne 1 pour 1750, et% 10garde le dernier chiffre de ce résultat. On obtient ainsi chaque chiffre, de gauche à droite.'0'vaut 48, le code du caractère zéro.'0' + 7donne donc le code du caractère7.- Le calcul est fait en
int: le cast(ubyte)le ramène au type qu'attendputChar.
La version PC Engine
Les calculs sont identiques. Seuls changent la bibliothèque (vdc. au lieu
de vic.) et la préparation de l'écran, vue au chapitre 2.
// Types et variables : un tableau de score (PC Engine)
architecture nec.pce;
addlib sys.resource;
addlib vce.base;
addlib vdc.base;
addlib vdc.screentext;
unit {
const int font = addResource("font.bin");
const uint POINTS_PAR_GEMME = 250; // une constante, dans la ROM
ubyte vies = 3; // une variable de la unit, avec sa valeur de départ
uint score; // une variable de la unit, à 0 au démarrage
main() {
ubyte gemmes = 7; // une variable locale de main()
int source;
int length;
// les calculs
.score = .POINTS_PAR_GEMME * gemmes; // 1750
.vies--; // 2
// l'écran, la police et les couleurs, comme au chapitre 2
vdc.setBATSize(vdc.BAT_32x32);
vdc.initScreen();
resource.mapToMPR2(.font);
source = 0x4000 + resource.getOffset(.font);
length = resource.getLength(.font);
vdc.writeToVRAM(source, 0x1000, length);
vce.setPalette(0, 0, RGB(0, 0, 7));
vce.setPalette(0, 1, RGB(7, 7, 7));
vdc.clearScreen();
vdc.setFontColor(0);
// le score, chiffre par chiffre
vdc.setCursor(10, 12);
vdc.print("SCORE ");
vdc.putChar((ubyte)('0' + .score / 10000));
vdc.putChar((ubyte)('0' + (.score / 1000) % 10));
vdc.putChar((ubyte)('0' + (.score / 100) % 10));
vdc.putChar((ubyte)('0' + (.score / 10) % 10));
vdc.putChar((ubyte)('0' + .score % 10));
// les vies
vdc.setCursor(10, 14);
vdc.print("VIES ");
vdc.putChar((ubyte)('0' + .vies));
}
}
À retenir
- Six types de nombres :
ubyte,byte,uint,int,ulong,long. Sur une machine 8 bits, le plus petit qui suffit est le plus rapide. - Une variable de la unit s'écrit
.nomdans une fonction ; une variable locale s'écrit sans point. - Sans valeur de départ, une variable vaut 0.
constpour une valeur qui ne change pas ;fast, avec parcimonie, pour une variable très utilisée.- Un calcul qui dépasse le type repart de l'autre bout.
- Vers un type plus petit, il faut un cast :
(ubyte),(int)…
À vous de jouer
- Passez à 300 gemmes : que dit le compilateur, et pourquoi ? Essayez ensuite 200, puis 255.
Voir la solutionMasquer la solution
gemmesest unubyte: il va de 0 à 255. La valeur 300 ne tient pas dedans, et le compilateur refuse le programme dès la déclaration :ubyte gemmes = 300; // une variable locale de main()Value out of range for gemmes (0..255) -> 300Il ne tronque pas la valeur en silence : il s'arrête, et il dit quel type accepte quelles valeurs. Pour compter 300 gemmes, il faudrait un
uint; mais le score, 250 × 300 = 75 000, ne tiendrait plus non plus dans unuint.Avec 200, le programme compile et affiche
SCORE 50000. Avec 255, la plus grande valeur d'unubyte, il afficheSCORE 63750(250 × 255). Le score est unuint, qui monte jusqu'à 65 535 : il a assez de place. Avec 263 gemmes ou plus, le score dépasserait 65 535 et déborderait ; mais unubytene peut pas valoir 263, le choix des types protège ici le calcul.
Avec 255 gemmes : 250 × 255 = 63750. - Ajoutez une constante
BONUSde 500 points et ajoutez-la au score avec+=.Voir la solutionMasquer la solution
Une constante se déclare en tête de la unit, avec
const. Elle s'écrit ensuite avec un point, comme.POINTS_PAR_GEMME. L'opérateur+=ajoute une valeur à une variable sans répéter son nom.const uint POINTS_PAR_GEMME = 250; // une constante, dans la ROM const uint BONUS = 500; // le bonus, une autre constante ubyte vies = 3; // une variable de la unit, avec sa valeur de départ uint score; // une variable de la unit, à 0 au démarrage// les calculs .score = .POINTS_PAR_GEMME * gemmes; // 1750 .score += .BONUS; // 2250Le score passe de 1750 à 2250, sur C64 comme sur PC Engine (les lignes sont les mêmes dans les deux programmes).
.score += .BONUS;fait exactement la même chose que.score = .score + .BONUS;, en plus court. CommeBONUSest une constante, le compilateur met directement 500 dans le code. - Déclarez
viesenbyteet retirez-lui 5 vies : que s'affiche-t-il ?Voir la solutionMasquer la solution
Un
byteaccepte les nombres négatifs, de -128 à 127. On change le type devies, puis on lui retire 5 avec-=. Après le.vies--de l'exemple, il valait 2 : il vaut maintenant -3.byte vies = 3; // un byte : il peut devenir négatif// les calculs .score = .POINTS_PAR_GEMME * gemmes; // 1750 .vies--; // 2 .vies -= 5; // -3
Avec -3 vies, l'écran affiche un tiret. L'écran affiche
VIES -. Le calcul'0' + .viesvaut 48 - 3 = 45, et 45 est le code du caractère-. Le programme n'a pas d'erreur, mais l'affichage chiffre par chiffre ne prévoit que les nombres de 0 à 9. Pour afficher un nombre négatif, il faudrait tester s'il est plus petit que 0, écrire un signe moins, puis écrire son opposé : c'est ce que permettront les conditions du chapitre suivant. - Remplacez
uint scoreparubyte score: quelle erreur donne le compilateur, et comment la corriger ?Voir la solutionMasquer la solution
1750 ne tient pas dans un
ubyte, qui s'arrête à 255. Le calcul.POINTS_PAR_GEMME * gemmesdonne unuint, et le compilateur refuse de le ranger dans un type plus petit :ubyte score; // un ubyte : 0 à 255 seulementCannot assign uint to ubyte without explicit cast. Use: score = (ubyte)expression;Le message propose un cast. Il fait taire l'erreur, mais le résultat est faux : le cast garde l'octet bas de 1750 (
0x06D6), soit0xD6, c'est-à-dire 214..score = (ubyte)(.POINTS_PAR_GEMME * gemmes); // 1750 ne tient pas : 214
Avec le cast, le score affiché est 214 au lieu de 1750. La bonne correction est de revenir à
uint score. Un cast se justifie quand on sait que la valeur tient dans le petit type, comme(ubyte)('0' + .vies); il ne doit pas servir à cacher une valeur trop grande. Ici, l'erreur du compilateur signalait un vrai problème.
