Partie I : Les bases du C++
Types de données, boucles, références, pointeurs, programmation non orientée objet
Articles
Chapitre 1 : Hello World et structures de base + Exercices
Chapitre 1 : Hello World et structures de base + Exercices
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
AccueilProgramme Hello World Le plus petit programme C++ conforme au standard est : Code 1.1 – Programme C++ vraiment minimal Ce programme ne produit aucune sortie visible. En revanche, il est bien compilé, chargé en mémoire, exécuté, puis termine son exécution en retournant la valeur 0. int main() est une fonction 1 Dans ce cas, la valeur retournée est de type int c’est un entier relatif. La fonction s’appelle main et ne prend aucun paramètre en entrée (la parenthèse est vide). main est la fonction que le compilateur cherche en premier lieu (elle doit s’appeler int main() car c’est le point d’entrée dans le programme. L’entier retourné est 0 si le programme se déroule bien (ou une autre valeur programmable si on veut signaler une erreur ou un comportement particulier). Attention ! Ce n’est pas un booléen, c’est un entier ! 0 ne doit pas être traduit par false. Programme « Hello World ! » Dans les IDE (Environnement de Développement Intégré) tels Xcode, CLion ou autre, le programme minimum ou généralement appelé Programme « Hello world ! » est de la forme suivante : Code 1.2 – Premier programme Cpp minimal int main(int argc, const char * argv[]) { std::cout << « Hello, World! » << std::endl; Étudions un peu ce programme qui amène déjà des instructions que l’on ne verra qu’assez loin dans le cours. Pour l’instant il suffit de savoir que la ligne #include <iostream> appelle une bibliothèque 1. Une fonction est un bloc d’instructions pouvant être exécuté lorsqu’il est appelé. Programme Hello World qui va fournir les fonctions std::cout, std::endl ainsi que l’opérateur d’écriture sur la console <<. On remarquera que les paramètres de int sont int argc et const char ∗ argv[]. Le premier des paramètres indique le nombre d’entrées exactes lors de l’appel de la fonction. Les variables const char ∗ argv[] sont des chaines de caractères. Pour l’instant, on en sait assez pour commencer à programmer. On reviendra sur les paramètres possibles de la fonction principale dès que l’on aura vu les pointeurs. **Programme Hello World plus avancé Code 1.3 – Programme C++ Hello World plus élaboré int main(int argc, char* argv[]) { std::cout << « Nombre d’arguments : » << argc << « \n »; for (int i = 0; i < argc; i++) { std::cout << « Argument » << i << » : » << argv[i] << « \n »; La compilation se fait dans un terminal (et depuis le dossier où se trouve main.cpp) avec la commande : g++ main.cpp −o main Une fois compilé, le programme est lancé avec la commande avec ou sans argument. Si on exécute le programme sans aucune valeurs d’entrée càd avec la commande ./main on obtient Nombre d’arguments : 1 On voit que même sans argument donné, il y en a un qui est le nom du programme lui-même. Si on lance ./main ceci est le programme Hello World 1 23 4 5 h dont la sortie sur le terminal donne : Nombre d’arguments : 12 Argument 4 : programme
AccueilProgramme Hello World On voit que la variable argv[] est un tableau de chaînes de carctères. On reviendra sur la signification du symbole ∗ dans char ∗ argv[] plus loin dans le cours • Tout programme C++ possède une fonction main(). • L’exécution commence toujours dans cette fonction. • La valeur retournée indique comment le programme s’est terminé. • Un programme peut recevoir des arguments lors de son lancement. Les types fondamentaux du C++ Valeur logique (true ou false) Caractère ASCII ou petit entier 8 octets (Mac/Linux) * Les tailles indiquées correspondent aux plateformes 64 bits les plus courantes (macOS/Linux). Elles peuvent varier suivant le compilateur et le système d’exploitation. Table 1.1 – Principaux types entiers. Version non signée de char Version non signée de short Version non signée de int 8 octets (Mac/Linux) Version non signée de long Version non signée de long long Les types non signés permettent uniquement de représenter des valeurs positives ou nulles. En contrepartie, ils offrent une plage de valeurs positives environ deux fois plus grande que les types signés de même taille. Table 1.2 – Versions non signées des types entiers.
AccueilProgramme Hello World Le type char occupe un octet comme signed char et unsigned char. Sa particularité est d’être destiné principalement à la représentation des caractères. En réalité, un caractère est mémorisé sous la forme d’un entier correspondant à son code (par exemple le code ASCII ou Unicode). Le standard C++ ne précise pas si char est signé ou non signé : ce choix dépend du compilateur et de la plateforme. Lorsqu’il est nécessaire de lever toute ambiguïté, on utilise explicitement signed char ou unsigned char. Illustration du type char std::cout << c << ’\n’; std::cout << static_cast<int>(c) << ’\n’; Valeur maximale d’une variable de type char Il faut distinguer la capacité du type char et les codes effectivement utilisés. • Valeur maximale d’un char Cela dépend de son signe. 1. Si char est signé (signed char) : minimum : -128 et maximum : 127. 2. Si char est non signé (unsigned char) : minimum : 0 et maximum = 255. Comme le standard C++ ne précise pas si char est signé ou non, il est préférable de ne pas supposer sa plage de valeurs. • Valeurs effectivement utilisées Cela dépend de l’encodage. En ASCII (historique) on utilise seulement 128 codes, de 0 à 127.
AccueilProgramme Hello World Historiquement, un char représentait un caractère codé sur un octet (ASCII). Aujourd’hui, avec l’encodage UTF-8, un caractère peut occuper plusieurs octets. Le type char représente donc un octet, et non toujours un caractère complet. Pour l’instant, par souci de simplicité, on ne s’occupe que de l’encodage ASCII. 16 octets (selon plateforme) * Les nombres réels sont stockés selon la norme IEEE 754. Table 1.3 – Principaux types réels. Les tailles mémoire Les résultats dépendent de la plateforme. Code 1.5 – Tailles de différents types std::cout << sizeof(bool) << ’\n’; std::cout << sizeof(char) << ’\n’; std::cout << sizeof(short) << ’\n’; std::cout << sizeof(int) << ’\n’; std::cout << sizeof(long) << ’\n’; std::cout << sizeof(long long) << ’\n’; std::cout << sizeof(float) << ’\n’; std::cout << sizeof(double) << ’\n’; Program ended with exit code: 0 Les types à taille fixe Jusqu’à présent, nous avons utilisé des types tels que int, long ou short. Bien qu’ils soient très pratiques, leur taille n’est pas entièrement fixée par le standard C++. Par exemple, un long occupe généralement 8 octets sur les ordinateurs récents sous macOS ou Linux, mais seulement 4 octets sur
AccueilProgramme Hello World certains systèmes Windows. Un même programme peut donc manipuler des tailles différentes selon la plateforme utilisée. Dans la plupart des applications, cette différence n’a aucune conséquence. En revanche, elle devient critique lorsqu’un programme communique avec du matériel électronique, lit ou écrit un fichier binaire, échange des données sur un réseau ou utilise un protocole de communication précis. Pour répondre à ce besoin, la bibliothèque standard C++ définit des types dont la taille est garantie. Leur nom indique directement le nombre de bits utilisés. Ainsi, int8_t représente un entier signé de 8 bits, uint16_t un entier non signé de 16 bits et uint32_t un entier non signé de 32 bits. Ces types sont très utilisés en programmation embarquée, notamment avec l’ESP32 et ESP-IDF, où la taille exacte des données est souvent imposée par le matériel ou par les protocoles de communication. Table 1.4 – Les types entiers à taille fixe définis dans <cstdint>. Les types à taille fixe ont été introduits avec la norme C99, puis repris par le C++ (dans l’entête <cstdint>). Ils sont aujourd’hui largement utilisés dans les domaines où la portabilité et la compatibilité matérielle sont essentielles. Le type size_t est utilisé pour représenter la taille d’un objet ou le nombre d’éléments contenus dans une structure de données. Il est renvoyé notamment par l’opérateur sizeof() et par les fonctions size() de nombreuses classes de la bibliothèque standard 2 . Les structures fondamentales du langage C++ Un bloc est une suite d’instructions comprise entre deux accolades : 2. On reviendra sur le type size_t par la suite
AccueilProgramme Hello World Exemple 1 : La portée Code 1.8 – La portée d’un bloc std::cout << « a = » << a << ’\n’; std::cout << « b = » << b << ’\n’; std::cout << « a = » << a << ’\n’; ne compile pas si on ajoute la ligne std::cout << « b = » << b << ’\n’; en dessous de la ligne std::cout << « a = » << a << ’\n’;. La variable b n’existe qu’entre les accolades où elle a été définie. Lorsque l’exécution quitte ce bloc, la variable est automatiquement détruite. Les blocs permettent de regrouper plusieurs instructions. Ils jouent également un rôle fondamental dans la gestion de la portée des variables et de la durée de vie des objets. Exemple 2 : Le masquage Code 1.9 – Masquage std::cout << a << ’\n’; std::cout << a << ’\n’; La seconde variable ne remplace pas la première. Elle la masque simplement à l’intérieur du bloc. Exemple 3 : La durée de vie Code 1.10 – Durée de vie
AccueilProgramme Hello World À la fermeture de l’accolade : 1. la mémoire de a est libérée ; 2. l’objet cesse d’exister. En C++, les accolades ne servent pas uniquement à regrouper des instructions. Elles définissent un bloc dans lequel peuvent être déclarées des variables. La portée de ces variables est limitée à ce bloc, et elles sont automatiquement détruites lorsque l’exécution en sort. Cette notion est fondamentale et sera réutilisée tout au long du langage. uniquement à améliorer la présentation du code. Elles définissent la portée des variables et la durée de vie des objets. Code 1.11 – Piège 1 ne compile pas car une déclaration n’est pas une instruction simple. Il faut écrire : Code 1.12 – Manière juste d’écrire 2. Ce code ne compile pas non plus : Code 1.13 – Piège 2 for (int i = 0; i < 10; ++i) car int i est déclaré dans le bloc du for. En C++, la destruction automatique des objets à la sortie d’un bloc est un mécanisme fondamental. C’est lui qui permettra plus tard de comprendre le fonctionnement du RAII, des flux de fichiers (std::ifstream, std::ofstream), des verrous (std::lock_guard) et des pointeurs intelligents
AccueilProgramme Hello World • Les accolades définissent un bloc. • Un bloc délimite la portée des variables. • Une variable n’existe que dans le bloc où elle est déclarée. • À la fin d’un bloc, les objets locaux sont automatiquement détruits. • Une variable peut masquer une autre variable de même nom dans un bloc intérieur. Les premiers langages de programmation, tels que le FORTRAN, ne possédaient pas de véritables blocs d’instructions. Plus tard, des langages comme Pascal ont introduit les mots-clés begin et end pour délimiter les blocs. Le langage C, puis le C++, ont remplacé ces mots-clés par les accolades { et }, plus compactes et plus faciles à lire lorsque les blocs sont fortement Les structures conditionnelles permettent de sélectionner les instructions qui seront exécutées en fonction de la valeur d’une expression. En C++, la condition d’un test est convertie en une valeur de type bool. Une expression n’a donc pas nécessairement besoin d’être explicitement égale à true ou à false. La forme générale d’un test est la suivante : // Instructions executees si la condition est vraie La condition est évaluée une seule fois. Si elle vaut true, le bloc est exécuté. Dans le cas contraire, Code 1.14 – Test simple int temperature = 18; if (temperature < 20) std::cout << « La temperature est inferieure a 20 degres.\n »; La temperature est inferieure a 20 degres. int temperature = 18; std::cout << « La temperature est inferieure a 20 degres.\n »;
AccueilProgramme Hello World donne également la sortie La temperature est inferieure a 20 degres. if (0.000000000000001) std::cout << « Vrai\n »; std::cout << « Faux\n »; std::cout << « Vrai\n »; std::cout << « Faux\n »; En C++, lorsqu’une valeur numérique est utilisée comme condition, elle est convertie implicitement en bool selon une règle très simple : • 0 ou 0.0 donne false • toute autre valeur donne true Lorsqu’une valeur numérique est utilisée comme condition, seule la valeur exactement égale à zéro est convertie en false. Toute autre valeur, même extrêmement petite, est convertie en Ainsi, les deux tests suivants donnent des résultats différents :
AccueilProgramme Hello World Lorsqu’une condition est une constante connue à la compilation (par exemple if (0.0) ou if (1)), le compilateur ou l’environnement de développement peut détecter qu’une branche ne sera jamais exécutée et émettre un avertissement. Il ne s’agit pas d’une erreur de syntaxe, mais d’une optimisation et d’une aide à la détection d’erreurs logiques. Les premières versions du langage C ne possédaient pas de type booléen. Les tests conditionnels utilisaient directement des valeurs entières : la valeur zéro représentait une condition fausse, tandis que toute valeur non nulle représentait une condition vraie. Le C++ a introduit le type bool, mais a conservé cette conversion implicite afin de rester compatible avec l’immense quantité de code C déjà existante. Les branches else if et else Plusieurs cas peuvent être distingués grâce à une chaîne de tests : std::cout << « Tres bien\n »; else if (note >= 4.0) std::cout << « Suffisant\n »; std::cout << « Insuffisant\n »; Les conditions sont évaluées dans l’ordre. Dès qu’une condition est vraie, le bloc correspondant est exécuté et les branches suivantes sont ignorées. Conversion implicite vers bool Une condition n’est pas obligatoirement le résultat d’une comparaison explicite. De nombreuses valeurs peuvent être converties en bool. Pour les types entiers : • la valeur zéro est convertie en false ; • toute valeur non nulle est convertie en true (voir ci-dessus avec l’exemple if (330) …. std::cout << « n est non nul\n »;
AccueilProgramme Hello World std::cout << *p << ’\n’; ne donne pas de sortie. Dans un test, l’expression if (p) est équivalente à if (p != nullptr). La seconde écriture est plus explicite, tandis que la première est très courante dans le code Affectation ou comparaison ? L’opérateur = réalise une affectation, tandis que l’opérateur == réalise une comparaison. Ces deux instructions ont des significations très différentes : // Affectation, puis conversion en bool, attention si b=0 alors if (a = b) retourne false La seconde écriture est syntaxiquement valide. La valeur de b est affectée à a, puis la nouvelle valeur de a est convertie en bool. La confusion entre = et == est une erreur classique. Lorsque l’affectation dans une condition est réellement volontaire, il est préférable de la rendre explicite au moyen de parenthèses supplémentaires. Évaluation paresseuse des opérateurs logiques Les opérateurs logiques && et || utilisent une évaluation dite paresseuse ou en court-circuit. condition1 && condition2 la seconde condition n’est évaluée que si la première est vraie. Dans l’expression condition1 || condition2 la seconde condition n’est évaluée que si la première est fausse.
AccueilProgramme Hello World if (p != nullptr && *p > 0) std::cout << « Valeur positive\n »; La première condition est fausse. L’expression ∗p > 0 n’est donc jamais évaluée, ce qui évite de déréférencer un pointeur nul. • Avec &&, l’évaluation s’arrête dès qu’une condition est fausse. • Avec ||, l’évaluation s’arrête dès qu’une condition est vraie. • L’ordre des conditions peut donc avoir une importance fondamentale. Le langage autorise l’omission des accolades lorsqu’un seul bloc d’instruction suit le test : std::cout << « Positif\n »; Cette écriture est correcte, mais elle devient rapidement dangereuse lorsque le programme est modifié. std::cout << « Positif\n »; Seule la première instruction dépend du test. L’incrémentation de compteur est toujours exécutée, malgré l’indentation trompeuse. Dans cet ouvrage, les accolades seront donc utilisées systématiquement. Le problème du else ambigu Lorsqu’un test est imbriqué sans accolades, un else est toujours associé au if précédent le plus proche qui ne possède pas encore de branche else. std::cout << « a et b sont positifs\n »; std::cout << « b n’est pas positif\n »; Le else est associé au second if, et non au premier. L’utilisation systématique des accolades élimine L’instruction switch Lorsque plusieurs choix dépendent de la valeur entière d’une même expression, l’instruction switch peut être plus lisible qu’une succession de if et de else if.
Accueilstd::cout << « Premier choix\n »; std::cout << « Deuxieme choix\n »; Programme Hello World std::cout << « Troisieme choix\n »; std::cout << « Choix inconnu\n »; Sans l’instruction break, l’exécution continue dans les branches suivantes. Ce comportement est appelé fallthrough. std::cout << « Acces niveau 3\n »; // poursuite volontaire std::cout << « Acces niveau 2\n »; // poursuite volontaire std::cout << « Acces niveau 1\n »; std::cout << « Aucun acces\n »; L’absence involontaire de break dans une branche case est une source classique d’erreurs. En C++ 11, lorsqu’une poursuite vers la branche suivante est volontaire, elle doit être clairement signalée par un commentaire. L’expression contrôlée par switch doit être de type entier, énuméré ou convertible vers un type entier. On ne peut pas utiliser directement un double ou une chaîne de caractères comme expression
AccueilProgramme Hello World • Une condition est convertie en une valeur de type bool. • Zéro correspond à false ; toute valeur entière non nulle correspond à true. • Un pointeur nul est converti en false. • = réalise une affectation, tandis que == réalise une comparaison. • Les opérateurs && et || utilisent une évaluation en court-circuit. • Les accolades doivent être utilisées systématiquement, même pour une seule instruction. • Dans un switch, l’instruction break empêche généralement l’exécution de la branche Les boucles permettent de répéter l’exécution d’un même bloc d’instructions tant qu’une condition Le langage C++ propose trois structures de répétition : • la boucle while ; • la boucle do…while ; Bien que leur syntaxe soit différente, ces trois structures permettent de réaliser les mêmes traitements. Le choix de l’une ou de l’autre dépend essentiellement de la lisibilité du programme. La boucle while exécute un bloc d’instructions tant que sa condition reste vraie. La condition est évaluée avant chaque itération. Si elle est fausse dès le départ, le bloc n’est jamais Code 1.15 – Compteur de 0 à 4 std::cout << i << ’\n’; Program ended with exit code: 0
AccueilProgramme Hello World La boucle do…while Contrairement à la boucle while, la boucle do…while execute toujours son bloc au moins une std::cout << i << ’\n’; La sortie est naturellement Program ended with exit code: 0 Même si la condition est fausse, le nombre 5 est affiché une fois. La boucle for est particulièrement adaptée lorsque le nombre d’itérations est connu ou lorsqu’un compteur est utilisé. Sa syntaxe générale est : for (initialisation ; condition ; mise_a_jour) Les trois expressions placées entre les parenthèses jouent des rôles différents : • l’initialisation est exécutée une seule fois ; • la condition est évaluée avant chaque itération ; • la mise à jour est exécutée à la fin de chaque tour de boucle. Code 1.16 – Affichage des dix premiers entiers for (int i = 0 ; i < 10 ; ++i) std::cout << i << ’\n’;
AccueilProgramme Hello World Program ended with exit code: 0 Cette boucle est strictement équivalente à : std::cout << i << ’\n’; dont la sortie est identique, à savoir Program ended with exit code: 0 La boucle for constitue donc essentiellement une écriture plus compacte du même algorithme. Pré-incrémentation et post-incrémentation Deux opérateurs permettent d’incrémenter une variable : Ils produisent le même effet sur la variable, mais ne retournent pas la même valeur. Après l’exécution : i vaut 6 et a vaut également 6.
AccueilProgramme Hello World Après l’exécution : i vaut 6 tandis que a vaut 5. Dans une boucle for, la valeur retournée par l’opérateur n’est généralement pas utilisée. Les deux écritures suivantes sont donc équivalentes : for (int i = 0 ; i < 100 ; ++i) for (int i = 0 ; i < 100 ; i++) Remarque 1.5.1. Avec certaines classes, notamment les itérateurs de la bibliothèque standard, la pré-incrémentation (++it) peut être légèrement plus efficace que la post-incrémentation (it++), car elle évite la création d’une copie temporaire de l’objet. C’est pourquoi la pré-incrémentation est souvent privilégiée dans le code C++ moderne. Pour un type fondamental, on peut considérer que ++i est équivalent à incrémenter d’abord la variable puis à utiliser sa nouvelle valeur. De même, i++ utilise d’abord la valeur actuelle puis effectue l’incrémentation. i = i + 1; puis utiliser la nouvelle valeur de i. Utiliser la valeur actuelle de i, puis exécuter i = i + 1;. Table 1.5 – Différence entre pré-incrémentation et post-incrémentation. Remarque 1.5.2. Ce tableau décrit le comportement conceptuel des deux opérateurs. Le compilateur est libre d’optimiser les instructions générées, mais le résultat observable reste toujours celui décrit Les boucles infinies Une boucle dont la condition reste toujours vraie ne se termine jamais. Les boucles infinies sont utilisées dans certains domaines, notamment pour les serveurs ou les systèmes embarqués, où un programme doit rester actif tant que la machine fonctionne. L’instruction break L’instruction break interrompt immédiatement la boucle la plus interne.
AccueilProgramme Hello World L’instruction break L’instruction break interrompt immédiatement la boucle la plus interne. L’instruction continue L’instruction continue interrompt uniquement l’itération en cours. L’exécution reprend directement au début de l’itération suivante. for (int i = 0 ; i < 10 ; ++i) std::cout << i << ’\n’; Le nombre 5 n’est pas affiché. L’instruction goto L’instruction goto permet de transférer directement l’exécution vers une étiquette située ailleurs dans la même fonction. Bien que cette instruction fasse toujours partie du langage C++, son utilisation est aujourd’hui très rare. Dans la plupart des situations, les structures de contrôle (if, for, while, break, continue et return) permettent d’obtenir un code plus lisible et plus facile à maintenir. std::cout << « Le nombre est positif.\n »; std::cout << « Erreur : le nombre est negatif.\n »;
AccueilProgramme Hello World Bien que l’instruction goto fasse toujours partie du langage C++, elle est aujourd’hui très rarement utilisée. Dans la quasi-totalité des cas, les structures de contrôle (if, for, while, break, continue et return) permettent d’écrire un code plus clair et plus facile à maintenir. • Les boucles while, do…while et for permettent de répéter un traitement. • La boucle while peut ne jamais être exécutée. • La boucle do…while est exécutée au moins une fois. • La boucle for regroupe l’initialisation, le test et la mise à jour. • Les opérateurs ++i et i++ ne retournent pas la même valeur. • Dans une boucle for, la pré-incrémentation est généralement préférée. • Les instructions break et continue modifient le déroulement normal d’une boucle. • Le goto n’est pas interdit. Si vous ressentez le besoin de l’utiliser, demandez-vous d’abord si votre algorithme ou la structure de votre programme ne pourrait pas être Les instructions de contrôle Les structures de contrôle étudiées jusqu’à présent (if, switch, while, do…while et for) déterminent le déroulement normal d’un programme. Le langage C++ fournit également plusieurs instructions permettant de modifier ce déroulement en interrompant une boucle, en quittant une fonction ou en effectuant un saut vers une autre partie du L’instruction break L’instruction break provoque l’arrêt immédiat de la boucle ou du switch le plus interne. L’exécution reprend alors avec la première instruction située après cette structure. Code 1.17 – Utilisation de break for (int i = 0; i < 10; ++i) std::cout << i << ’\n’; qui donne comme sortie
AccueilProgramme Hello World Program ended with exit code: 0 Remarque 1.5.3. L’instruction break ne quitte que la structure de contrôle la plus interne. Lorsqu’une boucle est imbriquée dans une autre, seule la boucle courante est interrompue. L’instruction continue L’instruction continue interrompt uniquement l’itération en cours. L’exécution reprend immédiatement à l’itération suivante de la boucle. Code 1.18 – Utilisation de continue for (int i = 0; i < 10; ++i) if (i == 5) continue; std::cout << i << ’\n’; Program ended with exit code: 0 Le nombre 5 n’est jamais affiché. Remarque 1.5.4. Dans une boucle for, l’expression de mise à jour est tout de même exécutée après un continue. Dans une boucle while, il appartient au programmeur de veiller à ce que la variable de contrôle soit correctement mise à jour afin d’éviter une boucle infinie. L’instruction return L’instruction return termine immédiatement l’exécution de la fonction courante 3 . Lorsqu’une valeur est attendue, elle est renvoyée à la fonction appelante. 3. Pour les fonctions voyez le paragraphe suivant.
AccueilProgramme Hello World std::cout << « Bonjour\n »; std::cout << « Ce texte ne sera jamais affiche.\n »; Remarque 1.5.5. L’instruction return met immédiatement fin à l’exécution de la fonction. Toutes les instructions situées après un return ne seront donc jamais exécutées. L’instruction goto L’instruction goto permet d’effectuer un saut direct vers une étiquette située ailleurs dans la même Code 1.19 – Exemple d’utilisation de goto std::cout << « Nombre positif\n »; std::cout << « Erreur\n »; Les premiers langages de programmation, notamment FORTRAN, utilisaient très largement l’instruction goto. Cette pratique conduisait souvent à un code difficile à comprendre, surnommé spaghetti code. À partir des années 1970, la programmation structurée a progressivement remplacé cette approche par des structures comme if, while et for. Aujourd’hui, l’utilisation de goto reste • break interrompt immédiatement la boucle ou le switch le plus interne. • continue interrompt uniquement l’itération courante d’une boucle. • return termine immédiatement l’exécution de la fonction courante. • goto effectue un saut direct vers une étiquette située dans la même fonction. • Les instructions de contrôle doivent être utilisées avec modération afin de préserver la lisibilité des programmes. • L’instruction goto provoque un saut vers une étiquette (ou label) située ailleurs dans la
AccueilProgramme Hello World Les commentaires permettent d’ajouter des explications dans un programme sans influencer son exécution. Ils sont ignorés par le compilateur et servent uniquement à améliorer la compréhension du code par le programmeur. Le C++ propose deux types de commentaires. Commentaires sur une ligne Un commentaire commençant par // se poursuit jusqu’à la fin de la ligne. // Age de l’etudiant double pi = 3.14159; // Valeur approchee de Ce type de commentaire est particulièrement adapté pour de courtes explications. Commentaires sur plusieurs lignes Lorsqu’une explication s’étend sur plusieurs lignes, on peut utiliser les délimiteurs /∗ et ∗/. Calcul de la moyenne des notes. Les notes sont supposees comprises Ces commentaires peuvent s’étendre sur plusieurs lignes, mais ils ne peuvent pas être imbriqués. Les commentaires de type /∗ … ∗/ ne peuvent pas être placés à l’intérieur d’un autre commentaire du même type. Pourquoi commenter un programme ? Le but d’un commentaire n’est pas de décrire chaque instruction, mais d’expliquer ce que le code ne peut pas exprimer naturellement. Par exemple, le commentaire suivant est inutile : En revanche, le commentaire ci-dessous apporte une information précieuse : // Recherche du premier element negatif du tableau. for (int i = 0; i < n; ++i) Le lecteur comprend immédiatement l’objectif de l’algorithme avant même d’en examiner les détails.
AccueilProgramme Hello World Avant d’ajouter un commentaire, demandez-vous si un nom de variable ou de fonction plus explicite ne permettrait pas de s’en passer. double vitesseMoyenne; est beaucoup plus explicite que Un bon choix de noms améliore la lisibilité du programme et réduit le besoin de commentaires. Dans les premiers langages de programmation, les programmes étaient souvent peu structurés et les noms de variables très courts. Les commentaires étaient alors indispensables pour comprendre le fonctionnement du code. Aujourd’hui, les langages modernes permettent d’utiliser des identificateurs explicites et de découper un programme en fonctions courtes. Les commentaires restent importants, mais ils servent surtout à expliquer les choix de conception, les hypothèses ou les algorithmes utilisés. • Les commentaires sont ignorés par le compilateur. • // permet d’écrire un commentaire jusqu’à la fin de la ligne. • /∗ … ∗/ permet d’écrire un commentaire sur plusieurs lignes. • Un bon commentaire explique une intention ou un choix, et non une instruction évidente. • Un code clair et des noms de variables explicites nécessitent généralement peu de commentaires. Les opérateurs permettent d’effectuer des calculs, de comparer des valeurs ou de modifier le contenu des variables. Certains sont hérités directement des mathématiques, tandis que d’autres sont propres Cette section présente les opérateurs les plus couramment utilisés. Les opérateurs plus spécialisés (opérateurs binaires, surcharge d’opérateurs, etc.) seront étudiés dans les chapitres correspondants. Les opérateurs arithmétiques Reste de la division entière
AccueilProgramme Hello World cout << a / b << endl; cout << 7.0 / 2 << endl; Lorsque les deux opérandes sont des entiers, la division est entière. La partie décimale est Les opérateurs d’affectation // x vaut maintenant 17 // x vaut maintenant 34 Les opérateurs d’incrémentation Les opérateurs d’incrémentation et de décrémentation permettent d’ajouter ou de retrancher une Remarque 1.5.6. La différence entre ++i et i++ sera étudiée plus loin dans le livre. Les opérateurs de comparaison Les opérateurs de comparaison renvoient une valeur booléenne (true ou false).
AccueilProgramme Hello World Plus petit ou égal à Plus grand ou égal à qui réalise une affectation, avec qui effectue une comparaison. Les opérateurs logiques Les opérateurs logiques permettent de combiner plusieurs conditions. if (age >= 18 && permis) Priorité des opérateurs Comme en mathématiques, certains opérateurs sont évalués avant d’autres. Dans les cas les plus courants : 2. Multiplication, division, modulo 3. Addition, soustraction 5. Opérateurs logiques En cas de doute, ajoutez des parenthèses. Elles améliorent souvent la lisibilité du programme.
AccueilProgramme Hello World Les opérateurs binaires Le C++ possède également des opérateurs agissant directement sur les bits. Ces opérateurs sont très utilisés en programmation système et dans le développement pour microcontrôleurs (ESP32, STM32, Arduino, etc.). Ils seront étudiés en détail dans un chapitre spécifique.
Accueilcpp-Chap1- Programme Hello World
Accueil
Chapitre 1 – Exercices
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
AccueilExercices Chapitre 1 – Partie I À propos de la présentation du code Dans les exercices et leurs corrections, la mise en page du code répond avant tout à des objectifs pédagogiques. Les indentations, les retours à la ligne, les espaces et la position des accolades sont choisis de manière à rendre aussi clairement que possible la structure logique du programme. Il ne s’agit pas d’imposer un style unique. Avec l’expérience, chacun développera naturellement ses propres habitudes de présentation, pour autant que le code reste cohérent et facilement lisible. Dans les corrections, nous privilégierons donc parfois une écriture plus développée que strictement nécessaire. Par exemple : peut parfaitement être écrit : if (n % 2 == 0) premier = false; puisque le bloc contrôlé par le if ne contient qu’une seule instruction. La première forme permet cependant de visualiser immédiatement le bloc associé à la condition. Elle peut donc être préférable dans un exemple destiné à expliquer le fonctionnement d’une structure de Inversement, lorsqu’une écriture plus compacte permet de mieux faire ressortir le raisonnement sans nuire à sa compréhension, elle pourra également être utilisée. L’objectif des corrections n’est donc pas de proposer une mise en page à reproduire mécaniquement, mais de montrer clairement la structure du programme et le rôle de chacune de ses parties. La compréhension doit précéder le choix du style. Finalement, chacun trouvera son style, on est pas sous Python 😉 ! Exercices – Partie I – Chapitre 1 Exercice 1 — Portée, blocs et masquage On considère le programme suivant : std::cout << « 1 : » << a << » » << b << ’\n’; std::cout << « 2 : » << a << » » << b << ’\n’; std::cout << « 3 : » << a << » » << b << ’\n’; std::cout << « 4 : » << a << » » << b << ’\n’; std::cout << « 5 : » << a << » » << b << ’\n’; std::cout << « 6 : » << a << » » << b << ’\n’; std::cout << « 7 : » << a << » » << b << ’\n’; Sans compiler ni exécuter le programme dans un premier temps : 1. Déterminer exactement ce qui est affiché par chacune des sept instructions std::cout. 2. Pour chaque affichage, préciser à quelle déclaration correspondent les variables a et b utilisées. 3. Identifier toutes les variables distinctes créées au cours de l’exécution du programme. Deux variables portant le même nom mais déclarées dans des blocs différents doivent être considérées comme deux variables différentes. 4. Pour chacune de ces variables, indiquer : • l’endroit où commence sa portée ; • l’endroit où se termine sa portée ; • à quel moment sa durée de vie prend fin.
AccueilExercices – Partie I – Chapitre 1 5. Expliquer pourquoi l’instruction ne modifie pas la variable a initialisée à la valeur 10. 6. En revanche, expliquer pourquoi l’instruction modifie bien la variable b initialisée à la valeur 20. 7. On ajoute l’instruction suivante immédiatement après la fermeture du bloc dans lequel b est initialisée à 40 : std::cout << b << ’\n’; Cette instruction compile-t-elle ? Si oui, quelle valeur affiche-t-elle et pourquoi ? 8. On ajoute maintenant l’instruction suivante immédiatement après la fermeture du bloc dans lequel a est initialisée par l’expression b – 5 : std::cout << a << ’\n’; Cette instruction compile-t-elle ? Si oui, quelle variable a est alors utilisée ? 9. Enfin, déterminer si les quatre variables suivantes constituent réellement quatre objets distincts Justifier la réponse en utilisant les notions de bloc, de portée, de masquage et de durée de vie. Une fois toutes les réponses établies sur papier, compiler et exécuter le programme afin de vérifier les résultats obtenus. En cas de différence entre le raisonnement et l’exécution, expliquer précisément l’origine de l’erreur de raisonnement avant de modifier les réponses.
AccueilExercices – Partie I – Chapitre 1 Solution de l’exercice 1 — Portée, blocs et masquage 1. Sortie du programme Le programme affiche : La difficulté de cet exercice vient du fait que plusieurs variables portent le même nom. Une nouvelle déclaration dans un bloc intérieur ne remplace pas la variable portant le même nom dans le bloc extérieur mais elle la masque temporairement. 2. Une méthode pour suivre les variables Une manière particulièrement efficace d’analyser le programme consiste à renommer mentalement les variables de même nom selon leur niveau d’imbrication. On peut par exemple utiliser : pour les trois variables appelées a, et pour les deux variables appelées b. Le programme peut alors être réécrit, uniquement pour faciliter son analyse, sous la forme suivante : std::cout << « 1 : » << a << » » << b << ’\n’; std::cout << « 2 : » << aa << » » << b << ’\n’; std::cout << « 3 : » << aa << » » << bb << ’\n’;
Accueilstd::cout << « 4 : » << aaa << » » << bb << ’\n’; std::cout << « 5 : » << aa << » » << bb << ’\n’; Exercices – Partie I – Chapitre 1 std::cout << « 6 : » << aa << » » << b << ’\n’; std::cout << « 7 : » << a << » » << b << ’\n’; Cette réécriture ne constitue naturellement pas le programme original car elle supprime volontairement le masquage des noms. Elle constitue en revanche une représentation très utile pour distinguer les différents objets et suivre leurs valeurs. 3. Analyse des sept affichages Au premier affichage, seules les variables du bloc principal existent : Dans le bloc suivant, une nouvelle variable a est créée avec la valeur 30. Elle masque la première variable a et uucune nouvelle variable b n’est déclarée dans ce bloc, l’instruction agit donc sur la variable b du bloc principal : Dans le bloc suivant, une nouvelle variable b est créée : Elle masque temporairement la variable b qui vaut 25, en revanche, aucune nouvelle variable a n’est déclarée dans ce bloc. L’instruction modifie donc la variable a du bloc précédent : Pour le 3e résultat on a :
AccueilExercices – Partie I – Chapitre 1 Dans le bloc le plus intérieur, une troisième variable a est créée : À cet endroit, la variable b visible est celle qui vaut 40 et la nouvelle variable vaut donc : agit également sur cette variable b, qui passe de 40 à 41. À la sortie de ce bloc, la variable a qui valait 35 est détruite. La variable a précédemment masquée redevient accessible. Elle vaut toujours 32 et la variable b valant 41 existe encore donc : À la sortie du bloc suivant, la variable b valant 41 est détruite. La variable b du bloc principal redevient donc accessible. Elle avait été modifiée précédemment et vaut 25, la variable a du bloc courant vaut toujours 32 : Enfin, lorsque ce bloc est quitté, cette variable a est également détruite et les deux variables du bloc principal sont alors de nouveau visibles : et le dernier affichage est : 4. Les objets réellement créés Le programme crée cinq variables distinctes : Nom dans le programme Nom utilisé pour l’analyse Les trois variables appelées a sont donc trois objets distincts. De même, les deux variables appelées b sont deux objets distincts. Le fait que plusieurs variables portent le même nom ne signifie pas qu’elles occupent le même emplacement mémoire.
AccueilExercices – Partie I – Chapitre 1 5. Portée et durée de vie Chaque variable locale commence son existence lorsque l’exécution atteint sa déclaration. Sa portée s’étend depuis sa déclaration jusqu’à la fin du bloc dans lequel elle a été déclarée, sous réserve qu’elle puisse être masquée par une nouvelle déclaration portant le même nom dans un bloc Lorsqu’une variable est masquée, elle continue d’exister mais n’est simplement plus accessible directement par son nom dans cette partie du programme. Lorsqu’on quitte le bloc dans lequel une variable locale a été créée, sa durée de vie prend fin et l’objet Il faut donc bien distinguer : • une variable qui existe et est accessible ; • une variable qui existe mais est masquée ; • une variable dont la durée de vie est terminée. 6. Pourquoi int a = 30; ne modifie-t-il pas a = 10 ? est une déclaration accompagnée d’une initialisation, elle crée une nouvelle variable appelée a dans le bloc intérieur. La variable a du bloc principal continue d’exister avec la valeur 10, mais elle est masquée tant que l’exécution reste dans la portée de la nouvelle variable. 7. Pourquoi b = b + 5; modifie-t-il b = 20 ? Dans ce bloc, aucune nouvelle variable appelée b n’a été déclarée, le compilateur recherche donc le nom b dans les blocs englobants et trouve la variable déclarée dans le bloc principal donc l’instruction est une affectation et non une déclaration donc elle modifie la variable existante : 8. Accès à b après le bloc de b = 40 Après la fermeture du bloc dans lequel la variable b valant 40 a été déclarée, cette variable est détruite. Une instruction std::cout << b << ’\n’; placée à cet endroit compile néanmoins et le nom b désigne alors de nouveau la variable du bloc principal, qui vaut : L’instruction affiche donc :
AccueilExercices – Partie I – Chapitre 1 9. Accès à a après le bloc le plus intérieur Après la fermeture du bloc dans lequel la variable a été déclarée, cette variable cesse d’exister. La variable a du bloc immédiatement supérieur redevient accessible avec la valeur 32. L’instruction std::cout << a << ’\n’; correspondent bien à quatre objets distincts, auxquels s’ajoute la variable b initialisée à 20. Le masquage ne détruit pas une variable et ne modifie pas sa valeur mais il rend simplement son nom temporairement inaccessible dans le bloc où une autre variable portant le même nom est visible. permet de rendre cette structure explicite et constitue une méthode simple pour analyser les blocs
AccueilExercices – Partie I – Chapitre 1 Exercice 2 – Le programme qui ne fait pas ce qu’il semble faire On considère le programme suivant : if (c > 1 || ++b > 0) if (a > 0 && (c = 0)) std::cout << « a = » << a << ’\n’; std::cout << « b = » << b << ’\n’; std::cout << « c = » << c << ’\n’; std::cout << « d = » << d << ’\n’;
AccueilExercices – Partie I – Chapitre 1 Sans compiler ni exécuter le programme dans un premier temps : 1. Déterminer les valeurs finales des variables a, b, c et d. 2. Déterminer exactement ce qui est affiché par le programme. 3. Pour chacun des six tests if, indiquer : • la valeur de l’expression conditionnelle ; • si le bloc associé est exécuté ou non ; • quelles variables sont éventuellement modifiées pendant l’évaluation de la condition ellemême. 4. Identifier toutes les affectations effectuées à l’intérieur d’une condition. Pour chacune d’elles, préciser : • la valeur affectée ; • la valeur obtenue après conversion implicite vers bool. 5. Dans l’instruction suivante : if (c > 1 || ++b > 0) déterminer si l’expression est effectivement évaluée. Justifier la réponse à l’aide du mécanisme d’évaluation en court-circuit de l’opérateur ||. 6. Dans l’instruction suivante : déterminer si l’expression Expliquer précisément le rôle joué par la valeur de b. 7. Étudier l’instruction : Décomposer son évaluation étape par étape. Indiquer notamment : • la valeur calculée par a – c ; • la nouvelle valeur de d ; • la conversion de cette valeur en bool ; • si ++b est ensuite évalué.
AccueilExercices – Partie I – Chapitre 1 8. Étudier maintenant : if (a > 0 && (c = 0)) Expliquer pourquoi l’affectation peut être exécutée alors que le bloc du if ne l’est pas. 9. Expliquer la différence fondamentale entre les deux instructions suivantes : Pourquoi la première est-elle syntaxiquement valide en C++ ? 10. Réécrire les tests du programme sous une forme plus claire, sans modifier son comportement, les affectations volontaires placées dans des conditions devront être rendues aussi explicites 11. Après avoir terminé l’analyse sur papier, compiler et exécuter le programme. Comparer le résultat obtenu avec les valeurs prévues et expliquer toute différence éventuelle.
AccueilExercices – Partie I – Chapitre 1 Solution de l’exercice 2 — Le programme qui ne fait pas ce qu’il Les valeurs finales sont : Le programme affiche donc : La difficulté vient du fait que certaines conditions modifient elles-mêmes les variables qu’elles testent. Il faut donc distinguer : • l’évaluation de la condition ; • les modifications produites pendant cette évaluation ; • l’exécution éventuelle du bloc associé au if. Le premier test est : est une affectation et non une comparaison. La valeur de b, soit 0, est affectée à a : L’expression d’affectation possède elle-même la valeur 0. Cette valeur est convertie implicitement en bool : Le bloc n’est donc pas exécuté et c reste égal à 2.
AccueilExercices – Partie I – Chapitre 1 Le deuxième test est : if (c > 1 || ++b > 0) La première condition vaut : Elle est donc vraie. L’opérateur || utilise une évaluation en court-circuit autrement dit, dès que son opérande gauche est vrai, l’opérande droit n’est pas évalué, l’expression n’est donc jamais exécutée et la variable b reste égale à 0 puis le bloc du if est exécuté : Le troisième test est : La première expression est simplement : La conversion implicite donne : Avec l’opérateur &&, dès que l’opérande gauche est faux, l’opérande droit n’est pas évalué.
AccueilExercices – Partie I – Chapitre 1 n’est donc jamais exécutée. Le bloc du if n’est pas exécuté non plus. Les valeurs restent : Le quatrième test est : On commence par évaluer : L’expression d’affectation vaut elle-même 1. Sa conversion en bool donne : Comme le premier opérande de && est vrai, le second doit également être évalué : La variable b passe de 0 à 1 : La valeur retournée par la pré-incrémentation est donc 1, ce qui est également converti en true. La condition complète est donc vraie et le bloc est exécuté : Les valeurs deviennent :
AccueilExercices – Partie I – Chapitre 1 Le cinquième test est : if (a > 0 && (c = 0)) la première condition est vraie et le second opérande de && est donc évalué : La variable c est modifiée et vaut : Mais l’expression d’affectation possède elle-même la valeur 0 donc après sa conversion vers bool : La condition complète est donc fausse et le bloc : n’est pas exécuté. La variable d reste donc égale à 3. Cet exemple montre qu’une condition peut modifier une variable alors que le bloc du if n’est finalement pas exécuté. Le dernier test est : La première expression vaut : Elle est donc convertie en false. Avec l’opérateur ||, lorsque l’opérande gauche est faux, l’opérande droit doit être évalué, on calcule donc :
AccueilExercices – Partie I – Chapitre 1 La variable a passe de 3 à 4 : La condition complète vaut donc : false || false = false. n’est pas exécuté. Les valeurs finales sont donc : 8. Tableau récapitulatif 9. Affectation et comparaison ont des significations totalement différentes. L’opérateur = effectue une affectation, dans : la valeur de b est d’abord copiée dans a, puis cette valeur est convertie en bool. L’opérateur ==, en revanche, effectue une comparaison et ne modifie aucune des deux variables. La première écriture est syntaxiquement correcte en C++ parce qu’une expression d’affectation produit elle-même une valeur qui peut être convertie en bool.
AccueilExercices – Partie I – Chapitre 1 10. Réécriture explicite des conditions On peut rendre le comportement du programme plus lisible en séparant les affectations et les effets de bord des expressions conditionnelles. Par exemple, le premier test : peut être développé en : Cette seconde écriture est plus longue, mais elle rend explicitement visibles les opérations réellement effectuées et l’ordre dans lequel elles sont évaluées. 11. Réduction au chemin réellement exécuté Pour les valeurs initiales imposées dans cet exercice, on peut aller plus loin et supprimer toutes les branches dont on sait qu’elles ne seront jamais exécutées.
AccueilExercices – Partie I – Chapitre 1 std::cout << « a = » << a << ’\n’; std::cout << « b = » << b << ’\n’; std::cout << « c = » << c << ’\n’; std::cout << « d = » << d << ’\n’; Cette version produit exactement le même résultat pour les valeurs initiales de l’exercice. Elle ne constitue toutefois pas une réécriture générale équivalente du programme original : si les valeurs initiales de a, b, c ou d étaient modifiées, le chemin d’exécution pourrait changer. Cette réduction représente donc uniquement le chemin d’exécution effectivement suivi dans le cas étudié. Cet exercice met en évidence quatre mécanismes importants du C++ : • une affectation est une expression et possède une valeur ; • cette valeur peut être convertie implicitement en bool ; • les opérateurs && et || utilisent une évaluation en court-circuit ; • une expression conditionnelle peut modifier l’état du programme même lorsque le bloc du if n’est finalement pas exécuté. Pour analyser ce type de programme, une bonne méthode consiste à suivre les variables instruction par instruction et à noter séparément les effets produits pendant l’évaluation de chaque condition.
AccueilExercices – Partie I – Chapitre 1 Exercice 3 – Représentation des entiers et portabilité On souhaite étudier expérimentalement la taille des principaux types entiers du C++ et déterminer quelles hypothèses peuvent raisonnablement être faites lorsqu’un programme doit être portable. Écrire un programme qui affiche la taille, en octets, des types suivants : • unsigned short ; • unsigned long long ; Les types à taille fixe nécessitent l’en-tête : 1. Écrire le programme permettant d’obtenir les tailles de tous les types précédents à l’aide de l’opérateur sizeof et présenter les résultats sous une forme suffisamment claire pour permettre de comparer facilement les différents types. 2. Quel est le type de la valeur retournée par l’opérateur sizeof ? Expliquer pourquoi ce type est particulièrement adapté pour représenter une taille mémoire. 3. Comparer systématiquement la taille d’un type signé avec celle du type non signé correspondant,
AccueilExercices – Partie I – Chapitre 1 Le mot-clé unsigned signifie-t-il que le type occupe davantage de mémoire ? Que constate-t-on concernant leur taille ? Peut-on en déduire que le type char est nécessairement signé ? 5. On exécute le programme sur une machine donnée et on obtient : Peut-on écrire un programme portable en supposant que ces deux résultats seront toujours identiques sur toutes les plateformes ? Justifier la réponse. 6. Comparer maintenant les résultats obtenus pour : Quelle information supplémentaire le nom de ces types fournit-il par rapport aux types short, int, long et long long ? 7. Un programme doit communiquer avec un périphérique dont la documentation impose le format Choisir, parmi les types étudiés, un type adapté à chacune de ces quatre données. Justifier les choix. 8. Un programmeur écrit :
AccueilExercices – Partie I – Chapitre 1 unsigned long compteur; parce qu’il souhaite disposer exactement de 32 bits. Expliquer pourquoi ce choix peut poser un problème de portabilité. Proposer un type plus approprié. 9. Considérons maintenant : std::size_t taille = sizeof(long long); Expliquer ce que représente exactement la valeur contenue dans taille. Représente-t-elle : • le nombre de bits ; • le nombre d’octets ; • la valeur maximale pouvant être stockée dans un long long ? 10. Supposons que l’on obtienne : sizeof(uint32_t) = 4 et que le type uint32_t possède exactement 32 bits. Combien de bits contient alors, sur cette plateforme, un octet mesuré par sizeof ? 11. Finalement, classer les affirmations suivantes en trois catégories : garantie par le type, observée sur la machine, (a) sizeof(char) == 1. (b) sizeof(int) == 4. (c) sizeof(long) == 8. (d) sizeof(unsigned int) == sizeof(int). (e) uint32_t représente un entier non signé de 32 bits. (f) unsigned long représente toujours un entier non signé de 32 bits. (g) size_t est destiné notamment à représenter des tailles. Vérification expérimentale Compiler et exécuter le programme. Comparer ensuite les résultats expérimentaux avec les réponses données aux questions précédentes. Il faut distinguer soigneusement ce qui a été observé lors de l’exécution du programme de ce qui peut être supposé lors de l’écriture d’un programme destiné à fonctionner sur une autre plateforme. Solution de l’exercice 3 – Représentation des entiers et portabilité 1. Programme d’expérimentation Les types entiers à largeur fixe sont déclarés dans l’en-tête <cstdint>. On peut utiliser le programme suivant :
AccueilExercices – Partie I – Chapitre 1 3 #include <typeinfo> std::cout << « char : » << sizeof(char) << ’\n’; std::cout << « signed char : » << sizeof(signed char) << ’\n’; std::cout << « unsigned char : » << sizeof(unsigned char) << ’\n’; std::cout << « short : » << sizeof(short) << ’\n’; std::cout << « unsigned short : » << sizeof(unsigned short) << ’\n’; std::cout << « int : » << sizeof(int) << ’\n’; std::cout << « unsigned int : » << sizeof(unsigned int) << ’\n’; std::cout << « long : » << sizeof(long) << ’\n’; std::cout << « unsigned long : » << sizeof(unsigned long) << ’\n’; std::cout << « long long : » << sizeof(long long) << ’\n’; std::cout << « unsigned long long : » << sizeof(unsigned long long) << ’\n’; std::cout << « std::int8_t : » << sizeof(std::int8_t) << ’\n’; std::cout << « std::uint8_t : » << sizeof(std::uint8_t) << ’\n’; std::cout << « std::int16_t : » << sizeof(std::int16_t) << ’\n’; std::cout << « std::uint16_t : » << sizeof(std::uint16_t) << ’\n’; std::cout << « std::int32_t : » << sizeof(std::int32_t) << ’\n’; std::cout << « std::uint32_t : » << sizeof(std::uint32_t) << ’\n’;
Accueilstd::cout << « std::int64_t : » << sizeof(std::int64_t) << ’\n’; Exercices – Partie I – Chapitre 1 std::cout << « std::uint64_t : » << sizeof(std::uint64_t) << ’\n’; std::cout << « std::size_t : » << sizeof(std::size_t) << ’\n’; Les résultats numériques dépendent en partie de la plateforme sur laquelle le programme est compilé et c’est précisément l’un des objectifs de l’exercice : distinguer ce que l’on observe sur une machine de ce que le C++ permet de garantir. 2. Type retourné par sizeof L’opérateur sizeof retourne une valeur de type : Ce type est un type entier non signé capable de représenter la taille en mémoire d’un objet. On peut donc écrire naturellement : std::size_t taille = sizeof(long long); L’utilisation d’un type non signé est logique puisqu’une taille mémoire ne peut pas être négative. 3. Types signés et non signés Pour un type entier donné, sa variante signée et sa variante non signée occupent la même quantité de mémoire, on a donc notamment : sizeof(short) == sizeof(unsigned short) sizeof(int) == sizeof(unsigned int) sizeof(long) == sizeof(unsigned long) 4 sizeof(long long) == sizeof(unsigned long long) Le mot-clé unsigned ne signifie donc pas que davantage de mémoire est utilisée. Il modifie l’ensemble des valeurs représentables, pour un même nombre de bits, un type signé doit permettre de représenter des valeurs négatives et positives, tandis qu’un type non signé utilise toutes les combinaisons disponibles pour représenter des valeurs positives ou nulles. 4. Le cas particulier de char ont la même taille : car, par définition :
AccueilExercices – Partie I – Chapitre 1 et signed char ainsi que unsigned char ont la même taille que char. Il ne faut cependant pas en conclure que char est nécessairement signé, le caractère signé ou non signé de char dépend de l’implémentation, ainsi : sont trois types distincts. 5. Taille de int et de long Supposons que l’expérience donne : On peut affirmer que ces résultats sont vrais sur la plateforme où l’expérience a été effectuée, mais on ne peut pas en déduire qu’ils seront identiques sur toutes les plateformes. Les types fondamentaux : ne doivent donc pas être choisis en supposant arbitrairement une taille précise en bits et c’est une distinction essentielle pour écrire du code portable. 6. Les types à largeur fixe fournit notamment, lorsqu’ils sont disponibles, les types : Le nombre présent dans le nom indique directement le nombre de bits du type, ainsi : est un entier non signé de exactement 32 bits, de même :
AccueilExercices – Partie I – Chapitre 1 est un entier signé de exactement 16 bits. Cette propriété constitue leur principal intérêt par rapport aux types fondamentaux lorsque la représentation binaire d’une donnée impose une largeur précise. 7. Choix des types pour un périphérique Le périphérique impose : Dans ce genre de situation, l’utilisation des types à largeur fixe est particulièrement appropriée puisque le format des données est imposé par une spécification extérieure au programme. Ce cas se rencontrera notamment lors de l’étude des microcontrôleurs, des périphériques et des protocoles de communication. 8. Pourquoi unsigned long n’est-il pas un bon choix pour imposer 32 bits ? unsigned long compteur; Cette déclaration garantit que compteur est un entier non signé, mais ne garantit pas qu’il possède exactement 32 bits. Si le format extérieur impose réellement 32 bits, on préférera : std::uint32_t compteur; Le choix du type exprime alors directement la contrainte imposée par le problème. 9. Que contient une variable de type size_t ? Prenons par exemple : std::size_t taille = sizeof(long long); La variable taille contient le nombre d’unités mémoire occupées par un objet de type long long qui sont des octets au sens du C++. Elle ne contient donc ni la valeur maximale pouvant être représentée par un long long, ni directement son nombre de bits, si, sur une machine donnée : sizeof(long long) = 8
AccueilExercices – Partie I – Chapitre 1 Supposons que l’expérience donne : sizeof(std::uint32_t) = 4 possède exactement 32 bits. Si ces 32 bits correspondent à 4 unités mesurées par sizeof, on obtient : L’octet de cette plateforme contient donc : Il faut cependant conserver une distinction importante : est garanti par le langage, mais cela ne signifie pas à lui seul que cette unité contient nécessairement 11. Garanties et observations On peut maintenant classer les affirmations proposées. Garantie par le langage Observation possible sur une machine, mais pas garantie générale Observation possible sur une machine, mais pas garantie générale sizeof(unsigned int) == sizeof(int) std::uint32_t représente un entier non signé de unsigned long représente toujours un entier non std::size_t est destiné notamment à représenter Garantie si ce type est fourni par l’implémentation Garantie par son rôle dans la bibliothèque et le langage 12. Expérience complémentaire avec typeid On peut chercher à savoir à quel type fondamental correspond std::size_t sur la plateforme utilisée.
AccueilExercices – Partie I – Chapitre 1 std::cout << typeid(std::size_t).name() << ’\n’; Avec Clang sur la machine utilisée pour cet exercice, le programme affiche : Cette lettre est un nom interne utilisé par l’implémentation et dans cet environnement, elle correspond On constate donc expérimentalement que, sur cette plateforme, std::size_t est un alias d’un type entier non signé approprié à l’architecture. Il ne faut cependant pas écrire un programme portable en supposant que std::size_t correspondra toujours à unsigned long, de plus, la chaîne retournée par : dépend de l’implémentation et n’est pas destinée à fournir un nom de type portable et uniformément Cet exercice met en évidence une distinction fondamentale entre deux familles de types entiers. sont des types fondamentaux du langage. Leur taille exacte ne doit pas être déduite uniquement d’une expérience effectuée sur une machine et les types : expriment au contraire explicitement une largeur en bits, lorsqu’ils sont disponibles. Enfin, std::size_t répond à un autre besoin : disposer d’un type entier non signé approprié pour représenter des tailles. Il faut donc toujours distinguer : ce que l’expérience montre sur une machine ce que le programme peut supposer de manière portable.
AccueilExercices – Partie I – Chapitre 1 Exercice 4 – Division euclidienne sans flottants On considère deux entiers : On souhaite effectuer la division euclidienne de a par b sans utiliser de nombres flottants. Pour deux entiers positifs, la division euclidienne s’écrit : Les valeurs q et r représentent respectivement le quotient entier et le reste. 1. Écrire un programme qui déclare deux variables entières a et b, puis calcule : • le quotient entier q de la division de a par b ; Le programme ne doit utiliser que des types entiers. déterminer sur papier les valeurs de : Vérifier ensuite le résultat à l’aide du programme. 3. Ajouter au programme une vérification de l’égalité : Le programme devra afficher la valeur de : et permettre de la comparer avec a. 4. Ajouter un test permettant de vérifier que le reste satisfait bien : Afficher un message indiquant si cette condition est satisfaite.
AccueilExercices – Partie I – Chapitre 1 5. Modifier les valeurs de a et b et tester notamment les cas suivants : Expliquer ce que deviennent alors le quotient et le reste. 6. Que se passe-t-il si l’on écrit : avec deux variables de type int ? Le résultat contient-il une partie décimale ? Expliquer pourquoi. 7. Comparer les deux expressions suivantes : static_cast<double>(a) / b Quelle différence observe-t-on ? 8. Sans utiliser l’opérateur modulo %, retrouver le reste uniquement à partir de a, b et du quotient Utiliser pour cela la relation : 9. Comparer ensuite les deux calculs du reste :
AccueilExercices – Partie I – Chapitre 1 Vérifier qu’ils donnent le même résultat pour plusieurs valeurs positives de a et de b. 10. Ajouter un contrôle empêchant une division par zéro. le programme ne doit effectuer ni division ni modulo et il doit afficher un message d’erreur. 11. Enfin, tester expérimentalement le comportement du programme avec des valeurs négatives, Noter les résultats donnés par les opérateurs : sans chercher pour l’instant à modifier leur comportement. Comparer ces résultats avec la définition mathématique usuelle de la division euclidienne pour les entiers positifs. Pour chaque couple de valeurs positives testé, vérifier simultanément : Le programme devra permettre de distinguer clairement le quotient entier, le reste et la reconstruction
AccueilExercices – Partie I – Chapitre 1 Solution de l’exercice 4 – Division euclidienne sans flottants 1. Quotient et reste Pour deux entiers positifs a et b, avec b différent de zéro, la division euclidienne s’écrit : En C++, les opérateurs : permettent respectivement d’obtenir le quotient entier et le reste : Un programme permettant d’effectuer les différents contrôles demandés peut être écrit de la manière std::cout << « Division par zero impossible. » std::cout << « b*q+r = » std::cout << « q = » << q << std::endl; std::cout << « r = » << r << std::endl; std::cout << « rr = » << rr << std::endl; if (r >= 0 && r < b)
AccueilExercices – Partie I – Chapitre 1 std::cout << « Valeurs OK » << std::endl; 3. Vérification de la relation fondamentale Le programme calcule : Avec les valeurs précédentes : Le reste satisfait également : Les deux propriétés caractéristiques de la division euclidienne sont donc vérifiées. 4. Retrouver le reste sans l’opérateur % on peut isoler le reste : On peut donc écrire : r r = 137 − 12 × 11 = 5.
AccueilExercices – Partie I – Chapitre 1 Cette égalité constitue également une manière de vérifier expérimentalement le fonctionnement de 5. Quelques cas particuliers Enfin, si a est un multiple exact de b, le reste est également nul. C’est la raison pour laquelle le test du reste doit être : puisque zéro est un reste parfaitement valide.
AccueilExercices – Partie I – Chapitre 1 6. Division entière Lorsque les deux opérandes sont de type entier, l’opérateur / effectue une division entière. La valeur mathématique : n’est pas conservée. Le résultat de l’opération est lui-même entier, il ne faut donc pas interpréter cette opération comme une division flottante suivie du stockage de son résultat dans un entier. 7. Forcer une division flottante Pour effectuer une division flottante, on peut convertir explicitement l’un des opérandes : double qq = static_cast<double>(a) / b; Le premier opérande devient alors un double et la division est effectuée en virgule flottante. on obtient approximativement : Il faut cependant faire attention au type de la variable qui reçoit le résultat, l’écriture : int qq = static_cast<double>(a) / b; effectue bien une division flottante, mais son résultat est ensuite converti en int lors de l’affectation et la partie fractionnaire est alors perdue et donc : Pour observer le résultat flottant, il faut donc écrire : double qq = static_cast<double>(a) / b; 8. Protection contre la division par zéro Une division entière par zéro n’est pas une opération valide. Il faut donc vérifier la valeur du diviseur avant d’effectuer les opérations / et % :
AccueilExercices – Partie I – Chapitre 1 std::cout << « Division par zero impossible. » Si b vaut zéro, le programme s’arrête avant d’effectuer la division. Dans cet exemple, indique une terminaison provoquée par une situation d’erreur, tandis que : correspond à une terminaison normale du programme. 9. Que se passe-t-il avec des nombres négatifs ? L’expérience devient particulièrement intéressante lorsque l’un ou les deux opérandes sont négatifs. Testons les quatre combinaisons correspondant aux valeurs absolues 137 et 12 on obtient : Dans tous les cas, la relation : reste satisfaite, par exemple, avec : (−12) × 11 + (−5) = −132 − 5 = −137. 10. Division entière et troncature vers zéro Pour les types entiers signés, le quotient produit par la division C++ est tronqué vers zéro, par donne en division entière :
AccueilExercices – Partie I – Chapitre 1 137/(−12) = −11 . (−137)/(−12) = 11 . Le reste est ensuite déterminé de manière à conserver la relation : On peut donc toujours le retrouver avec : Les expériences précédentes permettent également de constater que le reste obtenu avec l’opérateur % possède le même signe que le dividende a, sauf naturellement lorsque le reste est nul. Le signe du diviseur ne détermine pas celui du reste : (−137)%(−12) = −5. 12. Division euclidienne et opérateurs C++ Pour les valeurs positives utilisées au début de l’exercice : les opérateurs / et % fournissent directement le quotient et le reste de la division euclidienne : Lorsque des valeurs négatives sont introduites, la règle de troncature vers zéro utilisée par le C++ peut produire un reste négatif et la condition :
AccueilExercices – Partie I – Chapitre 1 utilisée au début de l’exercice ne doit donc pas être appliquée directement aux essais comportant des Il faut distinguer la définition mathématique choisie pour une division euclidienne de la définition précise des opérateurs entiers / et % du C++. Cet exercice met en évidence plusieurs propriétés importantes de l’arithmétique entière en C++. Pour deux entiers positifs : Le reste peut également être retrouvé par : constitue le lien fondamental entre les deux opérations. L’exercice montre également qu’une division entre deux entiers reste une division entière. Pour obtenir une division flottante, il faut qu’au moins l’un des opérandes soit de type flottant, par exemple : static_cast<double>(a) / b Enfin, lorsque des nombres négatifs interviennent, il faut tenir compte de la règle de troncature vers zéro utilisée par la division entière du C++ et du signe du reste produit par l’opérateur %.
AccueilExercices – Partie I – Chapitre 1 Exercice 5 – Décomposition décimale d’un entier On considère un entier strictement positif n duquel on souhaite analyser l’écriture décimale sans utiliser de chaîne de caractères, de tableau ou de fonction particulière de la bibliothèque standard. Le programme devra travailler uniquement à l’aide d’opérations sur les entiers. 1. Déclarer un entier : puis une seconde variable destinée à conserver la valeur originale de n. Expliquer pourquoi cette seconde variable sera n
Chapitre 2 - Les variables et la mémoire
Chapitre 2 – Les variables et la mémoire
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
AccueilLes variables et la mémoire Prérequis, nouvelles notions et objectifs 2. Types fondamentaux. 1. Adresse mémoire. 3. Le mot clé sizeof. • Qu’est-ce qu’une variable ? • Où est-elle stockée ? • Qu’est-ce qu’une adresse ? • Que signifie &a ? • Combien d’octets occupe un int ? • Les variables sont-elles côte à côte ? • Pourquoi une adresse est-elle écrite en hexadécimal ? • Une adresse est-elle une valeur ? Programme 1 — Variables et mémoire Les variables et la mémoire Code 2.1 – Programme C++ vraiment minimal std::cout << « sizeof(a)= » << sizeof(a) << ’\n’; Commençons par étudier la première ligne. Lorsque le compilateur voit le code il reconnait un objet de type 1 int, l’identificateur a, l’opérateur d’initialisation = et le littéral La sémantique de cette instruction est : Créer un objet nommé a, de type int, puis l’initialiser avec la valeur entière 10. Pour faciliter la compréhension, nous représenterons la mémoire de la manière suivante : Fig. 2.1 – Schéma. La Figure 2.1 est volontairement très simplifiée. Elle ne représente pas la mémoire réelle de l’ordinateur, mais un modèle qui nous permettra de comprendre progressivement le fonctionnement du Une première remarque importante s’impose. Le nom a n’est pas stocké dans la mémoire de l’ordinateur. Ce nom est uniquement connu du compilateur (et éventuellement du débogueur). En mémoire, il n’existe que des suites de bits représentant les différentes valeurs manipulées par le programme. a donc pour effet de réserver une zone mémoire suffisamment grande pour contenir un objet de type int, puis d’y placer la représentation binaire de la valeur entière 10. Le programme affiche ensuite trois informations : • la valeur de l’objet a, • son adresse mémoire à l’aide de l’opérateur \&, • la taille mémoire occupée par cet objet grâce à l’opérateur sizeof. 1. Le terme objet prendra son sens plus tard.
AccueilLes variables et la mémoire Une exécution typique peut produire un résultat semblable au suivant : Il est important de comprendre que l’adresse affichée dépend de nombreux facteurs (système d’exploitation, compilateur, options de compilation, etc.). Elle sera donc pratiquement toujours différente d’un ordinateur à l’autre et même d’une exécution à l’autre. Ce qui nous intéresse n’est donc pas la valeur numérique de cette adresse, mais sa signification. L’opérateur \& signifie « donner l’adresse mémoire de l’objet ». Dans notre exemple, l’expression se lit donc simplement : « Adresse mémoire de l’objet a. » Cette notion d’adresse est fondamentale. Toute l’étude des pointeurs reposera sur ce concept. Avant de manipuler des pointeurs, il est donc indispensable de bien comprendre ce que représente réellement une adresse mémoire. • Une variable est un objet possédant un type, une valeur et une adresse mémoire. • Le nom d’une variable n’est pas stocké dans la mémoire ; il est utilisé par le compilateur. • L’opérateur \& permet d’obtenir l’adresse d’un objet. • L’opérateur sizeof indique le nombre d’octets occupés par un objet. • Les adresses affichées lors de l’exécution peuvent varier d’un ordinateur à l’autre. Questions de réflexion 1. Pourquoi une adresse mémoire est-elle généralement affichée en hexadécimal ? 2. Deux variables déclarées successivement sont-elles toujours voisines en mémoire ? 3. La taille d’un int est-elle toujours de quatre octets ? 4. Pourquoi le nom d’une variable n’a-t-il pas besoin d’être conservé en mémoire pendant l’exécution du programme ? Réponses aux questions de réflexion 1. Pourquoi une adresse mémoire est-elle généralement affichée en hexadécimal ? Une adresse mémoire est un nombre entier. Elle pourrait être affichée en décimal, en binaire ou dans toute autre base. La notation hexadécimale est cependant beaucoup plus compacte que l’écriture binaire tout en conservant une correspondance directe avec celle-ci : un chiffre hexadécimal représente exactement quatre bits. Cette représentation est devenue un standard dans la plupart des outils de
AccueilLes variables et la mémoire 2. Deux variables déclarées successivement sont-elles toujours voisines en mémoire ? Non. Sur de nombreux compilateurs, deux variables locales déclarées successivement sont effectivement placées à des adresses proches. Cependant, le standard C++ ne garantit absolument pas cet ordre. Le compilateur est libre d’organiser la mémoire comme il le souhaite afin d’optimiser le programme. Il ne faut donc jamais écrire un programme en supposant que deux variables seront toujours contiguës 3. La taille d’un int est-elle toujours de quatre octets ? Non. Le standard C++ n’impose pas une taille fixe pour le type int. Il impose uniquement une taille minimale ainsi que des relations entre les différents types entiers. Aujourd’hui, sur les ordinateurs personnels (Windows, Linux, macOS) ainsi que sur les microcontrôleurs ESP32, un int occupe généralement quatre octets. Il ne faut toutefois pas considérer cette valeur comme une propriété du langage. Lorsque la taille doit être parfaitement connue, on utilise plutôt les types à taille fixe comme int32_t ou uint32_t, qui seront étudiés dans l’annexe A. 4. Pourquoi le nom d’une variable n’a-t-il pas besoin d’être conservé en mémoire pendant l’exécution du programme ? Le nom d’une variable est destiné au programmeur et au compilateur. Pendant la compilation, le compilateur remplace les noms symboliques par des adresses mémoire et d’autres informations internes. Une fois le programme compilé, le processeur ne manipule plus des noms tels que a ou compteur. Il travaille uniquement avec des adresses mémoire et des données binaires. Les noms des variables ne sont conservés que si le programme est compilé avec des informations de débogage. Elles permettent alors au débogueur d’afficher les variables avec leur nom d’origine.
Accueilcpp-Chap2- Les variables et la mémoire
Accueil
Chapitre 3 : Les références
Chapitre 3 – Les références
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
AccueilPrérequis, nouvelles notions et objectifs 2. Adresse mémoire. 3. Référence constante (const). • Qu’est-ce qu’une référence ? • Pourquoi parle-t-on d’alias ? • Une référence possède-t-elle sa propre mémoire ? • Une référence possède-t-elle une adresse ? • Peut-on modifier la variable originale via une référence ? • Une référence peut-elle être réaffectée ? • À quoi sert une référence constante ? Programme 1 — Une référence Code 3.1 – Référence std::cout << « Etat initial\n »; std::cout << « a = » << a << ’\n’; std::cout << « r = » << r << ’\n’; std::cout << « \nApres : a = 15\n »; std::cout << « a = » << a << ’\n’; std::cout << « r = » << r << ’\n’; std::cout << « \nApres : r = 42\n »; std::cout << « a = » << a << ’\n’; std::cout << « r = » << r << ’\n’; Une référence est un alias Une référence ne “prend” pas la valeur d’une variable. Elle devient un second nom désignant exactement le même objet.
AccueilRéférence et mémoire Fig. 3.1 – Illustration au niveau de la mémoire. Une seule case mémoire est partagée par les deux identificateurs. Les références constantes (const) Reprenons le programme ci-dessous et déclarons la référence comme étant constante. std::cout << « Etat initial\n »; std::cout << « a = » << a << ’\n’; std::cout << « r = » << r << ’\n’; std::cout << « \nApres : a = 15\n »; std::cout << « a = » << a << ’\n’; std::cout << « r = » << r << ’\n’; // Erreur de compilation std::cout << « \nApres : r = 42\n »; std::cout << « a = » << a << ’\n’; std::cout << « r = » << r << ’\n’; Le résultat nous montre qu’il est impossible de changer la valeur de r. La référence ne peut pas être affectée à une autre valeur. Le compilateur n’accepte pas l’instruction d’affectation. Pourquoi les références existent-elles ? Les références permettent de donner un second nom à un objet existant, sans effectuer de copie. Lorsqu’une référence est déclarée constante, elle ne permet que la lecture de l’objet auquel elle est liée. Cette propriété est particulièrement utile lors de l’appel des fonctions : la fonction peut accéder directement à l’objet original sans en faire une copie, tout en étant incapable de le modifier. Mais alors que se passe t’il si on affecte une autre valeur à a après coup, que va devenir r ?
Accueilstd::cout << « Etat initial\n »; std::cout << « a = » << a << ’\n’; std::cout << « r = » << r << ’\n’; std::cout << « \nApres : a = 15\n »; std::cout << « a = » << a << ’\n’; std::cout << « r = » << r << ’\n’; Le résultat après compilation et exécution est : Code 3.3 – Programme C++ vraiment minimal Donc le programme s’exécute sans erreur. En résumé, une référence constante interdit uniquement les modifications effectuées à travers cette référence. Elle ne rend pas l’objet lui-même constant. Ainsi, dans notre exemple, l’instruction est parfaitement autorisée. En revanche, l’instruction est refusée par le compilateur, car elle tenterait de modifier l’objet au moyen d’une référence déclarée • Une référence est un second nom donné à un objet existant. • Une référence doit être initialisée lors de sa création. • Une référence ne peut pas être réaffectée à un autre objet. • Une référence constante interdit les modifications effectuées à travers cette référence. • Une référence constante ne rend pas l’objet lui-même constant.
AccueilQuestions de réflexion 1. Une référence occupe-t-elle de la mémoire ? 2. Peut-on créer une référence sans objet ? 3. Peut-on faire en sorte qu’une référence désigne ensuite un autre objet ? 4. Quelle est la différence entre une référence et un pointeur ? Réponses aux questions de réflexion 1. Une référence occupe-t-elle de la mémoire ? Réponse : Non de code l’adresse de la référence sur une valeur. Demandons à un petit bout Code 3.4 – Adresse d’une référence std::cout << « &a = » << &a << ’\n’; std::cout << « &r = » << &r << ’\n’; Code 3.5 – Sortie. Les deux adresses sont identiques. Une référence ne désigne pas un nouvel objet ; demander son adresse revient donc à demander l’adresse de l’objet auquel elle est liée. Conceptuellement, une référence n’est pas un nouvel objet : c’est un alias. Son implémentation est laissée au compilateur, qui est libre d’utiliser ou non de la mémoire supplémentaire. 2.Peut-on créer une référence sans objet ? Réponse : Non un objet déjà existant. Une référence sur un objet nécessite 3. Peut-on faire en sorte qu’une référence désigne ensuite un autre objet ? Réponse : Non Code 3.6 – Programme C++ vraiment minimal
Accueilstd::cout << « a = » << a << ’\n’; std::cout << « r = » << r << ’\n’; dont la sortie est : Code 3.7 – Programme C++ vraiment minimal On se dit que oui, puisque r=20, mais ce n’est pas le cas ! C’est la valeur de a que r a transformé lors de l’affectation r = b;. C’est a qui vaut 20 à présent car r lui a affecté la valeur de b. r fait toujours référence à a. 4. Quelle est la différence entre une référence et un pointeur ? Une référence est un second nom donné à un objet existant. Une fois créée, elle reste liée à cet objet pendant toute sa durée de vie. Elle ne peut jamais être réaffectée à un autre objet et doit obligatoirement être initialisée lors de Un pointeur est, quant à lui, une variable dont la valeur est une adresse mémoire. Il peut être modifié pour désigner successivement plusieurs objets différents et peut également ne désigner aucun objet Les deux notions permettent donc d’accéder indirectement à un objet, mais leur philosophie est très différente. Une référence représente un alias permanent, alors qu’un pointeur représente une adresse pouvant être manipulée. Le chapitre suivant sera entièrement consacré aux pointeurs et montrera pourquoi ils constituent l’un des mécanismes les plus puissants du langage C++. Est un alias d’un objet existant. Est une variable contenant une Doit être initialisée lors de sa déclaration. Peut être initialisé plus tard (ce qui est généralement déconseillé). Ne peut jamais être réaffectée à un Peut désigner successivement plusieurs objets. Ne peut pas être nulle. Peut valoir nullptr. S’utilise comme l’objet lui-même. Nécessite un déréférencement (∗) pour accéder à l’objet pointé. Table 3.1 – Comparaison entre une référence et un pointeur.
Accueil
Accueilcpp-Chap3- Les références
Accueil
Chapitre 4 : Les pointeurs
Chapitre 4 – Les pointeurs
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
AccueilPrérequis, nouvelles notions et objectifs 2. Adresse mémoire. 2. Opérateur de déréférencement (∗). 3. Pointeur nul (nullptr). • Qu’est-ce qu’un pointeur ? • Que contient un pointeur ? • Quelle est la différence entre une valeur et une adresse ? • Que signifie l’opérateur ∗ ? • Que signifie l’opérateur & ? • Qu’est-ce que le déréférencement ? • À quoi sert un pointeur ? Programme 1 — Une adresse est une valeur Code 4.1 – Pointeur Exp 1 std::cout << « a = » << a << ’\n’; std::cout << « &a = » << &a << ’\n’; std::cout << ’\n’; Code 4.2 – Sortie du programme Que s’est-il passé ? Dans le chapitre précédent, nous avons vu qu’une variable possède une adresse mémoire. Par exemple, si l’on écrit : l’objet a possède la valeur 10 mais également une adresse mémoire que l’on peut faire imprimer avec std::cout << &a << \n; Cette valeur est une adresse mémoire. Jusqu’à présent, nous nous sommes contentés de l’afficher. Mais une question se pose naturellement : Peut-on conserver cette adresse dans une variable ? La réponse est oui. C’est précisément le rôle des pointeurs. Qu’est-ce qu’un pointeur ? Un pointeur est une variable dont la valeur est une adresse mémoire. La déclaration du pointeur p qui prend comme valeur l’adresse de a se fait par int∗ signifie je déclare une variable capable de contenir l’adresse d’un objet de type int. p est le nom de cette variable qui est initialisée avec la valeur &a qui est l’adresse mémoire de a.
AccueilPourquoi écrit-on int∗ p et non int ∗p ? Les deux notations sont équivalentes et le compilateur ne fait aucune différence. Dans ce cours, j’utiliserai de préférence l’écriture int ∗p, voici pourquoi. Soient les déclarations : Les deux sont acceptées par le compilateur. Je préfère la seconde car elle dit : On travaille avec des objets qui sont de type entiers. On définit un pointeur ∗p sur un entier qui recevra une adresse et une variable q qui recevra elle une valeur entière. La première notation, int∗ p,q; peut sembler vouloir dire que l’on déclare deux pointeurs ∗p et ∗q ce qui bien sûr n’est pas le cas. Finalement le mieux est de donner ces déclarations sur deux lignes, tout le monde est alors content : Le déréférencement Code 4.3 – Déréférencement //ou encore int* p = &a Code 4.4 – Programme C++ vraiment minimal
AccueilNous savons maintenant que la variable p contient l’adresse mémoire de l’objet a. Cependant, dans de nombreuses situations, ce n’est pas l’adresse qui nous intéresse mais la valeur stockée à cette Pour accéder à cette valeur, le langage C++ utilise l’opérateur de déréférencement ∗. « la valeur contenue à l’adresse mémorisée dans le pointeur p ». Autrement dit, le compilateur suit l’adresse contenue dans le pointeur puis accède à l’objet situé à Modifier une variable à travers un pointeur Code 4.5 – Programme C++ vraiment minimal std::cout << « Avant\n »; std::cout << « a = » << a << ’\n’; std::cout << « \nApres\n »; std::cout << « a = » << a << ’\n’; Le pointeur ne permet pas seulement de lire la valeur située à une adresse ; il permet également de la modifier.
AccueilUn pointeur est une variable Un pointeur étant une variable, sa valeur peut varier ! Code 4.6 – Un pointeur peut varier std::cout << *p << ’\n’; p = &b; // On affecte une autre adresse a p std::cout << *p << ’\n’; Contrairement à une référence, un pointeur peut être réaffecté. On remarque dans cet exemple que la forme de déclaration est peut-être plus logique que vu que lors de la réaffectation on écrit Un poonteur peut exister sans désigner d’objet. C’est justement une différence avec les références. On définit le pointeur nul avec la commande :
Accueiln’a aucun sens, Il tente d’accéder un objet qui n’exsiste pas. Pointeurs et mémoire Code 4.7 – Programme C++ vraiment minimal On peut représenter ces deux déclaration par le dessin suivant : Fig. 4.1 – Pointeurs et mémoire Les flèches représentent des valeurs. La case mémoire de p contient simplement le nombre 0x1000 qui est interprété comme une adresse. Il faut retenir ce tableau résumant la différence entre les pointeurs et les références : Toujours liée à un objet Peut être nul (nullptr) Alias d’un objet existant Variable contenant une adresse Table 4.1 – Comparaison entre une référence et un pointeur. L’opérateur ∗ possède deux significations différentes en C++. Lors d’une déclaration, il indique que la variable est un pointeur. En dehors d’une déclaration, il représente l’opérateur de déréférencement.
Accueil• Un pointeur est une variable dont la valeur est une adresse mémoire. • L’opérateur & permet d’obtenir l’adresse d’un objet. • L’opérateur ∗ permet d’accéder à l’objet situé à l’adresse contenue dans un pointeur. • Un pointeur possède sa propre case mémoire et donc sa propre adresse. • Un pointeur peut être réaffecté pour désigner un autre objet. • Un pointeur peut être nul (nullptr) et ne désigner alors aucun objet. Questions de réflexion 1. Quelle est la différence entre une valeur et une adresse mémoire ? 2. Pourquoi dit-on qu’un pointeur est une variable comme les autres ? 3. Quelle est la différence entre p, ∗p et &p ? 4. Pourquoi est-il dangereux de déréférencer un pointeur nul ? 5. Quelle est la principale différence entre une référence et un pointeur ? Réponses aux questions de réflexion 1. Une valeur représente une information (par exemple un entier ou un caractère), tandis qu’une adresse mémoire indique l’endroit où cette information est stockée dans la mémoire de l’ordinateur. 2. Comme toute variable, un pointeur possède sa propre case mémoire, sa propre adresse et une durée de vie. Sa particularité est que la valeur qu’il contient est une adresse mémoire. 3. Le symbole p représente le pointeur lui-même, c’est-à-dire la variable qui contient une adresse. L’expression ∗p représente l’objet situé à cette adresse. L’expression &p représente l’adresse mémoire du pointeur lui-même. 4. Un pointeur nul ne désigne aucun objet. Tenter d’accéder à l’objet qu’il est censé désigner conduit à un comportement indéfini. Il est donc indispensable de vérifier qu’un pointeur est valide avant de le déréférencer. 5. Une référence est un alias d’un objet existant. Elle doit être initialisée dès sa création et ne peut jamais être réaffectée. Un pointeur est une variable contenant une adresse mémoire. Il peut être réaffecté pour désigner successivement plusieurs objets et peut également ne désigner aucun objet (nullptr). Remarque 4.9.1. Nous savons maintenant manipuler un pointeur désignant un objet unique. Le chapitre suivant montrera que plusieurs objets peuvent être stockés côte à côte en mémoire. Nous découvrirons alors les tableaux et comprendrons pourquoi ils sont si étroitement liés aux pointeurs.
Accueil
Accueilcpp-Chap4- Les pointeurs
Accueil
Chapitre 5 - Les fonctions
Chapitre 5 – Les fonctions
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
Accueil• Comprendre le rôle d’une fonction. • Déclarer et définir une fonction. • Appeler une fonction. • Comprendre les paramètres et les arguments. • Différencier passage par valeur, par référence et par pointeur. • Comprendre l’instruction return. • Écrire des fonctions qui retournent une valeur. • Décomposer un programme en fonctions simples. 4. Instructions conditionnelles 7. Appel de fonction Une fonction est un bloc d’instructions auquel on attribue un nom afin de pouvoir l’utiliser plusieurs fois dans un programme. Au lieu d’écrire plusieurs fois le même morceau de code, on l’écrit une seule fois dans une fonction puis on appelle cette fonction chaque fois que cela est nécessaire. Les fonctions permettent notamment : • d’éviter les répétitions ; • de rendre un programme plus lisible ; • de faciliter les tests et le débogage ; • de construire progressivement des programmes de grande taille. Le langage C++, comme la plupart des langages modernes, est largement fondé sur cette idée de décomposition en fonctions. Pourquoi les fonctions ? Jusqu’à présent, tous nos programmes étaient relativement courts et tenaient entièrement dans la fonction main(). Cette approche est parfaitement adaptée pour découvrir les différents éléments du langage C++, mais elle atteint rapidement ses limites dès que les programmes deviennent plus Considérons par exemple un programme qui doit gérer les notes d’une classe. Celui-ci pourrait être amené à effectuer les opérations suivantes : • lire les notes des étudiants ; • calculer la moyenne de la classe ; • déterminer la meilleure note ; • afficher les résultats à l’écran ; • enregistrer les données dans un fichier. Écrire toutes ces opérations directement dans la fonction main() rendrait le programme difficile à lire, à comprendre et à modifier. Plus le programme grandit, plus cette difficulté devient importante. Une solution beaucoup plus efficace consiste à décomposer le problème en plusieurs tâches plus simples. Chaque tâche est alors regroupée dans une fonction qui possède un rôle bien précis.
Accueil• une fonction lit les notes ; • une fonction calcule la moyenne ; • une fonction recherche la meilleure note ; • une fonction affiche les résultats ; • une fonction enregistre les données. Le programme principal devient alors beaucoup plus clair. Il ressemble davantage à une suite d’actions qu’à une longue liste d’instructions. double moyenne = calculerMoyenne(); afficherResultats(moyenne); enregistrerDonnees(); À ce stade, peu importe la manière dont ces fonctions sont écrites. Ce qui est important est de comprendre que chacune d’elles réalise une tâche précise. Cette manière de programmer présente de nombreux avantages : • le programme est plus facile à lire ; • les différentes parties peuvent être testées séparément ; • une modification dans une fonction n’affecte généralement pas les autres ; • une même fonction peut être utilisée plusieurs fois dans un programme. Une fonction peut donc être considérée comme un outil spécialisé auquel on confie une tâche bien définie. En réunissant plusieurs fonctions, il devient possible de construire des programmes beaucoup plus importants tout en conservant une structure claire et facile à maintenir. Une première fonction Nous savons maintenant pourquoi les fonctions sont utiles. Voyons à présent comment en écrire une. La plus petite fonction possible est constituée de trois éléments : • une paire de parenthèses ; • un bloc d’instructions délimité par des accolades. En voici un exemple :
AccueilÀ première vue, cette fonction semble ne rien faire. Pourtant, elle est parfaitement valide. Nous venons de définir une fonction nommée bonjour. Examinons chacun des éléments qui la composent. • void indique que la fonction ne retourne aucune valeur. • bonjour est le nom de la fonction. • () indique que la fonction ne reçoit, pour l’instant, aucun paramètre. • {} délimite le corps de la fonction, c’est-à-dire l’ensemble des instructions qui seront exécutées lorsque cette fonction sera appelée. Pour le moment, le corps de la fonction est vide. Nous ajouterons des instructions dans les sections Il est important de comprendre qu’une fonction n’est pas exécutée au moment où elle est définie. Sa définition indique simplement au compilateur qu’une nouvelle fonction existe. Pour qu’elle soit exécutée, il faudra l’appeler explicitement depuis une autre partie du programme. Où placer les fonctions ? Dans les exemples de ce chapitre, les fonctions seront définies avant la fonction main(). std::cout << « Bonjour ! » << std::endl; Cette organisation présente un avantage important : lorsque le compilateur lit le programme de haut en bas, il connaît déjà la fonction bonjour() lorsqu’il rencontre son appel dans la fonction main(). Plus loin dans ce chapitre, nous découvrirons qu’il existe une autre manière d’organiser un programme. Pour l’instant, nous placerons donc toujours les fonctions avant main(). Appeler une fonction Définir une fonction ne signifie pas qu’elle sera exécutée automatiquement. Une fonction est simplement mise à la disposition du programme. Pour qu’elle soit exécutée, il faut l’appeler. Complétons notre première fonction en lui ajoutant une instruction. std::cout << « Bonjour ! » << std::endl;
AccueilCette fonction est maintenant capable d’afficher le message Bonjour !. Cependant, si nous écrivons simplement cette fonction dans un programme, rien ne sera affiché. Considérons le programme suivant. std::cout << « Bonjour ! » << std::endl; Lors de son exécution, ce programme n’affiche absolument rien. Pourquoi ? Tout simplement parce que la fonction bonjour() n’a jamais été appelée. Pour exécuter cette fonction, il suffit d’écrire son nom suivi d’une paire de parenthèses. std::cout << « Bonjour ! » << std::endl; Cette fois, le programme affiche : est appelée un appel de fonction. Lorsque le programme rencontre cette instruction, il interrompt momentanément l’exécution de main(), exécute les instructions contenues dans la fonction bonjour(), puis revient automatiquement à l’instruction suivante.
AccueilFig. 5.1 – Chronologie de l’appel de la fonction bonjour() ci-dessous. • Le programme commence sont exécution dans la fonction main() • Lorsqu’il rencontre l’appel de la fonction bonjour();, il saute vers la définition de la fonction • Il exécute les instructions contenues dans la fonction, càd qu’il affiche Bonjour ! et • il revient dans main() pour poursuivre son exécution. Ici, le programme se termine avec l’instruction return 0 ;. Une fonction peut être appelée plusieurs fois L’un des principaux intérêts d’une fonction est qu’elle peut être utilisée autant de fois que nécessaire. Considérons le programme suivant. std::cout << « Bonjour ! » << std::endl;
AccueilL’exécution du programme produit le résultat suivant. La fonction bonjour() n’a été écrite qu’une seule fois, mais elle a été exécutée trois fois. Chaque appel provoque une nouvelle exécution de toutes les instructions contenues dans la fonction. Une fonction peut être appelée une seule fois, plusieurs fois ou même jamais. Tout dépend des besoins • Une fonction est définie une seule fois, mais elle peut être appelée autant de fois que • Chaque appel exécute de nouveau toutes les instructions contenues dans cette fonction. Que va faire le compilateur dans ce cas ? Si le programme s’exécute, que va-t-il se passer ? Quelle va être la sortie ? std::cout << « Bonjour ! » << std::endl; On remarquera que la fonction bonjour() est appelée à l’intérieure d’elle-même ! Les fonctions communiquent avec le programme Notre fonction bonjour() présente toutefois une limitation importante : elle affiche toujours le même
Accueilstd::cout << « Bonjour ! » << std::endl; Quel que soit le nombre de fois où cette fonction est appelée, le résultat reste toujours identique. Supposons maintenant que nous souhaitions afficher plusieurs messages différents. Notre fonction actuelle ne permet pas de réaliser cette tâche puisqu’elle ne possède aucun moyen de savoir quel prénom doit être affiché. Pour rendre une fonction plus générale, il faut pouvoir lui transmettre des informations au moment Autrement dit, la fonction doit pouvoir communiquer avec le reste du programme. Dans la section suivante, nous allons découvrir le mécanisme utilisé en C++ pour transmettre ces informations à une fonction. Le programme ci-dessous compile parfaitement. Comment pourriez-vous le modifier afin qu’il affiche successivement sans écrire trois fonctions différentes ? std::cout << « Bonjour ! » << std::endl; Dans la section précédente, nous avons constaté que notre fonction bonjour() affichait toujours le même message. Pour la rendre plus générale, nous devons pouvoir lui transmettre une information : le prénom de la personne à saluer.
AccueilEn C++, les informations reçues par une fonction sont appelées des paramètres. Modifions notre fonction de la manière suivante. void bonjour(std::string prenom) std::cout << « Bonjour » << » ! » << std::endl; La différence avec la version précédente est la présence de entre les parenthèses. Cette déclaration indique que la fonction reçoit une chaîne de caractères nommée prenom. Cette information pourra être utilisée à l’intérieur de la fonction comme n’importe quelle autre variable. Dans notre exemple, le paramètre est utilisé pour compléter le message affiché. Une fonction peut recevoir aucun, un ou plusieurs paramètres. Nous étudierons les fonctions possédant plusieurs paramètres un peu plus loin dans ce chapitre. Fig. 5.2 – Paramètre. Déclarer un paramètre dans une fonction ne suffit pas. Il faut également lui fournir une valeur au moment où la fonction est appelée. Cette valeur est appelée un argument. Considérons le programme suivant. void bonjour(std::string prenom) std::cout << « Bonjour » << » ! » << std::endl;
AccueilL’exécution du programme produit le résultat suivant : À chaque appel de la fonction, une nouvelle valeur est transmise au paramètre prenom. Ainsi, • lors de l’appel bonjour(« Michel »);, le paramètre prenom reçoit la valeur « Michel » ; • lors de l’appel bonjour(« Alice »);, il reçoit la valeur « Alice » ; • lors de l’appel bonjour(« Bob »);, il reçoit la valeur « Bob ». Les paramètres rendent les fonctions beaucoup plus générales. Une seule fonction peut alors effectuer la même opération sur des données différentes simplement en recevant des arguments différents. Fig. 5.3 – Schéma. Lien avec les mathématiques : En mathématiques, on parle très souvent de l’argument d’une fonction, par exemple dans f(x). En programmation, on distingue le paramètre, qui apparaît dans la définition de la fonction, de l’argument, qui est la valeur transmise lors de son appel. Cette distinction permet de décrire plus précisément le fonctionnement d’un programme. Les fonctions peuvent recevoir plusieurs paramètres Jusqu’à présent, les fonctions que nous avons écrites ne recevaient qu’une seule information. Il est toutefois très fréquent qu’une fonction ait besoin de plusieurs données pour effectuer son travail. Considérons, par exemple, une fonction qui calcule l’aire d’un rectangle. Soit le programme complet avec la fonction void aireRectangle(double largeur, double longueur) qui calcule et imprime l’aire du rectangle.
Accueilvoid aireRectangle(double largeur, double longueur){ std::cout << « L’aire du rectangle est : » << largeur∗longueur << std::endl; int main(int argc, const char ∗ argv[]) { aireRectangle(5.0,8.0); Cette fonction possède deux paramètres : Les paramètres sont séparés par des virgules et sont déclarés entre les parenthèses de la fonction. Ils peuvent être de même type, comme dans cet exemple, ou de types différents. void afficherPersonne(std::string nom, int age) << » ans. » << std::endl; Le nombre de paramètres n’est pas limité à deux. Une fonction peut recevoir autant de paramètres que nécessaire. Il est cependant recommandé de ne pas en utiliser un trop grand nombre afin de conserver un programme clair et facile à comprendre. Cependant la fonction qui calcule l’aire du rectangle n’est pas satisfaisante car normalement une fonction mathématique retourne une valeur lorsqu’on lui a donné un argument. Notre fonction void aireRectangle(double largeur, double longueur) calcule l’aire certes, mais elle renvoie également une phrase avec la valeur. Cette valeur ne peut pas être réutilisée par le programme. Si l’on souhaite effectuer d’autres calculs avec cette aire, il faut la recalculer, ce qui est peu pratique. Les fonctions qui retournent une valeur Dans la section précédente, notre fonction calculait correctement l’aire d’un rectangle, mais elle affichait directement le résultat à l’écran. Cette solution présente un inconvénient important : une fois la valeur affichée, le programme ne peut plus la réutiliser. Par exemple, il est impossible d’effectuer un autre calcul avec cette aire ou de la mémoriser dans une variable. Il est donc préférable que la fonction renvoie le résultat au programme appelant, qui décidera lui-même de son utilisation. Pour cela, il suffit de remplacer le mot-clé void par le type de la valeur que la fonction doit retourner. Notre fonction devient alors :
Accueildouble aireRectangle(double largeur, double longueur) return largeur ∗ longueur; Le type double indique que cette fonction retourne un nombre réel. Le mot-clé return termine l’exécution de la fonction et renvoie immédiatement la valeur qui le suit au programme appelant. Dans notre exemple, la valeur retournée est le produit largeur ∗ longueur La fonction ne réalise plus aucun affichage. Elle se contente d’effectuer le calcul et de renvoyer le double aireRectangle(double largeur, double longueur) return largeur ∗ longueur; double aire = aireRectangle(5.0, 8.0); std::cout << « L’aire vaut » La valeur retournée par la fonction est ici mémorisée dans la variable aire. Elle pourra ensuite être affichée, utilisée dans un autre calcul ou comparée à une autre valeur. Il est également possible de ne pas utiliser la variable intermédiaire aire et d’écrire simplement double aireRectangle(double largeur, double longueur) return largeur ∗ longueur; std::cout << « L’aire vaut » << aireRectangle(5.0, 8.0) << std::endl;
Accueilvoid doubler(int x) { std::cout << a << « \n »; La réponse est 5, pourquoi ? Le passage par valeur Par défaut, les paramètres d’une fonction sont transmis par valeur. Cela signifie que la fonction reçoit une copie de chaque argument. Considérons le programme suivant. std::cout << a << std::endl; À première vue, on pourrait penser que la variable a vaudra 10 après l’appel de la fonction. Pourtant, ce programme affiche Lorsque la fonction est appelée, la valeur de a est copiée dans le paramètre x. La fonction travaille donc sur une nouvelle variable indépendante de la variable d’origine, et cela
Accueilstd::cout << a << std::endl; Les deux variables nommées a sont distinctes. Bien qu’elles portent le même nom, les deux variables a sont totalement indépendantes. Chacune possède son propre emplacement en mémoire. Ce mode de transmission est appelé passage par valeur. • Le passage par valeur protège les variables du programme appelant contre toute modification involontaire. Cependant, il existe des situations où l’on souhaite justement modifier la variable transmise à la fonction. Nous allons voir dans la section suivante Le passage par référence Dans la section précédente, nous avons vu que le passage par valeur protège la variable d’origine en travaillant sur une copie. Il existe cependant des situations où l’on souhaite que la fonction puisse modifier directement la variable transmise. Pour cela, il suffit de déclarer le paramètre comme une référence. void doubler(int& a) std::cout << a << std::endl;
AccueilCette fois, le programme affiche La seule différence avec l’exemple précédent est la présence du symbole & devant le paramètre. Ce symbole indique que le paramètre est une référence sur la variable transmise lors de l’appel de la La fonction ne travaille donc plus sur une copie, mais directement sur la variable d’origine. Toute modification du paramètre est immédiatement visible dans le programme appelant. • Le passage par référence est particulièrement utile lorsqu’une fonction doit modifier une ou plusieurs variables du programme appelant. Il évite également la création d’une copie, ce qui peut améliorer les performances pour les objets volumineux. Les références constituent aujourd’hui la méthode la plus simple et la plus sûre pour permettre à une fonction de modifier une variable. Il existe toutefois une autre technique reposant sur les pointeurs. Bien qu’elle soit un peu plus complexe, elle reste très utilisée, notamment dans certaines bibliothèques et dans les programmes proches du matériel. Le passage par pointeur Comme les références, les pointeurs permettent à une fonction de modifier directement une variable du programme appelant. La différence est que la fonction ne reçoit plus la variable elle-même, mais son adresse mémoire. Considérons le programme suivant. void doubler(int∗ a) std::cout << a << std::endl; L’exécution de ce programme affiche La fonction modifie donc directement la variable a du programme appelant.
AccueilÀ première vue, l’utilisation du symbole * peut sembler déroutante puisqu’il apparaît à deux endroits void doubler(int∗ a) Pourtant, ces deux symboles n’ont pas la même signification. Dans la déclaration le symbole * indique simplement que le paramètre a est un pointeur vers un entier. En revanche, dans l’instruction le symbole * signifie : « utiliser la valeur située à l’adresse contenue dans le pointeur ». On dit que le pointeur est déréférencé. Le passage par pointeur fait intervenir deux opérateurs déjà rencontrés dans les chapitres précédents. • &a signifie : « transmettre l’adresse de la variable a » ; • int* a signifie : « recevoir cette adresse dans un pointeur » ; • *a signifie : « accéder à la valeur située à cette adresse ». Ces trois écritures sont donc intimement liées. p est un pointeur vers un entier valeur située à l’adresse contenue dans p Les passages par référence et par pointeur permettent tous deux de modifier la variable d’origine. En C++, on privilégie généralement les références lorsque cela est possible, car leur syntaxe est plus simple et moins sujette aux erreurs. Les pointeurs restent néanmoins indispensables dans de nombreuses situations que nous rencontrerons plus loin dans cet ouvrage. Les prototypes de fonctions Pourquoi utiliser des prototypes ? Jusqu’à présent, toutes les fonctions ont été définies avant la fonction main(). Cette organisation fonctionne parfaitement pour les petits programmes.
AccueilLorsque les programmes deviennent plus importants, il est souvent préférable de regrouper les définitions des fonctions à la fin du fichier afin que la fonction main() apparaisse immédiatement. Le programme est alors plus facile à lire et sa structure générale est visible dès les premières lignes. Cependant, lorsqu’il rencontre un appel de fonction, le compilateur doit déjà connaître son existence. Les prototypes permettent de résoudre ce problème. Le programme suivant ne compile pas. std::cout << « Bonjour ! » << std::endl; Au moment où le compilateur rencontre l’appel à bonjour(), il ne connaît pas encore cette fonction. Il suffit alors d’ajouter son prototype avant main(). std::cout << « Bonjour ! » << std::endl; Le compilateur connaît maintenant l’existence de la fonction bonjour() avant son appel. Il peut donc vérifier que cet appel est correct et poursuivre la compilation du programme. Qu’est-ce qu’un prototype ? Un prototype est une déclaration qui indique au compilateur qu’une fonction existe. Il précise son nom, son type de retour et la liste de ses paramètres, mais ne contient pas le corps de la fonction. Contrairement à une définition de fonction, un prototype se termine toujours par un point-virgule.
AccueilLes noms des paramètres sont-ils obligatoires ? Considérons le prototype suivant. double aireRectangle(double largeur, double longueur); La définition de la fonction peut utiliser d’autres noms de paramètres. double aireRectangle(double a, double b) Le programme compile pourtant parfaitement. En effet, le compilateur ne tient compte que du type de retour et des types des paramètres. Les noms des paramètres servent uniquement à rendre le code plus facile à comprendre. C’est d’ailleurs pour cette raison que le prototype peut également s’écrire de la manière suivante. double aireRectangle(double, double); Cette écriture est parfaitement correcte. En pratique, on préfère cependant conserver les noms des paramètres afin de rendre le code plus lisible. • Les prototypes permettent de séparer la déclaration d’une fonction de sa définition. Ils rendent les programmes plus faciles à organiser tout en permettant au compilateur de connaître l’existence des fonctions avant leur utilisation. Dans les projets de grande taille, les prototypes sont généralement placés dans des fichiers d’en-tête (.h ou .hpp), tandis que les définitions des fonctions sont regroupées dans des fichiers sources (.cpp). Nous découvrirons cette organisation dans un chapitre ultérieur. Les paramètres par défaut Pourquoi utiliser des paramètres par défaut ? Il arrive qu’une fonction soit appelée très souvent avec la même valeur pour l’un de ses paramètres. Par exemple, une fonction peut calculer une puissance. Dans la majorité des cas, nous souhaitons simplement calculer le carré d’un nombre. Il serait alors dommage de devoir préciser systématiquement que l’exposant vaut 2. Les paramètres par défaut permettent de définir une valeur qui sera utilisée automatiquement lorsqu’aucune autre valeur n’est fournie lors de l’appel de la fonction. La fonction suivante calcule une puissance. Si aucun exposant n’est précisé, celui-ci vaut automatiquement 2.
Accueildouble puissance(double base, int exposant = 2) return std::pow(base, exposant); std::cout << puissance(5) << std::endl; std::cout << puissance(5, 3) << std::endl; Le programme affiche : Comment cela fonctionne-t-il ? Dans la déclaration de la fonction, indique que le paramètre exposant prendra automatiquement la valeur 2 si aucune autre valeur n’est Ainsi, les deux appels suivants sont équivalents : utilise la valeur 3, qui remplace la valeur par défaut. Plusieurs paramètres Une fonction peut posséder plusieurs paramètres, dont certains seulement ont une valeur par défaut. void rectangle(int largeur, Les appels suivants sont alors possibles :
AccueilDans le premier cas, la hauteur vaut automatiquement 5. Dans le second, elle vaut 10. Une règle importante Les paramètres possédant une valeur par défaut doivent toujours être placés à la fin de la liste des Par exemple, cette déclaration est correcte : void rectangle(int largeur, En revanche, celle-ci est interdite : void rectangle(int largeur = 20, Le compilateur ne pourrait plus déterminer correctement la valeur associée à chaque paramètre lors • Les paramètres par défaut permettent de simplifier les appels de fonction lorsqu’une même valeur est utilisée très souvent. Ils évitent de répéter inutilement certaines valeurs tout en laissant la possibilité de les modifier lorsque cela est nécessaire. Les paramètres par défaut offrent une certaine souplesse, mais ils ne permettent pas de répondre à toutes les situations. Il arrive que l’on souhaite définir plusieurs fonctions portant le même nom mais réalisant des traitements différents. C’est le principe de la surcharge des fonctions, que nous allons découvrir dans la section suivante. La surcharge des fonctions Pourquoi surcharger une fonction ? Il arrive que plusieurs fonctions réalisent la même tâche, mais avec des types de données différents. Par exemple, nous pourrions souhaiter calculer le carré d’un entier ou celui d’un nombre réel. Les deux fonctions effectuent exactement la même opération. Il serait dommage de devoir leur donner des noms différents. La surcharge des fonctions permet d’utiliser le même nom pour plusieurs fonctions, à condition que leur liste de paramètres soit différente. Considérons les deux fonctions suivantes.
Accueildouble carre(double x) Elles portent le même nom mais leurs paramètres sont différents. Nous pouvons alors écrire : std::cout << carre(5) << std::endl; std::cout << carre(2.5) << std::endl; Le programme affiche : Comment le compilateur choisit-il ? Lorsque le compilateur rencontre l’appel il choisit automatiquement la fonction car l’argument est un entier. En revanche, pour l’appel il sélectionne automatiquement double carre(double x) car l’argument est un nombre réel. Le programmeur utilise donc toujours le même nom de fonction. C’est le compilateur qui choisit automatiquement la version correspondant le mieux aux arguments fournis.
AccueilLes paramètres doivent être différents Deux fonctions peuvent être surchargées si leur liste de paramètres est différente. Par exemple, les deux fonctions suivantes sont parfaitement valides. int maximum(int a, int b); double maximum(double a, double b); En revanche, les deux déclarations suivantes sont interdites. int maximum(int a, int b); double maximum(int a, int b); Le type de retour ne suffit pas à distinguer deux fonctions. Le compilateur ne tient compte que du nombre et du type des paramètres. Surcharge et paramètres par défaut Dans certains cas, un paramètre par défaut permet d’éviter la création d’une deuxième fonction. Par exemple, au lieu d’écrire void dessiner(int largeur); void dessiner(int largeur, char caractere); il est possible d’écrire une seule fonction. void dessiner(int largeur, char caractere = ’∗’); Les paramètres par défaut et la surcharge des fonctions sont donc deux mécanismes différents qui permettent de rendre une fonction plus souple. Selon la situation, l’un ou l’autre sera plus approprié. • La surcharge permet de donner le même nom à plusieurs fonctions. • Pour que cela soit possible, les fonctions doivent posséder une liste de paramètres différente. • Le type de retour ne suffit pas à distinguer deux fonctions. Autre exemple de surcharge La fonction valeur absolue. int valeurAbsolue(int x)
Accueildouble valeurAbsolue(double x) Vous vous demandez peut-être comment le compilateur sait quelle fonction appeler. La réponse tient à une caractéristique fondamentale du C++ : c’est un langage fortement typé. Le C++ est un langage fortement typé. Cela signifie que le compilateur connaît le type de chaque variable, de chaque paramètre et de chaque valeur de retour. C’est grâce à cette information qu’il peut distinguer plusieurs fonctions portant le même nom. Lorsqu’une fonction est appelée, le compilateur choisit automatiquement celle dont les paramètres correspondent le mieux aux types des arguments fournis. Tous les langages modernes sont, d’une certaine manière, typés. En revanche, le C++ est généralement présenté comme un langage fortement typé : les types sont au cœur du langage, le compilateur les vérifie rigoureusement et ils influencent directement la résolution des appels de fonctions, la surcharge, les conversions, les templates, etc.
Accueilcpp-Chap5- Les fonctions
Accueil
Chapitre 6 - La récursivité
Chapitre 6 – La récursivité
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
AccueilUne autre manière de répéter Jusqu’à présent, nous avons appris à répéter des instructions grâce aux différentes boucles du C++, comme for, while ou do…while. Ces structures permettent d’exécuter plusieurs fois les mêmes instructions en faisant évoluer progressivement une ou plusieurs variables. Il existe cependant une autre manière de résoudre certains problèmes. Au lieu d’utiliser une boucle, une fonction peut tout simplement s’appeler elle-même. Cette technique porte le nom de récursivité. La récursivité est une approche très élégante qui permet de résoudre naturellement certains problèmes dont la structure se répète. On la rencontre dans de nombreux domaines de l’informatique : parcours d’arbres, exploration de labyrinthes, recherche dans des dossiers, algorithmes de tri, intelligence artificielle et bien d’autres encore. Avant d’étudier cette nouvelle technique, il est important de bien comprendre qu’il existe deux grandes façons de répéter une opération. Utilise une boucle (for, while, La répétition est contrôlée par une variable de boucle. Très adaptée aux traitements répétitifs simples. Une fonction s’appelle elle-même. La répétition est obtenue par des appels successifs de la fonction. Très adaptée aux problèmes qui se décomposent naturellement en sousproblèmes plus petits. Aucune de ces deux approches n’est systématiquement meilleure que l’autre. Certaines situations se prêtent naturellement à une solution itérative, tandis que d’autres sont beaucoup plus simples à exprimer de manière récursive. Un bon programmeur doit connaître les deux techniques afin de choisir la plus adaptée au problème qu’il souhaite résoudre. • Une itération répète des instructions grâce à une boucle. • Une récursivité répète un traitement en faisant s’appeler une fonction elle-même. • Les deux approches permettent souvent de résoudre un même problème. Comprendre la récursivité Avant d’écrire notre première fonction récursive, essayons de comprendre le principe de la récursivité à l’aide de quelques situations de la vie courante. Chaque appel récursif rapproche progressivement le programme du cas de base, c’est-à-dire du moment où la fonction peut terminer son travail sans effectuer de nouvel appel. Autrement dit, au lieu de résoudre directement un problème parfois complexe, on le simplifie petit à petit jusqu’à obtenir un cas très simple, dont la solution est immédiate. Imaginez une poupée gigogne (ou poupée russe). Lorsque vous l’ouvrez, vous découvrez une seconde poupée. À l’intérieur de celle-ci se trouve une troisième poupée. Puis une quatrième. Et ainsi de suite, jusqu’à la dernière, qui ne contient plus rien. Chaque poupée est construite sur le même principe que la précédente, mais elle est simplement plus proche du but qui est : ne rien La récursivité fonctionne exactement de la même manière : chaque appel de fonction traite une version du même problème, qui à chaque itération s’approche d’une solution élémentaire. La récursivité consiste toujours à rapprocher progressivement un problème d’un cas très simple dont la solution est immédiate. Ce cas particulier, appelé cas de base ou condition d’arrêt, est indispensable. Sans lui, les appels récursifs continueraient indéfiniment. Une première fonction récursive (récursion “descendante” uniquement) Imaginons que vous deviez annoncer un compte à rebours avant le décollage d’une fusée. Vous commencez par annoncer : Le travail n’est pas terminé. Il reste exactement le même travail à effectuer, mais en partant de 4. Après avoir annoncé 4, il reste exactement le même travail, mais en partant de 3. Enfin, lorsque le compte atteint 0, il n’y a plus rien à annoncer : le compte à rebours est terminé. Chaque étape rapproche donc naturellement du cas où le travail est terminé. • Une bonne fonction récursive ne cherche jamais à résoudre tout le problème. Elle résout seulement une petite partie, puis laisse la même fonction résoudre le reste. Fonction « Compte à rebours »
Accueilvoid compteARebours(int n) std::cout << n << ’\n’; // Travail avant compteARebours(n – 1); // Appel recursif Voilà une variation de la fonction « compte à rebours ». Pouvez-vous prédire le résultat ? void compteARebours(int n) std::cout << n << ’\n’; compteARebours(n – 1); std::cout << « Je remonte » << std::endl; Prenez le temps de réfléchir avant de lire la suite du chapitre. Jusqu’à présent, nous avons observé qu’une fonction récursive commence par descendre jusqu’au cas de base, puis remonte progressivement. Mais une question se pose naturellement : Comment le programme sait-il où reprendre son exécution après chaque appel récursif ? Pour répondre à cette question, imaginons un escalier numéroté de 10 à 1. Nous nous trouvons au sommet, sur la marche 10. Notre objectif est de calculer la somme des numéros de toutes les marches. Au lieu d’additionner immédiatement les valeurs, nous décidons de descendre jusqu’à la première marche. Ce n’est qu’une fois arrivé en bas que nous commencerons à remonter en effectuant les Le programme suivant réalise exactement cette opération. int sommeNEntiers(int n) std::cout << « Je descends sur la marche »
Accueilint resultat = sommePartielle + n; int sommePartielle = sommeNEntiers(n – 1); std::cout << « Je remonte a la marche » std::cout << « \nSomme = » << sommeNEntiers(10) Avant de comprendre le calcul lui-même, observons ce qui se passe pendant la descente. À chaque appel de la fonction, le programme doit mémoriser l’endroit où il devra reprendre son exécution une fois l’appel récursif terminé. Pour cela, il place les informations nécessaires au sommet d’une structure appelée pile d’appels (en anglais call stack). Au début de l’exécution, la pile contient uniquement la fonction main(). Après l’appel de sommeNEntiers(10) : —————–sommeNEntiers(10) —————–sommeNEntiers(9) —————–sommeNEntiers(10)
Accueil—————–sommeNEntiers(8) —————–sommeNEntiers(9) —————–sommeNEntiers(10) La pile continue ainsi à grandir jusqu’à atteindre la marche 1. À cet instant, la fonction sommeNEntiers(1) atteint le cas de base et renvoie simplement la valeur 1. La pile commence alors à se vider. La fonction sommeNEntiers(2) reprend son exécution exactement après l’appel récursif. Elle reçoit la valeur 1, calcule : et renvoie la valeur 3. La fonction sommeNEntiers(3) reprend ensuite son exécution. Elle reçoit la valeur 3, calcule : et renvoie la valeur 6. et ainsi de suite jusqu’à la marche 10. Finalement, la dernière fonction renvoie : Cette valeur est ensuite reçue par la fonction main(), qui l’affiche à l’écran. La sortie exact est : Somme = Je descends sur la marche 10 Je descends sur la marche 9 Je descends sur la marche 8 Je descends sur la marche 7 Je descends sur la marche 6 Je descends sur la marche 5 Je descends sur la marche 4 Je descends sur la marche 3 Je descends sur la marche 2 Je descends sur la marche 1 Je remonte a la marche 2 : 1 + 2 = 3 Je remonte a la marche 3 : 3 + 3 = 6 Je remonte a la marche 4 : 6 + 4 = 10
AccueilJe remonte a la marche 5 : 10 + 5 = 15 Je remonte a la marche 6 : 15 + 6 = 21 Je remonte a la marche 7 : 21 + 7 = 28 Je remonte a la marche 8 : 28 + 8 = 36 Je remonte a la marche 9 : 36 + 9 = 45 Je remonte a la marche 10 : 45 + 10 = 55 Program ended with exit code: 0 • Une fonction récursive ne perd jamais sa place dans le programme. • Lorsqu’elle appelle une nouvelle fois la même fonction, son exécution est simplement • Toutes les informations nécessaires pour reprendre l’exécution sont conservées dans la • Les fonctions reprennent ensuite leur exécution dans l’ordre inverse de leur appel. La factorielle est souvent utilisée pour illustrer la récursivité. Vous êtes maintenant capable de l’écrire vous-même en quelques lignes. Maintenant que nous avons compris le rôle de la pile d’appels et le déroulement de la récursivité, nous pouvons simplifier la fonction. Les variables intermédiaires, qui étaient utiles pour suivre le raisonnement, ne sont plus nécessaires. int sommeNEntiers(int n) return n + sommeNEntiers(n – 1); std::cout << sommeNEntiers(10) << ’\n’;
AccueilOn pourrait tout aussi bien écrire if (n == 1) return 1; Cependant pour des raisons pédagogiques (chaque instruction mérite sa ligne) on utilisera la Les Tours de Hanoï constituent l’un des exemples les plus célèbres de récursivité. Le jeu est composé de trois tours, notées A, B et C, ainsi que d’une pile de disques de diamètres Au départ, tous les disques sont empilés sur une même tour, du plus grand au plus petit. Le but est de déplacer toute la pile sur une autre tour en respectant les deux règles suivantes : • un seul disque peut être déplacé à la fois ; • un disque plus grand ne peut jamais être posé sur un disque plus petit. Avant d’écrire le moindre programme, essayons de résoudre le problème nous-mêmes. Commencez par dessiner trois tours et trois disques numérotés de 1 à 3, le disque 1 étant le plus Chaque déplacement sera noté sous la forme • déplacer le disque 1 ; • de la tour A vers la tour B. Essayez maintenant de résoudre le problème avant de poursuivre la lecture.
Accueil– un seul disque est déplacé ; – aucun disque plus grand n’est jamais posé sur un disque plus petit. La pile finale est bien sur B : 3 − 2 − 1. Recommencez le même exercice avec quatre disques. Cette fois, ne cherchez pas à résoudre complètement le problème. Arrêtez-vous dès que les trois plus petits disques sont empilés sur une autre tour. Que remarquez-vous ? Fig. 6.1 – Schéma. Les trois plus petits disques se retrouvent exactement dans la même configuration que le problème précédent. Autrement dit, le problème à quatre disques contient le problème à trois disques. • Le problème des Tours de Hanoï possède une propriété remarquable : Pour déplacer une pile de n disques, il faut d’abord savoir déplacer une pile de n − 1 disques. • Nous venons de découvrir le principe fondamental de la récursivité. Nous nous arrêtons ici. Inutile de poursuivre les déplacements. Observez simplement la configuration Les trois plus petits disques sont maintenant empilés exactement comme au début de l’exercice précédent. Nous avons donc retrouvé le problème à trois disques. Les essais réalisés avec trois puis quatre disques nous ont permis de faire une observation importante : le problème semble toujours se reproduire avec un disque de moins. Supposons maintenant que nous disposions d’une pile de n disques. Comment déplacer cette pile complète sur une autre tour ? Le plus grand disque est situé tout en bas de la pile. Tant qu’il est recouvert par les n − 1 autres disques, il est impossible de le déplacer. Il faut donc commencer par déplacer les n − 1 plus petits disques sur la troisième tour. Nous pouvons alors déplacer le plus grand disque vers sa tour de destination. Il ne reste plus qu’à replacer les n − 1 petits disques au-dessus de lui.
AccueilNous obtenons ainsi une méthode générale. • Pour déplacer une pile de n disques : • déplacer les n − 1 plus petits disques vers la tour auxiliaire ; • déplacer le plus grand disque vers la tour de destination ; • déplacer les n − 1 plus petits disques sur le plus grand disque. Remarquez que les première et troisième étapes consistent exactement à résoudre le même problème, mais avec une pile contenant un disque de moins. Nous retrouvons donc naturellement le principe de la récursivité. Exemple avec avec 5 disques Fig. 6.2 – Schéma pour n = 5. Les rôles des tours A, B et C sont interchangeables. Dans notre raisonnement, A est toujours la tour de départ, C la tour d’arrivee et B la tour auxiliaire. Mais rien n’empêche de choisir une autre tour comme destination. Écriture du programme Nous savons maintenant comment résoudre le problème des Tours de Hanoï. Il ne reste plus qu’à traduire notre raisonnement en langage C++. Pour déplacer une pile de disques, notre fonction devra connaître quatre informations : • le nombre de disques à déplacer ; • la tour de départ ; • la tour de destination ; • la tour auxiliaire. Nous pouvons donc commencer par écrire la déclaration de la fonction.
AccueilLe paramètre n représente le nombre de disques à déplacer. Les trois paramètres de type char contiennent simplement le nom des trois tours. Ils pourront prendre les valeurs ’A’, ’B’ et ’C’. Avant d’écrire le reste de la fonction, une question importante se pose. Quand la récursivité doit-elle s’arrêter ? Comme toute fonction récursive, notre fonction doit posséder un cas de base. Celui-ci est particulièrement simple. S’il ne reste qu’un seul disque à déplacer, aucune réflexion n’est nécessaire : il suffit de le déplacer directement vers sa tour de destination. Le début de notre fonction devient alors : L’instruction return met immédiatement fin à la fonction. Si nous arrivons dans ce cas, il est inutile de poursuivre l’exécution. Nous pouvons maintenant nous occuper du cas général. Reprenons le raisonnement découvert dans le paragraphe précédent. Pour déplacer une pile de n disques, il faut : 1. déplacer les n − 1 plus petits disques vers la tour auxiliaire ; 2. déplacer le plus grand disque vers la tour de destination ; 3. déplacer les n − 1 plus petits disques sur le plus grand disque. Il suffit maintenant de traduire chacune de ces trois étapes en C++. La première étape consiste à déplacer les n − 1 plus petits disques. Cette instruction ressemble beaucoup à la déclaration de la fonction. La seule différence est que le nombre de disques a diminué d’une unité et que les rôles des tours ont changé. Nous pouvons ensuite déplacer le plus grand disque.
AccueilEnfin, il reste à déplacer les n − 1 plus petits disques sur le plus grand. En réunissant toutes ces parties, nous obtenons la fonction complète. Cette fonction ne fait finalement rien d’autre que reproduire fidèlement le raisonnement que nous avons construit progressivement. Elle ne contient aucun « tour de magie ». Chaque appel récursif correspond simplement à l’une des étapes de notre méthode de résolution. Le programme complet s’écrit donc ainsi : Code 6.1 – Programme Les tours de Hanoï
Accueilstd::cout << depart << » -> » << arrivee << ’\n’; // Deplacer les n-1 plus petits disques hanoi(n – 1, depart, auxiliaire, arrivee); // Deplacer le plus grand disque std::cout << depart << » -> » << arrivee << ’\n’; int main(int argc, const char * argv[]) hanoi(20, ’A’, ’C’, ’B’); // Deplacer les n-1 plus petits disques hanoi(n – 1, auxiliaire, arrivee, depart); Avec 20 disques le nombre de déplacements est de 2n − 1 = 1048575.
AccueilΣ Complément mathématique Le nombre de déplacements effectués par notre fonction récursive vérifie la relation suivante : T (n) = 2T (n − 1) + 1, où T (n) désigne le nombre de déplacements nécessaires pour une pile de n disques. Cette relation traduit exactement notre algorithme : 1. déplacer les n − 1 plus petits disques ; 2. déplacer le plus grand disque (un seul déplacement) ; 3. déplacer à nouveau les n − 1 plus petits disques. Calculons les premières valeurs : T (2) = 2 × 1 + 1 = 3, T (3) = 2 × 3 + 1 = 7, T (4) = 2 × 7 + 1 = 15, T (5) = 2 × 15 + 1 = 31. On reconnaît immédiatement la suite 1, 3, 7, 15, 31, . . . qui s’écrit plus simplement Les lecteurs connaissant le triangle de Pascal remarqueront peut-être que la somme des nombres d’une ligne vaut toujours une puissance de deux : Les Tours de Hanoï conduisent elles aussi aux puissances de deux, car chaque problème se décompose en deux sous-problèmes identiques. Le terme −1 provient simplement du fait qu’il faut ajouter un unique déplacement entre les deux appels récursifs. On peut d’ailleurs écrire : T (n) + 1 = 2 T (n − 1) + 1 , ce qui conduit immédiatement à
AccueilLa légende raconte que, dans un temple de Bénarès, des moines déplacent sans interruption une tour de 64 disques selon les règles des Tours de Hanoï. Lorsque le dernier déplacement sera effectué… le 264 − 1 = 18 446 744 073 709 551 615 Soit environ 18, 4 milliards de milliards de déplacements ! Même en déplaçant un disque par seconde, il faudrait environ 585 milliards d’années. Pour donner un ordre de grandeur, l’âge actuel de l’Univers est estimé à environ 13, 8 milliards La récursivité permet à une fonction de résoudre un problème en le ramenant à un problème plus petit de même nature. Toute fonction récursive doit posséder un cas de base, sans lequel les appels se poursuivraient indéfiniment. Les exemples étudiés dans ce chapitre ont également montré que la récursivité peut produire un code très élégant, mais qu’elle utilise la pile d’appels et peut parfois conduire à un très grand nombre Une fonction récursive : • s’appelle elle-même ; • doit toujours posséder un cas de base ; • résout généralement un problème en le ramenant à un problème plus petit ; • utilise la pile d’appels pour conserver les appels encore inachevés.
Accueilcpp-Chap6- La récursivité
Accueil
Chapitre 7 - Les espaces de noms
Chapitre 7 : Les espaces de noms
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
AccueilLes espaces de noms (namespace) Pourquoi les espaces de noms ? Au fur et à mesure que les programmes grandissent, ils contiennent de plus en plus de fonctions, de variables Imaginons maintenant que deux bibliothèques différentes définissent chacune une fonction portant le même La première bibliothèque utilise cette fonction pour afficher des résultats mathématiques, tandis que la seconde l’utilise pour afficher des résultats physiques. Que se passerait-il si notre programme utilisait les deux bibliothèques ? Comment le compilateur pourrait-il savoir laquelle de ces deux fonctions nous souhaitons appeler ? Il ne le pourrait tout simplement pas. Il est donc nécessaire de disposer d’un mécanisme permettant de regrouper les éléments appartenant à une même bibliothèque afin d’éviter les conflits de noms. C’est précisément le rôle des espaces de noms (namespace). • Un espace de noms permet de regrouper des fonctions, des variables ou des classes ayant un lien logique et d’éviter les conflits entre des éléments portant le même nom. Créons deux espaces de noms contenant chacun une fonction portant le même nom. std::cout << « Affichage mathematique\n »; 1. On verra plus loin les classes très en détail. Les classes sont la base de la propgrammation orientée objets aussi appelée Les espaces de noms (namespace) std::cout << « Affichage physique\n »; Physique::afficher(); L’exécution de ce programme produit le résultat suivant : Affichage mathematique Les deux fonctions portent exactement le même nom : afficher(). Sans les espaces de noms, le compilateur ne pourrait pas savoir laquelle appeler. Grâce aux espaces de noms, chaque fonction est identifiée de manière unique. L’opérateur ::, appelé opérateur de résolution de portée, permet d’indiquer dans quel espace de noms se trouve la fonction que l’on souhaite utiliser. Ainsi, appelle la fonction afficher() de l’espace de noms Math, tandis que Physique::afficher(); appelle celle de l’espace de noms Physique. Le symbole : : est appelé opérateur de résolution de portée (scope resolution operator en anglais). • Deux espaces de noms différents peuvent contenir des fonctions, des variables ou des classes portant le même nom. • L’opérateur :: permet de préciser à quel espace de noms appartient l’élément que l’on souhaite
AccueilLes espaces de noms (namespace) Il ne faut pas confondre une bibliothèque et un espace de noms. Une bibliothèque est un ensemble de fonctions, de classes et d’autres éléments réutilisables. Un espace de noms est simplement un mécanisme qui permet d’organiser ces éléments et d’éviter les conflits de noms. Une même bibliothèque peut contenir plusieurs espaces de noms, et un programme peut créer ses propres espaces de noms sans pour autant constituer une bibliothèque. Pourquoi écrit-on std::cout ? Depuis le début de ce livre, nous écrivons des instructions telles que : std::cout << « Bonjour ! » << std::endl; Sans vraiment nous demander ce que signifie std. Nous pouvons maintenant répondre à cette question. std est le nom d’un espace de noms. Il regroupe les principaux éléments de la bibliothèque standard du C++, parmi lesquels cout, cin, endl et bien d’autres. std::cout << « Bonjour\n »; signifie simplement : Utiliser l’objet cout appartenant à l’espace de noms std. De la même manière, désigne la fonction afficher() appartenant à l’espace de noms Math. Il ne faut pas confondre une bibliothèque et un espace de noms. Une bibliothèque est un ensemble de fonctions, de classes, de variables et d’autres éléments réutilisables. Un espace de noms est un mécanisme qui permet d’organiser ces éléments et d’éviter les conflits de Une bonne image consiste à comparer une bibliothèque à un classeur et les espaces de noms à des intercalaires. Le classeur contient les documents. Les intercalaires ne contiennent rien par eux-mêmes : ils servent simplement à classer les documents afin de les retrouver facilement. La bibliothèque standard du C++ est un vaste classeur. L’un de ses principaux intercalaires s’appelle std. C’est la raison pour laquelle nous écrivons std::cout, std::endl, etc. Créer son propre espace de noms Les espaces de noms ne sont pas réservés à la bibliothèque standard du C++. Vous pouvez également créer les vôtres afin de regrouper des fonctions, des variables ou des classes ayant un lien logique.L’exemple suivant rassemble deux fonctions relatives à la géométrie dans un même espace de noms.
Accueildouble aireCarre(double cote) Les espaces de noms (namespace) std::cout << « Aire : » << Geometrie::aireCarre(5.0) double perimetreCarre(double cote) std::cout << « Perimetre : » << Geometrie::perimetreCarre(5.0) Les deux fonctions appartiennent maintenant à l’espace de noms Geometrie. Pour les utiliser, il suffit de faire précéder leur nom de Geometrie::. Même dans un projet qui ne constitue pas une bibliothèque, cette manière de regrouper les fonctions améliore la lisibilité du programme et facilite son organisation. • Les espaces de noms permettent non seulement d’éviter les conflits de noms, mais aussi de regrouper les éléments ayant un même domaine d’application. • Ils constituent un excellent moyen d’organiser un programme de grande taille. L’instruction using namespace Comme nous l’avons vu, les éléments de la bibliothèque standard appartiennent à l’espace de noms std. C’est pourquoi nous écrivons : std::cout << « Bonjour\n »; Il est toutefois possible d’éviter de répéter std:: grâce à l’instruction suivante : using namespace std; Après cette instruction, il devient possible d’écrire simplement : cout << « Bonjour\n »;
AccueilLes espaces de noms (namespace) • L’instruction using namespace std; est pratique dans les petits programmes et dans les exemples pédagogiques. • En revanche, dans les projets de grande taille, elle peut provoquer des conflits de noms avec d’autres bibliothèques. • C’est pourquoi de nombreux développeurs préfèrent conserver l’écriture std::cout, std::cin, Les espaces de noms permettent d’organiser un programme et d’éviter les conflits entre des éléments portant le même nom. Nous avons également découvert que les objets cout et cin appartiennent à l’espace de noms std, qui est le principal espace de noms de la bibliothèque standard du C++. Dans la suite de cet ouvrage, nous continuerons à utiliser cette notation afin de conserver des programmes clairs et faciles à comprendre.
Accueilcpp-Chap7- Les espaces de noms (namespace)
AccueilTermes à ajouter au glossaire
Chapitre 8 - Les tableaux
Chapitre 8 – Les tableaux
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
AccueilPourquoi un tableau ? Jusqu’à présent, toutes les variables que nous avons utilisées ne pouvaient contenir qu’une seule valeur. Par exemple, si nous voulons mémoriser l’âge d’une personne, une simple variable suffit : Mais imaginons maintenant que nous souhaitions enregistrer les notes de 30 étudiants. Une première idée serait de créer une variable pour chaque note : Cette solution présente plusieurs inconvénients : • elle est longue à écrire ; • elle devient difficile à modifier ; • il est pratiquement impossible de traiter toutes les notes avec une boucle. Nous avons besoin d’un moyen de regrouper plusieurs valeurs de même type sous un seul nom. C’est précisément le rôle d’un tableau. Dans les sections suivantes, nous allons découvrir comment déclarer un tableau, accéder à ses éléments et utiliser des boucles pour les parcourir efficacement. Déclarer un tableau La déclaration d’un tableau ressemble beaucoup à celle d’une variable classique. La différence est que l’on indique également le nombre d’éléments que le tableau pourra contenir. Par exemple, la déclaration suivante crée un tableau capable de mémoriser cinq entiers : Le nom du tableau est notes. Comme pour une variable classique, ce nom désigne une seule variable. La valeur placée entre crochets indique le nombre d’éléments que le tableau peut contenir. Ici, le tableau pourra mémoriser cinq entiers. Fig. 8.1 – Le tableau notes est une seule variable contenant cinq éléments de type int. Les éléments sont numérotés de 0 à Un tableau est une variable qui contient plusieurs éléments du même type. Chaque élément d’un tableau est repéré par un numéro appelé indice. Dans le tableau suivant, les cinq éléments sont numérotés de 0 à 4. La première question que l’on peut se poser est la suivante : Pourquoi le premier élément porte-t-il l’indice 0 et non 1 ? La réponse est simple : c’est une convention choisie par les concepteurs du langage C++. D’autres langages utilisent également cette convention. Même si ce choix peut sembler surprenant au début, il devient très naturel avec un peu de pratique. Pour accéder à un élément du tableau, on écrit le nom du tableau suivi de son indice entre crochets. Pour afficher la première note, on écrit simplement : std::cout << notes[0]; La taille d’un tableau et ses indices sont deux notions différentes. Un tableau de cinq éléments possède les indices : 0 1 2 3 4. Le plus grand indice vaut donc toujours taille − 1 et le premier élément d’un tableau possède toujours l’indice 0.
AccueilQue va donner le code suivant ? int main(int argc, const char * argv[]) Lire et modifier les éléments d’un tableau Une fois le tableau déclaré, chaque élément peut être utilisé comme une variable classique. Pour modifier la valeur d’un élément, il suffit d’utiliser son indice. Les cinq éléments du tableau contiennent maintenant les valeurs indiquées. Pour lire le contenu d’un élément, on utilise exactement la même notation. cout << notes[0] << endl; cout << notes[3] << endl; dont le résultat est : Un élément peut être modifié ainsi : cout << notes[2] << endl; ce qui donne bien entendu 14.
Accueilcout << « Premiere note : » << notes[0] << endl; cout << « Derniere note : » << notes[4] << endl; Le resultat est naturellement Parcourir un tableau Jusqu’à présent, nous avons accédé aux éléments du tableau un par un. Par exemple, pour afficher les cinq notes d’un tableau, nous pourrions écrire : cout << notes[0] << endl; cout << notes[1] << endl; cout << notes[2] << endl; cout << notes[3] << endl; cout << notes[4] << endl; Cette méthode fonctionne parfaitement. En revanche, elle devient rapidement impraticable lorsque le tableau contient un grand nombre d’éléments. Imaginez un tableau contenant 500, 10 000 ou même un million de valeurs ! Heureusement, une boucle permet de parcourir automatiquement tous les éléments du tableau. for (int i = 0; i < 5; i++) cout << notes[i] << endl; La variable i joue ici le rôle d’indice. À chaque tour de boucle, sa valeur augmente d’une unité, ce qui permet d’accéder successivement aux différents éléments du tableau. La boucle for ne connaît pas le tableau. Elle se contente d’incrémenter la variable i. C’est l’expression notes[i] qui permet d’accéder successivement aux différents éléments du tableau.
AccueilQue va-t-il se passer ? Le programme suivant affiche-t-il les cinq notes du tableau ? for (int i = 1; i < 5; i++) cout << notes[i] << endl; Si ce n’est pas le cas, quelle note ne sera jamais affichée ? Jusqu’à présent, nous avons appris à déclarer un tableau, à accéder à ses éléments et à les parcourir à l’aide d’une boucle. Les tableaux permettent de résoudre de nombreux problèmes très courants. Dans cette section, nous allons découvrir quelques applications classiques qui illustrent leur utilisation. À la fin de cette section, vous serez capable de : • calculer la somme des éléments d’un tableau ; • calculer une moyenne ; • rechercher la plus grande ou la plus petite valeur ; • compter les éléments répondant à une condition ; • rechercher la présence d’une valeur. Calcul de la somme des éléments Le calcul de la somme des éléments d’un tableau est une opération très fréquente. L’idée consiste à parcourir le tableau et à ajouter chaque élément à une variable appelée accumulateur. int notes[5] = {15, 12, 18, 14, 17}; for (int i = 0; i < 5; i++) somme += notes[i]; // somme = somme + notes[i] cout << « Somme = » << somme << endl; La variable somme est initialisée à zéro, puis chaque élément du tableau lui est ajouté successivement. Le résultat affiché est :
AccueilLa variable somme est appelée un accumulateur. Son rôle est de mémoriser progressivement le résultat obtenu au cours de la boucle. Calcul de la moyenne Après avoir calculé la somme des éléments d’un tableau, il est très simple d’en déduire la moyenne. La méthode consiste à additionner tous les éléments du tableau, puis à diviser cette somme par le nombre d’éléments. int notes[5] = {15, 12, 18, 14, 17}; for (int i = 0; i < 5; i++) double moyenne = static_cast<double>(somme) / 5; cout << « Moyenne = » << moyenne << endl; Le programme affiche : La fonction static_cast<double> convertit la variable somme en nombre réel avant la division. Sans cette conversion, la division serait effectuée entre deux entiers et la partie décimale serait perdue. Il est généralement préférable de calculer la somme dans une variable de type int, puis de convertir le résultat en double uniquement au moment de calculer la moyenne. Cette manière de procéder est à la fois simple et efficace. On peut aussi remplacer le nombres de notes par une constante, c’est une excellente habitude. const int NB_NOTES = 5; int notes[NB_NOTES] = {15, 12, 18, 14, 17}; for (int i = 0; i < NB_NOTES; i++) double moyenne = static_cast<double>(somme) / NB_NOTES;
AccueilRecherche de la plus grande valeur Il est souvent nécessaire de déterminer la plus grande valeur contenue dans un tableau. Pour cela, on suppose dans un premier temps que le premier élément est le plus grand, puis on compare successivement tous les autres éléments à cette valeur. Si un élément plus grand est trouvé, il devient la nouvelle valeur maximale. const int NB_NOTES = 5; for (int i = 1; i < NB_NOTES; i++) if (notes[i] > maximum) int notes[NB_NOTES] = {15, 12, 18, 14, 17}; int maximum = notes[0]; cout << « Maximum = » << maximum << endl; Le programme affiche : Pourquoi la boucle commence-t-elle avec l’indice 1 et non avec 0 ? En effet, la variable maximum a déjà été initialisée avec la valeur notes[0]. Il est donc inutile de comparer une seconde fois le premier élément avec lui-même. Les tableaux constituent l’un des outils fondamentaux de la programmation. Ils seront utilisés dans de nombreux chapitres de ce livre et serviront de base à des structures de données plus évoluées. Que se passerait-il si l’on écrivait : int maximum = notes[0]; Le programme fonctionnerait-il toujours correctement ?
Accueil• Un tableau permet de regrouper plusieurs valeurs de même type sous un même nom. • Les éléments d’un tableau sont repérés par un indice qui commence toujours à 0. • Chaque élément d’un tableau peut être lu ou modifié comme une variable ordinaire. • Une boucle for permet de parcourir facilement tous les éléments d’un tableau. • Les tableaux sont particulièrement adaptés pour effectuer des traitements répétitifs tels que le calcul d’une somme, d’une moyenne ou la recherche d’une valeur maximale.
Accueilcpp-Chap8- Les tableaux
AccueilTermes à ajouter au glossaire
Chapitre 9 - Les chaînes de caractères
Chapitre 9 – Les chaînes de caractères
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
AccueilLes chaînes de caractères Dans ce chapitre, nous utilisons les caractères classiques (char), suffisants pour comprendre le fonctionnement des chaînes de caractères. Les systèmes modernes utilisent souvent des encodages plus évolués (comme UTF-8) afin de représenter les caractères de toutes les langues. Ces encodages ne modifient pas les principes étudiés ici et seront abordés plus tard si nécessaire. Pourquoi les chaînes de caractères ? Jusqu’à présent, nous avons appris à mémoriser différents types de données. Par exemple, une variable de type int permet de stocker un nombre entier, tandis qu’une variable de type char permet de mémoriser un seul caractère. Imaginons maintenant que nous souhaitions enregistrer le prénom d’un étudiant. Nous pourrions écrire : char prenom = ’M’; Cette variable contient bien une lettre, mais seulement la première du prénom. Comment représenter le mot complet « Michel » ? Une première idée pourrait être d’utiliser plusieurs variables : Cette solution fonctionne, mais elle présente rapidement de nombreux inconvénients. • Chaque lettre doit être mémorisée dans une variable différente. • Le nombre de variables dépend de la longueur du mot. • Il devient difficile d’afficher, de copier ou de comparer un texte. • Cette méthode est pratiquement inutilisable pour représenter une phrase complète. Nous avons besoin d’une structure plus adaptée au stockage d’un texte. Or, nous connaissons déjà un outil capable de regrouper plusieurs valeurs de même type sous un seul Les chaînes de caractères Une chaîne de caractères n’est rien d’autre qu’un tableau de caractères. Nous allons découvrir dans ce chapitre comment ces tableaux sont organisés en mémoire et comment les manipuler efficacement. À votre avis, comment le langage C++ sait-il où se termine le mot « Michel » lorsqu’il est stocké en mémoire ? Nous répondrons à cette question dans la section suivante. Une première chaîne de caractères Comme une chaîne de caractères est un tableau de char, sa déclaration ressemble beaucoup à celle d’un tableau classique. Le programme suivant crée une chaîne contenant le prénom « Michel » : char prenom[] = « Michel »; À première vue, cette écriture paraît très simple. Pourtant, plusieurs questions se posent immédiatement. • Combien de cases mémoire ont été réservées ? • Chaque lettre occupe-t-elle une case ? • Pourquoi les guillemets (« ) sont-ils utilisés alors qu’un caractère s’écrit entre apostrophes (’) ? • Comment l’ordinateur sait-il où se termine le mot ? Pour répondre à ces questions, nous allons examiner ce qui est réellement stocké en mémoire. Combien de cases mémoire pensez-vous que le tableau prenom contient ? char prenom[] = « Michel »; • Impossible de le savoir Ne cherchez pas la réponse dans la documentation. Essayez simplement de réfléchir avant de poursuivre votre lecture. Regardons la mémoire Lorsqu’un programme est exécuté, les variables sont stockées dans la mémoire de l’ordinateur. Considérons la déclaration suivante : char prenom[] = « Michel »; À première vue, on pourrait penser que la variable prenom contient simplement le mot « Michel ». En réalité, ce qui est stocké en mémoire est beaucoup plus précis. En exécutant les instructions suivantes
AccueilLes chaînes de caractères char prenom[] = « Michel »; std::cout << sizeof(prenom) << std::endl; std::cout << strlen(prenom) << std::endl; • On constate que sizeof(prenom) et strlen(prenom) ne renvoient pas la même valeur. Nous pouvons représenter le contenu de la mémoire de la manière suivante : Les six premières cases ne posent aucun problème. Elles contiennent les six lettres du prénom. Mais une question reste sans réponse : Que contient la dernière case ? Cette dernière case n’est pas vide. Elle contient un caractère très particulier appelé caractère nul, noté : La mémoire contient donc en réalité : Le caractère ’\0’ marque la fin de la chaîne de caractères. Il ne s’agit pas de la valeur ’0’ (le chiffre zéro), mais d’un caractère spécial dont le code ASCII vaut Toutes les fonctions manipulant des chaînes de caractères recherchent ce caractère pour savoir où le Sans lui, l’ordinateur serait incapable de déterminer où s’arrête le mot. Comment fait strlen() pour trouver 6 ? strlen() est une fonction prédéfinie qui fonctionne comme la fonction longueur() du petit programme suivant : int longueur(const char chaine[]) while (chaine[i] != ’\0’) // fonction equivalente a strlen()
AccueilLes chaînes de caractères int main(int argc, const char * argv[]) { char prenom[] = « Michel »; std::cout << sizeof(prenom) << std::endl; std::cout << longueur(prenom) << std::endl; Le caractère ’\0’ sert de sentinelle. Lorsqu’un programme le rencontre, il sait que le texte est terminé et qu’il ne doit pas continuer sa lecture. • Une chaîne de caractères en C++ n’est pas un nouveau type de données. C’est simplement un tableau de char dont le dernier élément est toujours le caractère spécial ’\0’. • C’est ce caractère qui indique la fin de la chaîne. char prenom[20] = « Michel »; Que valent les grandeurs std::cout << sizeof(prenom) << std::endl; std::cout << longueur(prenom) << std::endl; Les chaînes littérales Depuis le début de ce chapitre, nous utilisons des écritures comme : char prenom[] = « Michel »; char ville[] = « Geneve »; Les mots « Michel », « Genève » et « Suisse » sont appelés des chaînes littérales. Une chaîne littérale est une suite de caractères écrite directement dans le programme entre guillemets (« ). Lors de la compilation, le compilateur crée automatiquement le tableau de caractères correspondant et ajoute le caractère spécial ’\0’ à la fin de la chaîne. Ainsi, les deux déclarations suivantes sont équivalentes : char prenom[] = « Michel »;
Accueil’M’,’i’,’c’,’h’,’e’,’l’,’\0’ Les chaînes de caractères La première écriture est simplement beaucoup plus pratique. • Les guillemets ( » « ) permettent d’écrire directement une chaîne de caractères. • Le compilateur ajoute automatiquement le caractère ’\0’ à la fin de la chaîne. ’M’,’i’,’c’,’h’,’e’,’l’ définit un tableau de caractères, mais pas une chaîne de caractères. Un tableau de char n’est pas nécessairement une chaîne de caractères. Pour qu’un tableau de caractères soit une véritable chaîne de caractères, son dernier élément doit être le caractère spécial ’\0’. La fonction strlen(prenom) provoque un comportement indéfini du programme dans ce cas. Lien avec les systèmes embarqués Cette convention est utilisée dans de très nombreuses bibliothèques C/C++, notamment celles destinées aux systèmes embarqués comme les ESP32. Parcourir une chaîne de caractères Comme une chaîne de caractères est un tableau de char, nous pouvons accéder à chacun de ses éléments à l’aide de son indice. Le programme suivant affiche les caractères de la chaîne un par un. char prenom[] = « Michel »; for(int i = 0; prenom[i] != ’\0’; i++) std::cout << prenom[i] << std::endl; Contrairement au chapitre précédent, la boucle ne s’arrête plus lorsque l’indice atteint la taille du tableau. Elle s’arrête lorsqu’elle rencontre le caractère spécial ’\0’.
AccueilLes chaînes de caractères Fig. 9.1 – Schéma du fonctionnement Si l’on déclare la fonction void afficher(const char chaine[]) la fonction ne connaît pas la taille du tableau reçu. Pour savoir où se termine le texte, elle doit parcourir les caractères jusqu’à rencontrer la sentinelle de fin de chaîne ’\0’. Parcours d’une chaîne avec un pointeur Reprenons la déclaration de la chaine de caractères suivante : char prenom[] = « Michel »; Nous allons parcourir cette chaine avec un pointeur. char prenom[] = »Michel »; char* p = &prenom[0]; while(*p != ’\0’) std::cout << std::endl; La sortie de ce programme est
AccueilLes chaînes de caractères On voit que le pointeur reçoit l’adresse du premier caractère. Ensuite le pointeur est incrémenté jusqu’à ce que la valeur du caractère lu soit le caractère ’\0’. Cette première écriture est parfaitement correcte et correspond directement à ce que nous avons appris sur les pointeurs : char* p = &prenom[0]; Cependant, dans la plupart des expressions, le nom d’un tableau est automatiquement converti en l’adresse de son premier élément. On écrit donc généralement : Les deux écritures sont équivalentes. En remplaçant la déclaration du pointeur par l’écriture habituelle, le reste du programme demeure std::endl ne produit aucun affichage à lui seul. Il doit être envoyé à un flux de sortie, comme std::cout, à l’aide de l’opérateur <<.
AccueilLes chaînes de caractères Essayez d’expliquer la sortie de ce programme. char prenom[] = »Michel »; char* p = &prenom[0]; while(*p != ’\0’) << static_cast<const void*>(p)
AccueilLes chaînes de caractères Programme qui retourne VRAI si un mot est un palindrome. bool pallindrome(char pal[]) char* debut = &pal[0]; = &pal[strlen(pal)-1]; char mot[] = »radar »; if (pallindrome(mot)) std::cout << « VRAI\n »; std::cout << « FAUX\n »; Copier une chaîne de caractères Après avoir appris à parcourir une chaîne de caractères, nous pouvons maintenant réaliser une première opération utile : copier son contenu dans une autre chaîne. Le principe est très simple. Nous parcourons la chaîne source caractère par caractère et nous recopions chaque caractère dans une seconde chaîne. Lorsque nous rencontrons le caractère ’\0’, nous savons que la copie est terminée. Il est indispensable de placer ce même caractère à la fin de la chaîne destination afin que la nouvelle chaîne soit elle aussi correctement terminée. void copier(char destination[], char source[]) while(source[i] != ’\0’) destination[i] = source[i]; destination[i] = ’\0’;
AccueilLes chaînes de caractères char source[] = « Bonjour »; char destination[20] = « »; copier(destination, source); std::cout << destination << std::endl; La boucle copie un à un tous les caractères de la chaîne source. Lorsque le caractère ’\0’ est rencontré, la boucle s’arrête. Il est alors indispensable de placer ce même caractère à la fin de la chaîne destination, faute de quoi celle-ci ne serait plus une chaîne de caractères valide. • Une fonction de copie ne doit pas seulement recopier les caractères visibles. Elle doit également ajouter le caractère ’\0’ qui marque la fin de la chaîne. Comparer deux chaînes de caractères Il est souvent nécessaire de savoir si deux chaînes de caractères contiennent exactement le même Pour cela, nous comparons leurs caractères un à un. Dès que deux caractères sont différents, nous pouvons conclure que les deux chaînes ne sont pas identiques. Si, au contraire, nous atteignons simultanément le caractère ’\0’ des deux chaînes, alors elles sont identiques. bool comparer(char chaine1[], char chaine2[]) while(chaine1[i] != ’\0’ && chaine2[i] != ’\0’) if(chaine1[i] != chaine2[i]) char mot1[] = « Michel »; char mot2[] = « Michel »; return chaine1[i] == chaine2[i];
AccueilLes chaînes de caractères if(comparer(mot1, mot2)) std::cout << « Les deux chaines sont identiques. » << std::endl; std::cout << « Les deux chaines sont differentes. » << std::endl; Les deux chaines sont identiques. La boucle compare les deux chaînes caractère par caractère. Si une différence est rencontrée, la fonction retourne immédiatement la valeur false. Lorsque la boucle se termine, deux situations sont possibles : • les deux chaînes sont arrivées simultanément au caractère ’\0’ : elles sont identiques ; • une seule chaîne est arrivée au caractère ’\0’ : l’une est plus courte que l’autre. return chaine1[i] == chaine2[i]; retourne donc true uniquement si les deux chaînes se terminent exactement au même endroit. • Deux chaînes de caractères sont identiques si elles contiennent les mêmes caractères dans le même ordre et si leur premier caractère ’\0’ apparaît au même endroit. • Deux tableaux de caractères peuvent donc être différents tout en contenant la même chaîne de caractères, car tout ce qui se trouve après le premier ’\0’ ne fait plus partie Concaténer deux chaînes de caractères Concaténer deux chaînes consiste à ajouter la seconde à la suite de la première. Pour cela, nous devons tout d’abord rechercher la fin de la première chaîne, c’est-à-dire son premier caractère ’\0’. Nous recopions ensuite les caractères de la seconde chaîne à partir de cet emplacement avant d’ajouter un nouveau caractère ’\0’ à la fin. void concatener(char destination[], char source[]) while(destination[i] != ’\0’) while(source[j] != ’\0’) destination[i] = source[j];
AccueilLes chaînes de caractères destination[i] = ’\0’; char texte[30] = « Bonjour »; char ajout[] = » Michel »; concatener(texte, ajout); std::cout << texte << std::endl; La première boucle recherche le premier caractère ’\0’ de la chaîne destination. La seconde boucle recopie ensuite les caractères de la chaîne source à partir de cet emplacement. Comme pour la fonction copier(), il est indispensable d’ajouter un nouveau caractère ’\0’ à la fin de la chaîne obtenue. • Concaténer deux chaînes consiste simplement à copier la seconde chaîne à partir de la fin de la première. • La chaîne destination doit être suffisamment grande pour contenir le résultat de la concaténation. Dans le cas contraire, le programme écrira au-delà des limites du tableau, ce qui conduit à un comportement indéfini. Dans les exemples précédents, nous avons supposé que la chaîne de destination était suffisamment grande. Nous verrons par la suite comment garantir qu’elle dispose toujours de la mémoire nécessaire. Les chaînes de caractères constituent un cas particulier de tableau de char. Leur principale caractéristique est la présence d’un premier caractère ’\0’ qui marque la fin de la chaîne. Dans ce chapitre, nous avons appris à parcourir une chaîne de caractères, à la copier, à la comparer et à la concaténer. Ces opérations, bien que simples, sont à la base de très nombreux programmes. Dans les chapitres suivants, nous verrons comment rendre ces fonctions plus générales et plus sûres. Nous découvrirons notamment comment transmettre efficacement des chaînes à des fonctions et comment lever la limitation imposée par la taille fixe des tableaux de caractères.
Accueilcpp-Chap9- Les chaînes de caractères
AccueilLes chaînes de caractères
AccueilTermes à ajouter au glossaire
Chapitre 10 - Les structures
Chapitre 10 – Les structures
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
AccueilCréer de nouveaux types de données Jusqu’à présent, nous avons manipulé des variables indépendantes les unes des autres. Supposons maintenant que nous souhaitions représenter une date. Ces trois variables représentent-elles trois informations différentes ? En réalité, non. Elles décrivent toutes les trois une seule et même information : une date. Il serait donc pratique de pouvoir les regrouper au sein d’une seule variable. Le langage C++ permet de créer de nouveaux types de données grâce aux structures. Déclarer une structure Une structure est un ensemble de variables regroupées sous un même nom. La structure suivante permet de représenter une date. Le mot-clé struct indique que nous définissons une nouvelle structure. Dans cet exemple, la structure Date contient trois membres : À partir de maintenant, Date devient un nouveau type de données, au même titre que int, double Créer une variable de type Date Une fois la structure définie, nous pouvons créer des variables de ce nouveau type exactement comme nous le faisons avec les types fondamentaux. Créer de nouveaux types de données aujourdHui.jour = 31; aujourdHui.mois = 7; aujourdHui.annee = 2026; std::cout << aujourdHui.jour << « / » << aujourdHui.mois << « / » << aujourdHui.annee << std::endl; La variable aujourdHui est une variable de type Date. Elle contient trois membres : jour, mois et annee. Pour accéder à un membre d’une structure, nous utilisons l’opérateur point (.). désigne le membre jour de la structure aujourdHui. désigne le membre mois, tandis que désigne le membre annee. L’opérateur . permet donc d’accéder individuellement à chacun des membres d’une structure. • Une structure est un nouveau type de données. Pour accéder à l’un de ses membres, on utilise l’opérateur point (.). • Par exemple, l’expression désigne le membre jour de la variable aujourdHui.
AccueilCréer de nouveaux types de données L’opérateur . sera très souvent utilisé dans la suite de ce livre. Il permet d’accéder aux membres d’une structure, mais également aux membres d’une classe que nous découvrirons plus tard. Initialiser une structure Dans l’exemple précédent, nous avons affecté une valeur à chacun des membres de la structure. aujourdHui.jour = 31; aujourdHui.mois = 7; aujourdHui.annee = 2026; Lorsque les valeurs sont connues dès la création de la variable, il est possible d’initialiser directement tous les membres de la structure. Date aujourdHui = {31, 7, 2026}; std::cout << aujourdHui.jour << « / » << aujourdHui.mois << « / » << aujourdHui.annee << std::endl; Les valeurs placées entre accolades sont affectées aux membres de la structure dans l’ordre de leur Date aujourdHui = {31, 7, 2026}; aujourdHui.jour = 31; aujourdHui.mois = 7; aujourdHui.annee = 2026; Cette écriture est plus concise lorsque toutes les valeurs sont connues au moment de la création de
AccueilCréer de nouveaux types de données • Une structure peut être initialisée en une seule instruction grâce aux accolades. • Les valeurs sont affectées aux membres dans l’ordre de leur déclaration. Le nom d’une structure doit représenter clairement ce qu’elle modélise. Par exemple, les noms décrivent immédiatement le rôle de la structure. À l’inverse, des noms tels que n’apportent aucune information sur leur contenu et rendent le programme plus difficile à Il n’existe pas de règle imposée par le langage C++ concernant le nom des variables, des fonctions ou des types. Cependant, il est recommandé d’adopter une convention de nommage Dans cet ouvrage, nous utiliserons les conventions suivantes : • Les noms des structures et des classes commencent par une majuscule. • Les noms des variables et des fonctions commencent par une minuscule. • Les constantes sont écrites entièrement en majuscules. const double PI = 3.141592653589793; Ces conventions facilitent la lecture des programmes en permettant de distinguer immédiatement les différents éléments du code. Copie et affectation d’une structure Comme les types fondamentaux, une structure peut être copiée à l’aide de l’opérateur d’affectation.
AccueilCréer de nouveaux types de données Date d1 = {31, 7, 2026}; std::cout << d2.jour << « / » << d2.annee << std::endl; copie automatiquement tous les membres de la structure d1 dans la structure d2. Elle est donc équivalente à d2.annee = d1.annee; Cette copie est réalisée automatiquement par le compilateur. Copie et affectation d’une structure Comme les types fondamentaux, une structure peut être copiée à l’aide de l’opérateur d’affectation. Date d1 = {31, 7, 2026}; std::cout << d2.jour << « / » << d2.annee << std::endl;
AccueilCréer de nouveaux types de données copie automatiquement tous les membres de la structure d1 dans la structure d2. Elle est donc équivalente à d2.annee = d1.annee; Cette copie est réalisée automatiquement par le compilateur. Que va-t-il se passer ? Date d1 = {31, 7, 2026}; std::cout << d2.annee << std::endl; • Une structure est un véritable type de données. • Une simple affectation copie automatiquement tous les membres de la structure. • Il est important de ne pas confondre une copie avec un alias. Deux variables de type Date obtenues par une affectation sont indépendantes. À l’inverse, une référence est un alias : elle désigne exactement la même variable. Passer une structure à une fonction Une structure étant un véritable type de données, elle peut être transmise à une fonction exactement comme une variable de type int, double ou char. L’exemple suivant affiche le contenu d’une structure représentant une date.
AccueilCréer de nouveaux types de données void afficher(Date d) std::cout << d.jour << « / » << d.annee << std::endl; Date aujourdHui = {31, 7, 2026}; afficher(aujourdHui); La fonction reçoit une copie de la structure. La variable d est donc indépendante de la variable aujourdHui. Que va-t-il se passer ? void modifier(Date d) Date aujourdHui = {31, 7, 2026}; modifier(aujourdHui); std::cout << aujourdHui.annee << std::endl; Retourner une structure Une fonction peut également retourner une structure. L’exemple suivant crée une date et la retourne au programme appelant. Date d = {31, 7, 2026};
AccueilCréer de nouveaux types de données aujourdHui = creerDate(); std::cout << aujourdHui.jour << « / » << aujourdHui.mois << « / » << aujourdHui.annee << std::endl; La fonction creerDate() retourne une structure de type Date. Cette structure est ensuite copiée dans la variable aujourdHui. Quelle est la sortie du code suivant : Date d = {31,7,2026}; Date d1 = creerDate(); Date d2 = creerDate(); std::cout << d2.annee << std::endl; Dans la suite de cet ouvrage, nous utiliserons principalement la syntaxe using, tout en sachant que l’on rencontre encore très fréquemment typedef dans les bibliothèques et les programmes plus Il est parfois utile de donner un autre nom à un type existant. Pour cela, le langage C++ met à disposition le mot-clé typedef.
AccueilCréer de nouveaux types de données typedef Date DateCalendrier; On peut alors déclarer une variable de la manière suivante : DateCalendrier aujourdHui; Le mot-clé typedef ne crée pas un nouveau type. Il définit simplement un autre nom (appelé alias) pour un type déjà existant. Depuis C++11, il est recommandé d’utiliser le mot-clé using, qui remplit le même rôle avec une syntaxe plus moderne. using DateCalendrier = Date; Cette écriture est également plus adaptée aux templates que nous étudierons plus loin dans Une énumération (enumeration) permet de définir un nouveau type dont les valeurs possibles sont limitées à une liste prédéfinie. Elle est particulièrement utile lorsque plusieurs valeurs symboliques représentent un même concept. On peut ensuite déclarer une variable de ce type. L’utilisation d’une énumération rend le programme plus lisible et limite les valeurs qu’une variable Depuis C++11, il existe également les enum class, qui offrent une meilleure sécurité de Dans un premier temps, nous utiliserons les énumérations classiques afin de nous concentrer sur les notions fondamentales.
AccueilCréer de nouveaux types de données • Une énumération permet de définir un nouveau type dont les valeurs possibles sont choisies par le programmeur. Les structures permettent de regrouper plusieurs informations décrivant un même objet au sein d’un nouveau type de données. Dans ce chapitre, nous avons vu qu’une structure peut être : • transmise à une fonction ; • retournée par une fonction. Les structures constituent une étape importante dans l’apprentissage du C++. Elles permettent d’organiser les données de manière claire et de manipuler des objets plus riches que les types fondamentaux. Dans les chapitres suivants, nous verrons comment transmettre efficacement ces structures aux fonctions, puis comment les enrichir de fonctionnalités grâce à la programmation orientée objet. • Une structure permet de regrouper plusieurs variables sous un même type. • Une structure est un véritable type de données. • Une structure peut être copiée, transmise à une fonction et retournée par une fonction. • Les structures préparent naturellement l’étude des classes et de la programmation orientée objet.
Accueilcpp-Chap10- Créer de nouveaux types de données
AccueilTermes à ajouter au glossaire
Chapitre 11 - Passage des paramètres à une fonction
Chapitre 11 – Passage des paramètres à une fonction
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
AccueilPassage des paramètres Passage des paramètres à une fonction Jusqu’à présent, nous avons utilisé des fonctions dont les paramètres étaient principalement des variables de type int, double ou char. Nous avons également vu qu’il était possible de transmettre des tableaux, des chaînes de caractères et des structures. Cependant, toutes ces données ne sont pas transmises de la même manière. Le choix de la méthode de transmission d’un paramètre est très important. Il influence le fonctionnement de la fonction, les performances du programme et la possibilité ou non de modifier les variables du programme appelant. Dans ce chapitre, nous allons étudier les quatre principales façons de transmettre un paramètre à • le passage par valeur (passage par copie) ; • le passage par référence ; • le passage par référence constante ; • le passage par pointeur. Afin de mettre en évidence les différences entre ces méthodes, nous utiliserons tout au long de ce chapitre la même structure Date. Seule la manière de transmettre le paramètre changera. Passage par valeur (passage par copie) Lorsqu’un paramètre est transmis par valeur, la fonction reçoit une copie de la variable passée en Les modifications effectuées sur cette copie n’ont donc aucun effet sur la variable du programme Le programme suivant illustre ce principe. Passage des paramètres à une fonction void modifier(Date d) Date aujourdHui = {31, 7, 2026}; modifier(aujourdHui); std::cout << aujourdHui.annee << std::endl; Program ended with exit code: 0 Bien que la fonction modifie le membre annee, la variable aujourdHui n’est pas modifiée. En effet, la fonction ne travaille pas directement sur la variable du programme appelant. Elle reçoit une copie de cette variable.
AccueilPassage des paramètres à une fonction Fig. 11.1 – Passage par valeur ou encore, passage par copie. • Lors d’un passage par valeur, la fonction reçoit une copie du paramètre. • Toute modification effectuée dans la fonction concerne uniquement cette copie et n’a aucun effet sur la variable du programme appelant. Le passage par valeur est particulièrement adapté aux types fondamentaux tels que En effet, ces types occupent très peu de mémoire et leur copie est extrêmement rapide.
AccueilPassage des paramètres à une fonction Il est utilisé lorsque la fonction doit travailler sur une copie indépendante du paramètre afin de ne pas modifier la variable du programme appelant. Passage par référence Lorsqu’un paramètre est transmis par référence, la fonction ne reçoit pas une copie de la variable, mais un alias de celle-ci. Les modifications effectuées dans la fonction agissent donc directement sur la variable du programme Le programme suivant illustre ce principe. void modifier(Date& d) Date aujourdHui = {31, 7, 2026}; modifier(aujourdHui); std::cout << aujourdHui.annee << std::endl; Program ended with exit code: 0 Contrairement au passage par valeur, aucune copie de la structure n’est réalisée. Le paramètre d est un alias de la variable aujourdHui. Les deux noms désignent donc exactement la même structure en mémoire. Toute modification effectuée sur d modifie également aujourdHui.
AccueilPassage des paramètres à une fonction • Lors d’un passage par référence, la fonction reçoit un alias du paramètre. • Aucune copie n’est réalisée. Toute modification effectuée dans la fonction agit directement sur la variable du programme appelant. Le passage par référence est recommandé lorsqu’une fonction doit modifier une variable du Comme aucune copie n’est réalisée, cette méthode est également plus efficace pour les objets void modifier(Date& d) Date d1 = {31, 7, 2026}; std::cout << d1.annee << std::endl; Une référence reste une référence. Qu’elle soit déclarée dans une fonction ou dans le programme principal, elle constitue toujours un alias de la variable à laquelle elle est liée.
AccueilPassage des paramètres à une fonction void modifier(int& w) std::cout << rd << » » << &d << std::endl; » << &rd << std::endl; Dans ce programme, le symbole & apparaît trois fois. A-t-il toujours la même signification ? La réponse est non. Déclare une référence. Déclare un paramètre transmis par référence. Donne l’adresse mémoire de la variable d. Table 11.1 – Les différentes significations du symbole &. Les opérateurs & et ∗ sont deux symboles du C++ dont la signification dépend du contexte dans lequel ils sont utilisés. Avec un peu de pratique, cette distinction devient rapidement Passage par référence constante Dans de nombreux cas, une fonction doit simplement consulter les informations d’une structure sans les modifier. Le passage par référence constante permet d’éviter une copie tout en garantissant que la fonction ne pourra pas modifier la variable du programme appelant.
AccueilPassage des paramètres à une fonction void afficher(const Date& d) std::cout << d.jour << « / » << d.annee << std::endl; Date aujourdHui = {31, 7, 2026}; afficher(aujourdHui); Program ended with exit code: 0 Fig. 11.2 – Le compilateur véri- fie que les règles du langage C++ sont respectées. Toute tentative de modifier une référence constante est détectée lors de la compilation, avant même que le programme ne • Le passage par référence constante permet de consulter un objet sans le copier et sans risquer de le modifier. • Cette méthode est généralement recommandée lorsqu’une fonction doit uniquement lire les informations contenues dans un objet.
AccueilPassage des paramètres à une fonction • Le passage par référence constante est particulièrement adapté aux objets volumineux qui ne doivent pas être modifiés. • Il évite la création d’une copie tout en protégeant la variable du programme appelant contre toute modification accidentelle. • Le mot-clé const permet au programmeur d’exprimer clairement son intention. Le compilateur vérifie ensuite que cette intention est respectée. • Pourquoi utiliser const alors que je sais déjà que je ne vais pas modifier le paramètre ? Parce que le code est souvent relu, modifié et complété plusieurs mois ou plusieurs années après son écriture. En ajoutant const, le programmeur fournit une garantie au compilateur, qui vérifiera automatiquement que cette garantie est toujours respectée. • Le compilateur ne vérifie pas si la valeur de l’objet change. Il vérifie si le programme tente d’effectuer une écriture dans un objet déclaré constant. Dans le doute, si une fonction doit simplement consulter un objet, le passage par référence constante est souvent le meilleur choix. Passage par pointeur Comme une référence, un pointeur permet à une fonction d’agir directement sur la variable du programme appelant. La différence est que la fonction reçoit l’adresse de cette variable et non un alias. Le programme suivant illustre ce principe. void modifier(Date* d) // ou (*d).annee = 2030 Date aujourdHui = {31, 7, 2026}; modifier(&aujourdHui); std::cout << aujourdHui.annee << std::endl;
AccueilPassage des paramètres à une fonction Program ended with exit code: 0 Le paramètre d est un pointeur. Il contient donc l’adresse de la variable aujourdHui. Pour appeler la fonction, il est nécessaire de transmettre cette adresse à l’aide de l’opérateur &. À l’intérieur de la fonction, l’opérateur −> permet d’accéder directement aux membres de la structure Le passage par pointeur est utilisé lorsque la fonction doit manipuler explicitement l’adresse Nous verrons plus loin que cette méthode est également indispensable pour les tableaux, les allocations dynamiques et certaines structures de données comme les listes chaînées et les Passage par référence Passage par pointeur Appel de la fonction Déclaration de la fonction Table 11.2 – Comparaison entre le passage par référence et le passage par pointeur. Tableau récapitulatif Mode de transmission Modifie l’original Passage par référence Passage par pointeur Paramètre de la fonction Table 11.3 – Comparaison des différents modes de transmission des paramètres.
AccueilPassage des paramètres à une fonction Mode de transmission Utilisation recommandée Types fondamentaux ou lorsqu’une copie indépendante est souhaitée. Passage par référence Lorsque la fonction doit modifier l’objet. Lorsque la fonction consulte un objet sans le modifier. C’est souvent le meilleur choix pour les objets volumineux. Passage par pointeur Lorsque la fonction manipule explicitement une adresse (tableaux, allocation dynamique, listes chaînées, arbres, objet éventuellement absent, etc.). Table 11.4 – Choix du mode de transmission des paramètres. Le choix du mode de transmission d’un paramètre fait partie de la conception d’une fonction. Il n’existe pas de mode de transmission meilleur que les autres. Le choix dépend du rôle que doit jouer le paramètre dans la fonction. Le choix du mode de transmission d’un paramètre constitue une décision importante lors de la conception d’une fonction. Selon les besoins, il est possible de transmettre une copie de la variable, un alias de celle-ci ou encore son adresse mémoire. Chaque solution possède ses avantages et répond à un objectif bien précis. Le programmeur ne choisit donc pas un mode de transmission au hasard. Il choisit celui qui exprime le mieux le rôle que devra jouer le paramètre dans la fonction. Les tableaux récapitulatifs présentés à la fin de ce chapitre pourront servir de guide lors de l’écriture de nouvelles fonctions. Une fonction bien conçue commence souvent par un bon choix de ses paramètres.
Accueilcpp-Chap11- Passage des paramètres à une fonction
AccueilTermes à ajouter au glossaire