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à.
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éristique | Ce 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. |
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.
| Étape | Ce qui se passe | Ce 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 |
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.
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.
#include <stdio.h>
int main()
{
printf("Bonjour");
return 0;
}| Élément | Rô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. |
return 0 et pas return tout courtCette 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.
int adresseIP;
int nombrePaquets;
float debit;| Règle | Valide | Invalide |
|---|---|---|
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.
int float char void const
if else switch case default
for while do break continue
return struct sizeof gotoLes séparateurs
| Signe | Rôle | Exemple |
|---|---|---|
; | Termine une instruction | int nombre; nombre = 10; |
( ) | Fonctions et expressions | printf("Bonjour"); |
{ } | Délimitent un bloc d'instructions | if (n > 0) { … } |
[ ] | Tableaux | int notes[5]; |
, | Sépare plusieurs déclarations | int a, b, c; |
Les commentaires
// Cette variable contient le nombre de paquets
int nombrePaquets;
/*
Calcul du débit réseau
à partir du nombre de paquets
*/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 pasLes 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.
_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 entrevitesseetmax.
C'est pour ça qu'on écrit vitesseMax ou
vitesse_max — jamais avec un tiret, contrairement au CSS.
05Les types de base
| Type | Contient | Exemple | Taille observée |
|---|---|---|---|
int | un nombre entier | 25 | 4 octets |
float | un nombre réel | 12.5 | 4 octets |
double | un réel plus précis | 12.56789 | 8 octets |
char | un seul caractère | 'A' | 1 octet |
void | absence de valeur | fonction sans retour | — |
L'opérateur sizeof donne la taille en octets. Les valeurs ci-dessus ne sont
pas à croire sur parole : elles se mesurent.
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));int : 4 octets
char : 1 octets
float : 4 octets
double : 8 octets4 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 choseLes 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.
char lettre = 'A'; // un caractère
char mot[] = "A"; // une chaîne : 'A' puis le marqueur de fin06Dé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.
int nombrePC; // un entier
int a, b, c; // trois entiers d'un coupint 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.
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ère | 25 |
| Constante flottante | 3.14 |
| Constante caractère | 'A' |
| Constante chaîne | "Bonjour" |
Pour nommer une constante et empêcher toute modification, on utilise le
mot-clé const :
const float PI = 3.14159;
const int PORT_HTTP = 80;
PI = 3; // ERREUR de compilation : PI est constanteÉ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.
| Famille | Opérandes | Exemple |
|---|---|---|
| Unaire | 1 | ++a |
| Binaire | 2 | a + b |
| Ternaire | 3 | a > b ? a : b |
Les opérateurs unaires
| Opérateur | Signification |
|---|---|
& | adresse d'une variable |
* | indirection : la valeur à une adresse |
++ | incrémentation (ajoute 1) |
-- | décrémentation (retire 1) |
sizeof | taille en octets |
! | NON logique |
- | moins unaire : change le signe |
a++ et ++a : la différence qui coûte des pointsLes 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.
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);a++ : renvoie 5, a vaut ensuite 6
++b : renvoie 6, b vaut ensuite 6Tant 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érateurs | Signification |
|---|---|
+ - * / % |
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 |
& et && ne font pas le même travailLe 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.
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); // logique1 7 6 10
1 1 05 & 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.
6 / 7 vaut 0Quand les deux opérandes sont des entiers, / fait une division
entière : la partie décimale est jetée, pas arrondie.
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 resteC'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.
condition ? expression_si_vrai : expression_si_fauxint a = 10;
int b = 20;
int max = (a > b) ? a : b; // a > b est faux → max = b = 20int 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 :
printf("bonjour !");bonjour !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.
int age = 20;
printf("%d", age); // 20| Code | Type | Exemple affiché |
|---|---|---|
%d | entier (int) | 25 |
%f | réel (float) | 15.500000 |
%lf | réel (double) | 15.567800 |
%c | caractère (char) | A |
%s | chaîne de caractères | Bonjour |
%e | réel en notation exponentielle | 1.5e+03 |
%u | entier non signé | 25 |
%x | entier en hexadécimal | ff |
Plusieurs valeurs, et le nombre de décimales
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);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 deuxPar 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.
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
| Écriture | Effet |
|---|---|
\n | passe à la ligne suivante |
\t | tabulation |
\\ | 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.
scanf("code_format", &variable);& — l'analogie de la boîte aux lettresAvec 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.
int age;
float note;
printf("Entrez votre age : ");
scanf("%d", &age);
printf("Entrez votre note : ");
scanf("%f", ¬e);
printf("\nVous avez %d ans et votre note est %.2f/20\n", age, note);Entrez votre age : 20
Entrez votre note : 15.5
Vous avez 20 ans et votre note est 15.50/20| Type de la variable | Code avec scanf | Exemple |
|---|---|---|
int | %d | scanf("%d", &n); |
float | %f | scanf("%f", &x); |
double | %lf | scanf("%lf", &x); |
char | %c |
scanf(" %c", &c); noter l'espace |
%c n'est pas une coquilleQuand 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.
Le cours donne :
float note;
printf("Entrez votre note : ");
scanf("%f", ¬e);
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);.
| Fonction | Rô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.
| Famille | Rôle | Instructions |
|---|---|---|
| Conditionnelles | choisir selon une condition | if, if…else, switch |
| Répétitives | répéter un bloc | while, do…while, for |
| Branchement | rompre le déroulement normal | break, continue, goto |
if — exécuter si
int age = 20;
if (age >= 18) {
printf("Majeur\n");
}if…else — choisir entre deux
int age;
printf("entrez votre age : ");
scanf("%d", &age);
if (age >= 18) {
printf("majeur\n");
} else {
printf("mineur\n");
}= affecte, == compareC'est l'erreur la plus coûteuse du langage, parce qu'elle compile.
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 =.
Sans accolades, un if ne gouverne qu'une seule instruction :
if (n > 0)
printf("positif\n");
printf("on continue\n"); // s'affiche TOUJOURSLa 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.
switch (expression) {
case valeur1:
// instructions
break;
case valeur2:
// instructions
break;
default:
// si aucune valeur ne correspond
}#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.
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");
}break, l'exécution continue dans le case suivantC'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.
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 ?
| Boucle | Condition testée | Quand l'employer |
|---|---|---|
while | avant chaque tour | on ne sait pas combien de fois ; peut ne jamais s'exécuter |
do…while | après chaque tour | il faut au moins un passage — une saisie à valider, un menu à réafficher |
for | avant chaque tour | le nombre de tours est connu, ou l'on compte |
while
#include <stdio.h>
int main()
{
int i = 1;
while (i <= 5) {
printf("%d\n", i);
i++;
}
return 0;
}1
2
3
4
5Le 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.
#include <stdio.h>
int main()
{
int i = 1;
do {
printf("%d\n", i);
i++;
} while (i <= 5);
return 0;
}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 billetswhile : « 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.
for (initialisation; condition; modification) {
instructions;
}#include <stdio.h>
int main()
{
int i;
for (i = 1; i <= 5; i++) {
printf("%d\n", i);
}
return 0;
}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.
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.
int n = 5, i;
for (i = 2; i < n; i++); // ← le corps de la boucle, c'est ce « ; »
printf("%d\n", i);5La 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.
| Tour | i au test | affiché | i après i++ | i après i += 3 |
|---|---|---|---|---|
| 1 | 3 | 3 - | 4 | 7 |
| 2 | 7 | 7 - | 8 | 11 |
| 3 | 11 | — | 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.
#include <stdio.h>
int main()
{
int i;
for (i = 1; i <= 10; i++) {
if (i == 5)
break;
printf("%d\n", i);
}
return 0;
}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.
#include <stdio.h>
int main()
{
int i;
for (i = 1; i <= 5; i++) {
if (i == 3)
continue;
printf("%d\n", i);
}
return 0;
}1
2
4
5Le 3 manque — mais 4 et 5 sont bien là : contrairement à break,
continue ne quitte pas la boucle.
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
#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;
}debut du programme
fin du programmegoto, et une majuscule de tropLe 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 sertIl 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 :
carre = nombre * nombre;Avec une fonction, il s'écrit une fois et se réutilise :
#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;
}25 100 400Vous 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.
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.
type_retour nom_fonction(paramètres)
{
instructions;
return valeur;
}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 ».
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.
| Sans valeur de retour | Avec 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.
#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;
}Bonjour !
Le carré de 5 est 25Le 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 :
#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é.
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 :
int nombre;
nombre = saisirNombre();() vide et (void) ne disent pas la même choseEn 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
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.
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 :
nom_fonction(arguments); /* sans retour : on l'exécute */
variable = nom_fonction(arguments); /* avec retour : on range le résultat */É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.
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.
| Avec prototype | Sans 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 |
#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;
}résultat = 8int 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.
#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.
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
#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;
}le carré de 2 est : 4Ici 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 = ….
#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;
}le carré de 2 est : 4Privilé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
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.
[5] sort déjà du tableau.#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;
}octet 0 : 10
octet 1 : 20
octet 2 : 30
octet 3 : 40
octet 4 : 50La 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.
É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.
& ne disparaît pasfloat 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 :
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 :
float notes[5];19.2 · Modifier une case après coup
Une case de tableau s'affecte comme n'importe quelle variable, à tout moment :
#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;
}nombres[1] = 50int nombres[] = {…};Quand les valeurs sont données à la déclaration, on peut laisser les crochets vides : le compilateur compte.
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 :
#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;
}somme = 150Oublier 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 boucleLe 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.
#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;
}dans main : sizeof(mesures) = 40
dans la fonction : sizeof(t) = 8Dans 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.
#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;
}Débit moyen : 102.88 Mb/s
Débit maximum : 115 Mb/sComme 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.
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
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.
#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;
} 120 80 200 45
90 150 60 110
200 170 95 30
trafic[1][2] vaut 60Les 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.
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] */
}
}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.
#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;
}commutateur 1 : 445
commutateur 2 : 410
commutateur 3 : 495
port 1 : 410
port 2 : 400
port 3 : 355
port 4 : 185Remplir au clavier
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.
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 */À 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.
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.
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 écrit | On demande | On obtient |
|---|---|---|
age | Quelle est la valeur ? | 20 |
&age | Où 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.
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
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.
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.
int age = 20; /* age contient 20 */
int *p; /* p contiendra l'adresse d'un int */
p = &age; /* desormais : p pointe vers age */Déclarer
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érateur | Se lit | Part de | Donne |
|---|---|---|---|
& | « 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 *.
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
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);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
#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;
}p contient l'adresse de age : oui
age vaut 20, et *p vaut 20
un int occupe 4 octetsUn 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.
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.
#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;
}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;.
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.
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.
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
#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;
}1
2
6
0
7p++ 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
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.
La diapositive 195 du polycopié donne ce programme, et annonce qu'il affiche
1 2 6 0 7 :
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 :
#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;
}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 18Une 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++.
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.
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.
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
#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
p = malloc(n * sizeof(int));
if (p == NULL) {
printf("memoire insuffisante\n");
return 1;
}malloc peut échouer, et il le dit en rendant NULLLe 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.
#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;
}p[0] = 10
p[1] = 20
p[2] = 30
p[3] = 40
p[4] = 50calloc — demander des cases, et les vouloir à zéro
int *a = malloc(4 * sizeof(int)); /* 4 cases, contenu indetermine */
int *b = calloc(4, sizeof(int)); /* 4 cases, toutes a ZERO */malloc | calloc | |
|---|---|---|
| Arguments | un total d'octets | un nombre d'objets, et la taille d'un objet |
| Contenu initial | ce qui traînait là | des zéros |
| Quand le choisir | on va tout écrire soi-même | on va accumuler, compter, ou lire avant d'écrire |
#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;
}calloc met tout a zero :
b[0] = 0
b[1] = 0
b[2] = 0
b[3] = 0Le 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
free(p); /* la zone redevient disponible pour le programme */Trois temps, toujours les mêmes, et dans cet ordre :
malloc() ou calloc() -> reserver
utiliser -> p[0], p[1], ...
free() -> rendre| Faute | Nom | Effet |
|---|---|---|
on oublie free | fuite mémoire | le programme grossit sans fin — invisible sur un exercice, fatal sur un service |
free deux fois | double libération | plantage, ou pire : une faille exploitable |
on utilise après free | pointeur 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.
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.
char nom[30]; /* trois variables independantes : */
int age; /* rien ne dit qu'elles decrivent */
float moyenne; /* le meme etudiant */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
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.
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
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
e1.age = 20;
e1.moyenne = 15.5;=#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 :
#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
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.
#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;
}Nom : Alice
Age : 20
Moyenne : 15.50La 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 :
Nom : Alice
Age : 20
Moyenne : 15.50nom : alice
age : 20
moyenne : 15.50Compilé 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 &
| Champ | Type | Saisie | Pourquoi |
|---|---|---|---|
e1.age | int |
scanf("%d", &e1.age); |
variable simple → il faut son adresse |
e1.moyenne | float |
scanf("%f", &e1.moyenne); |
idem |
e1.nom | char[] |
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].
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 ».
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.
#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;
}Alice 20 ans 15.50
Bilal 19 ans 12.00
Chloe 21 ans 17.25
moyenne de la classe : 14.92%-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.
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.
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);
}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 valeur | Par adresse | |
|---|---|---|
| Paramètre | struct Etudiant e |
struct Etudiant *e |
| Appel | afficher(e1); | vieillir(&e1); |
| Accès aux champs | e.age | e->age |
| Ce que voit la fonction | une copie | l'original |
| Quand l'employer | on veut seulement lire | on veut modifier, ou la structure est grosse |
#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;
}apres vieillirCopie : Alice, 20 ans, moyenne 15.50
apres vieillir : Alice, 21 ans, moyenne 15.50La 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
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.
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.
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
/* 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
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 :
e1.naissance.jour = 15; /* e1 -> naissance -> jour */
e1.naissance.mois = 4;
e1.naissance.annee = 2005;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.
#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;
}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.
#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;
}Nom : Alice
Age : 20
Moyenne : 15.50p = 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.
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.
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".
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.
char mot[] = "Salut";
/* s | a | l | u | t | \0 <- 6 cases pour 5 lettres */« 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 :
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' :
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.
#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;
}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.
#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;
}& 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.
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 :
#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>.
| Fonction | Rôle | Valeur 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 |
#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;
}Longueur : 7
Copie : Bonjour
Colle : Alice Dupont%zu, et non %d, pour strlenstrlen 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.
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.
#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;
}Les deux noms ne sont pas identiques.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 ».
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 :
#include <stdio.h>
int main(void)
{
char mot[] = "Bonjour";
int i = 0;
while (mot[i] != '\0') {
printf("%c\n", mot[i]);
i++;
}
return 0;
}B
o
n
j
o
u
rwhile (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.