Fonctions
Une fonction regroupe des instructions sous un nom. On l'écrit une fois, on l'appelle autant de fois qu'il faut. C'est l'outil qui permet à un programme de grandir sans devenir illisible.
Pourquoi des fonctions ?
Au chapitre 3, le tableau de score écrivait un nombre avec cinq lignes presque identiques :
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));
Pour écrire aussi les vies et le niveau, il faudrait recopier ce bloc, et corriger chaque copie à la moindre erreur. Avec une fonction, on l'écrit une seule fois, et on l'appelle par son nom :
ecrireNombre(.score, 5);
ecrireNombre(.vies, 1);
Les fonctions ont trois avantages :
- ne pas se répéter : une correction se fait à un seul endroit ;
- donner un nom à ce que fait un groupe d'instructions :
preparerEcran()se lit mieux que les lignes qu'elle contient ; - découper un problème en petits morceaux, chacun simple.
Vous en utilisez déjà : vic.print ou vdc.setCursor sont des
fonctions, écrites dans les bibliothèques.
Déclarer une fonction
uint addition(uint a, uint b) {
return a + b;
}
| Partie | Ici | Rôle |
|---|---|---|
| le type du résultat | uint | le type de la valeur que la fonction renvoie ; void si elle ne renvoie rien |
| le nom | addition | pour l'appeler |
| les paramètres | (uint a, uint b) | les valeurs qu'elle reçoit, chacune avec son type ; () s'il n'y en a pas |
| le corps | { return a + b; } | les instructions exécutées à chaque appel |
Les fonctions se déclarent dans la unit, après ses variables, avant ou après
main(). main() est elle-même une fonction, un peu à part : elle
n'a pas de type, et c'est elle que boot.asm appelle.
unit {
uint score; // les variables d'abord
main() {
afficher();
}
void afficher() { // une fonction qui ne renvoie rien
// ...
}
}
Appeler une fonction
On appelle une fonction par son nom, suivi des valeurs de ses paramètres entre parenthèses. Une fonction de la unit s'appelle sans point ; une fonction de bibliothèque, avec le nom de la bibliothèque :
afficher(); // une fonction de la unit
.total = addition(40, 2); // son résultat dans une variable : 42
vic.setCursor(10, 5); // une fonction de bibliothèque
Une fonction peut être appelée avant l'endroit où elle est écrite : main(),
en haut de la unit, appelle volontiers des fonctions écrites plus bas.
Une faute de frappe dans un nom de fonction n'est repérée qu'à la fin
de la compilation, par l'assembleur : Undefined symbol: Main_inconnue. Le
nom de la fonction apparaît après Main_, le nom de la unit.
Les paramètres
Une copie de la valeur
Un paramètre reçoit une copie de la valeur donnée à l'appel. La fonction peut le modifier : la variable de l'appelant, elle, ne change pas.
main() {
ubyte x = 5;
doubler(x);
// x vaut toujours 5
}
void doubler(ubyte v) {
v = v * 2; // v vaut 10, mais seulement dans doubler()
}
Pour qu'une fonction transmette un résultat, elle le renvoie avec return,
ou elle l'écrit dans une variable de la unit.
Combien de paramètres ?
Le nombre d'arguments doit être celui de la déclaration :
Function 'addition' takes 2 parameter(s), called with 1 argument(s)
Les machines 8 bits passent les paramètres dans une petite zone de mémoire fixe. Sur C64 et PC Engine, elle en contient 4 au plus :
Function Main_c has too many parameters (5 > 4): the parameter zone holds 4 of them
(maxParams in config.json or in simple8-config.json).
Une fonction qui aurait besoin de plus de valeurs peut les lire dans des variables de
la unit. On peut aussi relever la limite, avec la clé maxParams, à deux
endroits :
- dans
simple8-config.json, à la racine du projet : le réglage ne vaut que pour ce projet (c'est le plus sûr) ; - dans le
config.jsonde l'architecture (libs8/commodore/c64/config.json,libs8/nec/pce/config.json…) : il vaut alors pour tous les projets de cette machine.
{ "maxParams": 6 }
Chaque paramètre de plus prend 2 octets de mémoire vive dans la zone des paramètres
(la clé paramWidth fixe cette largeur, 16 bits par défaut). Le compilateur
décale lui-même ce qui suit la zone.
Un texte en paramètre
Un texte entre guillemets donné à une fonction est rangé dans le programme, et la
fonction reçoit son adresse, un nombre de 16 bits. Le paramètre est donc
un uint, que l'on passe tel quel à print :
void titre(uint texte) {
vic.setCursor(0, 0);
vic.print(texte);
}
titre("BONJOUR");
Renvoyer une valeur : return
return termine la fonction et renvoie une valeur, du type annoncé dans sa
déclaration. Il peut apparaître plusieurs fois : le premier atteint l'emporte.
ubyte plusGrand(ubyte a, ubyte b) {
if (a > b) {
return a; // on s'arrête ici si a est le plus grand
}
return b;
}
.max = plusGrand(3, 9); // 9
Dans une fonction void, return; sans valeur sert à sortir plus
tôt :
void retirerVie() {
if (.vies == 0) {
return; // déjà plus de vies : rien à faire
}
.vies--;
}
Les variables locales
Les variables déclarées dans une fonction, comme ses paramètres, n'appartiennent qu'à
elle. Deux fonctions peuvent avoir chacune un i : ce sont deux variables
différentes. Une fonction ne peut pas lire les variables locales d'une autre :
main() {
ubyte secret = 3;
lire();
}
void lire() {
.valeur = secret; // erreur : secret appartient à main()
}
Variable 'secret' not found !
Pour partager une valeur entre fonctions, on la passe en paramètre, ou on la range dans une variable de la unit.
Une fonction qui s'appelle elle-même
Une fonction récursive s'appelle elle-même. La factorielle en est l'exemple classique : 5! = 5 × 4 × 3 × 2 × 1 = 120.
uint factorielle(uint n) {
if (n <= 1) {
return 1;
}
return n * factorielle(n - 1);
}
Par défaut, Simple8 refuse ce programme :
Direct recursion: Main_factorielle -> Main_factorielle. Recursion needs a stack:
set "parameterStack" to "software" or "hardware" in config.json (or in simple8-config.json).
Pour aller vite, chaque paramètre et chaque variable locale a une adresse fixe en
mémoire. Or factorielle(5) appelle factorielle(4) avant d'avoir
fini : les deux appels voudraient le même n. Il faut alors une
pile, qui garde le n de chaque appel en cours. Elle se règle dans un
fichier simple8-config.json, à la racine du projet :
{ "parameterStack": { "mode": "software", "frameSize": 4, "maxDepth": 8 } }
Le programme compile alors, et factorielle(5) donne 120. Simple8 IDE règle
aussi cette pile, dans Configuration → Simple8.
À réserver aux vrais besoins. La pile coûte de la mémoire et du temps
à chaque appel récursif, et les machines 8 bits en ont peu. Une boucle fait souvent le
même travail : la factorielle se calcule aussi avec un simple for.
Plusieurs fonctions du même nom
Deux fonctions peuvent porter le même nom si leurs paramètres diffèrent : c'est la surcharge. Le compilateur choisit la bonne d'après les arguments de l'appel :
uint aire(uint c) { // un carré
return c * c;
}
uint aire(uint l, uint h) { // un rectangle
return l * h;
}
main() {
.carre = aire(4); // 16 : la première
.rectangle = aire(4, 6); // 24 : la seconde
}
- Les versions doivent différer par le nombre de paramètres, ou par
leur genre.
byte,ubyte,intetuintse convertissent l'un dans l'autre :f(byte x)etf(int x)ne se distinguent pas. - Pour l'instant, écrivez les versions surchargées avant l'endroit où
elles sont appelées, comme ici avant
main(). Écrites après, seule la première est reconnue à l'appel.
Un exemple : le tableau de score en fonctions
Le tableau de score du chapitre 3, découpé en fonctions. main() se lit
maintenant comme un résumé du programme ; chaque fonction fait une seule chose.
// Fonctions : le tableau de score, découpé en fonctions (Commodore 64)
architecture commodore.c64;
addlib vic.base;
addlib vic.screentext;
unit {
const uint POINTS_PAR_GEMME = 250;
ubyte vies = 3;
uint score;
main() {
ubyte gemmes = 7;
.score = points(gemmes);
.vies--;
preparerEcran();
ecrireLigne(10, "SCORE ", .score, 5);
ecrireLigne(12, "VIES ", .vies, 1);
ecrireLigne(14, "GEMMES ", gemmes, 2);
}
// Les points rapportés par des gemmes
uint points(ubyte gemmes) {
return .POINTS_PAR_GEMME * gemmes;
}
// L'écran : fond bleu, texte blanc
void preparerEcran() {
vic.setBorderColor(0);
vic.setBackgroundColor(6);
vic.initScreen();
vic.clearScreen();
vic.setFontColor(1);
}
// Un nombre, sur un nombre de chiffres donné (avec des zéros devant)
void ecrireNombre(uint valeur, ubyte chiffres) {
uint diviseur = 1;
for (ubyte i = 1; i < chiffres; i++) {
diviseur = diviseur * 10; // 10, 100, 1000...
}
while (diviseur > 0) {
vic.putChar((ubyte)('0' + (valeur / diviseur) % 10));
diviseur = diviseur / 10;
}
}
// Un texte puis un nombre, à la ligne y
void ecrireLigne(ubyte y, uint texte, uint valeur, ubyte chiffres) {
vic.setCursor(13, y);
vic.print(texte);
ecrireNombre(valeur, chiffres);
}
}
points()calcule et renvoie une valeur.preparerEcran()regroupe les réglages de l'écran.ecrireNombre()remplace les cinq lignes répétées par une boucle : leforcalcule le premier diviseur (10 000 pour 5 chiffres), puis lewhileécrit un chiffre et divise par 10, jusqu'à 1.ecrireLigne()appelle elle-même d'autres fonctions. Avec ses quatre paramètres, elle est à la limite du C64.
La version PC Engine
Seules preparerEcran(), le nom de la bibliothèque et la position du texte
changent. main(), points() et la logique de
ecrireNombre() sont identiques : c'est l'intérêt de bien découper.
// Fonctions : le tableau de score, découpé en fonctions (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;
ubyte vies = 3;
uint score;
main() {
ubyte gemmes = 7;
.score = points(gemmes);
.vies--;
preparerEcran();
ecrireLigne(11, "SCORE ", .score, 5);
ecrireLigne(13, "VIES ", .vies, 1);
ecrireLigne(15, "GEMMES ", gemmes, 2);
}
// Les points rapportés par des gemmes
uint points(ubyte gemmes) {
return .POINTS_PAR_GEMME * gemmes;
}
// L'écran, la police et les couleurs : fond bleu, texte blanc
void preparerEcran() {
int source;
int length;
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);
}
// Un nombre, sur un nombre de chiffres donné (avec des zéros devant)
void ecrireNombre(uint valeur, ubyte chiffres) {
uint diviseur = 1;
for (ubyte i = 1; i < chiffres; i++) {
diviseur = diviseur * 10; // 10, 100, 1000...
}
while (diviseur > 0) {
vdc.putChar((ubyte)('0' + (valeur / diviseur) % 10));
diviseur = diviseur / 10;
}
}
// Un texte puis un nombre, à la ligne y
void ecrireLigne(ubyte y, uint texte, uint valeur, ubyte chiffres) {
vdc.setCursor(9, y);
vdc.print(texte);
ecrireNombre(valeur, chiffres);
}
}
À retenir
- Une fonction a un type de résultat (
voids'il n'y en a pas), un nom, des paramètres et un corps. - Une fonction de la unit s'appelle sans point :
afficher(); une fonction de bibliothèque avec le nom de la bibliothèque :vic.print(). - Les paramètres sont des copies ;
returnrenvoie le résultat. - Les variables locales n'appartiennent qu'à leur fonction.
- Sur C64 et PC Engine, 4 paramètres au plus.
- La récursivité demande une pile, à régler dans
simple8-config.json.
À vous de jouer
- Ajoutez une ligne
NIVEAUau tableau, avec un seul appel deecrireLigne().Voir la solutionMasquer la solution
ecrireLigne()sait déjà écrire un texte suivi d'un nombre, à une ligne donnée. Pour le niveau, il suffit d'une variable de la unit et d'un appel de plus, deux lignes sous les gemmes :uint score; ubyte niveau = 4;main() { ubyte gemmes = 7; .score = points(gemmes); .vies--; preparerEcran(); ecrireLigne(10, "SCORE ", .score, 5); ecrireLigne(12, "VIES ", .vies, 1); ecrireLigne(14, "GEMMES ", gemmes, 2); ecrireLigne(16, "NIVEAU ", .niveau, 2); }
La ligne NIVEAU, ajoutée par un seul appel. Sur PC Engine, la ligne est la même, à la ligne 17 :
ecrireLigne(17, "NIVEAU ", .niveau, 2);. Aucune fonction n'a eu besoin d'être modifiée : c'est l'avantage d'avoir écritecrireLigne()une fois pour toutes. - Écrivez une fonction
uint bonus(ubyte vies)qui renvoie 1 000 points par vie restante, et ajoutez son résultat au score.Voir la solutionMasquer la solution
La fonction reçoit un nombre de vies et renvoie le bonus avec
return. Son résultat est unuint, assez grand pour plusieurs milliers de points. Dansmain(), on l'appelle après avoir retiré une vie, et on ajoute son résultat au score avec+=.// Le bonus de fin de niveau : 1000 points par vie restante uint bonus(ubyte vies) { return 1000 * vies; }main() { ubyte gemmes = 7; .score = points(gemmes); .vies--; .score += bonus(.vies); // 1750 + 2 x 1000 = 3750 preparerEcran(); ecrireLigne(10, "SCORE ", .score, 5); ecrireLigne(12, "VIES ", .vies, 1); ecrireLigne(14, "GEMMES ", gemmes, 2); }
1750 points de gemmes et 2 × 1000 points de bonus : 3750. Le paramètre s'appelle
vies, comme la variable de la unit : ce n'est pas gênant. Dans la fonction,viessans point est le paramètre,.viesserait la variable de la unit. On aurait pu écrirebonus()sans paramètre, en lisant.vies; avec un paramètre, la fonction ne dépend que de ce qu'on lui donne, et elle peut servir pour n'importe quel nombre de vies. - Modifiez
ecrireNombre()pour ne pas écrire les zéros de tête :01750devient1750. Indice : une variable locale qui retient si un chiffre non nul a déjà été écrit.Voir la solutionMasquer la solution
On parcourt toujours les chiffres de gauche à droite, mais on n'écrit un chiffre que dans trois cas : il n'est pas nul, ou un chiffre a déjà été écrit (les zéros au milieu, comme dans 1050, doivent rester), ou c'est le dernier chiffre (pour que la valeur 0 s'écrive
0et non rien). La variable localecommenceretient si l'écriture a commencé : elle part de 0 et passe à 1 au premier chiffre écrit.// Un nombre, sans les zéros de tête (au plus "chiffres" chiffres) void ecrireNombre(uint valeur, ubyte chiffres) { uint diviseur = 1; ubyte chiffre; ubyte commence = 0; // 1 dès qu'un chiffre a été écrit for (ubyte i = 1; i < chiffres; i++) { diviseur = diviseur * 10; // 10, 100, 1000... } while (diviseur > 0) { chiffre = (ubyte)((valeur / diviseur) % 10); // on écrit si le chiffre n'est pas nul, si on a déjà commencé, // ou si c'est le dernier (pour que 0 s'écrive 0) if (chiffre != 0 || commence || diviseur == 1) { vic.putChar((ubyte)('0' + chiffre)); commence = 1; } diviseur = diviseur / 10; } }
Les nombres sans leurs zéros de tête. Le chiffre est rangé dans une variable,
chiffre, parce qu'on s'en sert deux fois : pour le test et pour l'écrire. La condition utilisecommenceseul : 0 est faux, 1 est vrai. Nous avons vérifié aussi le cas de la valeur 0 : avec une seule vie au départ, la ligneVIESaffiche bien0. Sur PC Engine, la fonction est la même avecvdc.putChar.Les nombres sont maintenant alignés à gauche :
1750prend 4 cases au lieu de 5. Pour les garder alignés à droite, comme dans un vrai tableau de score, on écrirait une espace (putChar(' ')) à la place de chaque zéro de tête. - Ajoutez la colonne
xen paramètre deecrireLigne(). Que dit le compilateur ? Comment s'en sortir ?Voir la solutionMasquer la solution
Avec la colonne
x,ecrireLigne()a cinq paramètres :// Un texte puis un nombre, à la colonne x et à la ligne y void ecrireLigne(ubyte x, ubyte y, uint texte, uint valeur, ubyte chiffres) { vic.setCursor(x, y); vic.print(texte); ecrireNombre(valeur, chiffres); }Le compilateur refuse, car la zone des paramètres du C64 n'en contient que 4 :
Function Main_ecrireLigne has too many parameters (5 > 4): the parameter zone holds 4 of them (maxParams in config.json or in simple8-config.json).Première solution : relever la limite. Le message l'indique : un fichier
simple8-config.json, à la racine du projet, avec la clémaxParams. Le réglage ne vaut que pour ce projet. Le programme compile alors tel quel, et l'écran est le même qu'avant. Le prix : 2 octets de mémoire vive de plus pour la zone des paramètres.{ "maxParams": 5 }Seconde solution : un paramètre de moins. On sort la position de la fonction : l'appelant place le curseur avec
vic.setCursor(x, y), etecrireLigne()écrit là où se trouve le curseur. Elle n'a plus que trois paramètres, et la colonne se choisit librement à chaque appel.main() { ubyte gemmes = 7; .score = points(gemmes); .vies--; preparerEcran(); vic.setCursor(13, 10); ecrireLigne("SCORE ", .score, 5); vic.setCursor(13, 12); ecrireLigne("VIES ", .vies, 1); vic.setCursor(13, 14); ecrireLigne("GEMMES ", gemmes, 2); }// Un texte puis un nombre, à la position du curseur void ecrireLigne(uint texte, uint valeur, ubyte chiffres) { vic.print(texte); ecrireNombre(valeur, chiffres); }Les deux versions compilent et affichent le même tableau. La seconde ne demande aucun réglage, ni mémoire en plus ; la première garde des appels plus courts. Une troisième voie serait de lire la colonne dans une variable de la unit, mais une fonction qui reçoit tout ce dont elle a besoin en paramètres est plus facile à comprendre.
