Résumé
Cette leçon ouvre la cinquième unité du cours, consacrée aux fonctions. Elle explique à quoi sert l’unité : enseigner à décomposer un problème en parties nommées, chacune avec une interface et un contrat, et à résoudre certains problèmes en les ramenant à une instance plus petite d’eux-mêmes, par récursivité ; elle présente ses quatre leçons et le fil qui les relie ; elle rappelle ce que l’unité tient pour acquis, surtout les boucles de l’unité 4 et leurs invariants ; elle formule le problème que l’unité résout, celui d’un programme qui répète le même calcul copié et qui, à cause d’une inattention dans la copie, répond faux sans que le compilateur dise rien ; elle anticipe le contrat (la précondition et la postcondition) comme la forme sous laquelle l’unité spécifiera chaque partie ; et elle montre où elle mène, dans les unités suivantes et dans le cours Programmation en C++ II.
Objectifs d’apprentissage
- Décrire la séquence des contenus de l’unité et expliquer ce que chaque leçon apporte à la suivante.
- Rappeler les algorithmes de l’unité 4 (la trace d’une boucle, son invariant et son variant) et les revoir si nécessaire.
- Anticiper, sur un exemple, l’idée de contrat : ce qu’une partie du programme exige de celui qui l’utilise et ce qu’elle lui garantit en échange.
- Reconnaître ce que l’on attend de savoir faire à la fin de l’unité, et le distinguer de ce qui s’apprendra dans les unités suivantes.
Évaluation d’entrée
Avant de parcourir l’unité, il convient de mesurer le point de départ. Cette évaluation est une autoévaluation : elle n’est pas notée, et son seul but est d’orienter l’étude. Les cinq premières questions vérifient ce que l’unité tient pour acquis : la trace d’une boucle, son invariant, la précondition d’une spécification, la copie d’une valeur lors de l’initialisation et la recherche, dans un programme de l’unité 4, d’un calcul qui a ses propres entrées et sorties. Les cinq suivantes touchent, dans l’ordre des leçons, aux idées centrales de l’unité : la pile d’appels, le passage d’un argument, la séparation entre le calcul et la présentation, la définition récursive de la factorielle et l’exécution d’une fonction qui s’appelle elle-même. Personne n’est tenu de les réussir déjà, et pour certaines d’entre elles « je ne le sais pas encore » est une réponse honnête : chaque réponse, juste ou non, explique le point et nomme la leçon qui le traite. Les mêmes compétences sont mesurées de nouveau à la fin, dans la leçon de synthèse.
Avant de commencer l’unité, mesurez votre point de départ. C’est une autoévaluation brève : elle n’est pas notée et son résultat n’est conservé que dans votre navigateur.
Question 1Des unités précédentesQu’écrit
int n{305}; int c{1}; while (n >= 10) { n = n / 10; ++c; } std::println("{}", c);?Elle compte les itérations et oublie queccommence à 1.Elle fait une itération de trop : avec la conditionn >= 10est déjà fausse.Elle confond le compteur avec la valeur denaprès la première itération.La trace donne et : le corps s’exécute deux fois, et avec la condition échoue. La boucle compte les chiffres d’un entier naturel ; la leçon Fonctions l’enferme dans une fonction pourvue d’un nom et d’un contrat, et la leçon Récursivité l’écrit sans boucle.À revoir : Boucles
Question 2Des unités précédentesDans la boucle
int total{0}; int k{1}; while (k <= n) { total += k; ++k; }, avec , quelle relation est vraie chaque fois qu’on évalue la condition ?Elle est vraie au milieu de l’itération, après l’addition et avant l’incrément, mais pas quand on évalue la condition.Elle n’est vraie qu’à la fin : c’est la postcondition, non l’invariant.C’est la condition de la boucle, et elle est fausse justement à la sortie ; un invariant est vrai aussi à ce moment-là.Elle est vraie au début, avec la somme vide et ; et si elle est vraie avant une itération, elle l’est après, parce que l’itération ajoute puis l’incrémente. À la sortie, , et l’invariant donne la somme de 1 à . La leçon Récursivité justifie ses fonctions par le même raisonnement par récurrence.À revoir : Concevoir des boucles correctes
Question 3Des unités précédentesLa spécification de la division dit : « étant donné des entiers et , obtenir et avec et ». Quelle est sa précondition ?
Elle confond la précondition avec la postcondition : c’est ce que le résultat vérifie, non ce qu’on exige des données.C’est une condition sur la sortie, non sur l’entrée.Avec , il n’existe aucun tel que : le problème n’a pas de solution, et la précondition l’exclut.La précondition est ce qu’on exige des données pour que le problème ait une solution ; la postcondition, ce que le résultat vérifie. L’unité convertit ce couple en contrat de chaque fonction : la précondition oblige l’appelant, et la postcondition, la fonction.À revoir : Du problème à l’algorithme
Question 4Des unités précédentesAprès
int a{3}; int b{a}; b = 7;, que vauta?Elle croit quebetarestent liées pour toujours ; l’initialisation copie la valeur, non la case.Elle confond la copie avec une variable non initialisée :aa reçu 3 et rien ne l’a changé.L’initialisationint b{a}copie dans la case debla valeur qu’avaita; ensuite, les deux cases sont indépendantes. La leçon Paramètres et portée montre qu’un paramètre s’initialise de la même manière, avec une copie de l’argument, et enseigne l’autre possibilité : un second nom pour la même case.À revoir : Variables et mémoire
Question 5Des unités précédentesLe programme de la leçon 4.4 qui écrit les nombres premiers inférieurs à 100 a une boucle qui parcourt les candidats et, à l’intérieur, une autre qui essaie leurs diviseurs. Quelle partie est un calcul qui a ses propres entrées et sorties, qu’il conviendrait de nommer ?
Elle confond l’entrée et la sortie du programme entier avec celles d’un calcul intérieur.C’est une instruction, non un calcul doué d’un sens propre : il n’a pas une entrée et un résultat que quelqu’un voudrait utiliser.Elle confond une donnée avec une tâche : la variable garde le résultat du calcul, mais elle n’est pas le calcul.La boucle intérieure ne dépend de rien d’autre que du candidat, et son résultat est un oui ou un non. Lui donner un nom permet de lire la boucle extérieure comme sa spécification : « pour chaque candidat, s’il est premier, l’écrire ». La leçon Concevoir avec des fonctions réécrit ainsi ce programme.À revoir : Algorithmes numériques
Question 6Un programme commence dans
main;mainappelle une fonctiong, etgappelle une autre fonctionh. Pendant quehs’exécute, quels appels sont en cours ?Elle croit que l’appelant disparaît en appelant ; au contraire, il attend pour continuer là où il s’est arrêté.Elle croit quegs’est terminé en appelanth; il se terminera quandhlui rendra le contrôle.C’est une réponse honnête : la leçon 5.1 l’explique avec une figure de la pile.Question 7Avec la fonction
int f(int x) { x = x + 1; return x; }, qu’écritint a{3}; int b{f(a)}; std::println("{} {}", a, b);?Elle croit que la fonction modifie l’argument ; par défaut, elle reçoit une copie.Elle croit que le changement dexn’atteint même pas la valeur renvoyée.Elle croit qu’un paramètre ne peut pas être modifié ; il le peut, sauf s’il est déclaréconst.Le paramètrexest une variable defqui s’initialise avec une copie de l’argument : la fonction change la copie et renvoie 4, etaconserve son 3. C’est le passage par valeur, le mode par défaut de C++.À revoir : Paramètres et portée
Question 8Un programme lit une date et doit écrire son jour de la semaine. Quelle décomposition sépare le calcul de la présentation ?
Elle mélange deux tâches : ce qui est calculé ne peut être ni réutilisé ni comparé avec une valeur attendue.Elle sépare les tâches, mais les fait communiquer par un effet caché : qui lit l’appel ne voit pas d’où vient la donnée.C’est une réponse honnête : la leçon 5.3 enseigne à décider quelles fonctions écrire.Ainsi, on teste le calcul avec des nombres, sans regarder l’écran, et il peut servir à un autre programme qui écrive le résultat d’une autre manière. La leçon 5.3 appelle ce critère « une fonction, une tâche ».À revoir : Concevoir avec des fonctions
Question 9Laquelle des définitions suivantes définit pour tout entier naturel ?
Sans cas de base, la définition ne s’arrête jamais : renverrait à , et ainsi sans fin.Elle définit bien pour , mais laisse non défini , qui vaut 1.Elle définit à partir de lui-même : la définition est circulaire et ne réduit pas le problème.Un cas de base qui ne dépend de rien et un cas récursif qui ramène à : pour calculer , on descend jusqu’à et on multiplie au retour. La leçon Récursivité traduit cette définition en une fonction qui s’appelle elle-même.À revoir : Récursivité
Question 10Avec
int f(int n) { if (n == 0) { return 0; } return n + f(n - 1); }, qu’écritstd::println("{}", f(3));?Elle croit qu’un seul appel s’exécute, ou elle confond l’argument avec le résultat.Elle croit que lereturn 0du cas de base est le résultat de toute la chaîne ; ce n’est que celui du dernier appel.Le nom de la fonction est connu dès son en-tête, et on peut l’utiliser dans son propre corps.f(3)attendf(2), qui attendf(1), qui attendf(0); celle-ci renvoie 0, et au retour on ajoute 1, 2 et 3. La fonction calcule , la même somme que la boucle de la deuxième question.À revoir : Récursivité
Chargement du contenu…