LANGAGE C : LES BASES, GROUPE GENIUS
Ce cours pose les fondations du langage C et rien d'autre : comment un programme est fabriqué, comment il est structuré, comment on déclare des données, comment on calcule, comment on affiche et comment on lit une saisie. Tout y est détaillé, schémas à l'appui, avec quinze questions de cours et trente et un exercices corrigés.
Le périmètre, annoncé clairement. Vous ne trouverez ici ni structures conditionnelles, ni boucles, ni tableaux, ni matrices, ni structures, ni écriture de vos propres fonctions. Chacun de ces sujets mérite son propre cours.
En revanche, l'emploi des fonctions prédéfinies des bibliothèques standard est traité ici : c'est une base, pas un sujet avancé.
Ce que vous saurez faire à la fin
- Expliquer ce qui se passe entre votre fichier
.cet le programme qui s'exécute, étape par étape. - Écrire, compiler et exécuter un programme, et lire un message d'erreur pour savoir quelle étape se plaint.
- Choisir le bon type de données et prévoir ce qu'il peut contenir.
- Maîtriser
printfau caractère près, etscanfsans tomber dans ses deux pièges classiques. - Reconnaître les erreurs de débutant avant qu'elles ne vous coûtent une soirée.
À qui s'adresse ce cours
À toute personne qui n'a jamais écrit une ligne de C, ou qui l'a appris trop vite et veut reprendre des bases solides. Aucune connaissance préalable n'est supposée.
Tous les affichages montrés dans ce cours ont été
vérifiés par le calcul avant
publication : tailles de types, étendues, arrondis de
printf, résultats de division entière.
1. Comprendre ce qu'est le C avant d'en écrire
Le C a été créé en 1972 par Dennis Ritchie, dans les laboratoires Bell, pour écrire le système d'exploitation Unix. Plus de cinquante ans plus tard, il est toujours là, et pas par nostalgie.
Le noyau de Linux est écrit en C. Celui de Windows aussi, en grande partie. Les bibliothèques qui font tourner Python, PHP et Java sont écrites en C. Le programme qui pilote le capteur d'un téléphone, la puce d'une carte bancaire ou le calculateur d'un véhicule : en C.
La raison tient en une phrase : le C vous laisse voir la machine. Là où d'autres langages vous protègent derrière une couche d'abstraction, le C vous met devant la mémoire, les octets et les types. C'est plus exigeant. C'est aussi ce qui en fait le meilleur langage pour comprendre ce qu'un ordinateur fait réellement.
Ce que ce cours couvre, et ce qu'il ne couvre pas. Vous trouverez ici les fondations : comment un programme est fabriqué, comment il est structuré, comment on déclare des données et comment on dialogue avec l'utilisateur.
Les structures conditionnelles, les boucles, les tableaux, les matrices, les structures et l'écriture de vos propres fonctions font l'objet de cours séparés. Ici, on pose le socle, et on le pose solidement.
Un langage compilé : ce que cela change
Il existe deux grandes familles de langages, et la différence se voit dès le premier programme.
Un langage interprété, comme Python, est lu et exécuté ligne par ligne par un autre programme appelé interpréteur. Vous écrivez, vous lancez, ça tourne. Pratique, mais il faut que l'interpréteur soit installé sur la machine, et le programme est plus lent.
Un langage compilé, comme le C, est d'abord traduit entièrement en instructions que le processeur comprend directement. Cette traduction produit un fichier exécutable autonome. Le programme démarre plus vite, tourne plus vite, et n'a besoin de rien d'autre pour fonctionner.
Un programme C n'est pas lu par la machine : il est traduit une fois pour toutes dans la langue de la machine, puis exécuté.
Cela a une conséquence immédiate : vous ne pouvez pas exécuter un programme qui contient une faute. Le compilateur refuse de traduire et vous signale l'erreur. C'est pénible au début, et c'est un cadeau : la moitié des bogues sont attrapés avant même que le programme ne tourne.
Autre conséquence : le fichier produit ne fonctionne que sur le type de machine pour lequel il a été compilé. Un programme compilé sous Windows ne tourne pas tel quel sous Linux. Le code source, lui, est le même partout : il suffit de le recompiler.
Ce dont vous avez besoin
- Un éditeur de texte : Visual Studio Code, Code::Blocks, Notepad++, ou même le Bloc-notes. Pas un traitement de texte : Word ajoute de la mise en forme invisible qui casse tout.
- Un compilateur :
gccest le plus répandu. Sous Windows on l'installe avec MinGW-w64 ou MSYS2 ; sous Linux il est souvent déjà présent ; sous macOS il arrive avec les outils Xcode. - Un terminal : pour lancer la compilation et voir les messages d'erreur. C'est là que tout se passe.
Un fichier source C porte l'extension .c. Pas
.txt, pas .c.txt : .c. C'est une
erreur de débutant classique sous Windows, où l'explorateur cache les
extensions.
2. De votre fichier au programme : la chaîne de compilation
Quand vous tapez gcc bonjour.c -o bonjour, il ne se passe pas
une chose, mais quatre. Les connaître vous fera gagner des heures le jour où
un message d'erreur sera incompréhensible : parce que vous saurez
quelle étape se plaint.
Étape 1 : Le préprocesseur
Avant toute traduction, un programme appelé préprocesseur
relit votre fichier et traite toutes les lignes qui commencent par un dièse
#. Il ne comprend rien au C : il fait du copier-coller et du
remplacement de texte.
Il recopie le contenu des fichiers demandés par #include,
remplace les constantes définies par #define, et supprime les
commentaires. Le résultat est un fichier C « à plat », sans aucune directive.
On peut le voir avec gcc -E bonjour.c : attendez-vous à
plusieurs milliers de lignes, car stdio.h est volumineux.
Étape 2 : Le compilateur
C'est l'étape que l'on appelle « compilation » au sens strict. Le compilateur analyse le C, vérifie la syntaxe et les types, puis traduit en assembleur : un langage très proche du processeur, mais encore lisible par un humain.
C'est ici que naissent la plupart des messages d'erreur : point-virgule oublié, variable non déclarée, type incompatible.
Étape 3 : L'assembleur
L'assembleur traduit le texte assembleur en code machine :
des octets que le processeur exécute directement. Le résultat est un
fichier objet, d'extension .o (ou .obj sous
Windows). Cette étape échoue très rarement.
Étape 4 : L'éditeur de liens
Votre fichier objet contient votre code, mais pas celui de
printf. L'éditeur de liens va chercher ce code
dans les bibliothèques du système, le rattache au vôtre, et produit enfin le
programme exécutable.
Savoir lire l'erreur. Deux messages se ressemblent et ne veulent pas du tout dire la même chose.
error: 'sqrt' undeclared vient du compilateur :
il ne sait pas ce qu'est sqrt. Vous avez oublié
#include <math.h>.
undefined reference to 'sqrt' vient de l'éditeur de
liens : il sait ce qu'est sqrt, mais il n'en trouve pas
le code. Il faut ajouter -lm à la fin de la commande.
Compiler en pratique
gcc bonjour.c -o bonjour # produit le programme « bonjour » ./bonjour # l'exécute sous Linux ou macOS bonjour.exe # l'exécute sous Windows
Trois options méritent d'être prises comme une habitude dès le premier jour :
gcc -Wall -Wextra -std=c17 bonjour.c -o bonjour
-Wallet-Wextraactivent les avertissements. Ce ne sont pas des erreurs : le programme compile quand même. Mais un avertissement signale presque toujours un vrai problème. Traitez-les comme des erreurs.-std=c17fixe la version de la norme utilisée, pour que votre programme se comporte pareil chez tout le monde.
Un programme qui compile sans avertissement n'est pas forcément juste. Un programme qui compile avec des avertissements est presque toujours faux.
3. La structure d'un programme C
Voici le programme le plus court qui fasse quelque chose d'utile. Chaque ligne a un rôle précis, et nous allons les prendre une par une.
#include <stdio.h>
int main(void)
{
int age = 20;
printf("J'ai %d ans\n", age);
return 0;
}
La ligne #include <stdio.h>
Le C de base ne sait rien faire : il ne sait ni afficher, ni lire, ni calculer une racine carrée. Tout cela vient de bibliothèques.
stdio.h signifie standard input/output : entrées et
sorties standard. C'est le fichier qui décrit printf et
scanf. Sans cette ligne, le compilateur ne sait pas ce qu'est
printf et refuse de continuer.
Notez l'absence de point-virgule. Une directive du préprocesseur n'est pas une instruction C : elle s'arrête à la fin de la ligne.
La ligne int main(void)
C'est le point d'entrée du programme. Quand le système
lance votre exécutable, il cherche main et commence là. Un
programme C sans main ne peut pas démarrer.
intannonce quemainrenvoie un entier au système en se terminant.mainest le nom imposé par la norme. NiMain, niprincipal.(void)signifie « ce programme ne reçoit aucun argument ». On peut en recevoir, mais ce n'est pas le sujet ici.
Les accolades
L'accolade ouvrante { et l'accolade fermante }
délimitent le corps de main : tout ce que le
programme doit faire. Elles vont toujours par paire.
Entre les deux, on indente : quatre espaces, ou une tabulation. Le compilateur s'en moque complètement : il lirait tout aussi bien un programme écrit sur une seule ligne. L'indentation est pour les humains, et elle n'est pas optionnelle : un code mal indenté est un code où les erreurs se cachent.
Les instructions et le point-virgule
Chaque instruction se termine par un point-virgule. C'est lui qui dit « cette instruction est finie », et non le retour à la ligne.
int age = 20; /* une instruction */
printf("Bonjour\n"); /* une autre */
Le point-virgule oublié est l'erreur numéro un du débutant. Elle est d'autant plus déroutante que le compilateur signale souvent la ligne suivante : il continue de lire, croit que l'instruction se poursuit, et ne comprend qu'au mot d'après. Quand une erreur vous paraît absurde, regardez la ligne du dessus.
Les commentaires
/* un commentaire sur plusieurs lignes */ // un commentaire jusqu'à la fin de la ligne
Le préprocesseur les efface : ils ne coûtent rien à l'exécution. Écrivez-y pourquoi vous faites quelque chose, pas ce que vous faites : le code le dit déjà.
La ligne return 0;
Elle termine main et renvoie 0 au système d'exploitation. Par
convention universelle : 0 signifie que tout s'est bien passé,
toute autre valeur signale une erreur.
Ce n'est pas décoratif. Les scripts et les outils d'automatisation lisent ce code de retour pour savoir si votre programme a réussi.
Ce qu'il faut retenir
#includeapporte des outils ; pas de point-virgule.mainest le point d'entrée obligatoire.- Les accolades délimitent un bloc, toujours par paire.
- Chaque instruction se termine par un point-virgule.
return 0;annonce au système que tout s'est bien passé.- Le C distingue les majuscules des minuscules :
ageetAgesont deux variables différentes.
4. Les directives du préprocesseur
Toutes les lignes qui commencent par # s'adressent au
préprocesseur, pas au compilateur. Il faut bien comprendre ce qu'il fait :
du remplacement de texte, et rien d'autre. Il ne vérifie
aucun type, il ne calcule rien, il ne comprend pas le C.
#include : aller chercher du code ailleurs
Cette directive remplace la ligne par le contenu entier du fichier désigné. Il existe deux écritures, et elles ne cherchent pas au même endroit.
#include <stdio.h> /* chevrons : les bibliotheques du systeme */ #include "mes_outils.h" /* guillemets : d'abord a cote de mon fichier */
Les fichiers d'en-tête les plus utiles au début :
| Fichier | Ce qu'il apporte |
|---|---|
<stdio.h> |
printf, scanf, putchar,
getchar |
<stdlib.h> |
abs, rand, exit, gestion
mémoire |
<math.h> |
sqrt, pow, fabs,
sin, log |
<limits.h> |
INT_MAX, INT_MIN, CHAR_MAX… |
<float.h> |
FLT_MAX, DBL_MAX et les précisions |
#define : donner un nom à une valeur
#define PI 3.14159265 #define TVA 0.1925 #define NOM_ECOLE "Groupe Genius"
Partout où le préprocesseur rencontre PI, il écrit
3.14159265 à la place. Le compilateur ne verra jamais le mot
PI.
Pas de point-virgule à la fin, sinon il serait recopié lui aussi, ce qui produit des erreurs incompréhensibles.
L'usage veut qu'on écrive ces noms en majuscules. Ce n'est pas une obligation du langage, mais cela permet de reconnaître au premier coup d'œil ce qui est remplacé par le préprocesseur.
Les macros, et le piège des parenthèses
Un #define peut prendre des paramètres :
#define DOUBLE(x) x * 2
Écrivez DOUBLE(3) : le préprocesseur produit
3 * 2, soit 6. Parfait. Maintenant écrivez
DOUBLE(2 + 1) :
DOUBLE(2 + 1) devient 2 + 1 * 2 qui vaut 4
On attendait 6. Le préprocesseur a remplacé bêtement, et la priorité de la multiplication a fait le reste. La parade est de tout parenthéser :
#define DOUBLE(x) ((x) * 2) DOUBLE(2 + 1) devient ((2 + 1) * 2) qui vaut 6
Dans une macro, chaque paramètre va entre parenthèses, et l'expression entière aussi. Toujours.
#ifdef et les gardes d'inclusion
Le préprocesseur sait aussi inclure ou non une portion de code :
#define DEBUG
#ifdef DEBUG
printf("valeur intermediaire : %d\n", x);
#endif
Si DEBUG est défini, la ligne existe ; sinon elle disparaît
avant même la compilation. C'est ainsi qu'on laisse des traces de mise au
point sans ralentir le programme final.
La variante #ifndef (« si ce n'est pas défini ») sert aux
gardes d'inclusion, qui empêchent un même fichier d'être
inclus deux fois :
#ifndef MES_OUTILS_H #define MES_OUTILS_H /* le contenu du fichier */ #endif
Voir ce que fait vraiment le préprocesseur. La commande
gcc -E mon_fichier.c affiche le fichier après son passage, sans
compiler. C'est le meilleur moyen de comprendre une macro qui se comporte
bizarrement : on lit le texte réellement produit.
5. Les types de données
En C, toute donnée a un type, déclaré à l'avance. Ce n'est pas une formalité administrative : le type décide de trois choses à la fois.
- Combien de place la donnée occupe en mémoire.
- Comment les octets doivent être interprétés : le même motif binaire ne vaut pas la même chose selon qu'on le lit comme un entier ou comme un flottant.
- Quelles opérations ont un sens, et comment elles se comportent.
Les entiers
| Type | Taille habituelle | Étendue | Marqueur printf |
|---|---|---|---|
char | 1 octet | −128 à 127 | %c ou %d |
short | 2 octets | −32 768 à 32 767 | %hd |
int | 4 octets | −2 147 483 648 à 2 147 483 647 | %d |
long | 4 ou 8 octets | dépend du système | %ld |
long long | 8 octets | environ ±9,2×1018 | %lld |
int est le type de travail par défaut : c'est celui que le
processeur manipule le plus vite. Prenez-le, sauf raison précise de faire
autrement.
Pourquoi « taille habituelle » et pas « taille ». La norme
du C ne fixe pas les tailles exactes : elle impose seulement des minimums et
un ordre (char ≤ short ≤ int
≤ long). Le cas le plus connu : long fait
4 octets sous Windows 64 bits et 8 octets sous Linux
et macOS 64 bits. C'est une source de bogues bien réelle quand on
porte un programme d'un système à l'autre.
La bonne habitude est de ne jamais supposer : demandez à la machine avec
sizeof.
signed et unsigned
Par défaut, les entiers sont signés : ils peuvent être
négatifs, au prix d'un bit consacré au signe. En ajoutant
unsigned, on renonce aux négatifs et on double l'étendue
positive.
int n; /* -2 147 483 648 a 2 147 483 647 */ unsigned int m; /* 0 a 4 294 967 295 */
À réserver aux quantités qui ne peuvent pas être négatives : une taille, un compteur, une adresse. Et attention : mélanger signé et non signé dans un même calcul produit des résultats surprenants. En cas de doute, restez en signé.
Les nombres à virgule
| Type | Taille | Chiffres significatifs sûrs | Marqueur printf |
|---|---|---|---|
float | 4 octets | environ 7 | %f |
double | 8 octets | environ 15 | %lf ou %f |
long double | 8, 12 ou 16 octets | variable | %Lf |
Les deux suivent la norme IEEE 754 : un bit de signe, un exposant, une
mantisse. float dispose de 23 bits de mantisse,
double de 52 : d'où la différence de précision.
Prenez double par défaut. Le
float ne garantit que sept chiffres, et ils s'épuisent vite.
Un détail qui surprend : un nombre écrit 3.5 dans le code
source est un double. Pour obtenir un
float, il faut écrire 3.5f.
Les flottants ne sont pas exacts. 0.1 ne peut
pas s'écrire exactement en binaire, pas plus que 1/3 ne s'écrit exactement en
base dix. En double, 0.1 + 0.2 vaut
0,30000000000000004 et non 0,3.
Conséquence pratique : ne comparez jamais deux flottants avec
==, et ne comptez jamais de l'argent en
float. Le chapitre
sur le codage des réels explique exactement pourquoi.
Le caractère : un entier déguisé
Le type char stocke un caractère… sous la forme d'un nombre.
C'est son code ASCII.
char lettre = 'A';
printf("%c\n", lettre); /* affiche A */
printf("%d\n", lettre); /* affiche 65 */
Même case mémoire, deux façons de la lire. Cela autorise de l'arithmétique
sur les caractères : 'a' - 'A' vaut 32, et
'7' - '0' vaut 7, c'est ainsi qu'on convertit un chiffre
caractère en chiffre nombre.
Attention aux quotes : 'A' avec des apostrophes simples est un
caractère ; "A" avec des guillemets est une chaîne. Ce ne sont
pas les mêmes choses, et le compilateur le dira.
L'opérateur sizeof
printf("un int occupe %zu octets\n", sizeof(int));
printf("un double occupe %zu octets\n", sizeof(double));
sizeof répond en octets. Le marqueur %zu est
celui qui convient à son type de retour. C'est la seule façon fiable de
connaître les tailles sur votre machine.
Les conversions
Quand deux types se rencontrent dans un calcul, le C convertit automatiquement, et c'est là que les surprises arrivent.
printf("%d\n", 5 / 2); /* affiche 2 : deux entiers, division entiere */
printf("%f\n", 5 / 2.0); /* affiche 2.500000 : un double, division reelle */
printf("%f\n", (float)5 / 2); /* affiche 2.500000 : conversion explicite */
Deux entiers donnent toujours une division entière. La
partie décimale est jetée, pas arrondie. 7 / 2 vaut 3,
et 1 / 3 vaut 0.
Le piège classique :
double moyenne = 5 / 2; /* moyenne vaut 2.0, pas 2.5 ! */
La division est faite avant l'affectation, entre deux entiers. La
conversion en double arrive trop tard.
La conversion explicite, ou cast, s'écrit en mettant le type voulu entre parenthèses :
int somme = 17, nombre = 5; double moyenne = (double)somme / nombre; /* 3.4, correct */
Dans l'autre sens, convertir un flottant en entier tronque,
il n'arrondit pas : (int)3.99 vaut 3, et (int)-3.99
vaut −3.
Ce qu'il faut retenir
- Le type fixe la taille, l'interprétation et les opérations possibles.
intpar défaut pour les entiers,doublepour les réels.- Les tailles ne sont pas garanties : interrogez
sizeof. - Un
charest un petit entier :'A'vaut 65. - Deux entiers donnent une division entière.
5 / 2vaut 2. - Un cast vers un entier tronque, il n'arrondit pas.
- Jamais
==entre deux flottants.
6. Les variables
Une variable est une case de la mémoire à laquelle on a donné un nom et un type. Le nom vous évite de manipuler des adresses ; le type dit à la machine comment lire le contenu de la case.
Déclarer, initialiser
int age; /* declaration seule : la case existe, son contenu est inconnu */ age = 20; /* affectation : on y range 20 */ int annee = 2026; /* declaration et initialisation en une fois */ int x = 1, y = 2, z; /* trois variables du meme type sur une ligne */
La deuxième forme est préférable : une variable déclarée et initialisée d'un seul geste ne peut pas être utilisée par erreur avant d'avoir une valeur.
Le piège de la variable non initialisée. En C, déclarer une variable ne la met pas à zéro. La case mémoire contient ce qui s'y trouvait avant : un reste de ce qu'un autre programme y avait laissé.
int total;
printf("%d\n", total); /* affiche n'importe quoi */
Le plus traître est que cela affiche souvent 0, par hasard, pendant vos
essais, puis une valeur aberrante le jour de la démonstration. Compilez avec
-Wall : le compilateur vous avertira.
Les règles de nommage
- Lettres, chiffres et tiret bas
_uniquement. Pas d'espace, pas de tiret, pas d'accent. - Le premier caractère ne peut pas être un chiffre.
- Les majuscules comptent :
age,AgeetAGEsont trois variables différentes. - Un mot réservé du langage est interdit.
| Valide | Invalide | Pourquoi |
|---|---|---|
prix_total | prix total |
espace interdit |
note2 | 2note |
commence par un chiffre |
_temp | prix-total |
le tiret est l'opérateur de soustraction |
nbEleves | élèves |
accents interdits |
valeur_max | int |
mot réservé |
Les 32 mots réservés du C de base, qu'aucune variable ne peut porter :
auto break case char const continue default do double else enum extern float for goto if int long register return short signed sizeof static struct switch typedef union unsigned void volatile while
Donnez des noms qui parlent. prix_unitaire vaut mieux que
pu, et bien mieux que a. Vous relirez ce code dans
six mois.
const et #define
Deux façons de dire « cette valeur ne doit pas changer ».
#define TAUX_TVA 0.1925 /* le preprocesseur remplace le texte */ const double taux_tva = 0.1925; /* une vraie variable, en lecture seule */
#define | const |
|
|---|---|---|
| Traité par | le préprocesseur | le compilateur |
| A un type | non | oui |
| Occupe de la mémoire | non | oui, en général |
| Vérification de type | aucune | complète |
| Visible au débogueur | non | oui |
Préférez const : il a un type, donc le
compilateur peut vous protéger. Gardez #define pour ce qui n'est
pas une valeur : des options de compilation, des bouts de texte.
Dans les deux cas, toute tentative de modification est refusée :
const double taux_tva = 0.1925; taux_tva = 0.2; /* erreur : assignment of read-only variable */
7. Les opérateurs
Les opérateurs arithmétiques
| Opérateur | Rôle | Exemple | Résultat |
|---|---|---|---|
+ | addition | 7 + 2 |
9 |
- | soustraction | 7 - 2 |
5 |
* | multiplication | 7 * 2 |
14 |
/ | division | 7 / 2 |
3 (entiers) |
% | reste de la division entière | 7 % 2 | 1 |
Le modulo % ne fonctionne qu'entre entiers : jamais sur des
flottants. Il est bien plus utile qu'il n'en a l'air : associé à la division
entière, il permet de décomposer un nombre.
int n = 4729;
printf("%d %d %d %d\n",
n / 1000, /* 4 : les milliers */
(n / 100) % 10, /* 7 : les centaines */
(n / 10) % 10, /* 2 : les dizaines */
n % 10); /* 9 : les unites */
Les négatifs et la division. Depuis la norme C99, la division tronque vers zéro et le reste prend le signe du dividende.
-7 / 2 vaut -3 -7 % 2 vaut -1 7 / -2 vaut -3 7 % -2 vaut 1
On vérifie toujours la relation a == (a / b) * b + a % b.
L'affectation, et ses raccourcis
int total = 100; total = total + 50; /* la forme longue */ total += 50; /* exactement la meme chose */ total -= 20; /* total = total - 20 */ total *= 2; /* total = total * 2 */ total /= 4; /* total = total / 4 */ total %= 7; /* total = total % 7 */
Le signe = n'est pas une égalité mathématique. Il se lit
« reçoit » : total = total + 50 signifie « calcule
total + 50 et range le résultat dans total ».
Incrémentation et décrémentation
int i = 5; i++; /* i vaut maintenant 6 */ i--; /* i vaut de nouveau 5 */
Les deux écritures i++ et ++i augmentent
i de 1. La différence porte sur la valeur de
l'expression elle-même, quand on l'utilise dans un calcul.
int i = 5, a, b; a = i++; /* a recoit 5, PUIS i passe a 6 (post-incrementation) */ i = 5; b = ++i; /* i passe a 6, PUIS b recoit 6 (pre-incrementation) */
Tant que vous écrivez i++; seul sur sa ligne, les deux formes
sont interchangeables. C'est en les mêlant à une expression qu'il faut être
précis.
Un avertissement. N'écrivez jamais une expression où la
même variable est modifiée deux fois, comme i = i++ + ++i;. Le
résultat n'est pas défini par la norme : chaque compilateur peut donner une
réponse différente, et aucune n'est fautive.
Comparaison et logique
Ces opérateurs appartiennent au cours sur les structures conditionnelles, mais il faut savoir dès maintenant ce qu'ils produisent : un entier, 0 ou 1. Le C n'a pas de type booléen de base.
| Opérateur | Signification | Exemple | Valeur |
|---|---|---|---|
== | est égal à | 5 == 5 |
1 |
!= | est différent de | 5 != 5 | 0 |
< > | strictement inférieur, supérieur | 3 < 7 | 1 |
<= >= | inférieur ou égal, supérieur ou égal | 3 >= 7 | 0 |
&& | et | 1 && 0 | 0 |
|| | ou | 1 || 0 |
1 |
! | non | !0 | 1 |
On peut donc afficher le résultat d'une comparaison :
printf("%d\n", 10 > 3); /* affiche 1 */
printf("%d\n", 10 == 3); /* affiche 0 */
Ne confondez jamais = (affectation) et
== (comparaison). C'est la faute la plus coûteuse du C, car elle
compile sans erreur.
Les priorités
De la plus forte à la plus faible, pour ce qui nous concerne ici :
| Rang | Opérateurs | Sens de lecture |
|---|---|---|
| 1 | () | - |
| 2 | ++ -- !
(type) sizeof | droite à gauche |
| 3 | * / % |
gauche à droite |
| 4 | + - | gauche à droite |
| 5 | < <= >
>= | gauche à droite |
| 6 | == != |
gauche à droite |
| 7 | && | gauche à droite |
| 8 | || | gauche à droite |
| 9 | = += -=… |
droite à gauche |
2 + 3 * 4 vaut 14 (la multiplication d'abord) (2 + 3) * 4 vaut 20 10 % 3 * 2 vaut 2 (% et * ont le meme rang, lecture de gauche a droite) 10 - 4 - 3 vaut 3 (gauche a droite : (10-4)-3)
Le conseil le plus utile de cette section : mettez des parenthèses. Elles ne coûtent rien à l'exécution et elles vous dispensent d'apprendre ce tableau par cœur.
8. Afficher avec printf
printf vient de print formatted : afficher en
respectant un format. C'est la fonction que vous appellerez le plus souvent,
et elle mérite qu'on s'y arrête.
Le principe
Son premier argument est une chaîne de format : du texte
dans lequel certains endroits sont marqués par un %. Les
arguments qui suivent viennent remplir ces marques, dans
l'ordre.
printf("Bonjour Awa, tu as 17 ans.\n"); /* sans marqueur */
int age = 17;
printf("Bonjour Awa, tu as %d ans.\n", age); /* un marqueur */
Les marqueurs de conversion
| Marqueur | Pour quel type | Exemple | Affiche |
|---|---|---|---|
%d | int |
printf("%d", 42) | 42 |
%u | unsigned int |
printf("%u", 3000000000u) |
3000000000 |
%ld | long |
- | - |
%lld | long long |
- | - |
%f | float ou double |
printf("%f", 3.5) | 3.500000 |
%e | notation scientifique | printf("%e", 31400.0) |
3.140000e+04 |
%g | la plus courte des deux | printf("%g", 0.00001234) |
1.234e-05 |
%c | char |
printf("%c", 65) | A |
%s | chaîne de caractères | printf("%s", "Genius") | Genius |
%x %X | entier en hexadécimal | printf("%x", 255) | ff |
%o | entier en octal | printf("%o", 8) | 10 |
%zu | résultat de sizeof |
printf("%zu", sizeof(int)) | 4 |
%% | un signe pourcent littéral | printf("100%%") | 100% |
Largeur, précision, alignement
Entre le % et la lettre, on peut glisser des réglages. La
forme complète est %[drapeau][largeur].[précision]lettre.
printf("%5d|\n", 42); /* 42| largeur 5, cale a droite */
printf("%-5d|\n", 42); /* 42 | cale a gauche */
printf("%05d\n", 42); /* 00042 complete par des zeros */
printf("%+d\n", 42); /* +42 signe toujours affiche */
printf("%.2f\n", 3.14159); /* 3.14 deux decimales */
printf("%8.3f|\n", 3.14159); /* 3.142| largeur 8, trois decimales */
printf("%10.4f|\n", 2.0/3.0); /* 0.6667| */
La largeur est un minimum : si le nombre est plus long, il n'est jamais tronqué. La précision, elle, arrondit réellement.
Ces réglages servent surtout à aligner des colonnes de chiffres, ce qui rend une sortie immédiatement plus lisible.
Un arrondi qui surprend. printf("%.0f", 2.5)
affiche 2, et printf("%.0f", 3.5) affiche
4.
Ce n'est pas un bogue : la norme IEEE 754 impose l'arrondi
au plus proche, et au pair en cas d'égalité. Cela évite un biais
systématique vers le haut quand on arrondit beaucoup de valeurs. La fonction
round() de <math.h>, elle, s'éloigne de
zéro : round(2.5) vaut 3.
Les séquences d'échappement
Certains caractères ne peuvent pas s'écrire directement dans une chaîne. On les note avec une barre oblique inverse.
| Séquence | Effet |
|---|---|
\n | retour à la ligne |
\t | tabulation |
\\ | une barre oblique inverse |
\" | un guillemet dans une chaîne |
\' | une apostrophe dans un caractère |
\a | un bip sonore |
\0 | le caractère nul, fin de chaîne |
Oublier le \n final est sans gravité, mais les affichages se
collent les uns aux autres sur la même ligne.
L'erreur la plus courante
double prix = 1500.75;
printf("%d\n", prix); /* FAUX : %d attend un int, on lui donne un double */
Le programme compile, printf ne vérifie rien à l'exécution, et affiche une valeur absurde. Avec -Wall, gcc vous prévient :
format '%d' expects argument of type 'int', but argument 2 has type
'double'.
À chaque marqueur doit correspondre un argument, du bon
type, dans le bon ordre. printf ne s'en assure pas : c'est votre
travail, et celui de -Wall.
9. Lire une saisie avec scanf
scanf est le miroir de printf : elle lit ce que
l'utilisateur tape au clavier et le range dans vos variables.
int age;
printf("Quel est ton age ? ");
scanf("%d", &age);
printf("Tu as %d ans.\n", age);
Pourquoi cette esperluette
C'est la question que tout le monde pose, et la réponse éclaire une bonne partie du C.
printf a seulement besoin de lire votre variable : on
lui passe sa valeur, et cela suffit.
scanf, lui, doit écrire dedans. Si on lui passait la
valeur, il recevrait une copie, et modifier une copie ne changerait rien à
l'original. Il lui faut donc savoir où se trouve la variable :
son adresse en mémoire. C'est exactement ce que donne l'opérateur
&, qui se lit « adresse de ».
Oublier le & est une erreur grave. scanf
reçoit alors la valeur de la variable et la prend pour une adresse : il écrit
dans une zone mémoire qui ne vous appartient pas. Le programme s'arrête
brutalement, ou, bien pire, continue avec des données abîmées.
Les marqueurs de scanf
| Marqueur | Lit | Exemple |
|---|---|---|
%d | un int |
scanf("%d", &n); |
%f | un float |
scanf("%f", &prix); |
%lf | un double |
scanf("%lf", &moyenne); |
%c | un char |
scanf("%c", &lettre); |
%s | un mot, sans espace | scanf("%s", nom); |
Une différence entre printf et scanf qui piège tout le
monde. Pour un double, l'affichage accepte
%f mais la lecture exige %lf.
double x;
scanf("%lf", &x); /* correct */
printf("%f\n", x); /* correct aussi */
scanf("%f", &x); /* FAUX : scanf ecrira 4 octets dans une case de 8 */
La raison tient aux règles de conversion automatique des arguments, qui s'appliquent à l'envoi mais pas à l'écriture en mémoire.
Le retour chariot qui reste
Quand l'utilisateur tape 25 puis Entrée, scanf("%d")
consomme les deux chiffres… et laisse le retour à la ligne
dans la mémoire tampon du clavier.
Avec %d ou %f, ce n'est pas grave : ils sautent
automatiquement les espaces et les retours à la ligne. Mais
%c ne saute rien :
int age;
char initiale;
scanf("%d", &age);
scanf("%c", &initiale); /* lit le retour a la ligne, pas la lettre ! */
La parade tient à un espace, glissé devant le %c :
scanf(" %c", &initiale); /* l'espace avale les blancs en attente */
Vérifier que la saisie a réussi
scanf renvoie le nombre de valeurs correctement
lues. On peut donc savoir si l'utilisateur a tapé n'importe quoi.
int age;
int lus = scanf("%d", &age);
printf("scanf a lu %d valeur(s)\n", lus);
/* 1 : une valeur lue. 0 : l'utilisateur n'a pas tape un nombre. */
Un programme sérieux teste cette valeur. Le traitement qui suit relève du cours sur les conditions ; retenez simplement dès maintenant que l'information existe et qu'on ne doit pas la jeter.
Lire plusieurs valeurs d'un coup
int jour, mois, annee;
printf("Date (jj mm aaaa) : ");
scanf("%d %d %d", &jour, &mois, &annee);
Un marqueur, une esperluette, dans l'ordre. L'utilisateur peut séparer ses
nombres par des espaces, des tabulations ou des retours à la ligne :
scanf ne fait pas la différence.
Ce qu'il faut retenir
printfreçoit des valeurs ;scanfreçoit des adresses, d'où le&.- Pour un
double:%fà l'affichage,%lfà la lecture. - Devant
%c, mettez un espace :" %c". scanfrenvoie le nombre de valeurs lues. C'est précieux.- Affichez toujours une invite avant de lire, sinon l'utilisateur voit un écran vide.
10. Les fonctions prédéfinies
Écrire ses propres fonctions fait l'objet d'un autre cours. En revanche,
utiliser celles que l'on vous fournit fait partie des bases :
c'est ce que vous faites depuis le début avec printf.
Une bibliothèque est un ensemble de fonctions déjà
écrites, déjà testées et déjà compilées. Le fichier d'en-tête
(.h) décrit comment les appeler ; le code, lui, est rattaché à
votre programme par l'éditeur de liens.
Lire un prototype
La documentation d'une fonction se résume à une ligne, appelée prototype :
double sqrt(double x); ^ ^ ^ | | +--- ce qu'elle attend : un double | +---------- son nom +----------------- ce qu'elle renvoie : un double
Tout est là : le type de retour, le nom, les paramètres. Savoir lire un prototype, c'est savoir utiliser n'importe quelle bibliothèque.
<math.h> : les mathématiques
| Fonction | Rôle | Exemple | Résultat |
|---|---|---|---|
sqrt(x) | racine carrée | sqrt(16.0) | 4.0 |
pow(x, y) | x puissance y | pow(2.0, 10.0) | 1024.0 |
fabs(x) | valeur absolue (flottant) | fabs(-7.25) | 7.25 |
ceil(x) | arrondi au-dessus | ceil(3.2) | 4.0 |
floor(x) | arrondi en dessous | floor(3.8) | 3.0 |
round(x) | arrondi au plus proche | round(2.5) | 3.0 |
sin, cos, tan |
trigonométrie, en radians | sin(M_PI/6) | ≈ 0.5 |
log(x) | logarithme népérien | log(M_E) | 1.0 |
log10(x) | logarithme décimal | log10(1000.0) | 3.0 |
Toutes renvoient un double, même quand le résultat est un
entier rond : ceil(3.2) vaut 4.0, pas 4.
Affichez-les avec %f.
L'option -lm. Sous Linux et macOS, les
fonctions mathématiques vivent dans une bibliothèque séparée qu'il faut
réclamer explicitement :
gcc calcul.c -o calcul -lm
Sans elle : undefined reference to 'sqrt'. Le
#include ne suffit pas : il annonce la fonction, il ne la fournit
pas. L'option se place à la fin de la commande.
Un exemple complet, l'hypoténuse d'un triangle rectangle :
#include <stdio.h>
#include <math.h>
int main(void)
{
double a = 3.0, b = 4.0;
double c = sqrt(a * a + b * b);
printf("hypotenuse = %.2f\n", c); /* affiche hypotenuse = 5.00 */
return 0;
}
<stdlib.h> : les outils généraux
| Fonction | Rôle |
|---|---|
abs(n) | valeur absolue d'un int
(à ne pas confondre avec fabs) |
rand() | un entier pseudo-aléatoire entre 0 et
RAND_MAX |
srand(graine) | initialise la suite aléatoire |
exit(code) | arrête le programme immédiatement |
atoi(texte) | convertit une chaîne en
int |
Une remarque sur rand() : sans srand, la suite
est la même à chaque exécution. C'est voulu : cela rend les essais
reproductibles. Pour varier, on sème avec l'heure courante :
srand(time(NULL));, qui demande
#include <time.h>.
<stdio.h> : au-delà de printf
putchar('A'); /* affiche un seul caractere */
int c = getchar(); /* lit un seul caractere */
Ces deux-là sont plus simples et plus rapides que printf et
scanf quand on ne manipule qu'un caractère.
Avant d'écrire une fonction, demandez-vous si elle n'existe pas déjà. Le code de la bibliothèque standard est écrit et relu depuis cinquante ans : le vôtre, non.
11. Les erreurs du débutant, et comment les reconnaître
Les messages du compilateur paraissent hostiles tant qu'on n'a pas compris qu'ils suivent presque toujours le même schéma : fichier:ligne:colonne, puis la nature du problème. Voici les dix qui reviennent le plus.
| Ce que vous voyez | Ce que cela veut dire | Ce qu'il faut faire |
|---|---|---|
expected ';' before '}' |
point-virgule manquant | regarder la ligne précédente, pas celle indiquée |
'x' undeclared |
variable utilisée sans avoir été déclarée | vérifier l'orthographe et la casse ; déclarer la variable |
implicit declaration of function 'sqrt' |
le #include manque |
ajouter #include <math.h> |
undefined reference to 'sqrt' |
l'éditeur de liens ne trouve pas le code | ajouter -lm à la fin de la commande |
format '%d' expects argument of type 'int' |
mauvais marqueur dans printf | accorder le marqueur et le type de l'argument |
assignment of read-only variable |
on modifie une variable const |
retirer const, ou ne pas modifier |
'total' is used uninitialized |
variable lue avant d'avoir reçu une valeur | l'initialiser à la déclaration |
suggest parentheses around assignment |
vous avez écrit = où == était
attendu | relire la comparaison |
| le programme s'arrête sans message | très souvent un & oublié dans
scanf | vérifier chaque scanf |
| un résultat de division faux | division entière là où on attendait des décimales | (double)a / b |
Lisez la première erreur, pas la dernière. Une seule faute en entraîne souvent dix autres : le compilateur, désorienté, se plaint de tout ce qui suit. Corrigez la première, recompilez, et regardez ce qu'il reste. Très souvent, les neuf autres ont disparu.
Les cinq réflexes qui font gagner du temps
- Compilez souvent. Toutes les cinq ou dix lignes, pas à la fin. Une erreur dans dix lignes se trouve ; dans deux cents, elle se cherche.
- Compilez avec
-Wall -Wextraet traitez les avertissements comme des erreurs. - Indentez en écrivant, pas après coup.
- Initialisez vos variables au moment où vous les déclarez.
- Affichez les valeurs intermédiaires avec
printfquand un résultat vous surprend. C'est le débogueur du pauvre, et il résout la moitié des cas.
Et maintenant ?
Vous savez fabriquer un programme, le structurer, déclarer des données, calculer, afficher et lire. C'est le socle : tout le reste s'appuie dessus.
La suite logique est de donner à votre programme la capacité de choisir (les structures conditionnelles) puis de répéter (les boucles). Viennent ensuite les tableaux, les fonctions et les pointeurs.
Mais d'abord, les trente exercices qui suivent. Faites-les au crayon avant d'ouvrir les corrigés : c'est en butant qu'on apprend.
Questions de cours
Quinze questions que l'on pose en examen, et qu'il faut savoir traiter en quelques phrases. Cherchez votre réponse avant d'ouvrir.
1. Pourquoi dit-on que le C est un langage compilé ?1. Pourquoi dit-on que le C est un langage compilé ?
Parce que le code source est traduit entièrement en instructions machine avant d'être exécuté, par un programme appelé compilateur. Le résultat est un fichier autonome que le processeur exécute directement.
Un langage interprété, comme Python, est au contraire lu et exécuté ligne par ligne par un interpréteur qui doit être présent sur la machine.
Deux conséquences : le programme C démarre et tourne plus vite, mais il ne fonctionne que sur le type de machine pour lequel il a été compilé.
2. Que fait exactement #include <stdio.h> ?2. Que fait exactement #include <stdio.h> ?
Le préprocesseur remplace cette ligne par le contenu entier du fichier stdio.h.
Ce fichier ne contient pas le code de printf : il contient sa description : son prototype, c'est-à-dire son nom, ce qu'elle attend et ce qu'elle renvoie. Cela suffit au compilateur pour vérifier que vous l'appelez correctement.
Le code véritable est rattaché plus tard, par l'éditeur de liens.
3. Pourquoi main est-elle déclarée int ?3. Pourquoi main est-elle déclarée int ?
Parce qu'elle renvoie un entier au système d'exploitation au moment où le programme se termine : le code de retour.
Par convention universelle, 0 signifie que tout s'est bien passé ; toute autre valeur signale une erreur, et sa signification est propre au programme.
Ce n'est pas décoratif : les scripts et les chaînes d'automatisation lisent ce code pour décider s'ils continuent ou s'ils s'arrêtent.
4. Quelle est la différence entre #define et const ?4. Quelle est la différence entre #define et const ?
#define est traité par le préprocesseur : c'est un remplacement de texte, sans type et sans vérification. Le compilateur ne voit jamais le nom.
const crée une vraie variable, avec un type, que le compilateur protège en écriture. Elle est visible au débogueur et participe aux vérifications de type.
Pour une valeur numérique, préférez const.
5. Pourquoi faut-il une esperluette dans scanf et pas dans printf ?5. Pourquoi faut-il une esperluette dans scanf et pas dans printf ?
printf a seulement besoin de lire la variable : lui passer sa valeur suffit.
scanf doit écrire dedans. S'il recevait la valeur, il n'obtiendrait qu'une copie, et modifier une copie ne change rien à l'original. Il lui faut donc l'adresse de la variable, que donne l'opérateur &.
Sans elle, scanf prend la valeur pour une adresse et écrit dans une zone mémoire qui ne vous appartient pas.
6. Que vaut 5 / 2 en C, et pourquoi ?6. Que vaut 5 / 2 en C, et pourquoi ?
Il vaut 2.
Les deux opérandes sont des entiers : le C effectue une division entière et jette la partie décimale. Il n'arrondit pas : 7 / 2 vaut 3 et non 4.
Pour obtenir 2,5, il faut qu'au moins un opérande soit flottant : 5 / 2.0, 5.0 / 2 ou (double)5 / 2.
7. Quelle différence entre float et double ? Lequel choisir ?7. Quelle différence entre float et double ? Lequel choisir ?
float occupe 4 octets et garantit environ 7 chiffres significatifs. double occupe 8 octets et en garantit environ 15.
Choisissez double par défaut : sept chiffres s'épuisent vite, et les processeurs d'aujourd'hui calculent en double précision sans surcoût.
Le float ne se justifie que lorsque la mémoire ou la bande passante comptent : de grandes quantités de données, un microcontrôleur, une carte graphique.
8. Pourquoi sizeof(int) peut-il varier d'une machine à l'autre ?8. Pourquoi sizeof(int) peut-il varier d'une machine à l'autre ?
Parce que la norme du C ne fixe pas les tailles exactes. Elle impose des minimums et un ordre : char ≤ short ≤ int ≤ long ≤ long long.
Le cas le plus connu concerne long : 4 octets sous Windows 64 bits, 8 octets sous Linux et macOS 64 bits.
D'où la règle : ne supposez jamais une taille, demandez-la avec sizeof.
9. Que contient une variable qu'on vient de déclarer sans l'initialiser ?9. Que contient une variable qu'on vient de déclarer sans l'initialiser ?
Ce qui traînait déjà dans cette case mémoire : le reste d'un calcul précédent, ou de ce qu'un autre programme y avait laissé. La valeur est imprévisible.
Le C ne met pas les variables locales à zéro : ce serait du temps perdu dans la très grande majorité des cas.
Le danger est que cela affiche souvent 0 par hasard pendant les essais, puis une valeur aberrante au mauvais moment. Initialisez toujours à la déclaration, et compilez avec -Wall.
10. Pourquoi printf("%d", 3.5) n'affiche-t-il pas 3,5 ?10. Pourquoi printf("%d", 3.5) n'affiche-t-il pas 3,5 ?
Parce que %d annonce « l'argument suivant est un int ». printf lit alors 4 octets et les interprète comme un entier, alors qu'on lui a donné les 8 octets d'un double.
Le résultat est une valeur absurde, et le programme compile quand même, car printf ne vérifie rien à l'exécution.
Avec -Wall, gcc avertit : format '%d' expects argument of type 'int'. C'est exactement pour cela qu'il faut activer les avertissements.
11. À quoi sert le \n à la fin d'un printf ?11. À quoi sert le \n à la fin d'un printf ?
C'est une séquence d'échappement qui représente le retour à la ligne. Elle ne s'écrit pas directement dans une chaîne, d'où la notation avec la barre oblique inverse.
Sans elle, l'affichage suivant se colle au précédent sur la même ligne.
Elle a un second effet : sur beaucoup de systèmes, elle provoque l'envoi effectif du texte à l'écran. Un programme qui plante juste après un printf sans \n peut ne rien avoir affiché du tout.
12. Pourquoi mon scanf("%c") saute-t-il la saisie ?12. Pourquoi mon scanf("%c") saute-t-il la saisie ?
Parce que le scanf précédent a laissé le retour à la ligne dans la mémoire tampon du clavier.
%d et %f sautent automatiquement les espaces et les retours à la ligne. %c ne saute rien : il prend le premier caractère disponible, qui se trouve être ce retour à la ligne.
La parade tient en un espace devant le marqueur : scanf(" %c", &lettre);
13. Quelle différence entre #include <x.h> et #include "x.h" ?13. Quelle différence entre #include <x.h> et #include "x.h" ?
Les chevrons font chercher le fichier dans les répertoires système du compilateur : c'est la forme des bibliothèques standard.
Les guillemets font d'abord chercher à côté de votre fichier source, puis dans les répertoires système : c'est la forme de vos propres fichiers d'en-tête.
Le résultat est le même une fois le fichier trouvé ; seul l'ordre de recherche change.
14. Pourquoi gcc réclame-t-il -lm alors que j'ai écrit #include <math.h> ?14. Pourquoi gcc réclame-t-il -lm alors que j'ai écrit #include <math.h> ?
Parce que les deux choses sont différentes. math.h annonce les fonctions : il donne leur prototype au compilateur, qui peut ainsi vérifier vos appels.
Mais le code de sqrt vit dans une bibliothèque séparée, libm. L'option -lm demande à l'éditeur de liens d'aller l'y chercher.
D'où les deux messages distincts : implicit declaration vient du compilateur et signale un #include manquant ; undefined reference vient de l'éditeur de liens et signale -lm manquant. L'option se place à la fin de la commande.
15. Le C distingue-t-il les majuscules des minuscules ?15. Le C distingue-t-il les majuscules des minuscules ?
Oui, partout et sans exception. age, Age et AGE sont trois identificateurs différents.
Cela vaut aussi pour les mots du langage : int est valide, Int ne l'est pas. Et pour les fonctions : printf fonctionne, Printf produit undefined reference.
C'est la première chose à vérifier devant un message 'x' undeclared.
31 exercices corrigés
Six parties, qui suivent l'ordre du cours. Posez le calcul ou écrivez le programme sur une feuille avant d'ouvrir le corrigé : c'est en butant que l'on apprend.
Partie 1 : Structure, compilation et préprocesseur
Reconnaître les parties d'un programme, lire un message d'erreur et comprendre ce que fait le préprocesseur avant même la compilation.
Ce programme ne compile pas. Repérez les quatre erreurs et donnez la version corrigée.
#include <stdio.h>;
int Main(void)
{
int age = 20
printf("J'ai %d ans\n", age);
return 0;
Voir le corrigéMasquer le corrigé
1. #include <stdio.h>; : une directive du préprocesseur ne prend pas de point-virgule. Celui-ci serait recopié tel quel dans le code et provoquerait une erreur.
2. Main : le point d'entrée s'écrit main, tout en minuscules. Le C distingue la casse : l'éditeur de liens répondra undefined reference to 'main'.
3. int age = 20 : il manque le point-virgule. Le compilateur signalera probablement la ligne suivante.
4. l'accolade fermante } du bloc manque.
#include <stdio.h>
int main(void)
{
int age = 20;
printf("J'ai %d ans\n", age);
return 0;
}#include <stdio.h>
int main(void)
{
printf("Groupe");
printf(" Genius");
printf("\n");
printf("C\tbases\n");
return 0;
}Voir le corrigéMasquer le corrigé
Groupe Genius C bases
Les deux premiers printf n'ont pas de \n : leur texte se colle sur la même ligne. Le troisième ne fait que passer à la ligne.
Le \t insère une tabulation, qui aligne sur le prochain multiple de 8 colonnes : d'où l'espace apparent entre C et bases.
Écrivez le programme complet qui affiche exactement ceci, puis donnez la commande de compilation avec les avertissements activés :
Awa Nkolo Groupe Genius
Voir le corrigéMasquer le corrigé
#include <stdio.h>
int main(void)
{
printf("Awa Nkolo\n");
printf("Groupe Genius\n");
return 0;
}Une seule instruction aurait suffi :
printf("Awa Nkolo\nGroupe Genius\n");gcc -Wall -Wextra -std=c17 carte.c -o carte
Puis ./carte sous Linux ou macOS, carte.exe sous Windows.
Pour chacun de ces messages, dites quelle étape de la chaîne de compilation se plaint, et ce qu'il faut corriger.
fatal error: stdioo.h: No such file or directoryerror: expected ';' before 'return'undefined reference to 'pow'warning: unused variable 'x'
Voir le corrigéMasquer le corrigé
1. Le préprocesseur. C'est lui qui va chercher les fichiers. Il ne trouve pas stdioo.h : une faute de frappe, il faut écrire stdio.h.
2. Le compilateur. Il analyse la syntaxe du C. Un point-virgule manque à la ligne précédente.
3. L'éditeur de liens. Il connaît pow grâce à math.h, mais il n'en trouve pas le code. Il faut ajouter -lm à la fin de la commande.
4. Le compilateur, et ce n'est qu'un avertissement : le programme compile. Une variable déclarée n'est jamais utilisée : soit on l'enlève, soit on a oublié de s'en servir, ce qui cache souvent un vrai bogue.
Que vaut resultat dans chacun des deux cas ?
#define CARRE(x) x * x int resultat = CARRE(4); int resultat = CARRE(2 + 2);
Corrigez la macro.
Voir le corrigéMasquer le corrigé
Premier cas. Le préprocesseur écrit 4 * 4, donc 16. Correct.
Second cas. Il remplace x par le texte 2 + 2, sans rien comprendre :
CARRE(2 + 2) devient 2 + 2 * 2 + 2
La multiplication est prioritaire : 2 + 4 + 2 = 8. On attendait 16.
La correction : #define CARRE(x) ((x) * (x))
Chaque paramètre entre parenthèses, et l'expression entière aussi. CARRE(2 + 2) devient alors ((2 + 2) * (2 + 2)), soit 16.
Pour voir le texte réellement produit : gcc -E mon_fichier.c.
Vous écrivez un fichier outils.h placé à côté de main.c. Quelle forme d'inclusion employer, et pourquoi ?
Voir le corrigéMasquer le corrigé
#include "outils.h"
Les guillemets font chercher d'abord dans le répertoire du fichier source, puis dans les répertoires système.
Les chevrons ne cherchent que dans les répertoires système : #include <outils.h> échouerait avec fatal error: outils.h: No such file or directory.
Une fois le fichier trouvé, le résultat est identique. Seul l'ordre de recherche change.
À quoi sert ce motif, et que se passe-t-il si on l'omet ?
#ifndef OUTILS_H #define OUTILS_H const double TAUX_TVA = 0.1925; #endif
Voir le corrigéMasquer le corrigé
C'est une garde d'inclusion. Elle empêche le contenu du fichier d'être recopié plus d'une fois.
Au premier passage, OUTILS_H n'est pas défini : le bloc est inclus, et la ligne #define OUTILS_H le définit. Aux passages suivants, la condition est fausse et tout le bloc disparaît.
Sans la garde, un fichier inclus deux fois, ce qui arrive vite dès qu'un en-tête en inclut un autre, produirait deux déclarations de TAUX_TVA, et l'erreur redefinition of 'TAUX_TVA'.
Partie 2 : Types et variables
Choisir le bon type, prévoir une taille, repérer un débordement et nommer correctement ses variables.
Sur une machine 64 bits ordinaire, qu'affiche ce programme ?
printf("%zu %zu %zu %zu %zu\n",
sizeof(char), sizeof(short), sizeof(int),
sizeof(float), sizeof(double));Voir le corrigéMasquer le corrigé
1 2 4 4 8
Un char occupe toujours 1 octet, c'est le seul que la norme fixe avec certitude. Les autres valent 2, 4, 4 et 8 sur l'immense majorité des machines actuelles.
Remarquez que float et int occupent la même place, 4 octets, et ne représentent pourtant pas du tout les mêmes valeurs : c'est le type, pas la taille, qui dit comment lire les octets.
Le marqueur correct pour sizeof est %zu.
Quel type déclarer pour chacune de ces données ? Justifiez en une ligne.
- l'âge d'un étudiant
- la population mondiale
- le prix d'un article en FCFA
- l'initiale d'un prénom
- la moyenne d'un étudiant sur 20
- le nombre d'octets d'un fichier vidéo
Voir le corrigéMasquer le corrigé
int, ouunsigned charsi la place compte. Un âge tient largement dans un entier ordinaire.long long. Environ 8×109 : cela dépasse les 2,1 milliards d'unint.intoulong. Le franc CFA n'a pas de subdivision en usage : on compte en unités entières, jamais en flottant.char: un seul caractère, donc un octet.double. Une moyenne a des décimales.long long. Un fichier de 3 Go fait 3 221 225 472 octets, ce qui dépasse déjà l'étendue d'unintsigné.
Qu'affiche ce programme, et pourquoi ?
int n = 2147483647;
printf("%d\n", n);
n = n + 1;
printf("%d\n", n);Voir le corrigéMasquer le corrigé
2147483647 -2147483648
2 147 483 647 est la plus grande valeur d'un int sur 32 bits, soit 231 − 1. Son écriture binaire est 0111...1.
En ajoutant 1, la retenue se propage et atteint le bit de poids fort, qui passe à 1 : le motif devient 1000...0. En complément à deux, ce motif vaut −2 147 483 648.
Le programme n'affiche aucune erreur. C'est tout le danger : un compteur qui déborde devient brusquement négatif, et la suite du programme travaille sur une valeur absurde.
Qu'affichent ces deux lignes ?
char c = 300;
printf("%d\n", c);
char d = 'A';
printf("%d %c\n", d, d);Voir le corrigéMasquer le corrigé
44 65 A
Première partie. Un char signé couvre −128 à 127. La valeur 300 n'y tient pas : seuls les 8 bits de poids faible sont conservés.
300 s'écrit 1 0010 1100 en binaire. On garde 0010 1100, soit 44. Comme le bit de poids fort vaut 0, la valeur reste positive : 44.
Seconde partie. 'A' vaut 65, son code ASCII. La même case mémoire s'affiche 65 avec %d et A avec %c. Un char est un petit entier ; seul le marqueur change la lecture.
Lesquels de ces noms de variables sont acceptés par le compilateur ? Pour les autres, dites pourquoi.
prix_total 2eme_note _temp nbEleves prix-total moyenne int élèves PRIX_MAX note 1 Return x2
Voir le corrigéMasquer le corrigé
Valides : prix_total, _temp, nbEleves, moyenne, PRIX_MAX, Return, x2.
Return passe parce que le C distingue la casse : le mot réservé est return, tout en minuscules. C'est accepté, mais à éviter : cela prête à confusion.
Invalides :
2eme_note: commence par un chiffre.prix-total: le tiret est l'opérateur de soustraction ; le compilateur litprixmoinstotal.int: mot réservé.élèves: caractères accentués interdits dans un identificateur.note 1: espace interdit.
const ou #defineCe programme ne compile pas. Pourquoi ? Que faudrait-il écrire ?
const double TAUX = 0.1925;
int main(void)
{
TAUX = 0.20;
printf("%f\n", TAUX);
return 0;
}Voir le corrigéMasquer le corrigé
Le compilateur refuse la ligne TAUX = 0.20; avec le message :
error: assignment of read-only variable 'TAUX'
C'est précisément le rôle de const : garantir qu'une valeur ne changera pas. Le compilateur n'est pas en train de vous embêter, il fait son travail.
Deux corrections possibles, selon l'intention :
- si la valeur doit vraiment rester fixe, supprimer la ligne fautive ;
- si elle doit pouvoir changer, retirer
const, mais alors ce n'était pas une constante.
Avec #define TAUX 0.1925, l'erreur serait plus obscure : le préprocesseur produirait 0.1925 = 0.20;, et le message parlerait d'une lvalue invalide.
Pourquoi ce programme est-il dangereux, même s'il affiche souvent 0 ?
int main(void)
{
int total;
total = total + 100;
printf("%d\n", total);
return 0;
}Voir le corrigéMasquer le corrigé
total est utilisée avant d'avoir reçu une valeur. La ligne total = total + 100 lit le contenu de la case mémoire… qui contient ce qui traînait là avant.
Le résultat est imprévisible. Il affiche souvent 100, parce que la zone se trouve à zéro par hasard, et il affichera autre chose un autre jour, sur une autre machine, ou après avoir ajouté du code ailleurs.
C'est le pire type de bogue : celui qui ne se manifeste pas pendant les essais.
La correction : int total = 0;
Compilé avec -Wall, gcc prévient : warning: 'total' is used uninitialized.
Partie 3 : Opérateurs et conversions
Division entière, modulo, priorités et conversions explicites : là où se cachent la plupart des résultats faux.
Qu'affiche chacune de ces lignes ?
printf("%d\n", 7 / 2);
printf("%d\n", -7 / 2);
printf("%f\n", 7 / 2.0);
printf("%d\n", 1 / 3);
printf("%f\n", (double)7 / 2);Voir le corrigéMasquer le corrigé
3 -3 3.500000 0 3.500000
7 / 2 : deux entiers, division entière, partie décimale jetée : 3.
−7 / 2 : depuis C99, la division tronque vers zéro : −3, et non −4.
7 / 2.0 : 2.0 est un double : le 7 est converti, la division est réelle. %f affiche six décimales par défaut.
1 / 3 - 0. C'est l'erreur la plus fréquente du C.
(double)7 / 2 : le cast ne porte que sur le 7, mais cela suffit : un opérande flottant rend toute l'opération flottante.
Un entier à quatre chiffres vaut 4729. Écrivez les expressions qui extraient chacun de ses chiffres, puis la somme de ces chiffres.
Sans boucle : uniquement avec / et %.
Voir le corrigéMasquer le corrigé
int n = 4729; int milliers = n / 1000; /* 4 */ int centaines = (n / 100) % 10; /* 7 */ int dizaines = (n / 10) % 10; /* 2 */ int unites = n % 10; /* 9 */ int somme = milliers + centaines + dizaines + unites;
4 + 7 + 2 + 9 = 22
Le principe : % 10 isole le dernier chiffre, / 10 supprime le dernier chiffre. En combinant les deux, on atteint n'importe quelle position.
Attention aux parenthèses : n / 100 % 10 fonctionne aussi, car / et % ont le même rang et se lisent de gauche à droite. Mais les parenthèses rendent l'intention évidente.
Trois notes entières valent 12, 15 et 17. Ce programme affiche 14.000000. Pourquoi, et comment le corriger ?
int a = 12, b = 15, c = 17;
double moyenne = (a + b + c) / 3;
printf("%f\n", moyenne);Voir le corrigéMasquer le corrigé
La somme vaut 44. Mais 44 / 3 est une division entre deux entiers : elle vaut 14, et la partie décimale est perdue avant l'affectation. Convertir en double ensuite ne récupère rien.
La vraie moyenne est 44/3 = 14,666…
double moyenne = (a + b + c) / 3.0;
ou, de façon équivalente :
double moyenne = (double)(a + b + c) / 3;
Les deux affichent 14.666667.
À ne pas faire : (double)((a+b+c) / 3), les parenthèses imposent la division entière d'abord, et le cast arrive trop tard. On retrouve 14.000000.
Qu'affiche ce programme ?
int i = 5;
int a = i++;
printf("%d %d\n", a, i);
int j = 5;
int b = ++j;
printf("%d %d\n", b, j);Voir le corrigéMasquer le corrigé
5 6 6 6
i++ (post-incrémentation) : l'expression vaut 5, l'ancienne valeur. a reçoit 5, puis i passe à 6.
++j (pré-incrémentation) : j passe d'abord à 6, puis l'expression vaut 6. b reçoit 6.
Dans les deux cas la variable finit à 6 : la différence ne porte que sur la valeur de l'expression.
Tant qu'on écrit i++; seul sur sa ligne, les deux formes sont interchangeables.
Calculez, sans machine :
2 + 3 * 4 (2 + 3) * 4 10 % 3 * 2 10 - 4 - 3 20 / 4 / 5 7 + 3 % 4
Voir le corrigéMasquer le corrigé
| Expression | Lecture | Valeur |
|---|---|---|
2 + 3 * 4 | 2 + (3×4) | 14 |
(2 + 3) * 4 | 5×4 | 20 |
10 % 3 * 2 | (10%3)×2 = 1×2 | 2 |
10 - 4 - 3 | (10−4)−3 | 3 |
20 / 4 / 5 | (20/4)/5 = 5/5 | 1 |
7 + 3 % 4 | 7 + (3%4) = 7 + 3 | 10 |
*, / et % ont le même rang, supérieur à + et -. À rang égal, la lecture va de gauche à droite.
Le conseil pratique : mettez des parenthèses. Elles ne coûtent rien et dispensent d'apprendre le tableau.
Qu'affiche ce programme ?
printf("%d\n", 10 > 3);
printf("%d\n", 10 == 3);
printf("%d\n", 5 != 5);
printf("%d\n", !0);
printf("%d\n", (4 > 2) + (7 > 1));Voir le corrigéMasquer le corrigé
1 0 0 1 2
Le C n'a pas de type booléen de base : une comparaison produit un int valant 0 ou 1. On peut donc l'afficher avec %d, et même l'additionner : la dernière ligne vaut 1 + 1 = 2.
!0 vaut 1 : l'opérateur ! transforme zéro en 1, et toute valeur non nulle en 0.
Cette propriété explique pourquoi confondre = et == est si dangereux : if (x = 5) affecte 5 à x, l'expression vaut 5, donc vrai, et le test réussit toujours, sans erreur de compilation.
Partie 4 : Afficher avec printf
Prévoir une sortie au caractère près, formater des colonnes et repérer un marqueur qui ne correspond pas.
Qu'affiche exactement ce programme ? Notez bien les espaces.
printf("[%5d]\n", 42);
printf("[%-5d]\n", 42);
printf("[%05d]\n", 42);
printf("[%+d]\n", 42);
printf("[%.2f]\n", 3.14159);
printf("[%8.3f]\n", 3.14159);Voir le corrigéMasquer le corrigé
[ 42] [42 ] [00042] [+42] [3.14] [ 3.142]
%5d : largeur minimale 5, calé à droite : trois espaces puis 42.
%-5d : le tiret cale à gauche.
%05d : le zéro remplit avec des zéros au lieu d'espaces.
%+d : affiche toujours le signe, même positif.
%.2f : deux décimales, avec arrondi réel.
%8.3f : largeur 8 et trois décimales : 3.142 fait 5 caractères, donc trois espaces devant.
Écrivez les printf qui produisent exactement cette sortie, à partir des valeurs données.
Article Prix ---------------------- Cahier 1250.00 Stylo 350.50 Calculatrice 12500.75
Voir le corrigéMasquer le corrigé
printf("%-12s %8s\n", "Article", "Prix");
printf("----------------------\n");
printf("%-12s %8.2f\n", "Cahier", 1250.00);
printf("%-12s %8.2f\n", "Stylo", 350.50);
printf("%-12s %8.2f\n", "Calculatrice", 12500.75);Le principe : le texte à gauche avec %-12s, les nombres à droite avec %8.2f. C'est ce contraste qui rend un tableau lisible.
La largeur 12 est dictée par le mot le plus long, Calculatrice, qui fait exactement 12 caractères. La largeur 8 est dictée par 12500.75, qui en fait 8.
Si une valeur dépasse la largeur demandée, elle n'est jamais tronquée : la colonne se décale. La largeur est un minimum, pas un maximum.
Écrivez le printf qui affiche exactement cette ligne :
Le dossier "C:\Genius" contient 100% des cours.
Voir le corrigéMasquer le corrigé
printf("Le dossier \"C:\\Genius\" contient 100%% des cours.\n");Trois caractères demandent un traitement particulier :
- le guillemet fermerait la chaîne : on écrit
\"; - la barre oblique inverse introduit les séquences d'échappement : pour en afficher une, on en met deux,
\\; - le pourcent introduit les marqueurs : pour en afficher un, on en met deux,
%%.
C'est une source d'erreurs classique sous Windows, où les chemins de fichiers sont pleins de barres obliques inverses.
Que se passe-t-il à la compilation, puis à l'exécution ?
double prix = 1500.75;
printf("%d\n", prix);Voir le corrigéMasquer le corrigé
À la compilation : le programme compile. Avec -Wall, gcc avertit :
warning: format '%d' expects argument of type 'int', but argument 2 has type 'double'
Sans -Wall, rien du tout : d'où l'intérêt de l'option.
À l'exécution : une valeur absurde s'affiche. printf ne connaît pas le type réel de ses arguments : il croit la chaîne de format sur parole. Voyant %d, il lit 4 octets et les interprète comme un entier, alors que le double en occupe 8.
La valeur affichée n'est pas prévisible : elle dépend du compilateur et du processeur. C'est ce que la norme appelle un comportement indéfini.
La correction : printf("%f\n", prix); ou printf("%.2f\n", prix);
Écrivez le programme qui affiche :
A vaut 65 a vaut 97 ecart = 32 '7' - '0' = 7
Voir le corrigéMasquer le corrigé
#include <stdio.h>
int main(void)
{
char maj = 'A';
char min = 'a';
printf("%c vaut %d\n", maj, maj);
printf("%c vaut %d\n", min, min);
printf("ecart = %d\n", min - maj);
printf("'7' - '0' = %d\n", '7' - '0');
return 0;
}La même variable s'affiche en lettre avec %c et en nombre avec %d : un char est un petit entier.
L'écart de 32 entre majuscules et minuscules n'est pas un hasard : dans la table ASCII, c'est exactement le sixième bit qui change.
'7' - '0' vaut 7 parce que les chiffres se suivent dans la table. C'est la façon standard de convertir un chiffre écrit en chiffre calculable.
Partie 5 : Lire avec scanf
Saisir des données sans se faire piquer par l'esperluette manquante ni par le retour à la ligne resté dans le tampon.
Ce programme compile, puis s'arrête brutalement. Pourquoi ?
int age;
printf("Ton age : ");
scanf("%d", age);
printf("%d ans\n", age);Voir le corrigéMasquer le corrigé
Il manque l'esperluette : scanf("%d", &age).
scanf doit écrire dans la variable : il lui faut son adresse. Sans le &, il reçoit la valeur de age, qui n'est pas initialisée, donc quelconque, et la prend pour une adresse.
Il écrit alors à cette adresse, qui ne vous appartient pas. Le système arrête le programme : segmentation fault sous Linux, fermeture brutale sous Windows.
Avec -Wall, gcc avertit : format '%d' expects argument of type 'int *', but argument 2 has type 'int'. L'astérisque signale qu'une adresse était attendue.
scanf("%d", &age);
L'utilisateur tape 25 puis Entrée, puis A puis Entrée. Le programme n'attend pas la seconde saisie. Pourquoi, et comment corriger ?
int age;
char initiale;
printf("Age : ");
scanf("%d", &age);
printf("Initiale : ");
scanf("%c", &initiale);
printf("%d ans, initiale %c\n", age, initiale);Voir le corrigéMasquer le corrigé
Quand l'utilisateur tape 25 puis Entrée, scanf("%d") consomme les deux chiffres et laisse le retour à la ligne dans la mémoire tampon du clavier.
%d et %f sautent automatiquement les blancs ; %c ne saute rien. Le second scanf prend donc ce retour à la ligne comme étant l'initiale, et rend la main aussitôt.
scanf(" %c", &initiale);
L'espace devant %c demande à scanf d'avaler tous les blancs en attente, espaces, tabulations et retours à la ligne, avant de lire le caractère utile.
Un espace, un seul caractère, et le bogue disparaît.
Écrivez un programme qui lit un entier et affiche combien de valeurs scanf a réellement lues. Que vaut ce nombre si l'utilisateur tape bonjour ?
Voir le corrigéMasquer le corrigé
#include <stdio.h>
int main(void)
{
int n;
int lus;
printf("Entrez un entier : ");
lus = scanf("%d", &n);
printf("scanf a lu %d valeur(s)\n", lus);
return 0;
}scanf renvoie le nombre de valeurs correctement lues et rangées.
- L'utilisateur tape
42→ la fonction renvoie 1. - Il tape
bonjour→ elle renvoie 0. Aucune valeur n'a pu être lue, etnreste inchangée, donc non initialisée. - Le fichier d'entrée se termine → elle renvoie
EOF, une constante valant −1.
Un programme sérieux teste cette valeur avant d'utiliser la variable. Le traitement relève du cours sur les conditions ; retenez déjà que l'information existe.
Un détail important : en cas d'échec, le texte fautif reste dans le tampon. Relire sans l'avoir vidé échouerait à l'infini.
Partie 6 : Fonctions prédéfinies et programmes complets
Employer les bibliothèques standard et assembler tout ce qui précède dans de vrais petits programmes.
Écrivez un programme qui demande le rayon d'un cercle et affiche son périmètre et son aire, avec quatre décimales. Vérifiez avec un rayon de 7,5.
Voir le corrigéMasquer le corrigé
#include <stdio.h>
#include <math.h>
int main(void)
{
double rayon;
printf("Rayon du cercle : ");
scanf("%lf", &rayon);
printf("Perimetre : %.4f\n", 2 * M_PI * rayon);
printf("Aire : %.4f\n", M_PI * rayon * rayon);
return 0;
}Avec un rayon de 7,5 :
Perimetre : 47.1239
Aire : 176.7146
Trois points d'attention :
%lfpour lire undouble,%fpour l'afficher ;M_PIvient de<math.h>et vaut 3,14159265358979… ; sur certains compilateurs il faut-D_USE_MATH_DEFINES, sinon définissez-le vous-même avec#define;- compilez avec
-lmsous Linux et macOS.
Écrivez un programme qui lit les deux côtés de l'angle droit d'un triangle rectangle et affiche l'hypoténuse avec deux décimales. Testez avec 3 et 4.
Voir le corrigéMasquer le corrigé
#include <stdio.h>
#include <math.h>
int main(void)
{
double a, b;
printf("Premier cote : ");
scanf("%lf", &a);
printf("Second cote : ");
scanf("%lf", &b);
printf("Hypotenuse : %.2f\n", sqrt(a * a + b * b));
return 0;
}Avec 3 et 4 :
Hypotenuse : 5.00
On aurait pu écrire sqrt(pow(a, 2) + pow(b, 2)), mais a * a est plus rapide et plus lisible pour un carré. Réservez pow aux puissances qui ne sont pas des petits entiers.
Sans -lm sous Linux : undefined reference to 'sqrt'.
Écrivez un programme qui lit un prix hors taxes en FCFA et une quantité, puis affiche le montant hors taxes, la TVA à 19,25 % et le total toutes taxes comprises, alignés sur deux décimales.
Vérifiez avec un prix de 15 000 FCFA et une quantité de 1.
Voir le corrigéMasquer le corrigé
#include <stdio.h>
#define TAUX_TVA 0.1925
int main(void)
{
double prix_ht;
int quantite;
printf("Prix unitaire HT (FCFA) : ");
scanf("%lf", &prix_ht);
printf("Quantite : ");
scanf("%d", &quantite);
double total_ht = prix_ht * quantite;
double tva = total_ht * TAUX_TVA;
double total_ttc = total_ht + tva;
printf("\n%-16s %12.2f\n", "Total HT", total_ht);
printf("%-16s %12.2f\n", "TVA 19,25%%", tva);
printf("%-16s %12.2f\n", "Total TTC", total_ttc);
return 0;
}Avec 15 000 FCFA et une quantité de 1 :
Total HT 15000.00
TVA 19,25% 2887.50
Total TTC 17887.50
Notez le %% dans "TVA 19,25%%" : sans le doublement, printf croirait à un marqueur.
En production
Un vrai logiciel de facturation ne compte pas en double : il compte en unités entières, des francs, avec un long long, et n'arrondit qu'au moment précis où la réglementation l'impose. Les flottants accumulent des écarts qui finissent par se voir sur les cumuls.
Pour continuer
- Tous les tutoriels de langage C : la suite du parcours : structures de contrôle, boucles, et le reste.
- Exercices corrigés de langage C : une quarantaine de fiches, avec corrigés en accordéon.
- QCM de programmation C : pour vérifier ce qui est acquis.
- Codage
des nombres en machine : pourquoi un
floatne tombe pas juste, et ce que contient vraiment unint. - Cours d'algorithmique : pour travailler le raisonnement indépendamment du langage.
Approfondir : Les bases : cours de référence
Ce complément relie les fondations du cours à une méthode de lecture des programmes. Il ne remplace pas les explications et exercices précédents : il propose un petit bilan pour vérifier les affectations, les types et les conditions.
Distinguer valeur initiale et valeur courante
Une variable change au fil des affectations. Une trace doit noter l’état après chaque instruction et non retenir seulement sa valeur de départ. Lorsqu’une autre variable reçoit sa valeur, il s’agit d’une copie : les modifications suivantes de la première ne mettent pas à jour cette copie.
Passer de l’expression au résultat
Avant d’exécuter, repérer le type de chaque opérande et la priorité des opérations. Une division d’entiers reste entière, même si l’on affecte ensuite son résultat à un double. Les parenthèses rendent le regroupement plus clair ; elles ne corrigent pas un type mal choisi ni un accès mémoire invalide.
Tester avec une attente écrite
Pour chaque exemple, écrire d’abord le résultat supposé puis comparer avec la sortie. Une différence conduit à examiner la première instruction où la trace diverge. Cette démarche est plus instructive que de modifier plusieurs lignes au hasard. Garder aussi un exemple où les nombres sont différents, afin de révéler une copie ou une comparaison erronée.
Bilan : valeur copiée et valeur modifiée
#include <stdio.h>
int main(void) {
int a = 4;
int b = a;
a += 3;
double moyenne = (a + b) / 2.0;
printf("a = %d, b = %d\n", a, b);
printf("Moyenne = %.1f\n", moyenne);
return 0;
}| Instruction ou étape | Explication |
|---|---|
| a=4 puis b=a | Les deux objets valent 4. |
| a+=3 | a devient 7 ; b reste 4. |
| (a+b)/2.0 | La moyenne réelle vaut 5.5. |
Sortie écran : essai 1
Une affectation ne crée pas une liaison permanente entre variables.
a = 7, b = 4
Moyenne = 5.5S’entraîner et vérifier
Exercice 1. Que changer pour que b prenne ensuite la nouvelle valeur de a ?
Voir la correctionMasquer la correction
Écrire une nouvelle affectation b=a; après a+=3;. b devient alors 7 et la moyenne des deux vaut 7.0.
Pratiquer avec le TD/TP 1 corrigé · Tous les exercices C.
Repère de lecture : les codes proposés utilisent des constructions disponibles en C11. Les règles du langage et de sa bibliothèque sont consultables dans les projets publics du WG14 (C11) et du WG14 (C23). Les exemples n’utilisent pas les nouveautés propres à C23.
Questions / Réponses
Ajouter un commentaire