Types, variables, opérateursDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Programmation en C · C1 Prendre en main le langage · Chapitre 2 · 7 h

Types, variables, opérateurs

Types de base et taille mémoire, déclaration et initialisation, priorités des opérateurs, conversions implicites et explicites, printf et scanf et leurs pièges.

En Python, 7 / 2 vaut 3,5. En C, 7 / 2 vaut 3, sans le moindre avertissement. Le résultat n'est pas arrondi, il est tronqué, parce que les deux opérandes sont des entiers et qu'en C le type des opérandes décide de l'opération, pas le résultat attendu.

C'est le fil de ce chapitre. En C, un type n'est pas une catégorie abstraite : c'est un nombre d'octets et une façon de les interpréter. Le chapitre 2 d'architecture a décrit ces interprétations — complément à deux, IEEE 754, ASCII — et l'on va maintenant les manipuler directement.

Un type, c'est une taille et une lecture

TypeTaille usuelleDomaine
char1 octet−128 à 127, ou 0 à 255 selon la plateforme
short2 octets−32 768 à 32 767
int4 octetsenviron ±2,1 milliards
long8 octets sous Linux 64 bits±9,2 × 10¹⁸
float4 octetsIEEE 754 simple précision, 7 chiffres significatifs
double8 octetsIEEE 754 double précision, 15 chiffres
unsigned int4 octets0 à 4 294 967 295

Graphique

Taille des types en octets, sous Linux 64 bits

  • char : 11
  • short : 22
  • int : 44
  • float : 44
  • long : 88
  • double : 88
  • pointeur : 88
Ces valeurs sont USUELLES, pas garanties : la norme n'impose que des minimums et l'ordre char ≤ short ≤ int ≤ long. Un int fait 2 octets sur certains microcontrôleurs, et un pointeur 4 octets sur une machine 32 bits. C'est pourquoi on écrit sizeof(int) plutôt que 4, et pourquoi stdint.h existe : int32_t garantit exactement 32 bits, partout.

Le mot-clé sizeof rend la taille en octets d'un type ou d'une variable, et il s'évalue à la compilation. C'est le seul moyen correct d'écrire du code portable : sizeof(int) plutôt que 4, sizeof(tableau) / sizeof(tableau[0]) pour le nombre d'éléments — une formule qu'on retrouvera au chapitre 5, avec ses limites.

Deux pièges du tableau méritent d'être signalés tout de suite. Le type char est signé ou non selon la plateforme : c'est un des rares endroits où la norme laisse le choix, et un code qui suppose l'un ou l'autre est faux ailleurs. Et l'entier littéral 2147483648 ne tient pas dans un int : le débordement du chapitre 2 d'architecture n'est pas une curiosité, il arrive.

Déclarer, initialiser

int compteur = 0;        /* déclaration AVEC initialisation */int total;               /* déclaration seule */double moyenne = 12.5;char lettre = 'A';       /* apostrophes : un caractère, donc un entier */

Une règle vaut d'être posée en majuscules : une variable non initialisée ne vaut pas zéro. Elle contient ce que la pile contenait à cet endroit — le résidu d'un appel précédent. Le programme fonctionnera peut-être en test et échouera en production, ou l'inverse, selon ce qui traînait. C'est un comportement indéfini, et c'est la première ligne que -Wall signale.

La règle pratique est donc d'initialiser à la déclaration, systématiquement.

Opérateurs et priorités

Les opérateurs arithmétiques n'ont rien de surprenant, sauf deux.

La division entière tronque, comme on l'a vu. 7 / 2 vaut 3 ; pour obtenir 3,5 il faut qu'au moins un opérande soit flottant : 7.0 / 2. Et la troncature va vers zéro, donc -7 / 2 vaut −3, pas −4.

Le modulo % ne s'applique qu'aux entiers, et son signe suit celui du dividende : -7 % 2 vaut −1. Pour tester la parité d'un nombre possiblement négatif, on écrit donc n % 2 != 0 plutôt que n % 2 == 1.

Les opérateurs d'incrémentation ont deux formes qu'il ne faut pas mélanger : i++ rend l'ancienne valeur puis incrémente, ++i incrémente puis rend la nouvelle. Utilisés seuls, ils sont équivalents ; dans une expression, ils ne le sont pas.

Viennent enfin les opérateurs bit à bit&, |, ^, ~, <<, >> — qui sont exactement les portes du chapitre 4 d'architecture, et les décalages du chapitre 1. Ils ne doivent pas être confondus avec les opérateurs logiques && et ||, qui travaillent sur des conditions et ont la propriété d'être paresseux : dans a && b, b n'est pas évalué si a est faux. C'est cette paresse qui rend possible l'idiome if (p != NULL && p->valeur > 0) du chapitre 7.

Le piège le plus classique du C tient à un caractère : = est l'affectation, == est la comparaison. Écrire if (x = 5) est légal — cela affecte 5 à x puis teste 5, donc c'est toujours vrai — et le compilateur ne le signale qu'avec -Wall.

Les conversions

C'est le point qui produit le plus de bogues silencieux du chapitre.

Les conversions implicites ont lieu sans que rien ne soit écrit. Dans une expression mêlant des types, le C promeut vers le type le plus « large » : int + double donne un double. Cela paraît anodin, et pourtant :

int a = 7, b = 2;double r = a / b;        /* r vaut 3.0, pas 3.5 */

La division est effectuée avant l'affectation, donc entre deux int : le résultat 3 est ensuite converti en double. La conversion arrive trop tard. Il faut forcer avant : (double)a / b.

Plus dangereuse encore, la conversion signé vers non signé. Dans une comparaison entre un int et un unsigned int, le C convertit le signé en non signé — et 1-1 devient 4 294 967 295, la plus grande valeur possible.

int i = -1;unsigned int n = 1;if (i < n) ...          /* FAUX : -1 devient 4294967295 */

C'est la raison pour laquelle une boucle for (unsigned i = n - 1; i >= 0; i--) ne se termine jamais : une variable non signée est toujours supérieure ou égale à zéro.

La conversion explicite, ou transtypage, s'écrit (type)expression. Elle est nécessaire — comme ci-dessus — mais elle doit rester rare : un transtypage ajouté pour faire taire un avertissement masque généralement un vrai problème.

Quiz · 1 question

Que vaut r après int a = 7, b = 2; double r = a / b; et comment obtenir 3,5 ?

  • 3.5 : l'affectation à un double provoque une division flottante3.5
  • 3.0 : la division est faite entre deux int, donc tronquée à 3, et c'est ce 3 qui est converti en double. Il faut forcer AVANT : (double)a / b3.0
  • Une erreur de compilation : on ne peut pas affecter un int à un doubleerreur

Réponse : L'ordre des opérations est ce qui trompe. La division a / b est évaluée d'abord, avec deux opérandes int : le C fait donc une division ENTIÈRE, qui tronque à 3. C'est ce résultat, déjà perdu, qui est ensuite converti en double lors de l'affectation. La conversion arrive trop tard — elle porte sur le résultat, pas sur les opérandes. Pour obtenir 3,5 il faut qu'au moins un opérande soit flottant AVANT la division : (double)a / b, ou a / (double)b, ou même a / 2.0. Le compilateur ne signale rien, puisque le code est parfaitement légal : c'est le prototype du bogue silencieux, et la raison pour laquelle on relit toujours les expressions mêlant entiers et flottants.

Entrées/sorties formatées

printf et scanf méritent une section, parce qu'ils sont la source d'erreurs la plus fréquente du premier semestre — et qu'ils reposent sur un mécanisme fragile.

printf("%d ans, %.2f euros, %c, %s\n", age, prix, initiale, nom);scanf("%d", &age);
FormatType attendu
%dint
%uunsigned int
%fdouble (en printf), float * (en scanf)
%lfdouble en scanf
%cchar
%schaîne, c'est-à-dire char * (chapitre 6)
%ppointeur (chapitre 7)

La fragilité tient à ceci : printf ne vérifie rien à l'exécution. Elle reçoit une chaîne de format et une liste d'arguments sans type, et elle croit le format. Passer un double là où %d est écrit produit un nombre absurde, pas une erreur. Heureusement, gcc -Wall sait comparer le format aux arguments et signale l'écart — encore une raison de l'activer.

Trois pièges de scanf, et ils tombent tous au premier TP.

L'esperluette est obligatoire : scanf("%d", &age). La fonction doit modifier votre variable, donc elle a besoin de son adresse — c'est le passage par adresse du chapitre 7, et scanf en est la première rencontre. L'oublier écrit à une adresse aléatoire.

scanf("%d") laisse le retour à la ligne dans le tampon, ce qui fait qu'un scanf("%c") suivant lit ce retour au lieu d'attendre une frappe.

scanf rend le nombre de conversions réussies, et on l'ignore toujours. Si l'utilisateur tape des lettres là où un nombre est attendu, la variable n'est pas modifiée — elle garde sa valeur précédente, ou son contenu indéfini si elle n'était pas initialisée. Tester ce retour est le minimum : if (scanf("%d", &n) != 1) ....

Constantes

Trois façons de nommer une valeur fixe, et elles ne sont pas équivalentes.

#define TAILLE 100          /* préprocesseur : substitution de texte */const int taille = 100;     /* vraie variable, typée, non modifiable */enum { TAILLE_MAX = 100 };  /* constante entière, typée, visible au débogueur */

Le #define est traité par le préprocesseur du chapitre 1 : c'est un remplacement de texte, sans type et sans portée. Il reste indispensable pour les tailles de tableaux statiques, que la norme C89 exige constantes à la compilation. Le const est une vraie variable que le compilateur refuse de laisser modifier — mieux typée, visible au débogueur, mais inutilisable comme taille de tableau en C89. L'enum combine les deux avantages pour les entiers.

Quiz · 1 question

Pourquoi la boucle for (unsigned int i = n - 1; i >= 0; i--) ne se termine-t-elle jamais ?

  • Parce que i-- sur un unsigned est interdit et provoque un comportement indéfinidécrémentation interdite
  • Parce qu'un unsigned est toujours supérieur ou égal à zéro : la condition est toujours vraie, et quand i vaut 0 la décrémentation le fait passer à 4 294 967 295toujours positif
  • Parce que n - 1 vaut −1 quand n vaut 0, ce qui est le seul cas problématiquecas n = 0

Réponse : Un type non signé n'a pas de valeurs négatives : la condition i >= 0 est une tautologie, toujours vraie, et gcc -Wall la signale d'ailleurs comme comparaison toujours vraie. Le déroulé est instructif : la boucle descend correctement jusqu'à i = 0, exécute son corps, puis i-- fait déborder le compteur par le bas et i devient 4 294 967 295 — l'arithmétique modulo 2^32 du chapitre 2 d'architecture. La boucle repart alors pour quatre milliards de tours, en indexant très au-delà de tout tableau. Deux remèdes : utiliser un int signé, ou écrire la boucle en montant. Le cas n = 0 est un piège de plus — n − 1 vaut alors 4 294 967 295 dès le départ — mais ce n'est pas la cause principale.

À vous

L'exercice reproduit l'arithmétique du C en JavaScript, qui ne l'a pas : entiers de largeur fixe, troncature, débordement modulo, et comparaison signé contre non signé.

Trois choses à faire tomber. La division entière et son signe. Le débordement d'un int sur 32 bits, en retrouvant les valeurs exactes du chapitre 2 d'architecture. Et la boucle unsigned qui ne se termine pas — vous la ferez dérailler avec une garde, et vous verrez la valeur passer de 0 à 4 294 967 295.

Exercice de code

Reproduisez l'arithmétique entière du C : troncature, débordement modulo, signé contre non signé.

Point de départ

// ── Entiers de largeur fixe ───────────────────────────────────────────────
// JS calcule sur des flottants 64 bits : pour imiter le C, on tronque.
const MASQUE32 = 0xFFFFFFFF;

function versInt32(x) {
  // Complément à deux sur 32 bits : au-delà de 2^31 − 1, on repasse négatif.
  const brut = x & MASQUE32;
  return brut >= 2 ** 31 ? brut - 2 ** 32 : brut;
}

function versUint32(x) {
  return 0;   // ← à écrire : même masque, mais lecture NON signée
}

// ── Division entière du C ─────────────────────────────────────────────────
function divisionC(a, b) {
  return a / b;   // ← à écrire : le C TRONQUE vers zéro, il n'arrondit pas
}

function moduloC(a, b) {
  return a % b;   // en C comme en JS, le signe suit le DIVIDENDE
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Écrivez versUint32 et divisionC.
// 2. Retrouvez le débordement du chapitre 2 d'architecture : que vaut
//    INT_MAX + 1 lu en signé ? Et 255 + 1 sur 8 bits ?
// 3. Faites dérailler la boucle unsigned, avec une garde.

console.log("7 / 2   en C :", divisionC(7, 2), " (attendu 3)");
console.log("-7 / 2  en C :", divisionC(-7, 2), " (attendu -3, pas -4)");
console.log("-7 % 2  en C :", moduloC(-7, 2), " (attendu -1)");

Solution

const MASQUE32 = 0xFFFFFFFF;

function versInt32(x) {
  const brut = x & MASQUE32;
  return brut >= 2 ** 31 ? brut - 2 ** 32 : brut;
}

function versUint32(x) {
  // Mêmes 32 bits, autre lecture. C'est tout le chapitre 2 d'architecture :
  // le motif ne porte pas son interprétation, c'est le TYPE qui décide.
  return (x & MASQUE32) >>> 0;
}

function divisionC(a, b) {
  // Troncature VERS ZÉRO, et non arrondi : Math.trunc, pas Math.floor.
  // Math.floor(-3.5) donne -4, ce qui serait faux.
  return Math.trunc(a / b);
}

function moduloC(a, b) { return a % b; }

console.log("— division entière —");
for (const [a, b] of [[7, 2], [-7, 2], [7, -2], [10, 3]]) {
  console.log("   " + String(a).padStart(3) + " / " + String(b).padStart(2) +
    " = " + String(divisionC(a, b)).padStart(3) +
    "   reste " + String(moduloC(a, b)).padStart(3));
}
console.log("   parité d'un négatif : n % 2 == 1 échoue sur -7 (" + moduloC(-7, 2) +
            "), il faut n % 2 != 0");

console.log("");
console.log("— débordement, comme au chapitre 2 d'architecture —");
const INT_MAX = 2 ** 31 - 1;
console.log("   INT_MAX      = " + INT_MAX);
console.log("   INT_MAX + 1  = " + versInt32(INT_MAX + 1) + "   (repasse au minimum)");
console.log("   lu non signé = " + versUint32(INT_MAX + 1));
console.log("   -1 non signé = " + versUint32(-1) + "   (le fameux 4 294 967 295)");

// Sur 8 bits, l'exemple du cours d'architecture.
const versUint8 = (x) => x & 0xFF;
const versInt8 = (x) => (x & 0xFF) >= 128 ? (x & 0xFF) - 256 : (x & 0xFF);
console.log("   sur 8 bits : 255 + 1 = " + versUint8(256) +
            " | 100 + 50 lu en signé = " + versInt8(150) + " (et non 150)");

console.log("");
console.log("— la comparaison signé / non signé —");
const i = -1, n = 1;
console.log("   en JS  : -1 < 1 vaut " + (i < n));
console.log("   en C   : -1 est converti en non signé -> " + versUint32(i) +
            " < 1 vaut " + (versUint32(i) < n));

console.log("");
console.log("— la boucle unsigned qui ne se termine pas —");
let tours = 0;
let u = versUint32(3 - 1);
while (u >= 0) {                 // toujours vrai sur un non signé
  if (tours++ > 6) { console.log("   ... et ça continue. Garde déclenchée."); break; }
  console.log("   i = " + String(u).padStart(10) + (u > 1000 ? "   << débordement par le bas" : ""));
  u = versUint32(u - 1);
}
// La boucle descend bien 2, 1, 0 — puis 0 − 1 déborde et donne 4 294 967 295.
// Le programme repart pour quatre milliards de tours, en indexant très
// au-delà de tout tableau. Remède : un int signé, ou boucler en montant.

En travaux pratiques

Travaux pratiques 2 · 2 h

Les types mentent, mesurez-les

Vérifier expérimentalement ce que valent les types sur votre machine, et rencontrer les trois conversions implicites qui produisent le plus de bogues silencieux.

Avant de commencer

  • Le TP 1 : compilation avec avertissements
  • Le TP 2 d'Architecture, sur le complément à deux

Énoncé

  1. MesurerAffichez la taille en octets et les bornes de char, short, int, long, size_t et double. Ne les recopiez pas d'un cours : mesurez-les.
  2. Le débordementFaites déborder un int non signé puis un int signé. Comparez ce que fait le programme dans les deux cas, avec et sans -O2.
  3. La comparaison qui échoueComparez un int valant -1 avec un unsigned int valant 1. Prédisez le résultat, puis exécutez. Indice : Quand les deux opérandes n'ont pas le même type, l'un des deux est converti — lequel ?
  4. La division entièreCalculez la moyenne de 7 et 8 en int, puis le pourcentage que 3 représente sur 7. Corrigez les deux, de deux façons différentes.
  5. Lire une entréeÉcrivez un programme qui lit un entier au clavier avec scanf. Tapez des lettres. Puis tapez un nombre suivi de lettres. Expliquez les deux comportements.
  6. Lire correctementRéécrivez la lecture avec fgets et strtol, en signalant toute saisie invalide. Testez avec les mêmes entrées.
  7. Au fil rougeAjoutez à journal une option qui prend un seuil numérique en argument, en validant la conversion. Refusez proprement un argument non numérique.

C'est réussi quand

  • Vos tailles mesurées correspondent à votre machine, pas à une table apprise
  • Vous prédisez correctement le résultat de la comparaison signé/non signé
  • Votre programme refuse « abc » sans planter et sans boucler

Correction

Mesurer, pas supposer
printf("int   : %zu octets, %d..%d\n", sizeof(int), INT_MIN, INT_MAX);
printf("long  : %zu octets\n", sizeof(long));
printf("size_t: %zu octets\n", sizeof(size_t));

Linux 64 bits : int 4, long 8, size_t 8
Windows 64 bits : int 4, long 4, size_t 8   ← long DIFFÈRE

La norme ne garantit que des minimums : int fait au moins 16 bits, long au moins 32. Supposer que long fait 64 bits est un bogue de portabilité classique — il n'apparaît qu'en changeant de plateforme. Quand la taille compte, on utilise int32_t ou int64_t de stdint.h, qui la garantissent.

Les deux débordements
unsigned u = UINT_MAX; u + 1  →  0     DÉFINI (modulo 2^32)
int      i = INT_MAX;  i + 1  →  ???   INDÉFINI

gcc -O0 : -2147483648
gcc -O2 : le compilateur suppose que ça n'arrive pas
        et peut supprimer le test qui suit

« Indéfini » ne veut pas dire « imprévisible mais raisonnable » : cela veut dire que le compilateur a le droit de tout faire, y compris supprimer du code qui n'aurait de sens que si le débordement arrivait. Un test écrit APRÈS l'opération peut donc disparaître. La seule bonne pratique est de tester avant : si (a > INT_MAX - b) alors refuser.

La conversion qui retourne le test
int i = -1;
unsigned u = 1;
if (i < u) printf("attendu\n"); else printf("SURPRISE\n");

→ SURPRISE

/* i est converti en unsigned : -1 devient 4294967295 */

le cas réel :
for (int i = 0; i < strlen(s) - 1; i++)   /* si s est vide : */
 strlen("") - 1 = (size_t)-1 = énorme → boucle presque infinie

Dès qu'un signé et un non signé se rencontrent, le signé est converti — jamais l'inverse. Comme strlen, sizeof et la plupart des tailles renvoient un size_t non signé, le piège est omniprésent. La parade : ne jamais soustraire sur un type non signé sans avoir vérifié l'ordre, et laisser -Wextra signaler ces comparaisons.

La division entière, et ses deux corrections
(7 + 8) / 2      →  7    (et non 7,5)
3 / 7 * 100      →  0    (3/7 vaut 0, puis 0 × 100)

corrections :
100.0 * 3 / 7    →  42.857   /* un opérande flottant d'abord */
100 * 3 / 7      →  42       /* multiplier AVANT de diviser */

La division entière tronque toujours vers zéro, sans avertissement d'aucune sorte. La seconde correction est souvent la meilleure : elle reste en entier, donc exacte, et elle évite les flottants du TP 2 d'Architecture. L'ordre des opérations n'est pas un détail esthétique — il décide du résultat.

scanf, et pourquoi le remplacer
scanf("%d", &n);

entrée « abc » : scanf renvoie 0, n est INCHANGÉ (souvent indéterminé),
               et « abc » RESTE dans le tampon
               → une boucle qui relit tourne indéfiniment

entrée « 12abc » : n vaut 12, « abc » reste dans le tampon

/* la lecture correcte */
char ligne[64];
if (!fgets(ligne, sizeof ligne, stdin)) return 1;

char *fin;
errno = 0;
long v = strtol(ligne, &fin, 10);
if (fin == ligne)                    return erreur("pas un nombre");
while (*fin == ' ' || *fin == '\n') fin++;
if (*fin != '\0')                   return erreur("caractères en trop");
if (errno == ERANGE)                 return erreur("hors bornes");

strtol donne les trois informations que scanf ne donne pas : où la conversion s'est arrêtée, si elle a commencé, et s'il y a eu débordement. Le prix est une dizaine de lignes — et c'est le prix d'un programme qui ne boucle pas indéfiniment sur une saisie invalide. Tester la valeur de retour de scanf est le minimum ; ne pas l'utiliser du tout est mieux.

Ce que la suite en fait

Le chapitre 3 emploie ces types dans les structures de contrôle, et traduit en C le pseudocode d'Algorithmique 1. Les conversions y reviendront par la bande : une condition en C est un entier, où zéro est faux et tout le reste vrai, ce qui explique que if (x = 5) compile.

Le bloc IV en fera un usage plus fondamental. L'arithmétique des pointeurs du chapitre 7 dépend directement de sizeof : avancer un int * d'une unité déplace l'adresse de quatre octets, et c'est le type qui décide du pas.

À retenir

Flashcards · 5 cartes

Qu'est-ce qu'un type en C, et pourquoi écrit-on sizeof(int) plutôt que 4 ?
Un nombre d'OCTETS et une façon de les INTERPRÉTER — complément à deux, IEEE 754, ASCII. Les tailles usuelles (char 1, short 2, int 4, long et double 8) ne sont pas garanties : la norme n'impose que des minimums et l'ordre char ≤ short ≤ int ≤ long. Un int fait 2 octets sur certains microcontrôleurs. sizeof s'évalue à la compilation et rend du code portable ; stdint.h va plus loin avec int32_t, garanti partout.
Que vaut 7 / 2 en C, et comment obtenir 3,5 ?
3. La division entre deux int est ENTIÈRE et tronque vers zéro (donc −7 / 2 vaut −3, pas −4). Écrire double r = a / b ne change rien : la division est évaluée avant l'affectation, entre deux int, et la conversion arrive trop tard. Il faut forcer AVANT : (double)a / b. Le compilateur ne signale rien — le code est légal — c'est le prototype du bogue silencieux.
Pourquoi une variable non initialisée est-elle dangereuse en C ?
Parce qu'elle ne vaut PAS zéro : elle contient ce que la pile contenait à cet endroit, le résidu d'un appel précédent. Le programme peut fonctionner en test et échouer en production, ou l'inverse, selon ce qui traînait. C'est un comportement indéfini, signalé par -Wall. Règle : initialiser à la déclaration, systématiquement.
Que se passe-t-il quand on compare un int et un unsigned int ?
Le C convertit le SIGNÉ en NON SIGNÉ : −1 devient 4 294 967 295, la plus grande valeur possible. Donc int i = −1; unsigned n = 1; if (i < n) est FAUX. Même cause pour la boucle for (unsigned i = n−1; i >= 0; i--) qui ne se termine jamais : la condition est une tautologie, et quand i vaut 0 la décrémentation le fait déborder à 4 294 967 295.
Quels sont les trois pièges de scanf ?
1) L'ESPERLUETTE est obligatoire — scanf doit modifier votre variable, donc elle a besoin de son adresse ; l'oublier écrit à une adresse aléatoire. 2) scanf("%d") laisse le RETOUR À LA LIGNE dans le tampon, qu'un %c suivant lira au lieu d'attendre une frappe. 3) scanf rend le NOMBRE DE CONVERSIONS RÉUSSIES, qu'on ignore toujours : si l'utilisateur tape des lettres, la variable garde sa valeur précédente. Tester ce retour est le minimum.