Cours 1 · Prendre en main le langageLeçon 2 sur 2
Types, variables, opérateurs
7 h de lecture10 sections Version PDF
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 |
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.
Que vaut r après int a = 7, b = 2; double r = a / b; et comment obtenir 3,5 ?
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.
Pourquoi la boucle for (unsigned int i = n - 1; i >= 0; i--) ne se termine-t-elle jamais ?
À 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.
Reproduisez l'arithmétique entière du C : troncature, débordement modulo, signé contre non signé.
// ── 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)");
En travaux pratiques
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.
- Le TP 1 : compilation avec avertissements
- Le TP 2 d'Architecture, sur le complément à deux
- 1. 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.
- 2. 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.
- 3. La comparaison qui échoue
Comparez un int valant -1 avec un unsigned int valant 1. Prédisez le résultat, puis exécutez.
- 4. 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.
- 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 correctement
Réécrivez la lecture avec fgets et strtol, en signalant toute saisie invalide. Testez avec les mêmes entrées.
- 7. 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.
- 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
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
Vous avez parcouru les 10 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.