Programmation C · L2 · Section 2/12
Types et structures de contrôle
Progression
#Types et structures de contrôle
En C, la taille des types de base est laissée à l’implémentation : un int fait 16, 32 ou 64 bits selon la cible, et le compilateur applique des conversions implicites qui peuvent changer le résultat d’une comparaison sans produire le moindre diagnostic. Écrire du C portable, c’est d’abord savoir lire ces promotions. Ce chapitre détaille les types scalaires, les conversions arithmétiques usuelles, puis les types à largeur exacte de <stdint.h>. Tableaux et chaînes sont ensuite repris sous l’angle mémoire : décroissance en pointeurs, terminateur \0, fonctions sûres et leurs limites réelles. Enfin les structures de contrôle : switch exhaustif sur des enum, coût réel des boucles, et l’usage discipliné de goto pour libérer proprement des ressources.
#1. Conversions implicites : promotions et conversions usuelles
Trois mécanismes conversionnent silencieusement vos valeurs.
Promotion des petits entiers. Dans toute expression, un char ou un short est d’abord promu en int (ou unsigned int si int ne peut pas représenter toutes ses valeurs). Conséquence la plus célèbre : getchar renvoie un int, jamais un char.
1char c = getchar(); /* piégé : EOF ne tient pas forcément dans un char */2if (c == EOF) { /* ... */ } /* si char est non signé : jamais vrai */3 4int ch = getchar(); /* correct */5if (ch == EOF) { /* fin de flux ou erreur */ }Promotion des flottants en appel variadique. Un float est promu en double dans les arguments de printf : c’est pour cela que %f attend un double, jamais un float.
Conversions arithmétiques usuelles. Quand deux opérandes de types différents se rencontrent, ils sont convertis vers un type commun, décrit par une cascade de règles. Le cas qui surprend tous les débutants :
1#include <limits.h>2unsigned int u = 1;3if (-1 < u) {4 /* jamais exécuté : -1 est converti en unsigned int, soit UINT_MAX */5}-1 < u compare UINT_MAX < 1, donc faux. Ce n’est pas un comportement indéfini : le programme est prévisible, simplement contre-intuitif. La règle de survie : ne jamais mélanger signé et non signé dans une comparaison ; -Wsign-compare (inclus dans -Wextra ou -Wconversion) le signale.
À distinguer soigneusement des débordements : sur un type non signé, l’arithmétique est définie modulo 2^n et « enveloppe » (250u + 10u vaut 4 sur 8 bits). Sur un type signé, un débordement est un comportement indéfini, développé au chapitre suivant.
#2. Tailles garanties avec <stdint.h>
Les types de base n’offrent que des garanties de plages minimales. Depuis C99, <stdint.h> fournit trois familles :
intN_t/uintN_t(int32_t,uint64_t...) : largeur exacte. Optionnels en théorie, présents sur toute plateforme moderne à largeurs en puissances de deux. À privilégier pour les protocoles, fichiers et encodages binaires.int_leastN_tetint_fastN_t: au moins N bits, ou au moins N bits et rapide. Toujours définis.intptr_t/uintptr_t: capables de stocker un pointeur converti. Optionnels (une plateforme peut avoir des pointeurs plus larges que tout entier).
Pour les tailles et indices, size_t (non signé) est le type des sizeof et des longueurs de la bibliothèque. Son caractère non signé crée un piège classique en boucle décroissante :
1for (size_t i = n - 1; i >= 0; --i) { /* ... */ }2/* i est non signé : i >= 0 est toujours vrai, boucle infinie à i == 0 puis enveloppe */La forme usuelle pour parcourir en sens inverse :
1for (size_t i = n; i-- > 0; ) { /* i vaut n-1 ... 0 dans le corps */ }#3. Tableaux et chaînes : des pointeurs déguisés
Un tableau n’est pas recopié quand on le passe à une fonction : il décroît en pointeur vers son premier élément. sizeof le trahit :
1size_t longueur(int t[]) {2 return sizeof t / sizeof t[0]; /* faux : ici t est un int*, le rapport vaut 1 */3}La conséquence de conception : toute fonction recevant un tableau reçoit aussi sa longueur, en paramètre séparé. Le littéral "abc" est un tableau de 4 char (a, b, c, puis le terminateur \0 ajouté par le compilateur). Toutes les fonctions de <string.h> supposent ce terminateur ; oublier de l’écrire est la première cause de lecture hors bornes.
strncpy ne protège pas autant qu’on le croit : si la source est plus longue que la destination, il n’écrit pas de \0. Utilisation correcte :
1char dst[8];2strncpy(dst, src, sizeof dst - 1);3dst[sizeof dst - 1] = '\0'; /* toujours : strncpy ne le garantit pas */Pour construire du texte formaté, snprintf est l’outil sûr, avec vérification de troncature :
1char buf[32];2int n = snprintf(buf, sizeof buf, "%s=%d", cle, valeur);3if (n < 0 || (size_t)n >= sizeof buf) {4 /* tronqué ou erreur : à traiter explicitement */5}Sa cousine gets, quant à elle, a été retirée de la norme (C11) : elle rendait tout dépassement possible.
À propos des VLA (int t[n]; avec n de valeur d’exécution, introduites en C99) : elles restent optionnelles depuis C11. Une VLA trop grande fait déborder la pile sans aucun moyen de détecter l’échec ; réservez-les à des tailles garanties petites, et préférez malloc (chapitre gestion mémoire) sinon.
#4. Structures de contrôle
switch exhaustif sur enum. Associer un enum à un switch sans default donne un vérificateur de complétude gratuit : ajouter un énumérateur plus tard déclenche -Wswitch partout où le traitement n’a pas été mis à jour.
1typedef enum { S_DEBUT, S_MOT, S_ESPACE } EtatScanner;2 3EtatScanner transition(EtatScanner e, char c) {4 switch (e) {5 case S_DEBUT: return (c == ' ') ? S_ESPACE : S_MOT;6 case S_MOT: return (c == ' ') ? S_ESPACE : S_MOT;7 case S_ESPACE: return (c == ' ') ? S_ESPACE : S_MOT;8 }9 return e; /* inatteignable ; rassure seulement -Wreturn-type */10}Boucles. for, while et do/while se traduisent par les mêmes sauts ; le coût observé vient surtout de l’accès mémoire (succession contiguë pour un tableau, sauts imprévisibles pour une liste chaînée), pas du mot-clé choisi. Écrivez la boucle la plus lisible, mesurez avant d’optimiser.
goto de nettoyage. Décrié à juste titre pour les sauts libres, goto reste la construction idiomatique en C pour les sorties multiples d’une fonction qui acquiert des ressources dans un ordre quelconque :
1int traiter(const char *chemin) {2 int rc = -1;3 FILE *in = fopen(chemin, "r");4 if (!in) { perror("fopen"); goto fin; }5 char *tampon = malloc(4096);6 if (!tampon) { perror("malloc"); goto ferme_in; }7 /* ... travail ; rc = 0 en cas de succès ... */8 free(tampon);9ferme_in:10 fclose(in);11fin:12 return rc;13}Chaque étiquette libère les ressources acquises jusque-là, dans l’ordre inverse : une seule sortie, impossible d’en oublier une branche.
#Exercices
#Exercice 1 : scanner de tokens
Écrivez scan.c : le programme reçoit des mots en arguments et compte les symboles (suites de lettres) et les nombres (suites de chiffres).
1cc -std=c17 -Wall -Wextra -Wpedantic scan.c -o scan2./scan "alpha 12 beta 314"Sortie attendue, à la virgule près :
1symboles=2 nombres=2 total=4Vérification : ajoutez "x9" en argument. Si votre sortie devient symboles=3 nombres=3, vous comptez chaque transition au lieu de chaque token.
Piste de correction
Utilisez l’automate ci-dessus : une variable EtatScanner, et n’incrémentez le compteur qu’à l’entrée dans l’état S_MOT (pour un symbole) ou lors de la rencontre du premier chiffre après un état non nombre. Parcours de chaque argument caractère par caractère avec isalpha/isdigit (<ctype.h>), en vous rappelant que ces fonctions attendent un int convertible en unsigned char.
#Exercice 2 : machine à états pilotée par switch
Reprenez l’automate EtatScanner et écrivez transitions.c qui affiche la suite d’états pour l’entrée "a b" :
1./transitions "a b"1DEBUT -> MOT -> MOT -> ESPACE -> MOT(le caractère a provoque la transition DEBUT vers MOT, puis on reste dans MOT en fin de token simulée, etc.)
Vérification : retirez un cas du switch, recompilez : le compilateur doit signaler l’énumérateur manquant. C’est cette réaction que vous cherchez à obtenir.