Les fonctions
Ce que ce chapitre apporte 7 points
- 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, et écrire une fonction qui répond par oui ou non.
- Prévoir l'effet d'un passage par valeur sur la variable de l'appelant, et le représenter en mémoire.
- Passer un tableau et sa taille à une fonction.
- Conserver une valeur d'un appel à l'autre avec static, sans variable globale.
- Réécrire un programme recopié en une suite d'appels qui se lit comme sa description.
Un programme Arduino qui grossit devient vite illisible, parce que tout s'entasse dans loop(). Le découpage lui-même, et la question de savoir où couper, sont traités dans le chapitre d'algorithmique sur la modularité. 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.
Un appel suspend la fonction appelante. Quand loop() exécute allumerSeul(VERT, 3000), l'exécution saute au début de allumerSeul, en parcourt les instructions, puis revient à la ligne suivante de loop(). C'est pourquoi loop() n'avance vers l'orange qu'une fois les trois secondes de vert écoulées.
Pour revenir au bon endroit, la carte range l'adresse de retour et les variables de la fonction dans une zone de la mémoire vive appelée la pile, qui grandit à chaque appel et se vide à chaque retour. Le chapitre Les fonctions et la pile du parcours C explique ce que chaque appel y réserve, le cadre, et quand ce cadre est rendu. Sur l'Uno, elle partage les 2 Ko de mémoire vive avec toutes les variables : des appels trop imbriqués la font déborder sur les données, sans message d'erreur.
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.
Une fonction qui répond par oui ou non
Une question dont la réponse est vrai ou faux mérite une fonction de type bool. Son nom se lit alors comme une question, et l'appel se glisse directement dans un if.
return valeur >= minimum && valeur <= maximum; est l'écriture directe. Un débutant écrit souvent if (...) { return true; } else { return false; }, qui fait exactement la même chose en quatre lignes : la condition est déjà la réponse.
1.Quel type de retour prend une fonction qui allume une LED sans rien rendre ?
2.Que rend estDansPlage(30.0, 18.0, 26.0) ?
3.Pourquoi préférer qu'une fonction de conversion rende sa valeur plutôt que de l'afficher ?
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.
La mémoire rend la chose évidente. Pendant l'appel doubler(n), deux variables existent : n, qui appartient à setup(), et x, que l'appel vient de créer ailleurs en mémoire en y copiant 21. La multiplication agit sur x. À la fin de l'appel, x disparaît, et n n'a jamais été touché.
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.
Après void mettreAZero(int x) { x = 0; } et l'appel mettreAZero(n), que vaut n ?
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, c'est-à-dire un pointeur, au sens du parcours C. 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.
La figure montre ce que reçoit moyenne lors de l'appel moyenne(lectures, NB). Le paramètre valeurs n'est pas une copie des quatre lectures : c'est une case de deux octets qui contient l'adresse de lectures[0], d'où la flèche. Le paramètre n est une copie ordinaire du nombre 4.
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.
Puisque la fonction reçoit l'adresse, elle peut aussi écrire dans le tableau de l'appelant. C'est la seule façon propre, sans passer par une variable globale, de faire modifier plusieurs valeurs par une fonction :
Le mot-clé const du paramètre de moyenne prend alors tout son sens : il annonce, et le compilateur le fait respecter, que la fonction se contente de lire.
1.Que reçoit une fonction à laquelle on passe un tableau ?
2.Une fonction reçoit int t[] et exécute t[0] = 5. Que devient le tableau de l'appelant ?
Garder une valeur d'un appel à l'autre
Une variable locale naît à chaque appel et disparaît à la fin : une fonction ne se souvient de rien. Pour compter le nombre de fois où elle a été appelée, la tentation est de déclarer une variable globale. Le mot-clé static offre mieux.
Une variable locale static est initialisée une seule fois, au premier passage, et conserve sa valeur entre deux appels. Elle vit aussi longtemps qu'une variable globale, mais reste invisible hors de sa fonction.
Le compteur ne peut donc être modifié que par prochainNumero, et aucune autre partie du programme ne risque de le remettre à zéro par erreur. C'est la bonne place pour une valeur qui n'intéresse qu'une fonction : le dernier état d'un bouton, l'instant du dernier clignotement, un numéro de message.
Sans le mot static, que rendraient les trois appels à prochainNumero() ?
Réécrire un programme en fonctions
Un signal de détresse en morse, SOS, se compose de trois points, trois traits, trois points. Le code morse fixe tout en unités de temps : un point dure 1 unité, un trait 3, l'extinction entre deux signaux d'une même lettre 1, entre deux lettres 3, et entre deux messages 7.
Écrit d'un seul tenant, ce programme aligne dix-huit digitalWrite et autant de delay, et une erreur d'une unité ne se retrouve qu'en recomptant tout. Découpé en fonctions, il se lit comme la définition du morse :
Trois bénéfices, que la version recopiée n'avait pas.
loop()dit ce que fait le programme : S, O, S, une pause. Le détail des durées est descendu d'un niveau.- Chaque règle du morse est écrite une seule fois. Changer
UNITEà 100 accélère tout le message sans risque d'oublier undelay. - Chaque fonction se vérifie seule.
point()produit « 1 0 » sur le chronogramme,lettreS()produit « 1 0 1 0 1 0 0 0 » : le chronogramme complet se retrouve en mettant bout à bout ces morceaux.
impulsion, point et lettreS disent ce qu'elles représentent ; une fonction nommée allumerAttendreEteindre décrirait ses instructions et n'apprendrait rien de plus que leur lecture. Un bon nom de fonction permet de lire l'appel sans ouvrir la fonction.
Mettre en pratique
Suivre les appels et les durées
- 1.
Combien de millisecondes dure un appel à trait() avec UNITE = 200, extinction comprise ?
- 2.
Combien de millisecondes dure un message SOS complet, pause finale comprise ?
- 3.
Avec UNITE = 120, combien de messages SOS complets sont émis en une minute ? Donner le nombre entier de messages terminés.
- 4.
Après trois appels à doubler(n), n valant 21 au départ, que vaut n ?
- 5.
Dans une fonction, sizeof(valeurs) vaut 2 et sizeof(valeurs[0]) vaut 2. Que vaut le quotient, que la fonction prendrait pour le nombre d'éléments ?
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.
Écrire une fonction bool estPair(int n), puis l'utiliser pour n'allumer une LED qu'un passage de loop() sur deux.
Le compteur est static : il survit d'un passage de loop() au suivant, comme une globale, mais reste invisible du reste du programme. L'écriture condition ? HIGH : LOW choisit l'une des deux valeurs selon la condition.
Ajouter la lettre T (un seul trait) au programme morse, et émettre SOS puis T.
Une fonction de plus, sur le modèle des deux autres, et un appel de plus :
Aucune ligne existante n'a changé. C'est le signe d'un bon découpage : ajouter un comportement revient à ajouter un morceau, pas à retoucher tout le reste.
Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?
Dans la bibliothèque d'exercices
Ce qu'une fonction reçoit vraiment, et ce qu'un tableau perd en chemin.
- La fonction qui ne change rienNiveau 2
- La taille qui ne passe pas la porteNiveau 3
- Des paliers aux degrésNiveau 3