Langage C

Le premier langage du BTS où l'on doit traduire son programme avant de le lancer — et où le type de chaque variable est une déclaration, pas une devinette. Le chapitre couvre les trois séances : d'abord le langage lui-même, jusqu'aux boucles ; ensuite le découpage en fonctions et les tableaux ; enfin les pointeurs, la mémoire réservée à l'exécution et les structures.

📌

Résumé flash

Le C est compilé (traduit une fois en langage machine, puis exécuté), typé (chaque variable annonce ce qu'elle contient) et sensible à la casse (age et Age sont deux variables différentes). Tout programme démarre par int main(), chaque instruction se termine par ;, chaque bloc tient entre { }. On affiche avec printf et un code de format (%d, %f, %c), on lit avec scanf — et là, le & est obligatoire.

Une fonction a un type de retour, un nom, des paramètres et un corps. Elle se décline en quatre formes, selon qu'elle reçoit des données ou non et qu'elle en renvoie ou non ; void dit « ne renvoie rien ». Le prototype l'annonce avant main pour que le compilateur la connaisse. Une variable déclarée dans une fonction n'existe que là. Et un tableau passé à une fonction perd sa taille : il faut la transmettre à part — c'est la seule vraie difficulté du chapitre.

🔑 #compilation #int-main #stdio.h #printf #scanf #code-de-format #typage-statique #casse #point-virgule #accolades #if-else #switch #while #for #break #continue #fonction #void #return #parametres #arguments #prototype #portee #tableau #indice

01Pourquoi le C, et ce qui change

Le C est né au début des années 1970 chez Bell Labs, écrit par Dennis Ritchie. Un demi-siècle plus tard il est toujours là, et pour une raison simple : c'est le langage dans lequel sont écrits les systèmes — Linux, Windows, les pilotes réseau, les firmwares de switchs et de routeurs. Quand vous configurez un équipement Cisco, vous parlez à un programme écrit en C.

Son influence se lit dans la syntaxe de presque tout ce que vous connaissez déjà : C++, Java, C#, JavaScript, PHP. Les accolades et le point-virgule que vous mettez en JavaScript viennent de là.

🔍 Python vous parle, le C vous fait signer

En Python, vous écrivez age = 20 et l'interpréteur devine : c'est un entier. En C, il faut le déclarer : int age = 20;. C'est plus bavard, mais ce n'est pas de la bureaucratie — le compilateur a besoin de savoir combien d'octets réserver et comment interpréter ce qu'il y a dedans. Les mêmes 32 bits en mémoire ne se lisent pas de la même façon selon qu'on annonce un int ou un float.

Déclarer un type, c'est signer un contrat avec la mémoire. Le compilateur vérifie le contrat ; en Python, personne ne le vérifie avant l'exécution.

Quatre caractéristiques à retenir

CaractéristiqueCe que ça veut dire concrètement
Compilé Le code doit être traduit en langage machine avant d'être lancé. Une erreur de syntaxe est trouvée à la compilation, pas au milieu de l'exécution.
Structuré Le programme s'organise en fonctions, en blocs { } et en structures de contrôle.
Typé Chaque variable possède un type annoncé : int age = 20;, float temperature = 25.5;, char lettre = 'A';.
Sensible à la casse age, Age et AGE sont trois variables différentes. Et int s'écrit en minuscules : Int n'existe pas.
🤔 Pourquoi une faute de frappe dans un nom de variable est-elle plus grave en C qu'en Python ?

Elle est en réalité moins grave — et c'est tout l'intérêt. En Python, écrire tempreature = 25 après avoir créé temperature crée simplement une seconde variable, et le programme tourne en donnant un résultat faux. En C, la variable n'ayant pas été déclarée, le programme ne compile pas : l'erreur est signalée avant même d'être lancée.

La rigueur du C n'est pas une punition : c'est un filet. Ce qu'il refuse de compiler, Python l'aurait exécuté en silence.

02La compilation

Un processeur ne comprend pas le C. Il ne comprend que des instructions binaires. Entre les deux, il y a le compilateur : un programme dont le métier est de traduire.

Code source programme.c Compilateur gcc / Code::Blocks Exécutable programme COMPILER PRODUIRE EXÉCUTER Bonjour ① Vous écrivez ② La machine traduit ③ Le processeur exécute Une erreur de syntaxe est refusée ICI, à l'étape ②.
ÉtapeCe qui se passeCe qu'on obtient
1. Écrire Vous tapez le programme en langage C. un fichier .c — par exemple programme.c
2. Compiler Le compilateur traduit le source en instructions du processeur. un programme exécutable
3. Exécuter Le processeur déroule les instructions. le résultat à l'écran
🧰 Où écrire du C sans rien installer

Deux moyens, notés en cours :

  • En ligne, tout de suite — un compilateur C en ligne (onlinegdb.com) : on colle le code, on clique sur Run. Idéal pour tester un exercice en cinq secondes depuis n'importe quel poste.
  • Sur le poste — Code::Blocks avec MinGW sous Windows : un vrai environnement, avec le compilateur inclus.

Sur macOS et Linux, le compilateur est déjà là : cc programme.c -o programme puis ./programme.

⚠️ « Ça ne marche pas » ne veut pas dire la même chose qu'en Python

En C, deux échecs très différents portent le même nom dans la bouche d'un débutant :

  • Erreur de compilation — le programme n'existe même pas. Un ; oublié, une accolade non fermée, une variable non déclarée. Le compilateur indique le numéro de ligne : lisez-le.
  • Erreur d'exécution — le programme a été produit, il tourne, mais il donne un résultat faux ou plante. Là, le compilateur ne peut plus rien pour vous.

Le premier réflexe : regarder la première erreur de compilation, pas la dernière. Les suivantes sont souvent des conséquences.

03Le squelette d'un programme

Tout programme C, du plus court au plus long, a la même ossature.

C — le programme minimal
#include <stdio.h>

int main()
{
    printf("Bonjour");
    return 0;
}
ÉlémentRôle
#include <stdio.h> Donne accès à la bibliothèque standard d'entrées-sorties, donc à printf et scanf. Sans cette ligne, le compilateur ne connaît pas ces fonctions.
int main() La fonction principale. L'exécution commence toujours ici, quelle que soit la place de main dans le fichier.
{ } Délimitent le bloc d'instructions de main.
return 0; Annonce au système que le programme s'est terminé correctement. Une autre valeur signale une erreur.
💡 Pourquoi return 0 et pas return tout court

Cette valeur n'est pas décorative : c'est le code de retour que reçoit le système. Dans un script shell, ./programme && echo "ok" n'affiche ok que si le programme a renvoyé 0. C'est exactement ce mécanisme qu'utilisent les scripts d'administration pour enchaîner des commandes.

0 = tout s'est bien passé. C'est une convention Unix, pas une lubie du C.

04Les composants élémentaires

Les identificateurs

Un identificateur est un nom que vous choisissez : celui d'une variable, d'une fonction, d'un tableau.

C
int   adresseIP;
int   nombrePaquets;
float debit;
RègleValideInvalide
Lettres a-z A-Z, chiffres 0-9, et _ Tva p250 a_x x#y le # est interdit
Commence par une lettre ou _ _ax 2x commence par un chiffre
Jamais un mot-clé du langage retour int return; return est réservé

Les mots-clés

Ce sont les mots réservés au langage. Ils ont un sens fixé et ne peuvent pas servir de nom.

C — quelques mots-clés
int     float   char    void    const
if      else    switch  case    default
for     while   do      break   continue
return  struct  sizeof  goto

Les séparateurs

SigneRôleExemple
;Termine une instruction int nombre; nombre = 10;
( )Fonctions et expressions printf("Bonjour");
{ }Délimitent un bloc d'instructions if (n > 0) { … }
[ ]Tableauxint notes[5];
,Sépare plusieurs déclarationsint a, b, c;

Les commentaires

C
// Cette variable contient le nombre de paquets
int nombrePaquets;

/*
   Calcul du débit réseau
   à partir du nombre de paquets
*/
⚠️ La casse n'est pas un détail de présentation
C
int nombre;     // ces trois lignes déclarent
int Nombre;     // TROIS variables
int NOMBRE;     // différentes

int  age = 20;  // correct
Int  age = 20;  // ERREUR : « Int » n'existe pas

Les mots réservés s'écrivent en minuscules. C'est la source d'erreur numéro un quand on vient d'un langage plus tolérant.

Sous Linux c'est la même logique : ls est une commande, LS n'existe pas.

🤔 Parmi ces identificateurs, lesquels sont refusés — et pourquoi ?
_debit · 2emeEssai · nb_paquets · char · vitesse-max
  • _debit — valide, il commence par _.
  • 2emeEssai — refusé, il commence par un chiffre.
  • nb_paquets — valide.
  • char — refusé, c'est un mot-clé du langage.
  • vitesse-max — refusé : le tiret n'est pas autorisé, et le compilateur y verrait une soustraction entre vitesse et max.

C'est pour ça qu'on écrit vitesseMax ou vitesse_max — jamais avec un tiret, contrairement au CSS.

05Les types de base

TypeContientExempleTaille observée
intun nombre entier254 octets
floatun nombre réel12.54 octets
doubleun réel plus précis12.567898 octets
charun seul caractère'A'1 octet
voidabsence de valeurfonction sans retour—
🧰 Vérifier soi-même la taille d'un type

L'opérateur sizeof donne la taille en octets. Les valeurs ci-dessus ne sont pas à croire sur parole : elles se mesurent.

C
printf("int    : %zu octets\n", sizeof(int));
printf("char   : %zu octets\n", sizeof(char));
printf("float  : %zu octets\n", sizeof(float));
printf("double : %zu octets\n", sizeof(double));
sortie réelle
int    : 4 octets
char   : 1 octets
float  : 4 octets
double : 8 octets

4 octets = 32 bits. C'est exactement la taille d'une adresse IPv4 — et ce n'est pas un hasard si unsigned int sert souvent à stocker une adresse.

⚠️ 'A' et "A" ne sont pas la même chose

Les apostrophes simples entourent un caractère (char, 1 octet). Les guillemets doubles entourent une chaîne, qui occupe un octet par caractère plus un pour marquer la fin.

C
char lettre = 'A';     // un caractère
char mot[]  = "A";     // une chaîne : 'A' puis le marqueur de fin

06Déclarer, initialiser, figer

Une variable, c'est un emplacement mémoire qui a un nom, un type et une valeur qui peut changer. Toute variable doit être déclarée avant d'être utilisée.

C — déclaration
int nombrePC;          // un entier
int a, b, c;           // trois entiers d'un coup
C — initialisation : déclaration + première valeur
int   a = 80;
float b = 100.5;
char  c = 'O';

Au lieu d'écrire int a; puis a = 80;, on écrit directement int a = 80;. C'est la même chose en une ligne.

⚠️ Une variable déclarée mais non initialisée ne vaut pas zéro

En Python, une variable n'existe qu'une fois affectée. En C, int a; réserve de la place sans nettoyer ce qu'il y avait avant : la variable contient ce qui traînait à cet endroit de la mémoire. Le résultat change d'une exécution à l'autre.

Réflexe à prendre dès maintenant : initialisez. int somme = 0;, jamais int somme; suivi de somme = somme + i;.

Les constantes

Une constante est une valeur qui ne change pas pendant l'exécution.

SorteÉcriture
Constante entière25
Constante flottante3.14
Constante caractère'A'
Constante chaîne"Bonjour"

Pour nommer une constante et empêcher toute modification, on utilise le mot-clé const :

C
const float PI = 3.14159;
const int   PORT_HTTP = 80;

PI = 3;     // ERREUR de compilation : PI est constante
💡 Nommer une constante vaut mieux que répéter un nombre

Écrire PORT_HTTP plutôt que 80 à cinq endroits, c'est trois avantages d'un coup : le lecteur comprend ce que le nombre représente, on ne change la valeur qu'à un seul endroit, et le compilateur refuse qu'on l'écrase par accident.

07Les opérateurs

Un opérateur agit sur une ou plusieurs valeurs appelées opérandes. Dans a + b, a et b sont les opérandes, + l'opérateur. On les classe par nombre d'opérandes.

FamilleOpérandesExemple
Unaire1++a
Binaire2a + b
Ternaire3a > b ? a : b

Les opérateurs unaires

OpérateurSignification
&adresse d'une variable
*indirection : la valeur à une adresse
++incrémentation (ajoute 1)
--décrémentation (retire 1)
sizeoftaille en octets
!NON logique
-moins unaire : change le signe
⚠️ a++ et ++a : la différence qui coûte des points

Les deux ajoutent 1. La différence est ce qu'ils renvoient : a++ renvoie l'ancienne valeur puis incrémente ; ++a incrémente d'abord et renvoie la nouvelle.

C
int a = 5;
int r1 = a++;          // r1 reçoit l'ANCIENNE valeur
printf("a++ : renvoie %d, a vaut ensuite %d\n", r1, a);

int b = 5;
int r2 = ++b;          // r2 reçoit la NOUVELLE valeur
printf("++b : renvoie %d, b vaut ensuite %d\n", r2, b);
sortie réelle
a++ : renvoie 5, a vaut ensuite 6
++b : renvoie 6, b vaut ensuite 6

Tant que a++ est seul sur sa ligne, la différence ne se voit pas : dans les deux cas a augmente de 1. Elle n'apparaît que lorsque la valeur est utilisée — dans un printf, une affectation, une condition.

Tout exercice de trace — « que va afficher ce programme ? » — repose là-dessus : printf("%d - ", i++) affiche i avant de l'augmenter, puis l'augmente.

Les opérateurs binaires

OpérateursSignification
+ - * / % addition, soustraction, multiplication, division, modulo
& | ^ et / ou / ou exclusif bit à bit
>> << décalage de bits à droite, à gauche
< <= > >= comparaisons
== !=égal, différent
&& ||et / ou logiques
=affectation
🔧 Corrigé — & et && ne font pas le même travail

Le tableau du cours range & | ^ sous « et, ou, ou exclusif », juste au-dessus de && || « et logique, ou logique ». Rien ne dit que les premiers travaillent bit par bit et les seconds sur des conditions. La confusion est garantie.

C
printf("%d %d %d %d\n", 5 & 3, 5 | 3, 5 ^ 3, 5 << 1);   // bit à bit
printf("%d %d %d\n",    5 && 3, 5 || 3, !5);              // logique
sortie réelle
1 7 6 10
1 1 0

5 & 3 vaut 1 : en binaire 101 & 011 = 001, bit par bit. 5 && 3 vaut 1 aussi, mais pour une tout autre raison : « 5 est vrai ET 3 est vrai, donc vrai ». Les deux résultats coïncident ici par accident — avec 4 & 3 on obtient 0, alors que 4 && 3 vaut 1.

Règle : dans un if, toujours && et ||. Les versions simples servent aux masques — exactement comme un masque de sous-réseau, qui est un ET bit à bit.

⚠️ La division entière : 6 / 7 vaut 0

Quand les deux opérandes sont des entiers, / fait une division entière : la partie décimale est jetée, pas arrondie.

C
printf("%d\n",  6 / 7);      // 0     ← deux entiers
printf("%f\n",  6.0 / 7);    // 0.857143  ← un réel suffit à changer le calcul
printf("%d %d\n", 7 / 2, 7 % 2);   // 3 1  ← quotient et reste

C'est le piège classique des moyennes : somme / nombre entre entiers renverra 15 et non 15.5. Pour un résultat réel, il faut qu'au moins une des deux valeurs le soit.

L'opérateur ternaire

Le seul opérateur à trois opérandes. Il tient un if…else sur une ligne.

C
condition ? expression_si_vrai : expression_si_faux
C — exemple
int a = 10;
int b = 20;
int max = (a > b) ? a : b;     // a > b est faux  →  max = b = 20
🤔 int a = 15, b = 23; — que vaut (a>b) ? b : a ?

15. a > b s'écrit 15 > 23 : c'est faux. On prend donc la branche de droite, a, qui vaut 15.

Attention au piège de lecture : ici l'ordre est ? b : a et non ? a : b. Cet exercice n'est pas un « calcul de maximum » déguisé — il renvoie le minimum.

Vérifié à l'exécution : il affiche bien 15.

TP 2 — exercice 8, programme ④.

08Afficher : printf

printf vient de stdio.h. Dans sa forme la plus simple, elle affiche un texte tel quel :

C
printf("bonjour !");
sortie
bonjour !
🔧 Corrigé — le cours affiche Bonjour ! pour un code qui écrit bonjour !

Trois diapositives du cours montrent un résultat dont la casse ne correspond pas au code : printf("bonjour !") donnerait selon elles Bonjour ! ; printf("age : %d\n", age) donnerait Age : 20 ; et char lettre = 'a' s'afficherait Lettre : A.

printf recopie la chaîne caractère pour caractère. Elle ne met jamais de majuscule. Ce qui est écrit est ce qui sort.

Le détail compte doublement ici : la diapositive précédente explique justement que le C distingue majuscules et minuscules. Le contre-exemple se trouve trois pages plus loin.

Afficher la valeur d'une variable : les codes de format

Pour afficher une variable, on place dans la chaîne un code de format — un marqueur qui dit à printf de quel type est la valeur à insérer.

C
int age = 20;
printf("%d", age);        // 20
CodeTypeExemple affiché
%dentier (int)25
%fréel (float)15.500000
%lfréel (double)15.567800
%ccaractère (char)A
%schaîne de caractèresBonjour
%eréel en notation exponentielle1.5e+03
%uentier non signé25
%xentier en hexadécimalff

Plusieurs valeurs, et le nombre de décimales

C
int   age    = 20;
float taille = 1.75;
char  lettre = 'a';

printf("age : %d\n", age);
printf("taille : %f\n", taille);
printf("taille : %.2f\n", taille);
printf("lettre : %c\n", lettre);
printf("age : %d ans, moyenne : %.2f/20\n", age, 15.5);
sortie réelle
age : 20
taille : 1.750000
taille : 1.75
lettre : a
age : 20 ans, moyenne : 15.50/20
🧰 %f affiche six décimales, %.2f en affiche deux

Par défaut %f écrit toujours six chiffres après la virgule : 1.75 devient 1.750000. Le point suivi d'un nombre fixe la précision : %.2f pour deux décimales, %.0f pour aucune.

Pour une note sur 20 ou un prix, %.2f est presque toujours ce qu'on veut.

⚠️ Le code de format doit correspondre au type

Rien n'oblige le compilateur à refuser printf("%d", taille) où taille est un float. Le programme compile, tourne, et affiche n'importe quoi : printf lit les octets en les interprétant comme un entier, alors qu'ils codent un réel.

Si un affichage donne un nombre absurde — un très grand négatif, un zéro inattendu — vérifiez d'abord le code de format, avant de soupçonner le calcul.

Les caractères d'échappement

ÉcritureEffet
\npasse à la ligne suivante
\ttabulation
\\une barre oblique inverse
\"un guillemet à l'intérieur de la chaîne
%%un signe pourcent (sinon pris pour un code de format)

09Lire : scanf

scanf récupère au clavier une donnée saisie par l'utilisateur. Elle vient elle aussi de stdio.h.

C — la forme à connaître
scanf("code_format", &variable);
🔍 Pourquoi ce & — l'analogie de la boîte aux lettres

Avec printf("%d", age), vous donnez une valeur à afficher : la fonction lit le contenu, cela suffit.

Avec scanf, c'est l'inverse : la fonction doit écrire dans votre variable. Lui passer le contenu ne servirait à rien — il faut lui dire où ranger ce que l'utilisateur tape. &age se lit « l'adresse de age ».

Pour recevoir un colis, on ne donne pas ce qu'il y a dans la boîte aux lettres : on donne l'adresse de la boîte.

printf lit → pas de &. scanf écrit → & obligatoire.

C — lire un entier puis un réel
int   age;
float note;

printf("Entrez votre age : ");
scanf("%d", &age);

printf("Entrez votre note : ");
scanf("%f", &note);

printf("\nVous avez %d ans et votre note est %.2f/20\n", age, note);
sortie réelle — saisies : 20 puis 15.5
Entrez votre age : 20
Entrez votre note : 15.5

Vous avez 20 ans et votre note est 15.50/20
Type de la variableCode avec scanfExemple
int%dscanf("%d", &n);
float%fscanf("%f", &x);
double%lfscanf("%lf", &x);
char%c scanf(" %c", &c); noter l'espace
⚠️ L'espace avant %c n'est pas une coquille

Quand on tape 20 puis Entrée, scanf("%d", …) consomme 20 mais laisse le retour à la ligne dans le tampon clavier. Un scanf("%c", &c) qui suit récupérerait alors ce retour à la ligne comme s'il s'agissait de la réponse.

L'espace de scanf(" %c", &c) demande d'ignorer d'abord tous les blancs en attente. C'est la raison pour laquelle le cours l'écrit ainsi, et seulement pour %c.

🔧 Corrigé — l'exemple « lire un nombre réel » lit une variable et en affiche une autre

Le cours donne :

C — tel qu'imprimé : ne compile pas
float note;
printf("Entrez votre note : ");
scanf("%f", &note);
printf("votre note est %.2f/20\n", age);   /* ← age ! */

Le programme lit dans note puis affiche age — une variable qui n'est même pas déclarée dans l'extrait. Deux fautes à la fois : le programme ne compile pas, et s'il compilait il afficherait autre chose que ce qu'on vient de saisir.

La bonne ligne est printf("votre note est %.2f/20\n", note);.

💡 Les autres fonctions d'entrée-sortie citées
FonctionRôle
putchar()afficher un seul caractère
puts()afficher une chaîne, avec retour à la ligne automatique
getchar()lire un seul caractère

10Les conditions

Les instructions de contrôle modifient l'ordre d'exécution : prendre une décision, répéter, interrompre. On en distingue trois familles.

FamilleRôleInstructions
Conditionnelleschoisir selon une condition if, if…else, switch
Répétitivesrépéter un bloc while, do…while, for
Branchementrompre le déroulement normal break, continue, goto

if — exécuter si

C
int age = 20;

if (age >= 18) {
    printf("Majeur\n");
}

if…else — choisir entre deux

C
int age;

printf("entrez votre age : ");
scanf("%d", &age);

if (age >= 18) {
    printf("majeur\n");
} else {
    printf("mineur\n");
}
⚠️ = affecte, == compare

C'est l'erreur la plus coûteuse du langage, parce qu'elle compile.

C
if (age == 18) { … }    // compare : « age vaut-il 18 ? »
if (age = 18)  { … }    // AFFECTE 18 à age, puis teste 18 → toujours vrai !

En C, une condition est vraie dès qu'elle est différente de zéro. Donc if (age = 18) met 18 dans age, évalue 18, le trouve non nul, et entre systématiquement dans le bloc — en ayant au passage écrasé la valeur.

Le compilateur ne dit rien par défaut. Relisez chaque if : il doit contenir ==, jamais =.

⚠️ Les accolades ne sont facultatives qu'en apparence

Sans accolades, un if ne gouverne qu'une seule instruction :

C — ce que l'indentation laisse croire
if (n > 0)
    printf("positif\n");
    printf("on continue\n");     // s'affiche TOUJOURS

La seconde ligne n'appartient pas au if : l'indentation ne fait pas la syntaxe en C — contrairement à Python, où elle est la syntaxe.

Mettez les accolades même pour une ligne. Vous en ajouterez une deuxième un jour, et vous oublierez.

switch — choisir parmi plusieurs valeurs

Adapté quand on compare une variable à plusieurs valeurs précises : un menu, un choix d'option, un jour de la semaine, un code d'état.

C — la forme
switch (expression) {
    case valeur1:
        // instructions
        break;
    case valeur2:
        // instructions
        break;
    default:
        // si aucune valeur ne correspond
}
C — un menu complet
#include <stdio.h>

int main(void)
{
    int choix;

    printf("1 - ajouter\n");
    printf("2 - modifier\n");
    printf("3 - supprimer\n");
    printf("votre choix : ");
    scanf("%d", &choix);

    switch (choix) {
        case 1:
            printf("Ajouter\n");
            break;
        case 2:
            printf("Modifier\n");
            break;
        case 3:
            printf("Supprimer\n");
            break;
        default:
            printf("Choix invalide\n");
    }
    return 0;
}

Regrouper plusieurs case

Plusieurs valeurs peuvent mener au même bloc : il suffit d'enchaîner les case sans break entre eux.

C
char reponse;

printf("Voulez-vous continuer ? ");
scanf(" %c", &reponse);

switch (reponse) {
    case 'o':
    case 'O':
        printf("Oui\n");
        break;
    case 'n':
    case 'N':
        printf("Non\n");
        break;
    default:
        printf("Reponse incorrecte\n");
}
⚠️ Sans break, l'exécution continue dans le case suivant

C'est le comportement dit « en cascade ». Il est volontaire quand on regroupe 'o' et 'O' — et c'est un bug quand on l'a simplement oublié : choisir 1 afficherait alors Ajouter, puis Modifier, puis Supprimer.

Un break à la fin de chaque case, sauf quand le regroupement est intentionnel — et dans ce cas, un commentaire pour le dire.

🤔 Pourquoi switch ne peut-il pas remplacer if (note < 10) ?

Parce que switch compare à des valeurs exactes, pas à des intervalles. case 10: teste « égal à 10 », et il n'existe aucun case < 10:.

D'où la règle pratique : un intervalle → if…else if ; une liste de valeurs précises → switch. Une note sur 20 à classer en mentions, ce sont des tranches → if…else if. Un menu dont chaque entrée porte un numéro, ce sont des valeurs précises → switch.

11Les boucles

Trois boucles, et une seule question pour choisir : sait-on combien de fois ?

BoucleCondition testéeQuand l'employer
whileavant chaque tour on ne sait pas combien de fois ; peut ne jamais s'exécuter
do…whileaprès chaque tour il faut au moins un passage — une saisie à valider, un menu à réafficher
foravant chaque tour le nombre de tours est connu, ou l'on compte

while

C
#include <stdio.h>

int main()
{
    int i = 1;
    while (i <= 5) {
        printf("%d\n", i);
        i++;
    }
    return 0;
}
sortie
1
2
3
4
5

Le déroulé, dans l'ordre : on évalue la condition ; si elle est vraie on exécute le bloc ; on réévalue ; et ainsi de suite jusqu'à ce qu'elle devienne fausse.

do…while

Même travail, mais la condition est évaluée après le bloc : celui-ci est donc exécuté au moins une fois.

C
#include <stdio.h>

int main()
{
    int i = 1;
    do {
        printf("%d\n", i);
        i++;
    } while (i <= 5);
    return 0;
}
⚠️ Le point-virgule de do…while

} while (i <= 5); — ce point-virgule final est obligatoire, alors qu'une accolade fermante n'en prend jamais ailleurs. C'est la seule exception, et c'est une erreur de compilation classique.

🔍 while ou do…while : le distributeur de billets

while : « tant qu'il reste des billets, distribuer ». S'il n'y en a plus dès le départ, on ne distribue rien — zéro tour.

do…while : « demander le code, recommencer tant qu'il est faux ». On demande le code au moins une fois, forcément. Vérifier avant de demander n'aurait aucun sens.

Une saisie à contrôler appelle presque toujours do…while.

for

Trois parties entre parenthèses, séparées par des points-virgules : initialisation, condition, modification.

C
for (initialisation; condition; modification) {
    instructions;
}
C — exemple
#include <stdio.h>

int main()
{
    int i;
    for (i = 1; i <= 5; i++) {
        printf("%d\n", i);
    }
    return 0;
}
🔧 Corrigé — deux diapositives portent le mauvais titre

Dans le cours, l'exemple de la boucle for est titré « 2.2. La boucle do ….. while », et l'exemple de continue est titré « 2.1. L'instruction break ». Dans les deux cas le code est juste, seul l'intitulé a été recopié de la diapositive précédente.

La section « Branchement non conditionnel » est par ailleurs numérotée 2 alors qu'elle suit déjà une section 2 : elle devrait porter le 3.

À signaler si vous révisez sur le PDF : c'est le genre de détail qui fait chercher une différence là où il n'y en a pas.

⚠️ Le point-virgule assassin : for (…) ;

Un point-virgule juste après la parenthèse fermante donne à la boucle un corps vide. Elle tourne bel et bien — mais sans rien faire — et l'instruction qui suit ne s'exécute qu'une seule fois, après la boucle.

C — programme ① de l'exercice 8
int n = 5, i;
for (i = 2; i < n; i++);      // ← le corps de la boucle, c'est ce « ; »
printf("%d\n", i);
sortie réelle
5

La boucle incrémente i de 2 jusqu'à ce que i < 5 devienne faux : i vaut alors 5. Le printf, qui n'a jamais fait partie de la boucle, l'affiche une fois.

Deux des six programmes de l'exercice 8 reposent sur ce piège. Quand une boucle « ne fait rien », cherchez ce point-virgule avant toute autre hypothèse.

🤔 for (i = 3; i < 8; i += 3) printf("%d - ", i++); — qu'affiche ce programme ?

3 - 7 -

Le piège : i est modifié deux fois par tour — une fois par le i++ du printf, une fois par le i += 3 de la boucle.

Touri au testaffichéi après i++i après i += 3
133 - 47
277 - 811
311—11 < 8 est faux : la boucle s'arrête

Vérifié à l'exécution. Modifier la variable de boucle à l'intérieur du corps est une très mauvaise habitude — cet exercice est là pour montrer pourquoi.

12break, continue, goto

Ces trois instructions modifient le déroulement sans condition propre : c'est le programme qui décide de rompre le cours normal des choses.

break — sortir immédiatement

Quitte la boucle (for, while, do…while) ou le switch dans lequel on se trouve, et reprend après.

C
#include <stdio.h>

int main()
{
    int i;
    for (i = 1; i <= 10; i++) {
        if (i == 5)
            break;
        printf("%d\n", i);
    }
    return 0;
}
sortie
1
2
3
4

À i = 5, break sort de la boucle : 5 n'est jamais affiché, et les tours 6 à 10 n'ont pas lieu.

continue — passer au tour suivant

Abandonne la fin du tour en cours mais reste dans la boucle.

C
#include <stdio.h>

int main()
{
    int i;
    for (i = 1; i <= 5; i++) {
        if (i == 3)
            continue;
        printf("%d\n", i);
    }
    return 0;
}
sortie
1
2
4
5

Le 3 manque — mais 4 et 5 sont bien là : contrairement à break, continue ne quitte pas la boucle.

🧰 Les retenir sans les confondre

break = je quitte la salle. continue = je saute mon tour.

Dans un switch, seul break a un sens : continue ne s'applique qu'aux boucles.

goto — sauter à une étiquette

C
#include <stdio.h>

int main()
{
    printf("debut du programme\n");
    goto fin;
    printf("cette instruction ne sera pas executee\n");
fin:
    printf("fin du programme\n");
    return 0;
}
sortie réelle
debut du programme
fin du programme
🔧 Corrigé — la sortie du goto, et une majuscule de trop

Le cours annonce « Debut du programme Fin du programme », sur une seule ligne et avec des majuscules. Le code écrit "debut du programme\n" : la sortie est en minuscules, et le \n impose deux lignes.

💡 goto existe, mais presque personne ne s'en sert

Il fait partie du langage et il faut savoir le lire — un sujet d'examen peut le montrer. Mais il rend le programme difficile à suivre : on ne peut plus savoir, en lisant une ligne, par où on y est arrivé. Tout ce que fait goto s'écrit mieux avec une boucle, un break ou une fonction.

À connaître pour l'épreuve ; à éviter dans vos programmes.

13Pourquoi découper en fonctions

Une fonction est un bloc d'instructions qui réalise une tâche précise. Elle permet de découper un programme en parties indépendantes et réutilisables.

L'argument du cours tient en un exemple. Sans fonction, le calcul d'un carré se réécrit à chaque fois :

C — sans fonction
carre = nombre * nombre;

Avec une fonction, il s'écrit une fois et se réutilise :

C — avec une fonction
#include <stdio.h>

int carre(int nombre)
{
    return nombre * nombre;
}

int main(void)
{
    int a = carre(5);
    int b = carre(10);
    int c = carre(20);
    printf("%d %d %d\n", a, b, c);
    return 0;
}
sortie
25 100 400
🔍 Une fonction, c'est une commande qu'on invente

Vous utilisez printf sans jamais avoir lu son code. Vous savez ce qu'elle fait, ce qu'elle attend, ce qu'elle rend — cela suffit.

Écrire une fonction, c'est fabriquer une commande de plus, avec la même promesse : celui qui l'appelle n'a pas besoin de savoir comment elle s'y prend. Le découpage n'est pas une coquetterie d'écriture, c'est ce qui permet d'oublier des détails.

💡 Vous en écrivez déjà une depuis le premier jour

main est une fonction. Son type de retour est int, elle ne prend pas de paramètre, et son return 0; final est sa valeur de retour — celle que le système d'exploitation récupère pour savoir si le programme s'est bien terminé.

Tout ce que dit ce chapitre s'applique donc à quelque chose que vous écrivez déjà sans y penser.

14L'anatomie d'une fonction

Une fonction est constituée de quatre éléments, toujours dans le même ordre.

int additionner ( int a, int b ) { return a + b; } type de retour ce qu'elle rend nom ce qu'on écrit pour l'appeler paramètres ce qu'elle reçoit corps le travail
Les quatre éléments d'une fonction. Le corps est le seul qui contienne des instructions ; les trois autres sont un contrat passé avec celui qui appellera.
🧰 La syntaxe, dans les deux cas
C — avec valeur de retour
type_retour nom_fonction(paramètres)
{
    instructions;
    return valeur;
}
C — sans valeur de retour
void nom_fonction(paramètres)
{
    instructions;
}

Quand la fonction ne rend rien, le type de retour est void. Ce n'est pas « rien » : c'est un type à part entière, qui veut dire « vide ».

⚠️ Pas de point-virgule après l'accolade fermante

Une définition de fonction se termine par }, sans point-virgule. Un prototype, lui, en porte un — c'est justement ce qui les distingue (section 5).

15Les quatre formes

Une fonction se classe selon deux questions indépendantes : reçoit-elle des données ? et en renvoie-t-elle une ? Deux questions, deux réponses possibles chacune : quatre formes.

Les quatre formes, et un exemple de chacune
Sans valeur de retourAvec valeur de retour
Sans paramètre void afficherBonjour() int saisirNombre()
Avec paramètres void afficherCarre(int n) int carre(int n)

Sans paramètre, sans retour

Elle ne reçoit rien et ne rend rien : elle agit, c'est tout.

C
#include <stdio.h>

void afficherBonjour(void)
{
    printf("Bonjour !\n");
}

void afficherCarre(int n)
{
    printf("Le carré de %d est %d\n", n, n * n);
}

int main(void)
{
    afficherBonjour();
    afficherCarre(5);
    return 0;
}
sortie
Bonjour !
Le carré de 5 est 25
🔧 Corrigé — deux exemples du polycopié ne compilent pas

Le cours définit void affichercarre(int n) puis l'appelle afficherCarre(5);. Et plus loin, il définit int saisirnombre() puis appelle saisirNombre();.

Le C distingue les majuscules des minuscules. Ces deux appels désignent des fonctions qui n'existent pas, et le compilateur les refuse :

C — ne compile pas
#include <stdio.h>

void affichercarre(int n)
{
    printf("Le carré de %d est %d\n", n, n * n);
}

int main(void)
{
    afficherCarre(5);
    return 0;
}

Message obtenu : call to undeclared function 'afficherCarre' — et le compilateur ajoute même did you mean 'affichercarre'?. Les exemples publiés ici portent la casse corrigée, et verif-c.py compile chacun d'eux à chaque contrôle.

Sans paramètre, avec retour

Typiquement, une fonction de saisie : elle ne reçoit rien, elle rend ce que l'utilisateur a tapé.

C
int saisirNombre(void)
{
    int n;

    printf("Entrez un nombre : ");
    scanf("%d", &n);
    return n;
}

À l'appel, la valeur revient et se range dans une variable :

C — l'appel
int nombre;
nombre = saisirNombre();
⚠️ () vide et (void) ne disent pas la même chose

En C, int saisirNombre() signifie « je ne dis rien sur les paramètres » — le compilateur ne vérifiera donc pas les appels. int saisirNombre(void) signifie « aucun paramètre », et là il vérifie.

Le polycopié écrit (), qui fonctionne. Écrire (void) ne coûte rien et fait travailler le compilateur pour vous. Les deux sont acceptés en épreuve.

16L'appel : arguments et retour

🧰 Paramètre et argument : deux mots, deux moments

Un paramètre est une variable déclarée dans la définition de la fonction. Un argument est la valeur fournie lors de l'appel.

C
int additionner(int a, int b)   /* a et b sont les PARAMÈTRES */
{
    return a + b;
}

additionner(10, 20);           /* 10 et 20 sont les ARGUMENTS */

Le paramètre est un contenant, l'argument est le contenu. Le premier est écrit une fois ; le second change à chaque appel.

L'appel lui-même s'écrit de deux façons, selon qu'il y a une valeur à récupérer :

C — les deux formes d'appel
nom_fonction(arguments);              /* sans retour : on l'exécute */

variable = nom_fonction(arguments);   /* avec retour : on range le résultat */
⚠️ Une valeur retournée et non rangée est perdue

Écrire carre(4); sur une ligne à part est accepté par le compilateur : le calcul se fait, et le résultat part à la poubelle. Rien ne le signale.

C'est une des erreurs les plus difficiles à voir en TP, parce que le programme compile, tourne, et n'affiche simplement pas ce qu'on attendait.

🤔 Que vaut resultat après resultat = carre(4); ?

16. La fonction reçoit 4 comme argument, calcule 4 × 4, et return renvoie 16 à l'endroit de l'appel.

L'appel est remplacé par la valeur retournée : resultat = carre(4); devient, au moment de l'exécution, resultat = 16;. C'est la bonne façon de se représenter return.

17Le prototype, et l'ordre dans le fichier

Le compilateur lit le fichier de haut en bas. Quand il rencontre un appel, il doit déjà connaître la fonction : son nom, son type de retour, le nombre et le type de ses paramètres.

Il y a donc deux façons d'organiser un programme, et les deux sont justes.

Deux organisations possibles
Avec prototypeSans prototype
le prototype, puis main, puis les définitions les définitions, puis main
main se lit en premier aucune ligne à écrire deux fois
indispensable dès que deux fonctions s'appellent l'une l'autre suffit tant que l'ordre reste simple
C — avec prototype
#include <stdio.h>

int additionner(int a, int b);        /* le PROTOTYPE, avec un point-virgule */

int main(void)
{
    int resultat;
    resultat = additionner(5, 3);
    printf("résultat = %d\n", resultat);
    return 0;
}

int additionner(int a, int b)         /* la DÉFINITION, sans point-virgule */
{
    return a + b;
}
sortie
résultat = 8
🧰 Prototype ou définition : la différence tient à un caractère
C
int additionner(int a, int b);                       /* prototype  → ; */

int additionner(int a, int b) { return a + b; }      /* définition → { } */

Le prototype annonce l'existence de la fonction ; la définition contient ses instructions. Un fichier peut avoir les deux, jamais deux définitions.

💡 C'est exactement ce que fait #include <stdio.h>

Ce fichier ne contient pas le code de printf : il contient son prototype. Voilà pourquoi on l'inclut en première ligne — pour que le compilateur connaisse printf avant de rencontrer le premier appel.

Le mécanisme que vous apprenez ici est celui que vous utilisez depuis le début, sans l'avoir nommé.

18Variables locales et globales

La portée d'une variable, c'est la partie du programme où elle existe.

Locale : déclarée dans une fonction

Elle n'est accessible que dans la fonction où elle est déclarée, et elle disparaît quand la fonction se termine.

C
void calculer(void)
{
    int resultat = 0;      /* locale à calculer() */
    resultat = 10 + 20;
    printf("%d\n", resultat);
}

resultat ne peut pas être utilisée dans main : elle n'y existe pas.

Globale : déclarée en dehors de toute fonction

C
#include <stdio.h>

int val = 0;               /* globale : visible partout */

int carre(void)
{
    return val * val;
}

int main(void)
{
    val = 2;
    printf("le carré de 2 est : %d\n", carre());
    return 0;
}
sortie
le carré de 2 est : 4
⚠️ La version globale marche, et c'est le problème

Ici carre() ne prend aucun paramètre : elle lit val. Pour savoir ce qu'elle calcule, il faut chercher qui a modifié val, où, et quand. La fonction n'est plus lisible seule.

Et elle n'est plus réutilisable : impossible de s'en servir pour un autre nombre sans d'abord écrire val = ….

C — la version autonome
#include <stdio.h>

int carre(int val)
{
    return val * val;
}

int main(void)
{
    int resultat;
    resultat = carre(2);
    printf("le carré de 2 est : %d\n", resultat);
    return 0;
}
sortie
le carré de 2 est : 4
🧰 La bonne pratique, telle que le cours la formule

Privilégier les variables locales et le passage de paramètres chaque fois que c'est possible. Une fonction qui reçoit tout ce dont elle a besoin se comprend sans rien lire d'autre — et se réutilise ailleurs sans rien préparer.

19Les tableaux en C

🧭 Pourquoi cette section est ici

Le TP 3 s'intitule « les fonctions et les tableaux », et ses exercices 5 à 9 en demandent. Or le polycopié ne donne les tableaux qu'en pseudo-code, au chapitre 1 — la syntaxe C, elle, n'apparaît que dans une ligne du chapitre 2 : int notes[5];.

Cette section comble l'écart. La notion est au chapitre 11, en algorithmique ; ici, c'est le C.

Un tableau stocke plusieurs valeurs du même type, sous un seul nom. On accède à chacune par son indice, qui commence à 0.

int derniers_octets[5] = {10, 20, 30, 40, 50}; 10 20 30 40 50 [0] [1] [2] [3] [4] cinq cases contiguës en mémoire — indices de 0 à 4 le nom = la 1re case il n'y a pas de [5] c'est hors du tableau
Le nom du tableau désigne l'adresse de sa première case. Un tableau de cinq éléments s'arrête à l'indice 4 — [5] sort déjà du tableau.
🧰 Déclarer, puis parcourir
C
#include <stdio.h>

int main(void)
{
    int derniers_octets[5] = {10, 20, 30, 40, 50};
    int i;

    for (i = 0; i < 5; i++)
    {
        printf("octet %d : %d\n", i, derniers_octets[i]);
    }
    return 0;
}
sortie
octet 0 : 10
octet 1 : 20
octet 2 : 30
octet 3 : 40
octet 4 : 50

La boucle va de 0 à taille − 1, donc la condition s'écrit i < 5 et non i <= 5. C'est la faute la plus fréquente du chapitre.

⚠️ Le C ne vérifie jamais les indices

Écrire derniers_octets[7] dans un tableau de 5 cases ne produit aucune erreur : ni à la compilation, ni à l'exécution. Le programme lit ou écrit simplement ce qui se trouve à côté en mémoire.

Le résultat est imprévisible — parfois une valeur absurde, parfois un plantage, parfois rien du tout pendant des semaines. C'est la source numéro un de bogues en C, et l'origine d'une grande partie des failles de sécurité.

Personne ne vous protégera : c'est à la boucle d'être juste.

🧰 Saisir dans un tableau : le & ne disparaît pas
C
float temperatures[7];
int i;

for (i = 0; i < 7; i++)
{
    printf("Température %d : ", i + 1);
    scanf("%f", &temperatures[i]);
}

temperatures[i] est une variable comme une autre : scanf a besoin de son adresse, donc du &. On affiche i + 1 pour parler à l'utilisateur en « première, deuxième… » tout en gardant l'indice i pour le tableau.

19.1 · Pourquoi un tableau plutôt que cinq variables

Le polycopié ouvre le chapitre par la solution qu'on n'écrit pas, et c'est la meilleure façon de comprendre à quoi sert un tableau :

C — ce qu'on évite
float note1;
float note2;
float note3;
float note4;
float note5;

Cinq noms pour cinq valeurs de même nature. Le jour où il y en a trente, il faut trente noms — et surtout, aucune boucle ne peut les parcourir : une boucle sait faire varier un indice, pas un nom de variable. Une seule ligne remplace les cinq :

C — ce qu'on écrit
float notes[5];

19.2 · Modifier une case après coup

Une case de tableau s'affecte comme n'importe quelle variable, à tout moment :

C — le programme entier
#include <stdio.h>

int main(void)
{
    int nombres[5] = {10, 20, 30, 40, 50};

    nombres[1] = 50;          /* la deuxième case passe de 20 à 50 */

    printf("nombres[1] = %d\n", nombres[1]);
    return 0;
}
sortie réelle
nombres[1] = 50
🧰 Le tableau sans taille : int nombres[] = {…};

Quand les valeurs sont données à la déclaration, on peut laisser les crochets vides : le compilateur compte.

C
int nombres[] = {10, 20, 30, 40, 50};   /* cinq cases, comptées par le compilateur */

C'est à la fois plus court et plus sûr : ajouter une valeur à la liste agrandit le tableau tout seul, alors qu'un [5] écrit à la main deviendrait faux sans prévenir. Attention cependant — cela ne marche qu'avec une liste d'initialisation. int nombres[]; tout seul ne compile pas : le compilateur n'a rien à compter.

19.3 · L'accumulateur : additionner les cases

Le motif le plus réemployé de tout le chapitre. Une variable de cumul mise à zéro avant la boucle, et augmentée à chaque tour :

C — le programme entier
#include <stdio.h>

int main(void)
{
    int nombres[5] = {10, 20, 30, 40, 50};
    int somme = 0;
    int i;

    for (i = 0; i < 5; i++)
    {
        somme = somme + nombres[i];
    }

    printf("somme = %d\n", somme);
    return 0;
}
sortie réelle
somme = 150

Oublier le = 0 est la faute classique : somme partirait d'une valeur quelconque et le total serait faux sans que rien ne le signale. C'est exactement ce que l'atelier de ce site détecte et nomme.

💡 for (int i = 0; …) : la variable déclarée dans la boucle

Le polycopié écrit souvent for (int i = 0; i < 5; i++), alors que ce chapitre déclare int i; avant. Les deux sont justes, et il faut savoir lire les deux.

Déclarer dans le for est autorisé depuis C99, et c'est la meilleure forme : i n'existe que dans la boucle, donc on ne peut pas s'en servir par erreur après. Déclarer avant est la forme du C d'origine (C89), et c'est la seule qui compile avec un compilateur ancien ou l'option -std=c89.

En examen, écrivez celle de votre professeur. En projet, déclarez dans le for : une variable qui vit le moins longtemps possible est une variable qu'on ne peut pas confondre.

20Passer un tableau à une fonction

C'est le point qui fait échouer la plupart des programmes qui découpent un traitement de tableau en plusieurs fonctions, et il tient en une phrase : un tableau passé à une fonction perd sa taille.

C — la démonstration
#include <stdio.h>

void combien(int t[])
{
    printf("dans la fonction : sizeof(t) = %zu\n", sizeof(t));
}

int main(void)
{
    int mesures[10];

    printf("dans main       : sizeof(mesures) = %zu\n", sizeof(mesures));
    combien(mesures);
    return 0;
}
sortie
dans main       : sizeof(mesures) = 40
dans la fonction : sizeof(t) = 8
⚠️ 40 octets deviennent 8

Dans main, sizeof rend 40 : dix entiers de quatre octets. Dans la fonction, il rend 8 — la taille d'une adresse.

Car ce n'est pas le tableau qui est transmis, c'est l'adresse de sa première case. La fonction n'a donc aucun moyen de savoir combien de cases suivent.

D'où la règle absolue : on passe toujours la taille en second paramètre.

🧰 La forme à retenir
C
#include <stdio.h>

float CalculerMoyenne(float t[], int n)
{
    float somme = 0.0f;
    int i;

    for (i = 0; i < n; i++)
    {
        somme = somme + t[i];
    }
    return somme / n;
}

float Maximum(float t[], int n)
{
    float max = t[0];
    int i;

    for (i = 1; i < n; i++)
    {
        if (t[i] > max)
        {
            max = t[i];
        }
    }
    return max;
}

int main(void)
{
    float debits[8] = {95, 102, 98, 110, 105, 97, 115, 101};

    printf("Débit moyen : %.2f Mb/s\n", CalculerMoyenne(debits, 8));
    printf("Débit maximum : %.0f Mb/s\n", Maximum(debits, 8));
    return 0;
}
sortie
Débit moyen : 102.88 Mb/s
Débit maximum : 115 Mb/s
💡 Une conséquence qui arrange : la fonction peut modifier le tableau

Comme la fonction reçoit l'adresse et non une copie, ce qu'elle écrit dans t[i] est écrit dans le tableau d'origine. C'est exactement ce qui permet à SaisirTableau() de remplir un tableau déclaré dans main sans rien retourner.

Avec une variable simple, c'est l'inverse : la fonction reçoit une copie, et la modifier ne change rien chez l'appelant. Tableau et variable simple ne se comportent pas pareil — c'est la distinction à garder en tête.

🤔 Maximum commence sa boucle à i = 1. Pourquoi ?

Parce que max a été initialisé avec t[0] : la case 0 est déjà prise en compte, la comparer à elle-même ne servirait à rien.

Pourquoi ne pas initialiser max à 0 ? Parce que si toutes les valeurs étaient négatives, la fonction renverrait 0 — une valeur qui ne figure pas dans le tableau.

Partir de la première valeur réelle est la seule initialisation juste, quelles que soient les données.

21Les tableaux à deux dimensions

Un tableau à une dimension est une liste. Un tableau à deux dimensions est un tableau au sens courant du mot : des lignes et des colonnes. On l'appelle aussi une matrice, et c'est la forme naturelle de tout ce qui se croise — des serveurs et des jours, des utilisateurs et des tentatives, des capteurs et des relevés.

Déclarer : deux tailles, dans cet ordre

On écrit le nombre de lignes d'abord, puis le nombre de colonnes.

C
int trafic[3][4];      /* 3 lignes, 4 colonnes → 12 cases */
float temperatures[5][4];
int grille[2][2] = { {1, 2}, {3, 4} };

Le nombre total de cases est le produit des deux tailles : 3 × 4 = 12. C'est ce que demande la question « taille d'un tableau à deux dimensions ».

Accéder : deux indices, et toujours à partir de 0

C
trafic[0][0] = 120;     /* premiere ligne, premiere colonne */
trafic[2][3] = 30;      /* DERNIERE ligne, DERNIERE colonne d'un [3][4] */
printf("%d", trafic[1][2]);
⚠️ trafic[3][4] n'existe pas dans int trafic[3][4]

La déclaration donne des tailles ; l'accès donne des indices. Avec 3 lignes, les indices valides sont 0, 1 et 2 — jamais 3. Le dernier élément d'un [3][4] est donc [2][3].

C'est la même règle qu'à une dimension, appliquée deux fois. Et le C ne proteste pas : il lit la mémoire d'à côté, et affiche n'importe quoi.

Initialiser à la déclaration

Des accolades dans des accolades : une paire par ligne.

C — le programme entier
#include <stdio.h>

int main(void)
{
    /* 3 lignes, 4 colonnes : le trafic de trois commutateurs sur quatre ports */
    int trafic[3][4] = {
        { 120,  80, 200,  45 },
        {  90, 150,  60, 110 },
        { 200, 170,  95,  30 }
    };
    int i, j;

    for (i = 0; i < 3; i++) {
        for (j = 0; j < 4; j++)
            printf("%6d", trafic[i][j]);
        printf("\n");
    }

    printf("\ntrafic[1][2] vaut %d\n", trafic[1][2]);
    return 0;
}
sortie
   120    80   200    45
    90   150    60   110
   200   170    95    30

trafic[1][2] vaut 60

Les accolades intérieures ne sont pas obligatoires pour le compilateur — {120, 80, 200, 45, 90, …} marche aussi. Elles le sont pour le lecteur : sans elles, personne ne voit où finit une ligne.

Parcourir : une boucle dans une boucle

C'est le geste à retenir du chapitre, et il est toujours le même.

C — le squelette
for (i = 0; i < LIGNES; i++) {       /* la boucle du dehors : les lignes */
    for (j = 0; j < COLONNES; j++) { /* la boucle du dedans : les colonnes */
        /* ici, on travaille sur tableau[i][j] */
    }
}
💡 Ce qui change selon la boucle qu'on met dehors

Pour une somme par ligne, la remise à zéro va dans la boucle des lignes. Pour une somme par colonne, on échange les deux boucles : j dehors, i dedans.

Beaucoup d'énoncés sur les matrices ne demandent rien d'autre que ce choix-là : laquelle des deux boucles met-on à l'extérieur ? Celle qui porte ce qu'on veut regrouper.

C — sommes par ligne, puis par colonne
#include <stdio.h>

#define LIGNES   3
#define COLONNES 4

int main(void)
{
    int trafic[LIGNES][COLONNES] = {
        { 120,  80, 200,  45 },
        {  90, 150,  60, 110 },
        { 200, 170,  95,  30 }
    };
    int i, j;

    for (i = 0; i < LIGNES; i++) {
        int somme = 0;
        for (j = 0; j < COLONNES; j++)
            somme += trafic[i][j];
        printf("commutateur %d : %d\n", i + 1, somme);
    }

    for (j = 0; j < COLONNES; j++) {
        int somme = 0;
        for (i = 0; i < LIGNES; i++)
            somme += trafic[i][j];
        printf("port %d : %d\n", j + 1, somme);
    }
    return 0;
}
sortie
commutateur 1 : 445
commutateur 2 : 410
commutateur 3 : 495
port 1 : 410
port 2 : 400
port 3 : 355
port 4 : 185

Remplir au clavier

C
for (i = 0; i < LIGNES; i++)
    for (j = 0; j < COLONNES; j++) {
        printf("Ligne %d, colonne %d : ", i + 1, j + 1);
        scanf("%d", &tableau[i][j]);
    }

Le & est là, comme toujours avec scanf sur un entier : on donne l'adresse de la case. Et i + 1 dans le message, parce qu'on parle à un humain qui compte à partir de 1.

Passer une matrice à une fonction

Même règle qu'à une dimension — le tableau n'est pas copié — avec une obligation de plus.

C
int totalLigne(int t[][5], int ligne);   /* la 2e taille est OBLIGATOIRE */
int totalLigne(int t[4][5], int ligne);  /* ou les deux, au choix */
⚠️ La seconde taille ne s'omet jamais

À une dimension, int t[] suffit. À deux dimensions, le compilateur doit savoir la longueur d'une ligne pour calculer où commence la suivante : t[i][j] se trouve en mémoire à la position i × COLONNES + j. Sans COLONNES, il ne peut rien calculer.

int t[][] est donc refusé, et le message — array has incomplete element type — ne dit pas franchement ce qui manque.

🤔 Combien de cases dans float mesures[5][4], et quelle est la dernière ?

20 cases — 5 × 4 — et la dernière est mesures[4][3].

5 lignes numérotées 0 à 4, 4 colonnes numérotées 0 à 3. L'erreur consiste à écrire mesures[5][4], qui est la déclaration, pas un élément.

Un moyen simple de ne jamais se tromper : la dernière case est toujours la taille moins un, dans chaque dimension.

22Une variable, et l'endroit où elle est rangée

Jusqu'ici, une variable était un nom et une valeur. Il lui manquait une troisième chose, qui ne se voyait pas et qui va désormais tout commander : une adresse. Quand on écrit int age = 20;, le programme réserve une case dans la mémoire, y range 20, et retient à quel numéro se trouve cette case.

🏠 Le casier et son numéro

Un vestiaire. Le casier nº 1000 contient un blouson. « Le blouson », c'est la valeur ; « 1000 », c'est l'adresse. Deux questions différentes : qu'est-ce qu'il y a dedans ? et où est-il ?

En C, ces deux questions s'écrivent age et &age. Tout le chapitre tient dans cette distinction.

On écritOn demandeOn obtient
ageQuelle est la valeur ?20
&ageOù est rangée la variable ? un numéro de case, par exemple 0x16b39e858

Le & n'est pas nouveau : il était déjà là dans scanf("%d", &age). On l'écrivait sans savoir pourquoi — maintenant on sait. scanf doit écrire dans la variable, donc il lui faut son adresse, pas sa valeur.

💡 L'adresse change à chaque exécution

Lancez deux fois le même programme : les adresses affichées ne seront pas les mêmes. Le système d'exploitation place le programme où il veut, et il le fait exprès — c'est une protection contre certaines attaques. Une adresse ne se retient pas, elle se manipule.

C'est aussi pourquoi aucun affichage d'adresse n'est recomparé par la chaîne de contrôle de ces fiches : une valeur qui change à chaque exécution n'est pas une promesse vérifiable.

Afficher une adresse : %p

C
printf("adresse de age : %p\n", (void *)&age);

%p est le format des adresses, et (void *) la conversion qu'il attend — un « pointeur sur n'importe quoi ». Sans elle, le compilateur avertit.

🤔 Dans scanf("%d", &n), pourquoi le & — alors que printf("%d", n) n'en a pas ?

printf lit la variable : la valeur lui suffit. scanf écrit dedans : il lui faut savoir où, donc l'adresse. Un paramètre reçu en C est toujours une copie ; l'adresse est la seule façon de remonter jusqu'à l'original.

23Le pointeur : une variable qui contient une adresse

Un pointeur est une variable comme les autres. Sa particularité tient en une phrase : ce qu'elle contient est une adresse, pas une donnée.

C
int  age = 20;    /* age contient  20                     */
int *p;           /* p contiendra l'adresse d'un int       */
p = &age;         /* desormais : p pointe vers age         */

Déclarer

C
type *nom;

int    *p;      /* p pointe vers un int    */
float  *q;      /* q pointe vers un float  */
char   *r;      /* r pointe vers un char   */

Le type du pointeur dit vers quoi il pointe, jamais ce qu'il contient. int *p; ne range pas un entier dans p : il y range le moyen d'en atteindre un.

Les deux opérateurs, et le sens de la flèche

OpérateurSe litPart deDonne
&« adresse de »une variable son adresse
*« valeur à l'adresse »un pointeur la variable pointée

Les deux se défont l'un l'autre : *(&age), c'est age. On range l'adresse avec &, on revient à la valeur avec *.

⚠️ L'étoile ne veut pas dire la même chose aux deux endroits

Dans int *p;, l'étoile fait partie de la déclaration : elle dit « p est un pointeur ». Dans *p = 30;, l'étoile est un opérateur : elle dit « va à l'adresse rangée dans p ».

Même symbole, deux rôles. C'est la confusion la plus fréquente du chapitre, et la seule façon de s'en sortir est de regarder s'il y a un type devant.

Un pointeur se range avant de s'utiliser

C
int *p = NULL;     /* « pour l'instant, p ne pointe vers rien »  */

if (p != NULL)     /* on peut donc tester avant d'y toucher     */
    printf("%d", *p);
⚠️ Déréférencer un pointeur non initialisé

int *p; *p = 10; — p contient ce qui traînait dans la mémoire, c'est-à-dire une adresse au hasard. Le programme écrit 10 n'importe où. Parfois il plante tout de suite ; souvent il continue, et le bogue sort trois jours plus tard, ailleurs.

D'où = NULL à la déclaration : une valeur qui ne pointe nulle part se teste, tandis qu'une adresse au hasard ne se voit pas.

Le programme complet

C — le programme entier
#include <stdio.h>

int main(void)
{
    int age = 20;
    int *p;

    p = &age;

    printf("p contient l'adresse de age : %s\n", (p == &age) ? "oui" : "non");
    printf("age vaut %d, et *p vaut %d\n", age, *p);
    printf("un int occupe %zu octets\n", sizeof(int));
    return 0;
}
sortie réelle
p contient l'adresse de age : oui
age vaut 20, et *p vaut 20
un int occupe 4 octets

Un pointeur, lui, occupe 8 octets sur une machine 64 bits et 4 sur une machine 32 bits — l'atelier C de ce site est en 32 bits, il affichera donc 4. La taille d'un pointeur ne dépend pas du type pointé : une adresse d'int et une adresse de double font la même taille.

🤔 Que vaut p et que vaut *p après int age = 20; int *p = &age; ?

p vaut l'adresse de age — un numéro de case qui change à chaque exécution. *p vaut 20, la valeur rangée à cette adresse.

24Modifier une variable sans la nommer

C'est ici que les pointeurs cessent d'être une curiosité. *p n'est pas seulement une lecture : c'est un accès, et un accès se lit comme il s'écrit.

C — le programme entier
#include <stdio.h>

int main(void)
{
    int age = 20;
    int *p = &age;

    printf("avant  : age = %d\n", age);
    *p = 30;                      /* on ecrit DANS la case pointee par p */
    printf("apres  : age = %d\n", age);
    return 0;
}
sortie réelle
avant  : age = 20
apres  : age = 30

*p = 30; n'a pas touché à p : le pointeur contient toujours la même adresse. Ce qui a changé, c'est ce qu'il y a au bout. La ligne se lit « range 30 dans la case dont p détient l'adresse », et comme cette case est age, cela revient exactement à age = 30;.

💡 À quoi ça sert, puisque age = 30; marche aussi ?

Ici, à rien. Mais dans une fonction, age n'existe pas : on ne connaît que ce qu'on a reçu. Le chapitre sur les fonctions l'avait dit — « une fonction travaille sur une copie » — et c'était une limite qu'on subissait. Le pointeur la lève : on donne l'adresse, la fonction écrit dans l'original.

C'est aussi la réponse à la question qu'on se pose depuis le début : pourquoi scanf a-t-il besoin d'un & ? scanf est une fonction qui modifie votre variable.

C
void doubler(int *n)      /* n recoit une ADRESSE  */
{
    *n = *n * 2;          /* on ecrit dans l'original */
}

/* a l'appel : */
int v = 21;
doubler(&v);              /* v vaut maintenant 42     */
⚠️ p = 30; n'est pas *p = 30;

Sans l'étoile, on remplace l'adresse contenue dans p par le nombre 30 : le pointeur désigne désormais la case nº 30, qui n'appartient probablement pas au programme. age, lui, n'a pas bougé.

Le compilateur le signale (incompatible integer to pointer conversion) — c'est l'un des rares cas où il voit la faute. N'ignorez pas l'avertissement.

25Pointeurs et tableaux : la même chose, vue autrement

Le chapitre sur les tableaux avait laissé une anomalie sans explication : « un tableau passé à une fonction perd sa taille, mais la fonction peut le modifier ». La raison tient en une ligne, et c'est la clé de ce chapitre.

C
int tab[5] = {1, 2, 6, 0, 7};

tab == &tab[0]      /* le nom du tableau EST l'adresse de son premier element */

Un tableau ne se copie jamais parce qu'on n'a jamais passé le tableau : on passait son adresse, sans le savoir.

Parcourir avec un pointeur

C — le programme entier
#include <stdio.h>

int main(void)
{
    int tab[5] = {1, 2, 6, 0, 7};
    int *p = tab;
    int i;

    for (i = 0; i < 5; i++) {
        printf("%d\n", *p);
        p++;
    }
    return 0;
}
sortie réelle
1
2
6
0
7

p++ n'ajoute pas 1 à l'adresse : il la fait avancer d'une case du type pointé, soit 4 octets pour un int. C'est le type du pointeur qui décide du pas — et c'est pour ça qu'il doit être juste.

Les crochets ne sont qu'une écriture

C
p[i]   ==   *(p + i)        /* strictement la meme chose */

p[0]   ==   *p
tab[2] ==   *(tab + 2)

Les crochets fonctionnent donc sur un pointeur, et l'étoile sur un tableau. Ce ne sont pas deux mondes : c'est une seule notion avec deux notations.

🔧 Corrigé — le cours fait avancer le pointeur deux fois

La diapositive 195 du polycopié donne ce programme, et annonce qu'il affiche 1 2 6 0 7 :

C
for (i = 0; i < 5; i++) {
    printf("%d\n", p[i]);   /* p[i] avance deja de i cases... */
    p++;                     /* ...et p++ avance encore d'une  */
}

Les deux avancent. Au tour nº i, on ne lit pas tab[i] mais tab[2i] : on saute un élément sur deux, puis on sort du tableau. Compilé et exécuté, le programme affiche 1, 6, 7, puis deux valeurs lues hors du tableau — différentes à chaque exécution. Le détecteur de débordement confirme : stack-buffer-overflow, ligne 8.

La démonstration, avec neuf cases pour rester à l'intérieur :

C — le programme entier
#include <stdio.h>

int main(void)
{
    int tab[9] = {10, 11, 12, 13, 14, 15, 16, 17, 18};
    int *p = tab;
    int i;

    /* LES DEUX A LA FOIS : p[i] avance deja, et p++ avance encore */
    for (i = 0; i < 5; i++) {
        printf("tour %d : p[%d] vaut %d\n", i, i, p[i]);
        p++;
    }
    return 0;
}
sortie réelle
tour 0 : p[0] vaut 10
tour 1 : p[1] vaut 12
tour 2 : p[2] vaut 14
tour 3 : p[3] vaut 16
tour 4 : p[4] vaut 18

Une case sur deux, 10 · 12 · 14 · 16 · 18. On choisit une des deux écritures, jamais les deux : soit *p avec p++ (diapositive 192, qui est juste), soit p[i] sans p++.

⚠️ Un tableau n'est pas tout à fait un pointeur

tab vaut l'adresse du premier élément, mais ce n'est pas une variable : on ne peut pas écrire tab++ ni tab = autreChose. Un pointeur se déplace, un tableau non.

Autre différence, déjà rencontrée : sizeof(tab) rend 20 pour un int[5], mais sizeof(p) rend la taille d'une adresse. Un tableau devient un pointeur dès qu'on le passe ; il ne l'est pas chez lui.

🤔 Avec int tab[5] = {1, 2, 6, 0, 7}; et int *p = tab;, que vaut *(p + 3) ?

0. *(p + 3) s'écrit aussi p[3], c'est-à-dire tab[3] — le quatrième élément, puisqu'on compte à partir de zéro.

26Réserver la mémoire pendant l'exécution

Un tableau déclaré int tab[10]; a sa taille écrite dans le programme. Pour en avoir 100, il faut modifier le fichier et recompiler. Pour un programme qui demande « combien de mesures ? », c'est une impasse.

🍽️ Réserver au restaurant

Le tableau de taille fixe, c'est réserver vingt couverts tous les soirs : trop peu quand on est vingt-cinq, gâché quand on est six. L'allocation dynamique, c'est appeler le jour même et dire combien on sera.

malloc — demander des octets

C
#include <stdlib.h>          /* malloc, calloc et free vivent ici */

int *p;
p = malloc(5 * sizeof(int));   /* « reserve de quoi loger 5 int »  */

malloc réserve une zone et rend son adresse. On la range donc dans un pointeur — et à partir de là, p s'utilise exactement comme un tableau : p[0], p[1]…

On écrit 5 * sizeof(int) et jamais 20 : sizeof donne la taille réelle sur la machine qui compile. Un nombre écrit en dur serait juste ici et faux ailleurs.

Vérifier, toujours

C
p = malloc(n * sizeof(int));
if (p == NULL) {
    printf("memoire insuffisante\n");
    return 1;
}
⚠️ malloc peut échouer, et il le dit en rendant NULL

Le polycopié ne teste pas le retour de malloc dans ses premiers exemples — c'est pourtant le réflexe que tout énoncé un peu sérieux exige. Les trois lignes ci-dessus sont le réflexe à prendre : sans elles, un échec d'allocation donne un pointeur nul qu'on déréférence aussitôt, donc un plantage sans message.

Ce n'est pas une divergence avec le cours : le polycopié l'écrit lui-même à la diapositive 242, pour une structure. On applique simplement partout ce qu'il montre une fois.

C — le programme entier
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *p;
    int i;

    p = malloc(5 * sizeof(int));
    if (p == NULL) {
        printf("memoire insuffisante\n");
        return 1;
    }

    for (i = 0; i < 5; i++)
        p[i] = (i + 1) * 10;

    for (i = 0; i < 5; i++)
        printf("p[%d] = %d\n", i, p[i]);

    free(p);
    return 0;
}
sortie réelle
p[0] = 10
p[1] = 20
p[2] = 30
p[3] = 40
p[4] = 50

calloc — demander des cases, et les vouloir à zéro

C
int *a = malloc(4 * sizeof(int));   /* 4 cases, contenu indetermine */
int *b = calloc(4, sizeof(int));    /* 4 cases, toutes a ZERO       */
malloccalloc
Argumentsun total d'octetsun nombre d'objets, et la taille d'un objet
Contenu initialce qui traînait làdes zéros
Quand le choisiron va tout écrire soi-mêmeon va accumuler, compter, ou lire avant d'écrire
C — le programme entier
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *a = malloc(4 * sizeof(int));
    int *b = calloc(4, sizeof(int));
    int i;

    if (a == NULL || b == NULL) return 1;

    printf("calloc met tout a zero :\n");
    for (i = 0; i < 4; i++)
        printf("b[%d] = %d\n", i, b[i]);

    free(a);
    free(b);
    return 0;
}
sortie réelle
calloc met tout a zero :
b[0] = 0
b[1] = 0
b[2] = 0
b[3] = 0

Le programme n'affiche pas le contenu de a, et c'est volontaire : une mémoire non initialisée n'a pas de valeur annonçable. L'afficher serait écrire une sortie qu'on ne peut pas promettre.

free — rendre ce qu'on a pris

C
free(p);        /* la zone redevient disponible pour le programme */

Trois temps, toujours les mêmes, et dans cet ordre :

méthode
malloc() ou calloc()  ->  reserver
      utiliser          ->  p[0], p[1], ...
        free()          ->  rendre
⚠️ Trois fautes classiques, et ce qu'elles donnent
FauteNomEffet
on oublie freefuite mémoire le programme grossit sans fin — invisible sur un exercice, fatal sur un service
free deux foisdouble libération plantage, ou pire : une faille exploitable
on utilise après freepointeur fou on lit une zone qui appartient déjà à autre chose

Un réflexe suffit contre les deux dernières : free(p); p = NULL;. Libérer NULL est autorisé et ne fait rien, et déréférencer NULL plante tout de suite, à l'endroit de la faute.

🤔 Pourquoi écrit-on malloc(n * sizeof(int)) plutôt que malloc(n) ?

malloc compte en octets, pas en cases. Un int en occupe 4 : malloc(n) ne réserverait donc qu'un quart de la place nécessaire, et l'écriture des cases suivantes déborderait sur la mémoire voisine.

27Les structures : ranger ensemble ce qui va ensemble

Pour décrire un étudiant, il faut un nom, un âge et une moyenne — trois types différents. Un tableau ne sait pas faire : il range des éléments du même type. Trois variables séparées le font, mais rien ne dit qu'elles parlent de la même personne, et il faut les passer une par une à chaque fonction.

C
char  nom[30];      /* trois variables independantes :        */
int   age;          /* rien ne dit qu'elles decrivent          */
float moyenne;      /* le meme etudiant                        */
📋 La fiche d'inscription

Trois variables séparées, c'est trois listes affichées au mur : les noms d'un côté, les âges de l'autre, les moyennes plus loin. Une structure, c'est une fiche par étudiant — on la prend, on l'a toute.

Définir le modèle

C
struct Etudiant {
    char  nom[30];
    int   age;
    float moyenne;
};                    /* ce point-virgule est OBLIGATOIRE */

Ces lignes ne créent aucun étudiant. Elles décrivent à quoi ressemble un étudiant — c'est un moule, pas une pièce.

⚠️ Le point-virgule après l'accolade fermante

Une fonction se termine par } tout court ; une structure par };. L'oublier donne une cascade d'erreurs sur les lignes suivantes, ce qui est le pire cas : on cherche la faute là où elle n'est pas.

Créer une variable

C
struct Etudiant e1;              /* une fiche vierge               */
struct Etudiant e1, e2, e3;      /* trois fiches, independantes    */

Le type s'écrit en deux mots : struct Etudiant. C'est lourd, et la section 30 montre comment s'en débarrasser.

Accéder aux champs : le point

C
e1.age     = 20;
e1.moyenne = 15.5;
⚠️ Une chaîne ne se copie pas avec =
C — ne compile pas
#include <stdio.h>

struct Etudiant { char nom[30]; int age; float moyenne; };

int main(void)
{
    struct Etudiant e1;
    e1.nom = "Alice";    /* refuse : on n'affecte pas a un tableau */
    return 0;
}

Le champ nom est un tableau, et un tableau n'est pas une variable qu'on affecte. On copie son contenu :

C
#include <string.h>

strcpy(e1.nom, "Alice");

C'est la même règle qu'au chapitre des tableaux, à un endroit où on l'oublie — parce que e1.nom ressemble à une variable simple.

Initialiser à la déclaration

C
struct Etudiant e1 = {"Alice", 20, 15.5};        /* dans l'ordre des champs */

struct Etudiant e2 = {                           /* ou en nommant les champs */
    .nom     = "Bilal",
    .age     = 19,
    .moyenne = 12.0
};

La seconde écriture coûte deux lignes et évite une classe entière de bogues : le jour où un champ est ajouté au milieu de la structure, la première décale tout en silence.

C — le programme entier
#include <stdio.h>
#include <string.h>

struct Etudiant {
    char  nom[30];
    int   age;
    float moyenne;
};

int main(void)
{
    struct Etudiant e1;

    strcpy(e1.nom, "Alice");      /* une chaine ne se copie pas avec = */
    e1.age = 20;
    e1.moyenne = 15.5;

    printf("Nom     : %s\n", e1.nom);
    printf("Age     : %d\n", e1.age);
    printf("Moyenne : %.2f\n", e1.moyenne);
    return 0;
}
sortie réelle
Nom     : Alice
Age     : 20
Moyenne : 15.50
🔧 Corrigé — la sortie annoncée ne correspond pas au programme

La diapositive 219 du polycopié écrit le programme entièrement en minuscules — strcpy(e1.nom, "alice"), printf("nom : %s\n", …) — puis annonce une sortie en capitales :

ce que le polycopié annonce
Nom : Alice
Age : 20
Moyenne : 15.50
ce que le programme affiche vraiment
nom : alice
age : 20
moyenne : 15.50

Compilé et exécuté pour en avoir le cœur net. Ce n'est pas une faute de logique, c'est une faute de recopie — mais c'est exactement celle qui fait douter de son propre code quand on compare l'écran au cours. Le C distingue les majuscules des minuscules, y compris à l'intérieur d'une chaîne qu'il se contente de recopier. La version publiée ci-dessus écrit les deux de la même façon.

Saisir au clavier : où mettre le &

ChampTypeSaisiePourquoi
e1.ageint scanf("%d", &e1.age); variable simple → il faut son adresse
e1.moyennefloat scanf("%f", &e1.moyenne); idem
e1.nomchar[] scanf("%s", e1.nom); un tableau est déjà une adresse

La règle du & n'a pas changé d'un mot depuis le chapitre 9 : scanf écrit, donc il lui faut une adresse. Et la section 25 vient d'expliquer pourquoi un tableau n'en a pas besoin — tab est déjà &tab[0].

🤔 Pourquoi struct Etudiant { … }; ne crée-t-il aucun étudiant ?

Parce qu'il définit un type, pas une variable — exactement comme int ne crée aucun entier. Il faut ensuite écrire struct Etudiant e1; pour qu'une fiche existe en mémoire.

28Un tableau de structures

Une structure décrit un étudiant ; un tableau de structures décrit une classe. C'est la forme que prend, dans tout programme réel, une liste de serveurs, de produits, de relevés — tout ce qui est « plusieurs choses, chacune ayant plusieurs champs ».

C
struct Etudiant classe[5];        /* cinq fiches, numerotees de 0 a 4 */

classe[0].age  = 20;              /* tableau[indice].champ            */
classe[2].moyenne = 17.25;

On lit de gauche à droite : classe[2] est un étudiant, et .moyenne est son champ. Les deux notations se combinent sans règle nouvelle à apprendre.

C — le programme entier
#include <stdio.h>

struct Etudiant {
    char  nom[30];
    int   age;
    float moyenne;
};

int main(void)
{
    struct Etudiant classe[3] = {
        { "Alice",  20, 15.5f },
        { "Bilal",  19, 12.0f },
        { "Chloe",  21, 17.25f }
    };
    float somme = 0;
    int i;

    for (i = 0; i < 3; i++) {
        printf("%-8s %3d ans  %5.2f\n", classe[i].nom, classe[i].age, classe[i].moyenne);
        somme += classe[i].moyenne;
    }
    printf("moyenne de la classe : %.2f\n", somme / 3);
    return 0;
}
sortie réelle
Alice     20 ans  15.50
Bilal     19 ans  12.00
Chloe     21 ans  17.25
moyenne de la classe : 14.92
🔧 Deux formats d'alignement qui servent tout le temps

%-8s — une chaîne sur 8 colonnes, collée à gauche (c'est le tiret qui fait le collage à gauche). %5.2f — un réel sur 5 colonnes avec 2 décimales. Ensemble, ils donnent un tableau lisible sans un seul espace écrit à la main.

⚠️ Un tableau de structures ne se remplit pas « d'un coup »

Il n'existe pas de raccourci pour saisir toute une classe : il faut une boucle, et dans la boucle une saisie par champ. C'est ce que demande l'exercice du cours, et c'est trois scanf par tour.

C
for (i = 0; i < 3; i++) {
    printf("Nom de l'etudiant %d : ", i + 1);
    scanf("%s", classe[i].nom);        /* tableau de char : pas de & */
    printf("Age : ");
    scanf("%d", &classe[i].age);       /* int : & obligatoire        */
    printf("Moyenne : ");
    scanf("%f", &classe[i].moyenne);
}
🤔 Que désigne classe[2].nom ?

Le champ nom du troisième étudiant — on compte à partir de zéro. Et comme nom est un tableau de caractères, classe[2].nom est déjà une adresse : scanf s'en contente sans &.

29Donner une structure à une fonction : . ou ->

Une structure se passe à une fonction comme n'importe quelle valeur. Et comme n'importe quelle valeur, elle est copiée — ce qui change tout dès qu'on veut la modifier.

Par valeurPar adresse
Paramètrestruct Etudiant e struct Etudiant *e
Appelafficher(e1);vieillir(&e1);
Accès aux champse.agee->age
Ce que voit la fonctionune copiel'original
Quand l'employeron veut seulement lireon veut modifier, ou la structure est grosse
C — le programme entier
#include <stdio.h>

struct Etudiant {
    char  nom[30];
    int   age;
    float moyenne;
};

/* PAR VALEUR : la fonction recoit une copie, l'original ne bouge pas */
void vieillirCopie(struct Etudiant e)      { e.age = e.age + 1; }

/* PAR ADRESSE : la fonction recoit l'adresse, elle ecrit dans l'original */
void vieillir(struct Etudiant *e)          { e->age = e->age + 1; }

void afficher(struct Etudiant e)
{
    printf("%s, %d ans, moyenne %.2f\n", e.nom, e.age, e.moyenne);
}

int main(void)
{
    struct Etudiant e1 = { "Alice", 20, 15.5f };

    vieillirCopie(e1);
    printf("apres vieillirCopie : ");  afficher(e1);

    vieillir(&e1);
    printf("apres vieillir      : ");  afficher(e1);
    return 0;
}
sortie réelle
apres vieillirCopie : Alice, 20 ans, moyenne 15.50
apres vieillir      : Alice, 21 ans, moyenne 15.50

La démonstration tient dans la différence entre les deux lignes : vieillirCopie a bien fait e.age + 1 — sur sa copie, détruite au retour. vieillir a reçu l'adresse, et l'âge a changé pour de bon.

La flèche

C
struct Etudiant e1;
struct Etudiant *p = &e1;

e1.age   = 20;      /* une structure      -> le point  */
p->age   = 20;      /* un pointeur dessus -> la fleche */

(*p).age = 20;      /* exactement la meme chose, en plus lourd */

p->age n'est qu'un raccourci pour (*p).age : « va à l'adresse, puis prends le champ ». Les parenthèses y sont obligatoires, et c'est précisément pour ne plus les écrire que la flèche existe.

💡 Le point ou la flèche : une seule question à se poser

Est-ce que ma variable est une structure, ou l'adresse d'une structure ? Structure → point. Adresse → flèche. Il n'y a pas d'autre critère, et le compilateur refuse l'autre choix — c'est l'une des rares fautes qu'il attrape toujours.

🤔 Pourquoi écrit-on vieillir(&e1) et non vieillir(e1) ?

Parce que la fonction doit modifier l'original. Avec vieillir(e1) elle recevrait une copie, la modifierait, et la copie disparaîtrait au return — l'étudiant garderait son âge. C'est la même raison que le & de scanf.

30Alléger, imbriquer, allouer

typedef : se passer du mot struct

C
/* sans typedef */                   /* avec typedef */
struct Etudiant {                     typedef struct {
    char nom[30];                         char nom[30];
    int  age;                             int  age;
};                                    } Etudiant;

struct Etudiant e1;                   Etudiant e1;
struct Etudiant e2;                   Etudiant e2;

typedef ne crée rien de nouveau : il donne un second nom à un type qui existe déjà. Le gain est de lisibilité, et il devient sensible dès qu'on écrit des prototypes, des tableaux et des pointeurs.

Une structure dans une structure

C
struct Date {
    int jour;
    int mois;
    int annee;
};

struct Etudiant {
    char        nom[30];
    int         age;
    float       moyenne;
    struct Date naissance;      /* un champ qui est lui-meme une structure */
};

On descend alors d'un point à chaque étage :

C
e1.naissance.jour  = 15;        /* e1 -> naissance -> jour */
e1.naissance.mois  = 4;
e1.naissance.annee = 2005;
⚠️ L'ordre des définitions n'est pas libre

struct Date doit être défini avant d'être employé comme champ de struct Etudiant : le compilateur lit le fichier de haut en bas, et il ne peut pas réserver de la place pour un type qu'il ne connaît pas encore.

C'est la même règle que pour les fonctions, où le prototype devait venir avant l'appel. Le C ne revient jamais en arrière.

C — le programme entier
#include <stdio.h>
#include <string.h>

struct Date {
    int jour;
    int mois;
    int annee;
};

typedef struct {
    char        nom[30];
    int         age;
    float       moyenne;
    struct Date naissance;
} Etudiant;                       /* typedef : plus besoin d'ecrire « struct » */

int main(void)
{
    Etudiant e1;

    strcpy(e1.nom, "Alice");
    e1.age = 20;
    e1.moyenne = 15.5;
    e1.naissance.jour  = 15;
    e1.naissance.mois  = 4;
    e1.naissance.annee = 2005;

    printf("%s, ne le %02d/%02d/%d\n", e1.nom,
           e1.naissance.jour, e1.naissance.mois, e1.naissance.annee);
    return 0;
}
sortie réelle
Alice, ne le 15/04/2005

%02d complète avec un zéro à gauche jusqu'à deux chiffres : c'est ce qui donne 04 et non 4 pour le mois.

Une structure réservée à l'exécution

Les deux moitiés du chapitre se rejoignent ici : une structure peut être allouée par malloc, exactement comme un tableau.

C — le programme entier
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Etudiant {
    char  nom[30];
    int   age;
    float moyenne;
};

int main(void)
{
    struct Etudiant *p;

    p = malloc(sizeof(struct Etudiant));
    if (p == NULL) {
        printf("Erreur d'allocation memoire\n");
        return 1;
    }

    strcpy(p->nom, "Alice");
    p->age = 20;
    p->moyenne = 15.5;

    printf("Nom     : %s\n", p->nom);
    printf("Age     : %d\n", p->age);
    printf("Moyenne : %.2f\n", p->moyenne);

    free(p);
    return 0;
}
sortie réelle
Nom     : Alice
Age     : 20
Moyenne : 15.50
🔧 Le geste complet, en cinq lignes
C
p = malloc(sizeof(struct Etudiant));   /* 1. reserver         */
if (p == NULL) return 1;               /* 2. verifier         */
p->age = 20;                           /* 3. la fleche        */
strcpy(p->nom, "Alice");               /* 4. strcpy, toujours */
free(p);                               /* 5. rendre           */

sizeof(struct Etudiant) — jamais la somme des champs calculée à la main. Le compilateur ajoute des octets de remplissage entre les champs pour les aligner, et un total écrit à la main serait trop petit.

🤔 Dans strcpy(p->nom, "Alice"), pourquoi la flèche puis pas de & ?

La flèche parce que p est un pointeur sur la structure. Pas de & parce que le champ atteint, nom, est un tableau de caractères — donc déjà une adresse. strcpy veut une adresse de destination, elle l'a.

31Une chaîne, c'est un tableau de char

Le C n'a pas de type « chaîne de caractères ». Il a des tableaux, et des char : une chaîne est un tableau de caractères, et rien d'autre. Tout ce qui suit découle de cette seule phrase.

C
char nom[30];             /* 30 cases, chacune un caractere */
char mot[] = "Salut";     /* le compilateur compte pour vous */

Une chaîne peut contenir des lettres, des chiffres, des espaces et des caractères spéciaux — "BTS CIEL" et "12345" sont des chaînes tout autant que "Bonjour".

🔗 Une rangée de casiers

Chaque case du tableau contient un caractère, comme une rangée de casiers portant chacun une lettre. Ce qui fait la chaîne, ce n'est pas le tableau : c'est la marque de fin posée dans le casier d'après.

32Le '\0' : la marque de fin

Rien, dans un tableau, ne dit où la chaîne s'arrête. Le C résout cela par une convention : la chaîne se termine par le caractère spécial '\0', dit caractère de fin de chaîne.

C
char mot[] = "Salut";

/*  s  |  a  |  l  |  u  |  t  | \0   <- 6 cases pour 5 lettres */
⚠️ Il faut TOUJOURS une case de plus

« Un mot de 49 caractères maximum » se déclare char mot[50] : 49 lettres plus le '\0'. Oublier cette case est l'erreur la plus fréquente du chapitre, et elle ne se voit pas tout de suite — le programme écrit dans la case d'après, qui appartient à quelqu'un d'autre.

Deux écritures équivalentes, et la seconde montre ce que fait vraiment la première :

C
char nom[] = "Alice";
char nom[] = {'A', 'l', 'i', 'c', 'e', '\0'};   /* exactement la meme chose */

Et si l'on précise une taille plus grande, les cases restantes sont remplies de '\0' :

C
char nom[20] = "Alice";   /* A l i c e \0 \0 \0 ... jusqu'a la 20e case */

33Afficher : %s et %c

Deux formats, et il ne faut pas les confondre : %s affiche une chaîne, %c affiche un seul caractère.

C
#include <stdio.h>

int main(void)
{
    char lettre = 'A';          /* apostrophes simples : UN caractere  */
    char mot[] = "Alice";       /* guillemets : une CHAINE             */

    printf("%c\n", lettre);
    printf("%s\n", mot);

    return 0;
}
sortie
A
Alice
⚠️ 'A' et "A" ne sont pas la même chose

'A' est un caractère : une case. "A" est une chaîne : deux cases, le A et le '\0'. Le compilateur ne les accepte pas aux mêmes endroits, et c'est lui qui vous le dira.

34Saisir : scanf s'arrête à l'espace

scanf("%s", chaine) lit une chaîne au clavier. Deux choses à remarquer, et la seconde est un piège d'examen.

C
#include <stdio.h>

int main(void)
{
    char nom[30];

    printf("Entrez votre nom : ");
    scanf("%s", nom);                 /* pas de & : voir l'encart */
    printf("Bonjour %s !\n", nom);

    return 0;
}
💡 Pourquoi pas de & devant nom ?

Parce qu'un tableau est déjà une adresse — c'est tout le chapitre sur les pointeurs qui ressort ici. scanf("%d", &age) a besoin du & parce qu'age est une variable ordinaire ; nom, lui, désigne déjà l'endroit où écrire.

⚠️ La lecture s'arrête au premier espace

Avec %s, la lecture s'arrête dès qu'elle rencontre un espace, une tabulation ou un retour à la ligne. Taper Jean Dupont ne range que Jean — et Dupont reste dans le tampon, prêt à être lu par le scanf suivant, qui semblera alors « ne pas attendre ».

D'où la seconde fonction, fgets, qui lit la ligne entière et à qui l'on donne la taille du tableau :

C
#include <stdio.h>

int main(void)
{
    char nom[50];

    printf("Entrez votre nom : ");
    fgets(nom, 50, stdin);            /* stdin : le clavier */
    printf("Bonjour %s", nom);

    return 0;
}

fgets lit Jean Dupont en entier. En contrepartie il garde le retour à la ligne dans la chaîne — c'est la raison pour laquelle le printf ci-dessus n'a pas de \n.

35Les quatre fonctions de <string.h>

Elles font ce qu'un opérateur ferait dans un autre langage. En C, = et == ne marchent pas sur des chaînes — il faut ces fonctions, et il faut inclure <string.h>.

Ce que chacune fait, et ce qu'elle rend
FonctionRôleValeur rendue
strlen(ch)la longueur un size_t — sans compter le '\0'
strcpy(dest, src)copier en remplaçant — (on l'utilise pour son effet)
strcat(dest, src)coller à la suite —
strcmp(a, b)comparer 0 si identiques, non nul sinon
C
#include <stdio.h>
#include <string.h>

int main(void)
{
    char source[] = "Bonjour";
    char destination[20];
    char prenom[50] = "Alice";
    char nom[] = " Dupont";

    printf("Longueur : %zu\n", strlen(source));

    strcpy(destination, source);
    printf("Copie    : %s\n", destination);

    strcat(prenom, nom);
    printf("Colle    : %s\n", prenom);

    return 0;
}
sortie
Longueur : 7
Copie    : Bonjour
Colle    : Alice Dupont
⚠️ %zu, et non %d, pour strlen

strlen rend un size_t, qui n'est pas un int. Le format correct est %zu. Avec %d, le compilateur avertit et l'affichage peut être faux.

⚠️ La destination doit être assez grande — personne ne vérifie

strcpy et strcat écrivent sans se demander s'il y a la place. char prenom[50] = "Alice" peut recevoir " Dupont" ; un char prenom[6] aurait débordé, sans le moindre message. C'est pour cela que la destination se déclare large.

Et strcat ajoute là où strcpy remplace : c'est la seule différence entre les deux, et elle est entière.

36strcmp : le piège du 0

Pour savoir si deux chaînes sont identiques, on ne peut pas écrire nom1 == nom2 : cela comparerait deux adresses, pas deux contenus. On compare avec strcmp — qui rend 0 quand elles sont identiques.

C
#include <stdio.h>
#include <string.h>

int main(void)
{
    char nom1[] = "Alice";
    char nom2[] = "Sara";

    if (strcmp(nom1, nom2) == 0) {
        printf("Les deux noms sont identiques.\n");
    } else {
        printf("Les deux noms ne sont pas identiques.\n");
    }

    return 0;
}
sortie
Les deux noms ne sont pas identiques.
⚠️ Zéro veut dire « égales », et c'est contre-intuitif

Partout ailleurs en C, 0 veut dire faux. Ici il veut dire identiques. Écrire if (strcmp(a, b)) teste donc exactement le contraire de ce qu'on croit : la condition est vraie quand les chaînes sont différentes.

On écrit toujours la comparaison en entier : strcmp(a, b) == 0 pour « identiques », strcmp(a, b) != 0 pour « différentes ».

💡 La valeur non nulle n'est pas portable, son SIGNE l'est

strcmp("Dupont", "Durand") rend −2 sur cette machine : 'p' vaut 112, 'r' vaut 114, et la fonction s'arrête au premier caractère qui diffère. Mais la norme ne garantit que le signe — négatif si la première chaîne vient avant dans l'ordre alphabétique, positif sinon.

Donc jamais if (strcmp(a, b) == -1). On teste < 0, == 0 ou > 0.

37Parcourir caractère par caractère

Puisqu'une chaîne est un tableau, on la parcourt avec une boucle. Et puisqu'elle se termine par '\0', on sait quand s'arrêter sans connaître sa longueur :

C
#include <stdio.h>

int main(void)
{
    char mot[] = "Bonjour";
    int i = 0;

    while (mot[i] != '\0') {
        printf("%c\n", mot[i]);
        i++;
    }

    return 0;
}
sortie
B
o
n
j
o
u
r
💡 Deux façons de s'arrêter, et elles ne coûtent pas pareil
C
while (mot[i] != '\0')              /* on s'arrete sur la marque de fin */

for (i = 0; i < strlen(mot); i++)   /* on appelle strlen a CHAQUE tour */

La seconde relit toute la chaîne à chaque tour pour en recompter la longueur. Sur un mot de 25 lettres, cela fait 25 parcours au lieu d'un. Si l'on veut la forme for, on range la longueur avant : size_t n = strlen(mot); puis i < n.

⚡ Récap 2 minutes

Le langage

  • Compilé : écrire .c → compiler → exécuter. Une erreur de syntaxe est refusée avant le lancement.
  • Squelette : #include <stdio.h>, int main(), { }, return 0;.
  • Typé et sensible à la casse : int age = 20; — age ≠ Age, et Int n'existe pas.
  • Chaque instruction finit par ; · chaque bloc tient entre { } · l'indentation ne fait pas la syntaxe.
  • printf lit, scanf écrit : d'où le &, obligatoire uniquement avec scanf.
  • Codes de format : %d entier · %f réel (six décimales) · %.2f deux décimales · %c caractère · %s chaîne.
  • 6 / 7 vaut 0 : entre entiers, la division est entière.
  • = affecte, == compare. Dans un if, toujours ==.
  • && et || dans les conditions ; & et | travaillent bit à bit.
  • a++ renvoie l'ancienne valeur, ++a la nouvelle.
  • Un break par case, sinon l'exécution continue en cascade.
  • for (…) ; — ce point-virgule vide la boucle : le corps devient l'instruction vide, et ce qui suit ne s'exécute qu'une fois.
  • Choisir sa boucle : nombre de tours connu → for ; au moins un passage → do…while ; sinon while.

Fonctions et tableaux

  • Type de retour · nom · paramètres · corps — toujours dans cet ordre. void quand il n'y a rien à rendre.
  • Paramètre = la variable de la définition. Argument = la valeur de l'appel.
  • Le prototype se termine par ;, la définition par }. Sans prototype, la définition doit précéder l'appel.
  • Le C distingue majuscules et minuscules : afficherCarre et affichercarre sont deux fonctions différentes — dont une seule existe.
  • Une valeur retournée et non rangée est perdue, sans le moindre avertissement.
  • Locale par défaut, globale en dernier recours. Une fonction qui reçoit tout ce dont elle a besoin se lit seule.
  • Les indices vont de 0 à taille − 1 : la boucle s'écrit i < n, jamais i <= n.
  • Le C ne vérifie aucun indice. Sortir du tableau ne provoque aucune erreur — seulement un bogue plus tard.
  • Un tableau passé à une fonction perd sa taille : sizeof y rend 8, pas 40. On passe toujours n à côté.
  • Mais la fonction peut le modifier, puisqu'elle en reçoit l'adresse. C'est ce qui fait marcher SaisirTableau().

Pointeurs, mémoire et structures

  • Une variable a trois choses : un nom, une valeur, une adresse. age demande la valeur, &age demande l'adresse.
  • Un pointeur contient une adresse. int *p; ne range pas un entier : il range de quoi en atteindre un.
  • L'étoile a deux rôles : dans int *p; elle déclare, dans *p = 30; elle accède. Regarder s'il y a un type devant.
  • tab == &tab[0] — le nom d'un tableau est l'adresse de son premier élément. C'est pour ça qu'un tableau n'est jamais copié.
  • p[i] et *(p + i) sont la même chose. On choisit l'une OU l'autre : p[i] avec p++ saute une case sur deux.
  • p++ avance d'une case du type pointé, pas d'un octet.
  • malloc(n * sizeof(int)) réserve à l'exécution et rend une adresse ; calloc(n, sizeof(int)) fait pareil et met tout à zéro.
  • On teste toujours if (p == NULL), et on finit toujours par free(p).
  • struct regroupe des champs de types différents ; l'accolade fermante est suivie d'un ;.
  • Un champ chaîne se remplit avec strcpy, jamais avec =.
  • Structure → le point · pointeur sur structure → la flèche. p->age est un raccourci pour (*p).age.
  • Passée par valeur, une structure est copiée : pour la modifier, on passe son adresse.

Sources exploitées

  • « Algorithmique et programmation » — le professeur, BTS CIEL 2e année 2026-2027, séance du 18/09/2026 (112 diapositives : chapitres 1 à 3)
  • TP2 — Langage C, deux versions du 18/09/2026 (4 puis 8 exercices)
  • notes personnelles d'élève — compilateur en ligne, Code::Blocks
  • Polycopié « Algorithmique et programmation », chapitre 4 — les fonctions (28 diapositives, pages 113 à 140)
  • Le même polycopié, chapitre 1 pour la notion de tableau en pseudo-code et chapitre 2 pour la déclaration en C
  • Sujet « TP 3 : les fonctions et les tableaux », neuf exercices
  • Le même polycopié, séance du 09/10/2026 (243 diapositives) — chapitre 5, les pointeurs (pages 167 à 207) et chapitre 6, les structures (pages 208 à 243)
  • Sujet « TP 5 : les pointeurs », quatre exercices

Toutes les sorties annoncées « sortie réelle » ont été obtenues en compilant et en exécutant le programme, pas en le lisant.