cursus.

Cours 1 · Prendre en main le langageLeçon 2 sur 2

Types, variables, opérateurs

7 h de lecture10 sections Version PDF

À la fin de cette leçon, vous saurez

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
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 · vérifiez votre compréhension Sans réponse

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);
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 · vérifiez votre compréhension Sans réponse

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.

Exercice · JavaScript · à vous de jouer

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

En attente
// ── 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)");

Console de sortie
Le résultat s'affiche dans la console

En travaux pratiques

Travaux pratiques 2 · sur machine

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.

2 h
Avant de commencer
  • Le TP 1 : compilation avec avertissements
  • Le TP 2 d'Architecture, sur le complément à deux
  1. 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. 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. 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. 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. 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. 6. Lire correctement

    Réécrivez la lecture avec fgets et strtol, en signalant toute saisie invalide. Testez avec les mêmes entrées.

  7. 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.

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

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 · 1 / 5Toucher pour retourner
Fin de la leçon

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.