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