Récursivité en C : comprendre la descente et la remontée
Suis une fonction récursive en C étape par étape : ordre des printf, copies de n, cas de base et différence entre n - 1 et --n lors des retours.
Tu places un printf après un appel récursif et les nombres sortent dans l'ordre inverse. Pourtant, ton code ne contient aucune boucle qui remonte les valeurs. Il faut suivre les appels encore en attente pour comprendre cet affichage.
En bref
Dans une fonction récursive en C, le code avant l'appel s'exécute à la descente, puis l'appelant attend que cet appel se termine. Le code situé après reprend au retour, du dernier appel terminé vers le premier. Chaque appel possède son propre paramètre n : passer n - 1 ne modifie pas le n de l'appelant. Le cas de base arrête les nouveaux appels ; son return rend la main à l'appelant immédiat.
Dans quel ordre les printf s'exécutent-ils ?
Le premier printf s'exécute avant de descendre ; le second attend le retour de walk(n - 1). Voici un programme assez court pour suivre chaque ligne.
#include <stdio.h>
static void walk(int n){ if (n <= 0) { printf("base %d\n", n); return; } printf("before %d\n", n); walk(n - 1); printf("after %d\n", n);}
int main(void){ walk(2); return 0;}$ gcc -std=c17 -Wall -Wextra -Werror -O0 -g walk.c -o walk$ ./walkbefore 2before 1base 0after 1after 2walk(2) affiche before 2, puis appelle walk(1). Son dernier printf reste à faire. walk(1) fait la même chose avec walk(0). Celui-ci atteint le cas de base et retourne : walk(1) peut alors afficher after 1 et se terminer. Enfin, walk(2) reprend et affiche after 2.
Le mot « remontée » désigne cet ordre des retours. Le programme ne rembobine pas les lignes déjà exécutées et ne recommence pas la fonction depuis son début.
Quel appel est actif à chaque étape ?
Un seul des appels de notre programme exécute une instruction à la fois. Les autres attendent ou ont déjà terminé. Dans cette trace, chaque instantané se place juste avant le printf annoncé dans next ; les cases gardent la même identité pendant tout le parcours.
Étape 1 sur 5
Avant before 2
- n
- 2
- next
- before 2
- walk(2)n = 2actif
- walk(1)pas appeléà venir
- walk(0)pas appeléà venir
walk(2) vient de passer le test n <= 0. Il va afficher before 2, puis appeler walk(1).
Lire toutes les étapes
1. Avant before 2
- n
- 2
- next
- before 2
- walk(2)n = 2actif
- walk(1)pas appeléà venir
- walk(0)pas appeléà venir
walk(2) vient de passer le test n <= 0. Il va afficher before 2, puis appeler walk(1).
2. Avant before 1
- n
- 1
- next
- before 1
- walk(2)n = 2en attente
- walk(1)n = 1actif
- walk(0)pas appeléà venir
walk(1) a reçu sa propre valeur 1. walk(2) attend le retour de cet appel ; son n vaut toujours 2.
3. Avant base 0
- n
- 0
- next
- base 0
- walk(2)n = 2en attente
- walk(1)n = 1en attente
- walk(0)n = 0actif
walk(0) atteint le cas de base. Il va afficher base 0 puis retourner uniquement à walk(1).
4. Avant after 1
- n
- 1
- next
- after 1
- walk(2)n = 2en attente
- walk(1)n = 1actif
- walk(0)terminéretour effectué
walk(0) est terminé. walk(1) reprend après walk(n - 1), avec son n resté à 1.
5. Avant after 2
- n
- 2
- next
- after 2
- walk(2)n = 2actif
- walk(1)terminéretour effectué
- walk(0)terminéretour effectué
walk(1) est terminé à son tour. walk(2) reprend avec n égal à 2, puis rendra la main à main.
Les cases « terminé » rappellent l'historique. Elles ne représentent plus des variables accessibles dans une fonction en cours.
Pourquoi n retrouve-t-il 1 puis 2 au retour ?
Il ne retrouve pas une ancienne valeur : ces valeurs n'ont jamais été écrasées. L'expression n - 1 calcule un argument pour le prochain appel, sans affectation dans l'appelant. Au cas de base, trois instances distinctes du paramètre existent dans les appels de walk :
Pile (stack)
- intn
- 0
- intn
- 1
- intn
- 2
Le schéma montre les appels de walk, sans le cadre de main ni ceux de la bibliothèque. La pile d'appels est un modèle pratique pour suivre ce qui attend. Les détails des cadres machine peuvent changer avec les optimisations ; l'ordre observable de ce programme reste celui imposé par les instructions.
Une variable locale static serait partagée entre les appels. Ce n'est pas le cas du paramètre int n ici. Pour relier ce modèle à la durée de vie des objets, lis Pile et tas en C. Le passage par valeur et les pointeurs expliquent aussi pourquoi modifier un paramètre ordinaire ne modifie pas directement la variable de l'appelant.
Que change walk(--n) à la place de walk(n - 1) ?
--n diminue d'abord le paramètre de l'appel courant, puis transmet cette valeur au suivant. Les arguments de la descente sont encore 1 puis 0, mais les appels en attente ont eux aussi changé leur propre n. Remplace seulement cette expression pour le constater :
$ sed 's/walk(n - 1)/walk(--n)/' walk.c > changed.c$ gcc -std=c17 -Wall -Wextra -Werror changed.c -o changed$ ./changedbefore 2before 1base 0after 0after 1Dans la version modifiée, l'appel parti avec 2 a déjà ramené son paramètre à 1 avant d'attendre. L'appel parti avec 1 l'a ramené à 0. Leurs derniers printf affichent donc 0 puis 1. Le passage par valeur fonctionne toujours ; c'est l'expression dans l'appelant qui a ajouté une modification.
Garde n - 1 quand tu veux transmettre une valeur plus petite tout en conservant le niveau courant pour la remontée. Quant à n--, il transmet la valeur avant la décrémentation : le prochain appel reçoit encore le même nombre positif. Avec notre cas de base, cela ne fait pas progresser la descente vers zéro.
Pourquoi le cas de base doit-il précéder le prochain appel ?
Il doit empêcher d'appeler encore la fonction quand l'entrée est déjà un cas terminal. Ici, n <= 0 affiche une ligne base puis quitte l'appel courant, avant toute soustraction. walk(0) affiche donc seulement base 0 ; walk(-1) affiche seulement base -1.
Pour une entrée positive, n - 1 rapproche chaque appel de zéro. Les deux propriétés vont ensemble : un cas d'arrêt et une progression qui l'atteint. Une condition présente dans le code ne suffit pas si les arguments s'en éloignent.
Le return de walk(0) ne quitte ni walk(1) ni main. Il revient au site qui l'a appelé. Les instructions encore à faire dans les appels parents expliquent les lignes after.
descend
Dans descend, transforme ce parcours en notifications : appelle on_enter pendant la descente et on_leave au retour. L'exercice ne demande aucun printf dans ta fonction et les callbacks peuvent être NULL. Son cas de base ne produit aucune notification, contrairement à la ligne base de notre démonstration.
Comment suivre une autre fonction récursive sur papier ?
Note chaque appel séparément, avec ses arguments et la prochaine instruction à exécuter au retour. Arrête la descente dès le cas de base, puis termine d'abord l'appel le plus récent. Écris une ligne de sortie seulement au moment où son instruction est atteinte.
Avec walk(3), prédis les lignes avant de changer main : les before descendent de 3 à 1, puis viennent base 0 et les after de 1 à 3. Vérifie ensuite dans le visualiseur ou avec gdb(1). Si une fonction contient plusieurs appels récursifs, garde aussi la position du prochain appel à traiter ; une simple colonne de valeurs ne suffit plus.
Questions fréquentes
Une fonction void peut-elle être récursive ?
Oui. void indique l'absence de valeur de retour, pas l'absence de retour de contrôle. Notre fonction affiche des lignes puis rend la main à son appelant.
Les appels parents continuent-ils pendant l'appel enfant ?
Pas dans ce programme séquentiel. L'appelant attend la fin de l'appel enfant avant d'exécuter l'instruction suivante. La récursivité ne crée aucun thread.
Combien d'appels walk(2) crée-t-il ?
Trois appels de walk au total : avec 2, 1 et 0. Pour une entrée positive n, cette version en crée n + 1. Le cas de base compte comme un appel, même s'il n'en crée pas d'autre.
Un cas de base protège-t-il de tout débordement de pile ?
Non. Une descente peut être finie mais trop profonde pour la pile disponible. Il n'existe pas de profondeur maximale portable en C. Pour une grande entrée, envisage une boucle ou une pile explicite ; ne compte pas sur une optimisation des appels terminaux pour garantir la sécurité.
Sources
