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 .c et 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 printf au caractère près, et scanf sans 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 : gcc est 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.

Les quatre etapes de la compilation : le preprocesseur traite les directives et produit un fichier .i, le compilateur traduit en assembleur .s, l'assembleur produit le code machine .o, l'editeur de liens rassemble le tout en un executable.

É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
  • -Wall et -Wextra activent 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=c17 fixe 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;
}

Le programme ligne par ligne : la directive include, la fonction main point d'entree, les accolades qui delimitent le bloc, la declaration d'une variable, l'appel a printf et le return zero final.

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.

  • int annonce que main renvoie un entier au système en se terminant.
  • main est le nom imposé par la norme. Ni Main, ni principal.
  • (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

  • #include apporte des outils ; pas de point-virgule.
  • main est 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 : age et Age sont 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 :

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

Tableau des types de base du C avec leur taille en octets et leur etendue : char un octet, short deux, int et float quatre, long long et double huit.

Les entiers

TypeTaille habituelleÉtendue Marqueur printf
char1 octet−128 à 127 %c ou %d
short2 octets −32 768 à 32 767%hd
int4 octets −2 147 483 648 à 2 147 483 647%d
long4 ou 8 octets dépend du système%ld
long long8 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

TypeTailleChiffres significatifs sûrs Marqueur printf
float4 octetsenviron 7 %f
double8 octetsenviron 15 %lf ou %f
long double8, 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.
  • int par défaut pour les entiers, double pour les réels.
  • Les tailles ne sont pas garanties : interrogez sizeof.
  • Un char est un petit entier : 'A' vaut 65.
  • Deux entiers donnent une division entière. 5 / 2 vaut 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, Age et AGE sont trois variables différentes.
  • Un mot réservé du langage est interdit.
ValideInvalidePourquoi
prix_totalprix total espace interdit
note22note commence par un chiffre
_tempprix-total le tiret est l'opérateur de soustraction
nbElevesélèves accents interdits
valeur_maxint 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 */
#defineconst
Traité parle préprocesseurle compilateur
A un typenonoui
Occupe de la mémoirenonoui, en général
Vérification de typeaucunecomplète
Visible au débogueurnonoui

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érateurRôleExempleRésultat
+addition7 + 2 9
-soustraction7 - 2 5
*multiplication7 * 2 14
/division7 / 2 3 (entiers)
%reste de la division entière 7 % 21

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érateurSignificationExemple Valeur
==est égal à5 == 5 1
!=est différent de 5 != 50
< >strictement inférieur, supérieur3 < 71
<= >=inférieur ou égal, supérieur ou égal3 >= 70
&&et 1 && 00
||ou1 || 0 1
!non!01

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 :

RangOpérateursSens de lecture
1()-
2++ -- ! (type) sizeofdroite à 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.

Un appel a printf : la chaine de format contient trois marqueurs, pourcent s, pourcent d et pourcent point un f, relies chacun par une fleche a l'argument correspondant, 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

MarqueurPour quel typeExemple Affiche
%dint printf("%d", 42)42
%uunsigned int printf("%u", 3000000000u) 3000000000
%ldlong --
%lldlong long --
%ffloat ou double printf("%f", 3.5)3.500000
%enotation scientifique printf("%e", 31400.0) 3.140000e+04
%gla plus courte des deux printf("%g", 0.00001234) 1.234e-05
%cchar printf("%c", 65)A
%schaîne de caractères printf("%s", "Genius")Genius
%x %Xentier en hexadécimal printf("%x", 255)ff
%oentier en octal printf("%o", 8)10
%zuré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équenceEffet
\nretour à la ligne
\ttabulation
\\une barre oblique inverse
\"un guillemet dans une chaîne
\'une apostrophe dans un caractère
\aun bip sonore
\0le 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 ».

Une case memoire nommee age contenant 20. printf recoit la valeur 20 ; scanf recoit l'adresse de la case et y ecrit la saisie de l'utilisateur.

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

MarqueurLitExemple
%dun int scanf("%d", &n);
%fun float scanf("%f", &prix);
%lfun double scanf("%lf", &moyenne);
%cun char scanf("%c", &lettre);
%sun 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

  • printf reçoit des valeurs ; scanf reçoit des adresses, d'où le &.
  • Pour un double : %f à l'affichage, %lf à la lecture.
  • Devant %c, mettez un espace : " %c".
  • scanf renvoie 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

FonctionRôleExempleRé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

FonctionRô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 voyezCe 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 attendurelire la comparaison
le programme s'arrête sans message très souvent un & oublié dans scanfvé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

  1. Compilez souvent. Toutes les cinq ou dix lignes, pas à la fin. Une erreur dans dix lignes se trouve ; dans deux cents, elle se cherche.
  2. Compilez avec -Wall -Wextra et traitez les avertissements comme des erreurs.
  3. Indentez en écrivant, pas après coup.
  4. Initialisez vos variables au moment où vous les déclarez.
  5. Affichez les valeurs intermédiaires avec printf quand 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.

Exercice 1 : Trouver les quatre fautes

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;
}
Exercice 2 : Qu'affiche ce programme ?
#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.

Exercice 3 : Le programme minimal

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

Exercice 4 : À quelle étape l'erreur apparaît-elle ?

Pour chacun de ces messages, dites quelle étape de la chaîne de compilation se plaint, et ce qu'il faut corriger.

  1. fatal error: stdioo.h: No such file or directory
  2. error: expected ';' before 'return'
  3. undefined reference to 'pow'
  4. 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.

Exercice 5 : La macro qui ment

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.

Exercice 6 : Chevrons ou guillemets

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.

Exercice 7 : Les gardes d'inclusion

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

Exercice 8 : Prévoir les tailles

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.

Exercice 9 : Choisir le bon type

Quel type déclarer pour chacune de ces données ? Justifiez en une ligne.

  1. l'âge d'un étudiant
  2. la population mondiale
  3. le prix d'un article en FCFA
  4. l'initiale d'un prénom
  5. la moyenne d'un étudiant sur 20
  6. le nombre d'octets d'un fichier vidéo
Voir le corrigéMasquer le corrigé
  1. int, ou unsigned char si la place compte. Un âge tient largement dans un entier ordinaire.
  2. long long. Environ 8×109 : cela dépasse les 2,1 milliards d'un int.
  3. int ou long. Le franc CFA n'a pas de subdivision en usage : on compte en unités entières, jamais en flottant.
  4. char : un seul caractère, donc un octet.
  5. double. Une moyenne a des décimales.
  6. long long. Un fichier de 3 Go fait 3 221 225 472 octets, ce qui dépasse déjà l'étendue d'un int signé.
Exercice 10 : Le débordement silencieux

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.

Exercice 11 : Un caractère trop grand

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.

Exercice 12 : Noms valides ou non

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 lit prix moins total.
  • int : mot réservé.
  • élèves : caractères accentués interdits dans un identificateur.
  • note 1 : espace interdit.
Exercice 13 : const ou #define

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

Exercice 14 : La variable fantôme

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.

Exercice 15 : Division entière

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.

Exercice 16 : Décomposer un nombre avec le modulo

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.

Exercice 17 : La moyenne qui tombe faux

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.

Exercice 18 : Post- et pré-incrémentation

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.

Exercice 19 : Priorités des opérateurs

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é
ExpressionLectureValeur
2 + 3 * 42 + (3×4)14
(2 + 3) * 45×420
10 % 3 * 2(10%3)×2 = 1×22
10 - 4 - 3(10−4)−33
20 / 4 / 5(20/4)/5 = 5/51
7 + 3 % 47 + (3%4) = 7 + 310

*, / 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.

Exercice 20 : Le résultat d'une comparaison

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.

Exercice 21 : Prévoir la sortie au caractère près

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.

Exercice 22 : Aligner des colonnes

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

Exercice 23 : Afficher des caractères spéciaux

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

Exercice 24 : Le mauvais marqueur

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);

Exercice 25 : Caractère et code ASCII

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

Exercice 26 : L'esperluette oubliée

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);

Exercice 27 : Le retour à la ligne resté dans le tampon

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.

Exercice 28 : Vérifier que la saisie a réussi

É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, et n reste 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.

Exercice 29 : Le cercle

É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 :

  • %lf pour lire un double, %f pour l'afficher ;
  • M_PI vient 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 -lm sous Linux et macOS.
Exercice 30 : L'hypoténuse

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

Exercice 31 : La facture

É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

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;
}
Suivre le programme : Bilan, valeur copiée et valeur modifiée
Instruction ou étapeExplication
a=4 puis b=aLes deux objets valent 4.
a+=3a devient 7 ; b reste 4.
(a+b)/2.0La 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.5

S’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

Aucune question. Soyez le premier à poser une question.
★ ★ ★ ★ ★
2 votes. Moyenne 5 sur 5.

Ajouter un commentaire

Anti-spam