Cours Langage C Complet ⚙️

Les 12 chapitres essentiels — de la syntaxe de base aux pointeurs et à la gestion mémoire, avec exemples et quiz

12
Chapitres
100+
Exemples de code
C17/C23
Standard
A1→C2
Niveaux

CHAPITRE 01

Introduction et premier programme

⚙️ Hello World
#include

int main(void)

Anatomie : #include importe les fonctions d'entrée/sortie. main est le point d'entrée. return 0 indique au système que le programme s'est terminé sans erreur.

🔧 Compiler et exécuter
# Compiler avec gcc
gcc hello.c -o hello

# Exécuter
./hello

# Compiler avec warnings (TOUJOURS recommandé)
gcc -Wall -Wextra -Werror -std=c17 hello.c -o hello

# Étapes de compilation :
# 1. Préprocesseur (#include, #define → .i)
# 2. Compilateur (code C → assembleur .s)
# 3. Assembleur (assembleur → objet .o)
# 4. Linker (objets → exécutable)

🏗️ Le langage C en résumé
Caractéristique C
Typage Statique et faible — types déclarés, mais casts implicites fréquents
Paradigme Impératif, procédural (pas d'objet)
Compilation Compilé directement en code machine natif
Mémoire Gestion manuelle (malloc/free — pas de garbage collector)
Performance Très rapide — proche du matériel
Usage OS (Linux), embarqué, drivers, jeux, systèmes critiques

Pourquoi apprendre C ? Le C est la base de presque tout : Linux, Windows, Python (interpréteur CPython), et la majorité des langages modernes dérivent de sa syntaxe. Comprendre le C, c'est comprendre comment un ordinateur fonctionne réellement.

🧠 Quiz
Que fait return 0 dans main ?
Il retourne le code de sortie 0 au système d'exploitation, indiquant que le programme s'est terminé avec succès. Toute valeur non-zéro signale une erreur.

CHAPITRE 02

Variables et types de données

📦 Types fondamentaux
Type Taille typique Plage Usage
char 1 octet -128 à 127 Caractère ASCII / petit entier
short 2 octets -32 768 à 32 767 Petit entier
int 4 octets ±2.1 milliards Entier standard
long 4-8 octets Varie Grand entier
long long 8 octets ±9.2 × 10¹⁸ Très grand entier
float 4 octets ~7 chiffres Flottant simple
double 8 octets ~15 chiffres Flottant double (par défaut)
_Bool 1 octet 0 ou 1 Booléen (C99, ou stdbool.h)

Les tailles ne sont pas garanties ! Le standard C garantit seulement des minimums. Utilisez pour des tailles exactes : int8_t, int16_t, int32_t, int64_t, uint32_t, etc.

📝 Déclaration et initialisation
#include
#include // pour bool, true, false
#include // pour int32_t, etc.

int main(void)

🖨️ printf — format de sortie
int n = 42;
double f = 3.14159;
char c = 'X';
char nom[] = « Alice »;

printf(« %d\n », n); // 42 (int)
printf(« %f\n », f); // 3.141590 (double)
printf(« %.2f\n », f); // 3.14 (2 décimales)
printf(« %c\n », c); // X (char)
printf(« %s\n », nom); // Alice (string)
printf(« %p\n », &n); // 0x7ff… (adresse)
printf(« %x\n », 255); // ff (hexadécimal)
printf(« %05d\n », 42); // 00042 (padding)

// scanf — lire une entrée
int x;
scanf(« %d », &x); // ⚠️ & obligatoire (adresse de la variable)

🧠 Quiz
Pourquoi utiliser int32_t au lieu de int ?
Parce que la taille de int varie selon la plateforme (2 ou 4 octets). int32_t (de ) garantit exactement 32 bits partout.

CHAPITRE 03

Opérateurs et expressions

➕ Opérateurs
// Arithmétique
+ * / % ++

// ⚠️ Division entière
7 / 2 // 3 (pas 3.5 — les deux sont int)
7.0 / 2 // 3.5 (un opérande est double)

// Comparaison
== != < > <= >=

// Logique
&& // ET (short-circuit)
|| // OU (short-circuit)
! // NON

// Bitwise (opérations bit à bit)
& // AND bit à bit
| // OR bit à bit
^ // XOR
~ // NOT (complément)
<< // Décalage gauche (×2)
>> // Décalage droite (÷2)

// Ternaire
int max = (a > b) ? a : b;

// Casting explicite
double result = (double)7 / 2; // 3.5

Piège classique : if (x = 5) est un assignment, pas une comparaison ! C'est toujours vrai. L'erreur correcte : if (x == 5). Activez -Wall pour détecter ce bug.

CHAPITRE 04

Structures conditionnelles

🔀 if / else / switch
// if / else
if (age < 18) else if (age < 65) else

// switch — int ou char uniquement (pas de string !)
switch (note)

Pas de booléen natif en C89. En C, 0 = false, tout le reste = true. Depuis C99, fournit bool, true et false.

CHAPITRE 05

Les boucles

🔁 for, while, do-while
// for
for (int i = 0; i < 5; i++)

// while
int i = 0;
while (i < 5)

// do-while — exécuté AU MOINS une fois
do while (n <= 0);

// break = sortir de la boucle
// continue = passer à l'itération suivante

📊 Les tableaux (statiques)
// Déclaration et initialisation
int nombres[5] = ;
int zeros[100] = ; // Tous à 0
char msg[] = « Hello »; // Taille déduite (6 avec '\0')

// Accès
nombres[0] // 10
nombres[4] // 50
nombres[5] // ⚠️ UNDEFINED BEHAVIOR (hors limites)

// Taille d'un tableau
int len = sizeof(nombres) / sizeof(nombres[0]); // 5

// Parcourir
for (int i = 0; i < len; i++)

// Tableau 2D
int matrice[3][3] = ;

Pas de vérification des limites ! En C, accéder à arr[10] sur un tableau de taille 5 ne produit aucune erreur à la compilation. C'est un undefined behavior — le programme peut crasher, corrompre la mémoire, ou pire : sembler fonctionner.

🧠 Quiz
Comment calculer la taille d'un tableau en C ?
sizeof(arr) / sizeof(arr[0]) — sizeof donne la taille totale en octets, divisée par la taille d'un élément. Attention : ne fonctionne PAS si le tableau a été passé en paramètre (il devient un pointeur).

CHAPITRE 06

Fonctions

🔧 Déclarer et appeler des fonctions
#include

// Prototype (déclaration) — avant main
int addition(int a, int b);
void saluer(const char *nom);

int main(void)

// Définition (implémentation)
int addition(int a, int b)

void saluer(const char *nom)

⚠️ Passage par valeur vs par adresse
// Par VALEUR — la fonction reçoit une COPIE
void doubler(int x)

int n = 5;
doubler(n);
printf(« %d », n); // 5 — pas modifié !

// Par ADRESSE (pointeur) — modifie l'original
void doubler(int *x)

int n = 5;
doubler(&n); // Passe l'adresse de n
printf(« %d », n); // 10 — modifié ✅

// Tableau en paramètre = toujours un pointeur
void remplir(int arr[], int taille)

En C, tout est passé par valeur. Même les pointeurs sont passés par valeur (la fonction reçoit une copie de l'adresse). Mais puisqu'on a l'adresse, on peut modifier la donnée pointée. Les tableaux « deviennent » un pointeur quand passés en paramètre.

🧠 Quiz
Comment modifier une variable dans une fonction en C ?
En passant son adresse avec &variable et en la recevant comme pointeur int *x. On accède à la valeur avec *x (déréférencement).

CHAPITRE 07

Les pointeurs

📍 Le concept fondamental
// Un pointeur = une variable qui contient une ADRESSE mémoire

int x = 42;
int *ptr = &x; // ptr contient l'adresse de x

printf(« %d\n », x); // 42 — la valeur
printf(« %p\n », (void*)ptr); // 0x7ff… — l'adresse
printf(« %d\n », *ptr); // 42 — valeur à l'adresse (déréférencement)

*ptr = 100; // Modifie x à travers le pointeur
printf(« %d\n », x); // 100

// Résumé des opérateurs
// &x → adresse de x
// *ptr → valeur à l'adresse contenue dans ptr
// int *p → déclaration d'un pointeur vers int

🔗 Pointeurs et tableaux
// En C, un tableau EST un pointeur vers son premier élément
int arr[5] = ;
int *p = arr; // p pointe vers arr[0]

printf(« %d\n », *p); // 10 (arr[0])
printf(« %d\n », *(p+1)); // 20 (arr[1])
printf(« %d\n », *(p+2)); // 30 (arr[2])

// Arithmétique des pointeurs
p++; // Avance de sizeof(int) octets (pas de 1 !)

// Ces notations sont ÉQUIVALENTES :
arr[i] // ← notation tableau
*(arr + i) // ← notation pointeur

⚠️ Pointeur NULL et pièges
// Pointeur NULL — pointe vers « rien »
int *p = NULL;

// TOUJOURS vérifier avant de déréférencer
if (p != NULL)

// Dangling pointer — pointe vers de la mémoire libérée
int *p = malloc(sizeof(int));
free(p);
p = NULL; // ✅ Toujours mettre à NULL après free

// Pointeur de pointeur
int x = 5;
int *p = &x;
int **pp = &p; // Pointeur vers un pointeur
printf(« %d », **pp); // 5

Les pointeurs sont la source n°1 de bugs en C : segfault (déréférencement de NULL), buffer overflow (écrire hors limites), memory leak (oublier free), use-after-free (utiliser après free). Des langages comme Python ou JavaScript éliminent ces problèmes avec un garbage collector.

🧠 Quiz
Que signifie int *p = &x; ?
p est un pointeur vers un int, initialisé avec l'adresse de x. *p donne la valeur de x (déréférencement). Modifier *p modifie x.

CHAPITRE 08

Allocation dynamique

🧱 malloc, calloc, realloc, free
#include

// malloc — alloue des octets (non initialisés)
int *arr = malloc(5 * sizeof(int));
if (arr == NULL)

// calloc — alloue ET initialise à 0
int *arr2 = calloc(5, sizeof(int)); // 5 éléments, chacun sizeof(int)

// realloc — redimensionner
arr = realloc(arr, 10 * sizeof(int)); // Agrandit à 10 éléments

// Utiliser comme un tableau normal
arr[0] = 42;
arr[1] = 84;

// free — TOUJOURS libérer la mémoire
free(arr);
arr = NULL; // Bonne pratique

⚠️ Les pièges mémoire
Erreur Cause Conséquence
Memory leak malloc sans free La mémoire n'est jamais libérée → le programme consomme de plus en plus
Use-after-free Utiliser un pointeur après free Données corrompues, crash imprévisible
Double free free() deux fois sur le même pointeur Crash ou corruption du heap
Buffer overflow Écrire au-delà de la zone allouée Corruption mémoire, faille de sécurité
Null dereference *ptr quand ptr == NULL Segmentation fault

Outil indispensable : Valgrind. valgrind ./mon_programme détecte les fuites mémoire, les accès invalides, et les use-after-free. Utilisez-le systématiquement pendant le développement.

🧠 Quiz
Différence entre malloc et calloc ?
malloc = alloue N octets, contenu non initialisé (valeurs aléatoires). calloc = alloue N × taille, contenu initialisé à 0. calloc est plus sûr pour les tableaux.

CHAPITRE 09

Les chaînes de caractères

📝 Les strings en C = tableaux de char
#include

// String = tableau de char terminé par '\0' (null terminator)
char nom[] = « Alice »;
// En mémoire : ['A']['l']['i']['c']['e']['\0']
// Taille = 6 (5 caractères + '\0')

// Déclaration manuelle
char msg[20] = « Hello »; // Buffer de 20, contenu « Hello\0 »

// Pointeur vers string (en lecture seule !)
const char *s = « World »; // Pointe vers un littéral (non modifiable)

🔧 Fonctions string.h
char s1[50] = « Hello »;
char s2[] =  » World »;

strlen(s1); // 5 (ne compte pas '\0')
strcpy(s1, « Bonjour »); // Copie dans s1
strncpy(s1, « Hi », 49); // Copie avec limite (sûr)
strcat(s1, s2); // Concatène s2 à s1
strncat(s1, s2, 49); // Concatène avec limite
strcmp(s1, s2); // 0 si égaux, <0 ou >0 sinon
strncmp(s1, s2, 3); // Compare les 3 premiers chars
strchr(s1, 'o'); // Pointeur vers 1re occurrence de 'o'
strstr(s1, « jour »); // Pointeur vers 1re occurrence de « jour »

// Conversion
atoi(« 42 »); // 42 (string → int)
atof(« 3.14 »); // 3.14 (string → double)
sprintf(buf, « Age: %d », 25); // printf dans un buffer
snprintf(buf, 50, « Age: %d », 25); // Avec limite (sûr)

TOUJOURS les versions « n » : Utilisez strncpy, strncat, snprintf au lieu de strcpy, strcat, sprintf. Les versions sans limite sont des failles de sécurité majeures (buffer overflow).

CHAPITRE 10

Structures et unions

📦 struct — grouper des données
// Définition
typedef struct Personne;

// Création et initialisation
Personne alice = ;
Personne bob = ; // Designated init (C99)

// Accès avec le point (.)
printf(« %s a %d ans\n », alice.nom, alice.age);

// Pointeur vers struct — accès avec la flèche (->)
Personne *ptr = &alice;
printf(« %s\n », ptr->nom); // Équivalent à (*ptr).nom
ptr->age = 26;

// Struct dynamique
Personne *p = malloc(sizeof(Personne));
strcpy(p->nom, « Charlie »);
p->age = 28;
free(p);

🔄 enum, union, typedef
// enum — constantes nommées
typedef enum Couleur;

Couleur c = VERT;

// union — un seul membre actif à la fois (partage la mémoire)
typedef union Valeur;

Valeur v;
v.entier = 42; // OK
v.flottant = 3.14; // Écrase v.entier !
// sizeof(Valeur) = taille du PLUS GRAND membre (20)

🧠 Quiz
Différence entre . et -> pour accéder à un membre de struct ?
. = accès direct sur une struct : personne.nom. -> = accès via un pointeur : ptr->nom (raccourci de (*ptr).nom).

CHAPITRE 11

Fichiers et I/O

📄 Lire et écrire des fichiers
#include

// Ouvrir un fichier
FILE *f = fopen(« data.txt », « r »); // r=lire, w=écrire, a=ajouter
if (f == NULL)

// Lire ligne par ligne
char ligne[256];
while (fgets(ligne, sizeof(ligne), f) != NULL)

// Écrire
FILE *out = fopen(« output.txt », « w »);
fprintf(out, « Nom: %s, Age: %d\n », « Alice », 25);
fputs(« Ligne de texte\n », out);

// TOUJOURS fermer
fclose(f);
fclose(out);

🔧 Modes d'ouverture et binaire
Mode Action
« r » Lire (fichier doit exister)
« w » Écrire (crée ou écrase)
« a » Ajouter à la fin
« r+ » Lire et écrire (doit exister)
« w+ » Lire et écrire (crée ou écrase)
« rb » / « wb » Mode binaire
// Fichier binaire — lire/écrire des structures
Personne p = ;

FILE *f = fopen(« data.bin », « wb »);
fwrite(&p, sizeof(Personne), 1, f);
fclose(f);

FILE *g = fopen(« data.bin », « rb »);
Personne lu;
fread(&lu, sizeof(Personne), 1, g);
fclose(g);

CHAPITRE 12

Préprocesseur et bonnes pratiques

⚙️ Directives du préprocesseur
// #include — inclure un fichier
#include // Librairie standard (cherche dans /usr/include)
#include « mon_header.h » // Fichier local (cherche dans le dossier courant)

// #define — constante ou macro
#define PI 3.14159
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define TAILLE_MAX 1024

// #ifdef / #ifndef — compilation conditionnelle
#ifdef DEBUG
printf(« Mode debug\n »);
#endif

// Include guard — éviter la double inclusion
#ifndef MON_HEADER_H
#define MON_HEADER_H

// … contenu du header …

#endif

// Ou pragmatiquement (non-standard mais supporté partout)
#pragma once

📁 Organisation d'un projet C
// Structure typique
// projet/
// ├── src/
// │ ├── main.c
// │ ├── personne.c
// │ └── utils.c
// ├── include/
// │ ├── personne.h
// │ └── utils.h
// └── Makefile

// personne.h (header = déclarations)
#pragma once

typedef struct Personne;

Personne personne_creer(const char *nom, int age);
void personne_afficher(const Personne *p);

// personne.c (implémentation)
#include « personne.h »
#include
#include

Personne personne_creer(const char *nom, int age)

✅ Bonnes pratiques

✅ À FAIRE
-Wall -Wextra -Werror toujours
const pour les paramètres en lecture
strncpy/snprintf (versions sûres)
• Vérifier malloc != NULL
free() puis ptr = NULL
Valgrind pour les fuites mémoire
• Include guards / #pragma once
sizeof(*ptr) dans malloc
• Séparer .h (déclaration) et .c (code)

❌ À ÉVITER
gets() (supprimé — faille buffer overflow)
strcpy/sprintf sans limite
• Variables non initialisées
• Caster le retour de malloc (inutile en C)
• Ignorer les warnings
• Magic numbers (utiliser #define)
• Variables globales
goto (sauf gestion d'erreur)
• Fonctions de plus de 40 lignes

🔧 Makefile minimal
# Makefile
CC = gcc
CFLAGS = -Wall -Wextra -Werror -std=c17
SRC = src/main.c src/personne.c
OBJ = $(SRC:.c=.o)
TARGET = app

$(TARGET): $(OBJ)
$(CC) $(CFLAGS) -o $@ $^

%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $< clean: rm -f $(OBJ) $(TARGET)

🧠 Quiz
Pourquoi ne jamais utiliser gets() ?
gets() ne vérifie pas la taille du buffer — un utilisateur peut écrire au-delà et provoquer un buffer overflow. C'est une faille de sécurité si critique que gets() a été supprimé du standard C11. Utilisez fgets().
À quoi sert un include guard ?
Il empêche un header d'être inclus plusieurs fois dans la même unité de compilation (ce qui causerait des erreurs de redéfinition). #pragma once ou #ifndef/#define/#endif.

Cours Langage C Complet — Des bases aux pointeurs et à la gestion mémoire

Référence : cppreference.com/c | man pages