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
| Type | Taille usuelle | Domaine |
|---|---|---|
char | 1 octet | −128 à 127, ou 0 à 255 selon la plateforme |
short | 2 octets | −32 768 à 32 767 |
int | 4 octets | environ ±2,1 milliards |
long | 8 octets sous Linux 64 bits | ±9,2 × 10¹⁸ |
float | 4 octets | IEEE 754 simple précision, 7 chiffres significatifs |
double | 8 octets | IEEE 754 double précision, 15 chiffres |
unsigned int | 4 octets | 0 à 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
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 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 flottante — 3.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 / b — 3.0
- Une erreur de compilation : on ne peut pas affecter un int à un double — erreur
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);| Format | Type attendu |
|---|---|
%d | int |
%u | unsigned int |
%f | double (en printf), float * (en scanf) |
%lf | double en scanf |
%c | char |
%s | chaîne, c'est-à-dire char * (chapitre 6) |
%p | pointeur (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éfini — dé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 295 — toujours positif
- Parce que n - 1 vaut −1 quand n vaut 0, ce qui est le seul cas problématique — cas 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é
- Mesurer — Affichez 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.
- Le débordement — Faites déborder un int non signé puis un int signé. Comparez ce que fait le programme dans les deux cas, avec et sans -O2.
- La comparaison qui échoue — Comparez 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 ?
- La division entière — Calculez 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.
- 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.
- Lire correctement — Réécrivez la lecture avec fgets et strtol, en signalant toute saisie invalide. Testez avec les mêmes entrées.
- Au fil rouge — Ajoutez à 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
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ÈRELa 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.
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.
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 infinieDè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.
(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("%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.