Les fonctions
Ce que ce chapitre apporte
- Découper un programme en fonctions nommées d'après ce qu'elles font.
- Déclarer une fonction avec des paramètres et une valeur de retour, puis l'appeler.
- Distinguer une fonction qui agit sur la carte d'une fonction qui calcule une valeur.
- Prévoir l'effet d'un passage par valeur sur la variable de l'appelant.
- Passer un tableau et sa taille à une fonction.
Un programme Arduino qui grossit devient vite illisible, parce que tout s'entasse dans loop(). Une fonction donne un nom à un morceau de traitement : allumerSeul(VERT, 3000) se lit, trois digitalWrite suivis d'un delay demandent à être déchiffrés. C'est la différence entre un programme qu'on relit et un programme qu'on réécrit. Ce chapitre montre aussi deux comportements du C qui surprennent : une fonction ne modifie pas les variables qu'on lui passe, et un tableau n'y arrive jamais entier.
Donner un nom à un traitement
Un feu tricolore écrit sans fonction répète la même séquence trois fois : éteindre les trois LEDs, en allumer une, attendre. Une fonction la décrit une fois.
typeRetour nomDeLaFonction(type parametre1, type parametre2) {
// instructions
return valeur; // absent si le type de retour est void
}
loop() se lit maintenant comme la description du feu. Et la fonction garantit une propriété que le code recopié ne garantissait pas : à aucun instant deux feux ne sont allumés ensemble.
Les paramètres sont les variables déclarées dans l'en-tête de la fonction : broche et duree.
Les arguments sont les valeurs fournies à l'appel : VERT et 3000. Chaque argument est copié dans le paramètre correspondant, dans l'ordre.
loop() et de l'appeler avant. En C++ ordinaire, une fonction doit être connue avant d'être appelée. Placer les fonctions avant setup() fonctionne dans les deux cas.
Agir ou calculer
Deux sortes de fonctions coexistent, et les distinguer rend un programme plus facile à corriger.
Une fonction qui agit modifie l'état de la carte : elle allume, attend, envoie un message. Elle ne rend rien, et son type de retour est void.
Une fonction qui calcule reçoit des valeurs et en rend une autre par return, sans rien toucher d'autre. Sa réponse ne dépend que de ses arguments.
enCelsius pourrait afficher le résultat au lieu de le rendre. Elle deviendrait alors inutilisable dans un test de seuil, dans une moyenne, dans un envoi sur une autre liaison. Rendre la valeur laisse à l'appelant le choix de ce qu'il en fait. Pour une lecture de 62, elle rend 30,3.
Le passage par valeur
En C, un argument est copié dans le paramètre. La fonction travaille sur cette copie : modifier le paramètre ne modifie pas la variable de l'appelant.
Pour qu'une fonction change une valeur, elle la rend, et c'est l'appelant qui l'affecte. Cette règle a une exception, et elle concerne les tableaux.
Passer un tableau
Un tableau n'est pas copié quand on le passe à une fonction : celle-ci reçoit seulement l'adresse de son premier élément. Deux conséquences en découlent.
La fonction peut modifier les éléments du tableau de l'appelant, puisqu'elle travaille sur la même mémoire. Et elle ne connaît pas la taille du tableau : sizeof y mesure l'adresse, pas le tableau. D'où la règle : la taille voyage toujours avec le tableau, en second paramètre.
sizeof(valeurs) / sizeof(valeurs[0]) à l'intérieur de moyenne compile sans avertissement. Sur l'Uno, une adresse occupe 2 octets, comme un int : le quotient vaut 1, et la fonction calcule la « moyenne » du seul premier élément, quelle que soit la taille du tableau.
Exercices type
Pourquoi la fonction allumerSeul éteint-elle les trois LEDs avant d'en allumer une, plutôt que d'éteindre seulement la précédente ?
Parce qu'elle ne sait pas quelle LED était allumée avant son appel. En éteignant tout, elle garantit l'état final quel que soit l'état de départ : une seule LED allumée, celle qui est demandée.
Une version qui éteindrait « la précédente » dépendrait de l'ordre des appels dans loop(). Le jour où cet ordre change, deux feux se retrouvent allumés ensemble, et le défaut ne se voit que sur le montage.
Après int n = 21; doubler(n);, n vaut toujours 21. Comment corriger sans changer le rôle de la fonction ?
doubler reçoit une copie de n et ne modifie que cette copie. La correction consiste à lui faire rendre le résultat, et à l'affecter chez l'appelant :
Une fonction moyenne(int valeurs[]) calcule la taille avec sizeof(valeurs) / sizeof(valeurs[0]). Sur un tableau de quatre lectures, elle rend la première lecture. Pourquoi ?
Dans la fonction, valeurs n'est pas le tableau mais l'adresse de son premier élément. Sur l'Uno, une adresse occupe 2 octets, un int aussi : le quotient vaut 1, et la boucle ne traite qu'un élément.
La taille se calcule là où le tableau est déclaré, puis se passe en paramètre : moyenne(lectures, NB).
Le capteur LM35 renvoie une lecture de 62 sur A0. Que rend enCelsius(62) ?
La tension vaut V. Le LM35 délivrant 10 mV par degré, cela fait 30,3 °C.
Une fonction de calcul se vérifie ainsi, à la main sur une valeur connue, avant d'être branchée sur un capteur réel. C'est précisément ce que permet le fait qu'elle rende sa valeur au lieu de l'afficher.
Vérification
1.Quel type de retour prend une fonction qui allume une LED sans rien rendre ?
2.Après void mettreAZero(int x) { x = 0; } et l'appel mettreAZero(n), que vaut n ?
3.Que reçoit une fonction à laquelle on passe un tableau ?
4.Pourquoi préférer qu'une fonction de conversion rende sa valeur plutôt que de l'afficher ?
5.Dans l'environnement Arduino, peut-on appeler une fonction définie plus bas que loop() ?
La méthode
- Repérer les séquences répétées dans
loop()et leur donner un nom de verbe qui dit ce qu'elles font. - Décider si la fonction agit ou calcule : une fonction qui calcule rend sa valeur et n'affiche rien.
- Garantir l'état final dans une fonction qui agit, sans supposer ce qui a été fait avant son appel.
- Faire rendre le résultat quand une valeur doit changer, et l'affecter chez l'appelant.
- Passer la taille avec tout tableau, calculée là où le tableau est déclaré.
- Vérifier une fonction de calcul à la main sur une valeur connue avant de la brancher sur un capteur.
Synthèse
- Une fonction donne un nom à un traitement ;
loop()devient alors la description du comportement. - Les arguments sont copiés dans les paramètres, dans l'ordre.
- Une fonction qui agit est de type
void; une fonction qui calcule rend sa valeur parreturn. - Passage par valeur : modifier un paramètre ne modifie pas la variable de l'appelant.
- Un tableau passé à une fonction n'y arrive que par son adresse : la fonction peut le modifier, mais ignore sa taille.
sizeofdans une fonction mesure une adresse ; la taille se passe en paramètre.- Placer les fonctions avant
setup()fonctionne dans tous les environnements.