Structure générale

Un code C est découpé en fonctions. Les instructions sont toujours écrites dans le corps des fonctions. À l’extérieur des fonctions (c’est-à-dire au niveau global du module), vous pouvez déclarer de nouveaux types, de nouvelles variables, ou indiquer l’existence d’autres fonctions. Tout programme C a un point d’entrée. C’est la fonction main. Lorsque vous exécuterez votre programme, le flot d’exécution commencera par rentrer dans cette fonction. Dans cette fonction, vous effectuerez des opérations, calculs, mais vous pouvez bien sûr appeler d’autres fonctions.

Les blocs d’instructions sont placés entre accolades { }. Les instructions sont terminées par un point virgule ;.

int main(void) {
    // instructions ici
    return 0;
}

Pour créer des objets complexes, il faut d’abord disposer de types simples. Les types suivants :

  • Caractère : char
  • Entiers : char, short, int, long, long long
  • Boolean : c’est le type int ! (tout ce qui n’est pas zero est vrai)
    • Vous pouvez #include <stdbool.h> pour avoir bool, true, false de défini mais ce sont des raccourcis
  • Nombres à virgule flottante : float, double, long double
  • Type pointeur : pointeur d’un autre type, qui est toujours représenté par une adresse en mémoire (sur 32 ou 64 bit selon la taille du bus d’adresse).

Attention la place que prennent chaqu’un de ces types dépend de l’architecture matériel cible l’or de la compilation. Et donc a destination d’un système hôte précis et indirectement de la taille du bus d’adresse. Il est possible de lancé un binnaire 32bits sur une architecture 64bits. Dans ce cas les systeme passe par un couche d’abstraction (WOW64 sous windows). Il existent également des architecture étendue (128bits, 256bits, 512bits, …).

Type C 8 bits 16 bits 32 bits 64 bits
char 1 octet 0:1 1 octet 1 octet 1 octet
short 1 ou 2 octets 2 octets 2 ou 4 octets 2 ou 4 octets
int 1 ou 2 octets 2 ou 4 octets 4 octets 4 ou 8 octets
long 2 ou 4 octets 4 octets 4 ou 8 octets 8 octets (LP64)
long long 4 ou 8 octets 8 octets 8 octets 8 octets
float 4 octets 4 octets 4 octets 4 octets
double 4 ou 8 octets 8 octets 4 ou 8 octets 8 octets
long double 4 ou 8 octets 8 ou 12 octets 8, 12 ou 16 octets 16 octets (selon ABI)
Pointeur (void*) 1 ou 2 octets 2 octets 4 octets 8 octets

La taille d’un pointeur dépend uniquement de la largeur d’adresse, pas du type pointé.

Notez également le return 0; dans la fonction main c’est une convention sur les système Unix. C’est une information que le système pourras récupéré pour savoir si le programme c’est terminé correctement. Dans ce cas (0) tout c’est bien déroulé. Dans le cas contraire (1) c’est une erreur général ou autre autre valeur -> à vous de définir, juste pour dire fin du programme imprévue. C’est un peu la version C des exceptions (pour ceux qui ont fait du Java ou du C#), il n’y a pas de message direct mais c’est super rapide.

Déclaration et affectations

Une affectation est l’opération qui attribue une valeur à une variable. L’opérateur d’affectation simple est =.

int x ;        // Déclaration de la variable (Dit coucou @x éxiste)
x = 20;      // @x prend la valeur 20
int y = 10; // Les deux à la fois

Le C propose également des opérateurs d’affectation composés qui combinent une opération arithmétique avec l’affectation :

  • += : addition et affectation
  • -= : soustraction et affectation
  • *= : multiplication et affectation
  • /= : division et affectation
  • %= : modulo et affectation
  • &=, |=, ^= : opérateurs bit à bit avec affectation
  • <<=, >>= : décalages avec affectation
int x = 10;
x += 5;      // équivalent à x = x + 5; donc x = 15
x -= 3;      // équivalent à x = x - 3; donc x = 12
x *= 2;      // équivalent à x = x * 2; donc x = 24
x /= 4;      // équivalent à x = x / 4; donc x = 6

Affectation multiple

Le C permet les affectations multiples dans une même expression :

int a, b, c;
a = b = c = 5;  // a, b et c reçoivent tous la valeur 5

Cela fonctionne parce que l’affectation est une expression qui retourne la valeur affectée. L’évaluation se fait de droite à gauche : c reçoit 5, puis b reçoit le résultat de c = 5 (qui est 5), puis a reçoit le résultat de b = 5.

graph TD A[Assign int] B[Assign int] C[Assign int] V5[Literal 5 int] VarA[Id a int] VarB[Id b int] VarC[Id c int] A --> VarA A --> B B --> VarB B --> C C --> VarC C --> V5

Portée des variables

La portée (scope) d’une variable détermine la région du programme où cette variable peut être utilisée. Le C reconnaît plusieurs niveaux de portée : globale, locale (au niveau d’une fonction) et au niveau d’un bloc (sur les nouvelles normes).

Variables globales

Une variable déclarée en dehors de toute fonction a une portée globale. Elle est accessible depuis n’importe quel endroit du programme, y compris depuis d’autres fichiers sources si elle est déclarée comme extern.

int compteur = 0;  // variable globale

void incrementer(void) {
    compteur++;
}

int main(void) {
    printf("Compteur = %d\n", compteur);  // 0
    incrementer();
    printf("Compteur = %d\n", compteur);  // 1
    return 0;
}

Les variables globales sont stockées dans la section des données globales du programme. Elles existent tout aux long de l’éxécution du processus.

Variables locales

Une variable déclarée à l’intérieur d’une fonction a une portée locale à cette fonction. Elle n’existe que durant l’exécution de la fonction et n’est pas accessible depuis l’extérieur.

void ma_fonction(void) {
    int x = 5;  // variable locale à ma_fonction
    printf("x = %d\n", x);
}

int main(void) {
    ma_fonction();
    // printf("%d\n", x);  // ERREUR : x n'est pas accessible ici
    return 0;
}

Les variables locales sont stockées sur la pile (stack). Leur durée de vie est limitée à l’exécution de la fonction.

Variables de bloc

Une variable peut être déclarée à l’intérieur d’un bloc (section de code délimitée par { }), y compris à l’intérieur d’une boucle ou d’une condition. Elle est alors locale à ce bloc.

int main(void) {
    int x = 10;
    printf("x = %d\n", x);  // 10

    {
        int x = 20;  // nouvelle variable, même nom, portée limitée au bloc
                int y = 42;
        printf("x = %d\n", x);  // 20
    }

    printf("x = %d\n", x);  // 10 (x du bloc n'existe plus) et y n'existe plus
    return 0;
}

Cette technique, appelée shadowing, permet de réutiliser un nom de variable dans un bloc imbriqué. Cependant, elle peut rendre le code confus et est généralement déconseillée.

Durée de vie

La durée de vie (lifetime) d’une variable est distinct de sa portée. Une variable peut avoir une portée locale mais une durée de vie plus longue grâce au mot-clé static.

void compteur_appels(void) {
    static int appels = 0;  // initialisée une seule fois, persiste entre les appels
    appels++;
    printf("Appelée %d fois\n", appels);
}

int main(void) {
    compteur_appels();  // Appelée 1 fois
    compteur_appels();  // Appelée 2 fois
    compteur_appels();  // Appelée 3 fois
    return 0;
}

Une variable statique locale est initialisée une seule fois au lancement du programme, puis sa valeur persiste entre les appels de la fonction.

Visibilité

En préfixant une variable globale par le mot-clé static, on limite sa visibilité au fichier courant. Cela crée une portée au niveau du fichier.

fichier1.c :

static int secret = 42;  // visible uniquement dans fichier1.c

int get_secret(void) {
    return secret;
}

fichier2.c :

extern int secret;  // ERREUR : secret n'est pas accessible (static)

Le préprocesseur

Le préprocesseur est un étape de compilation qui traite les directives débutant par # avant que le compilateur ne voie le code. Le préprocesseur effectue des transformations textuelles sur le code source : inclusions de fichiers, substitutions de macros, compilation conditionnelle, etc.

Inclusions de fichiers

La directive #include demande au préprocesseur d’insérer le contenu d’un fichier à l’endroit où elle est écrite. Il existe deux formes :

  • #include <fichier.h> : cherche le fichier dans les répertoires standards du système (fichiers d’en-tête système)
  • #include "fichier.h" : cherche le fichier dans le répertoire courant, c’est généralement un path relatif
#include <stdio.h>      // bibliothèque standard d'entrée-sortie
#include <math.h>       // bibliothèque mathématique
#include "mon_header.h" // en-tête local

Les fichiers *.h contiennent typiquement des déclarations de fonctions, des définitions de types et des constantes. Ils ne doivent pas contenir l’implémentation de fonctions (sauf pour les fonctions inline). Une bonne pratique est de protéger les en-têtes contre les inclusions multiples :

// mon_header.h
#ifndef MON_HEADER_H
#define MON_HEADER_H

// contenu de l'en-tête

#endif  // MON_HEADER_H

Si MON_HEADER_H a déjà été défini, le fichier entier est ignoré. Cela prévient les redéfinitions.

Dans les nouvelles normes C20 et C++ il y a la directive #pragma once qui simplifie la démarche, plus qu’une ligne pour protéger.

Substitution

La directive #define définit une macro : une substitution textuelle effectuée par le préprocesseur.

#define PI 3.14159265
#define MAX_TAILLE 100

float aire = PI * rayon * rayon;
int tableau[MAX_TAILLE];

À la compilation, PI est remplacé partout par 3.14159265 et MAX_TAILLE par 100. Contrairement à une variable, une macro n’a pas de type et n’occupe pas d’espace mémoire à l’exécution : c’est une substitution pure du texte. Dans le cours précédent nous avons vue le flag --save-temp qui sauvegarde les fichier intermédiaires. Notament le fichier .i qui est la transformation après le préprocessing. On auras donc :

float aire = 3.14159265 * rayon * rayon;
int tableau[10];

Macros avec arguments

Les macros peuvent accepter des arguments, ce qui permet de créer des pseudo-fonctions :

#define CARRE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))

int resultat = CARRE(5);      // remplacé par ((5) * (5)) = 25
int max_val = MAX(10, 20);    // remplacé par ((10) > (20) ? (10) : (20)) = 20

Attention : les macros n’effectuent qu’une substitution textuelle. Les parenthèses sont essentielles pour éviter les problèmes de priorité d’opérateurs

#define MAUVAISE_MACRO(x) x * x
int resultat = MAUVAISE_MACRO(2 + 3);  // remplacé par 2 + 3 * 2 + 3 = 11, pas 25 !

Opérateurs du préprocesseur

Stringification : # convertit l’argument en chaîne de caractères :

#define AFFICHER(x) printf(#x " = %d\n", x)
int a = 42;
AFFICHER(a);  // affiche : a = 42

Concatenation : ## fusionne deux tokens :

#define CONCATENER(a, b) a ## b
int xy = 100;
int z = CONCATENER(x, y);  // z = xy = 100

Compilation conditionnelle

Les directives #if, #ifdef, #ifndef, #else, #elif et #endif permettent d’inclure ou d’exclure du code selon des conditions évaluées à la compilation.

#ifdef DEBUG
printf("Mode debug activé\n");
#endif

#if defined(WIN32)
// code spécifique à Windows
#elif defined(__linux__)
// code spécifique à Linux
#else
// code par défaut
#endif

Cela permet d’adapter le code à différentes plateformes ou configurations sans devoir maintenir plusieurs versions du source. Les defines WIN32, __linux__ sont principalement géré par le system hôte ou par la chaine de compilation (ex: CMake)

Porté des defines

En C/C++, les #define sont traités par le préprocesseur, avant la compilation. Ils n’ont pas de portée au sens des blocs { } : ils sont valides depuis l’endroit où ils sont définis jusqu’à la fin du fichier, sauf si on les annule avec #undef.

void fonction() {
    #define TRUC 42   // Défini ici...
}

// ...mais valable en réalité au-delà du bloc !

int main() {
    int x = TRUC;     // OK : TRUC existe ici !
}

Le préprocesseur ignore les blocs ({ }) : il ne connaît que le texte. Donc définir une macro dans une fonction est possible mais déconseillé car la macro sort du bloc et pollue le code après. Dans ce cas il faut utiliser la directive #undef qui annule la définition d’une macro :

#define TEMP 100
// utiliser TEMP
#undef TEMP
// TEMP n'existe plus après

Expressions

Une expression en C est une combinaison de variables, de constantes et d’opérateurs qui se réduit à une valeur unique. Le C supporte plusieurs catégories d’opérateurs permettant de construire des expressions complexes et puissantes.

Opérateurs arithmétiques

Les opérateurs arithmétiques permettent des calculs numériques. Les cinq opérateurs de base sont :

  • Addition : +
  • Soustraction : -
  • Multiplication : *
  • Division : / (division entière si les deux opérandes sont des entiers, division réelle sinon)
  • Modulo : % (reste de la division entière, n’est pas alloué sur les float et double)
int a = 10, b = 3;
int somme = a + b;      // 13
int diff = a - b;       // 7
int prod = a * b;       // 30
int div = a / b;        // 3 (division entière)
int reste = a % b;      // 1

L’ordre de priorité des opérateurs arithmétiques suit les règles mathématiques usuelles : la multiplication, la division et le modulo sont évalués avant l’addition et la soustraction. Les parenthèses permettent de forcer un ordre d’évaluation.

int resultat = 2 + 3 * 4;      // 14 (3*4 d'abord, puis +2)
int resultat2 = (2 + 3) * 4;   // 20 (2+3 d'abord, puis *4)

**Attention aux risque d’overflow et d’underflow : **

unsigned int x = UINT_MAX;  // 4294967295
x = x + 1;  // devient 0
x = x - 1;  // devient UINT_MAX

Opérateurs de comparaison

Les opérateurs de comparaison produisent une valeur booléenne (0 pour faux, 1 pour vrai en C) :

  • Égalité : ==
  • Inégalité : !=
  • Inférieur : <
  • Inférieur ou égal : <=
  • Supérieur : >
  • Supérieur ou égal : >=
int a = 5, b = 3;
int est_egal = a == b;       // 0 (faux)
int est_different = a != b;  // 1 (vrai)
int est_plus_grand = a > b;  // 1 (vrai)

Opérateurs logiques

Les opérateurs logiques permettent de combiner des conditions :

  • ET logique : && (évalue le deuxième opérande seulement si le premier est vrai)
  • OU logique : || (évalue le deuxième opérande seulement si le premier est faux)
  • NON logique : ! (négatif d’une expression booléenne)
int a = 5, b = 3, c = 8;
int cond1 = (a > b) && (b < c);  // 1 (vrai ET vrai)
int cond2 = (a < b) || (b > c);  // 0 (faux OU faux)
int cond3 = !(a == b);           // 1 (NON(faux) = vrai)

Opérateurs bit à bit

Pour manipuler directement les bits :

  • ET bit à bit : &
  • OU bit à bit : |
  • OU exclusif : ^
  • Décalage à gauche : <<
  • Décalage à droite : >>
  • Complément bit à bit : ~
unsigned int a = 5;     // 0101 en binaire
unsigned int b = 3;     // 0011 en binaire
unsigned int et = a & b;   // 0001 = 1
unsigned int ou = a | b;   // 0111 = 7
unsigned int xor = a ^ b;  // 0110 = 6
unsigned int dec_g = a << 1;  // 1010 = 10
unsigned int dec_d = a >> 1;  // 0010 = 2

Opérateur ternaire

L’opérateur ternaire ? : permet de choisir entre deux expressions selon une condition :

int a = 5, b = 3;
int max = (a > b) ? a : b;  // max = 5

C’est une condition ! Le code fais la même chose avec if / else (modulo une petite opération fut un temps)

Tableaux statiques

Un tableau est une collection d’éléments du même type, stockés de manière contiguë en mémoire. Un tableau statique a sa taille fixée à la compilation.

int scores[10];              // tableau de 10 entiers (non initialisés)
int notes[5] = {18, 15, 20, 17, 19};  // initialisation au moment de la déclaration
int valeurs[3] = {0};        // tous les éléments à zéro
int data[] = {1, 2, 3, 4};   // taille déduite de l'initialisation : 4 (uniquement depuis C99)

En C, les indices commencent à zéro. L’accès à un élément se fait via l’opérateur [] :

notes[0] = 18;
printf("%d\n", notes[2]);  // affiche 20

L’accès hors limites (par exemple notes[100]) n’est pas vérifié par le compilateur : le programme accède à une zone mémoire arbitraire, conduisant généralement à une erreur d’exécution ou à un comportement imprévisible. C’est un des pièges courants du C.

Tableaux multidimensionnels

Les tableaux peuvent avoir plusieurs dimensions :

int matrice[3][4];          // tableau 3x4 d'entiers
matrice[0][2] = 42;

En mémoire, un tableau multidimensionnel est stocké de façon linéaire, ligne par ligne (ordre row-major) :

int tableau[2][3] = {{1, 2, 3}, {4, 5, 6}};
// En mémoire : 1, 2, 3, 4, 5, 6

Tableaux de caractères (chaînes)

Une chaîne de caractères en C est un tableau de char terminé par un caractère nul \0 :

char nom[20] = "Alice";     // "Alice\0" en réalité (6 caractères dont le \0)
char msg[] = "Bonjour";     // taille déduite

De nombreuses fonctions de la bibliothèque standard (comme strlen, strcpy, printf) exploitent ce caractère nul pour détecter la fin de la chaîne.

Taille et allocation statique

Pour obtenir le nombre d’éléments d’un tableau statique, on peut utiliser :

int tab[] = {1, 2, 3, 4, 5};
int nb_elements = sizeof(tab) / sizeof(tab[0]);  // 5

Cette technique ne fonctionne que pour les tableaux statiques. Pour les tableaux passés en paramètre à une fonction, sizeof retourne la taille d’un pointeur, non celle du tableau. DANS LA TRÈS GRANDE MAJORITÉ DES CAS VOUS VOUS PLANTÉ !

Instructions conditionnelles

Les instructions conditionnelles permettent d’exécuter du code différent selon que certaines conditions sont vraies ou fausses.

L’instruction if

L’instruction if exécute un bloc d’instructions si et seulement si une condition est vraie :

if (age >= 18) {
  printf("Vous êtes adulte\n");
}

En C, une condition est considérée comme vraie si sa valeur est non zéro, et fausse si sa valeur est zéro. Pour les booliens, les compilateurs C99 et ultérieurs reconnaissent le type _Bool (ou bool si on inclut <stdbool.h>) cela reste des alias du type int. Et c’est une propriété intéréssante !

Assembler (x86, pseudo-code) :

mov eax, [age]        ; charge la valeur de age dans eax
cmp eax, 18           ; compare eax à 18
jl  fin_if            ; si age < 18, saute la partie printf
; ici : code pour printf("Vous êtes adulte\\n")
fin_if:

Ici, cmp effectue la comparaison, et jl (“jump if less”) saute l’instruction si le résultat est inférieur à 18. Sinon, on exécute le bloc du if.

if-else

La clause else permet de spécifier du code à exécuter si la condition est fausse :

if (age >= 18) {
  printf("Vous êtes adulte\n");
} else {
  printf("Vous êtes mineur\n");
}

Assembler (x86, pseudo-code) :

mov eax, [age]
cmp eax, 18
jl  else_block         ; saute vers else_block si age < 18
; ici : code pour printf("Vous êtes adulte\\n")
jmp fin_ifelse         ; saute vers la fin de la structure
else_block:
; ici : code pour printf("Vous êtes mineur\\n")
fin_ifelse:

On utilise un saut conditionnel suivi d’un saut inconditionnel pour gérer les deux branches.

else if (chaînage)

On peut enchaîner plusieurs conditions avec else if :

if (note >= 16) {
  printf("Excellent\n");
} else if (note >= 12) {
  printf("Bien\n");
} else if (note >= 10) {
  printf("Passable\n");
} else {
  printf("Insuffisant\n");
}

Assembler (x86, pseudo-code) :

mov eax, [note]
cmp eax, 16
jl  test_bien           ; si note < 16, passe à la suite
; printf("Excellent")
jmp fin_ifelse

test_bien:
cmp eax, 12
jl  test_passable       ; si note < 12, passe à la suite
; printf("Bien")
jmp fin_ifelse

test_passable:
cmp eax, 10
jl  else_block
; printf("Passable")
jmp fin_ifelse

else_block:
; printf("Insuffisant")

fin_ifelse:

Chaque comparaison est suivie d’un saut conditionnel vers le test suivant ou, en cas de succès, vers la fin de la structure, comme en C.

Le switch

L’instruction switch permet de sélectionner une branche d’exécution parmi plusieurs cas discrets. Elle est souvent plus lisible que les chaînes if-else if pour comparer une variable à plusieurs valeurs constantes.

int jour = 2;
switch (jour) {
  case 1:
    printf("Lundi\n");
    break;
  case 2:
    printf("Mardi\n");
    break;
  case 3:
    printf("Mercredi\n");
    break;
  default:
    printf("Jour invalide\n");
}

Pseudo-code goto + label :

switch_start:
if (jour == 1) goto case1;
if (jour == 2) goto case2;
if (jour == 3) goto case3;
goto default_case;

case1:
printf("Lundi\n");
goto switch_end; // correspond au break;
case2:
printf("Mardi\n");
goto switch_end; // correspond au break;
case3:
printf("Mercredi\n");
goto switch_end; // correspond au break;
default_case:
printf("Jour invalide\n");
switch_end:

Chaque condition teste la valeur, puis saute vers le bon bloc. Le break est simulé par le saut vers le label de fin.

Sans break, l’exécution continue dans le cas suivant (fall-through) :

int choix = 1;
switch (choix) {
  case 1:
  case 2:
    printf("Options 1 ou 2 sélectionnées\n");
    break;
  case 3:
    printf("Option 3 sélectionnée\n");
    // pas de break ici
  case 4:
    printf("Option 3 ou 4\n");  // exécuté si choix == 3
    break;
  default:
    printf("Choix invalide\n");
}

Pseudo-code goto + label avec fall-through :

switch_fallthrough_start:
if (choix == 1) goto case1_2;
if (choix == 2) goto case1_2;
if (choix == 3) goto case3;
if (choix == 4) goto case4;
goto default_ft;

case1_2:
printf("Options 1 ou 2 sélectionnées\n");
goto switch_fallthrough_end; // correspond au break;
case3:
printf("Option 3 sélectionnée\n");
// pas de break, on continue sur case4
case4:
printf("Option 3 ou 4\n");
goto switch_fallthrough_end; // correspond au break;
default_ft:
printf("Choix invalide\n");
switch_fallthrough_end:

Ici, l’absence de break provoque la continuation du flot sur le bloc case4.

Structures répétitives

Les structures répétitives (boucles) permettent d’exécuter un bloc d’instructions plusieurs fois.

La boucle for

La boucle for est particulièrement adaptée quand le nombre d’itérations est connu.

for (int i = 0; i < 10; i++) {
  printf("%d\n", i);
}

Pseudo-code avec goto + label :

int i = 0;
for_start:
if (i >= 10) goto for_end;
printf("%d\n", i);
i++;
goto for_start;
for_end:

On revient sans cesse au début tant que la condition est vraie.

La boucle while

La boucle while exécute un bloc tant qu’une condition reste vraie :

int compteur = 0;
while (compteur < 5) {
  printf("%d\n", compteur);
  compteur++;
}

Pseudo-code goto + label :

int compteur = 0;
while_start:
if (compteur >= 5) goto while_end;
printf("%d\n", compteur);
compteur++;
goto while_start;
while_end:

Structure identique à la boucle for, l’incrément se fait dans le corps.

La boucle do-while

La boucle do-while garantit que le bloc est exécuté au moins une fois :

int choix;
do {
  printf("Entrez un nombre entre 1 et 10 : ");
  scanf("%d", &choix);
} while (choix < 1 || choix > 10);

Pseudo-code goto + label :

do_while_start:
printf("Entrez un nombre entre 1 et 10 : ");
scanf("%d", &choix);
if (choix < 1 || choix > 10) goto do_while_start;

On exécute le bloc une première fois avant de tester. D’un point de vue compilation on a simplement déplacé la condition.

Contrôle des boucles

L’instruction break :

for (int i = 0; i < 100; i++) {
  if (i == 10) {
    break;  // sort de la boucle
  }
  printf("%d\n", i);
}

Pseudo-code goto + label :

int i = 0;
for_break_start:
if (i >= 100) goto for_break_end;
if (i == 10) goto for_break_end;
printf("%d\n", i);
i++;
goto for_break_start;
for_break_end:

Le break est représenté par un saut vers le label de sortie.

L’instruction continue :

for (int i = 0; i < 10; i++) {
  if (i == 5) {
    continue;  // saute à i++ et à l’itération suivante
  }
  printf("%d\n", i);  // n’affiche pas 5
}

Pseudo-code goto + label :

int i = 0;
for_continue_start:
if (i >= 10) goto for_continue_end;
if (i == 5) goto for_continue_next;
printf("%d\n", i);
for_continue_next:
i++;
goto for_continue_start;
for_continue_end:

Le continue saute la partie affichage pour l’itération courante.

Boucles imbriquées

Les boucles peuvent être imbriquées, chaque break ne sort que de la boucle la plus interne :

for (int i = 0; i < 3; i++) {
  for (int j = 0; j < 3; j++) {
    if (i == 1 && j == 1) {
      break;  // sort uniquement de la boucle j
    }
    printf("(%d, %d) ", i, j);
  }
  printf("\n");
}

Le break n’affecte que la boucle interne, l’externe continue normalement.

Entrées-Sorties Simples

La bibliothèque standard C fournit plusieurs fonctions pour lire des données depuis l’entrée et afficher des résultats à la sortie.

printf : sortie formatée

La fonction printf affiche du texte formaté à la sortie standard (l’écran) :

printf("Bonjour, monde!\n");
printf("Un entier : %d\n", 42);
printf("Un réel : %f\n", 3.14);

Les formats spécifiés commencent par % :

  • %d ou %i : entier (int)
  • %u : entier non signé (unsigned int)
  • %f : nombre flottant (float ou double)
  • %e : notation scientifique
  • %x : hexadécimal
  • %c : caractère (char)
  • %s : chaîne de caractères (char*)
  • %p : adresse mémoire (pointeur)
  • %% : caractère pourcent

On peut spécifier la largeur et la précision :

printf("%5d\n", 42);        // "   42" (5 caractères)
printf("%05d\n", 42);       // "00042" (remplissage avec zéros)
printf("%.2f\n", 3.14159);  // "3.14" (2 décimales)
printf("%10.2f\n", 3.14);   // "      3.14" (largeur 10, 2 décimales)

getchar et putchar

La fonction getchar lit un caractère depuis l’entrée standard :

int c = getchar();  // lit un caractère et retourne son code ASCII
if (c != EOF) {
    printf("Caractère reçu : %c\n", c);
}

getchar retourne un int (le code ASCII du caractère ou EOF en fin de fichier).

La fonction putchar affiche un caractère :

putchar('A');       // affiche 'A'
putchar('\n');      // affiche une nouvelle ligne

scanf : entrée formatée

La fonction scanf lit des données depuis l’entrée standard selon un format :

int age;
printf("Quel âge avez-vous ? ");
scanf("%d", &age);
printf("Vous avez %d ans.\n", age);

Important : on doit passer l’adresse de la variable (avec &), pas la variable elle-même. C’est parce que scanf doit modifier la variable en place.

Formats courants :

  • %d : entier
  • %f : flottant (float)
  • %lf : flottant (double)
  • %s : chaîne (attention : pas de protection contre le débordement de buffer)
  • %c : caractère
  • %p : addresse
int entier;
float reel;
char nom[50];
char c;

scanf("%d", &entier);
scanf("%f", &reel);
scanf("%s", nom);         // pas de & pour les tableaux (ils se convertissent en pointeur)
scanf("%c", &c);

gets et puts (déconseillés)

La fonction gets lit une ligne complète depuis l’entrée, mais est dangereuse car elle ne vérifie pas la taille du buffer. Elle est dépréciée depuis C99 et supprimée en C11.

char ligne[100];
gets(ligne);  // DANGEREUX : risque de débordement

Préférer fgets :

fgets(ligne, 100, stdin);  // lit au maximum 99 caractères

La fonction puts affiche une chaîne suivie d’une nouvelle ligne :

puts("Bonjour");  // équivalent à printf("Bonjour\n");