Module L07 · Partie F · Coder comme un professionnel
C et OCaml : les deux langages de la prépa.
En MP2I/MPI, on programme en C (pour comprendre la mémoire, les pointeurs, ce que fait vraiment la machine) et en OCaml (pour la programmation fonctionnelle, les types, la récursion structurelle et les preuves). Vous connaissez déjà le C par Arduino ; ce module va plus loin et introduit OCaml. Le C et OCaml ne tournent pas dans la page : les exemples sont à compiler sur votre PC, et Python sert de miroir pour comparer.
Durée : 4 séances · Prérequis : séances 22-23 (Arduino), L03. Objectifs : compilation, types et mémoire en C, pointeurs, tableaux, chaînes, structures, allocation dynamique, listes chaînées en C ; OCaml : expressions, types, filtrage, listes, récursion, types algébriques, modules. Un même algorithme écrit dans les trois langages.
Ce que vous saurez faire à la fin
- Compiler un programme C avec gcc, comprendre les erreurs du compilateur et de valgrind.
- Manipuler pointeurs et mémoire sans fuite ni débordement.
- Écrire en OCaml des fonctions récursives sur listes et arbres avec filtrage de motifs.
- Traduire un algorithme entre Python, C et OCaml.
Références : The C Programming Language (Kernighan & Ritchie), CS50 semaines 1-5, OCaml from the very beginning, programme officiel MP2I, cours OCaml de Xavier Leroy (Collège de France).
Fiche de cours · Définitions
C et OCaml : les notions à définir sans hésiter
*p déréférence, &x prend l’adresse ; p + i avance de i × sizeof(*p) octets. Un tableau se convertit en pointeur vers son premier élément.let f x = x + 1 a le type int -> int. Le polymorphisme paramétrique : 'a list est une liste de n’importe quoi. Les valeurs sont immuables par défaut ; les références (ref) et tableaux sont mutables.type forme = Cercle of float | Rect of float * float énumère les formes possibles ; le filtrage match f with Cercle r -> … | Rect (l, h) -> … décompose, et le compilateur vérifie l’exhaustivité. Les listes sont le type récursif 'a list = [] | 'a :: 'a list.Fiche de cours · Formules
Tables à connaître
| C : type | Taille usuelle | Plage | Débordement |
|---|---|---|---|
| char / uint8_t | 1 octet | −128..127 / 0..255 | non signé : modulo 2⁸ |
| int / int32_t | 4 octets | −2³¹..2³¹−1 | signé : indéfini |
| unsigned / uint32_t | 4 octets | 0..2³²−1 | modulo 2³² (défini) |
| long long / int64_t | 8 octets | ±9,2·10¹⁸ | signé : indéfini |
| float / double | 4 / 8 octets | ≈ 7 / 16 chiffres significatifs | inf, nan |
| pointeur | 8 octets (64 bits) | adresse | — |
| OCaml | Type | Sens |
|---|---|---|
List.map f l | ('a -> 'b) -> 'a list -> 'b list | Appliquer f à chaque élément |
List.fold_left f acc l | ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a | Accumuler de gauche à droite (récursif terminal) |
List.filter p l | ('a -> bool) -> 'a list -> 'a list | Garder les éléments vérifiant p |
x :: l, l1 @ l2 | O(1), O(|l1|) | Ajout en tête, concaténation |
Array.make n v, t.(i) <- v | tableau mutable | Accès O(1) |
Fiche de cours · Théorèmes et démonstrations
Démonstrations à savoir refaire
:: à partir de [] (c’est la définition du type). Récurrence sur n : n = 0 est P([]) ; si P est vraie pour toute liste de longueur n, une liste de longueur n + 1 s’écrit x :: r avec r de longueur n, et l’hérédité donne P(x :: r). C’est la récurrence sur les entiers, transportée par la longueur. Elle s’applique à tout type algébrique récursif (arbres : prouver pour la feuille et pour le nœud sachant les sous-arbres).length (l1 @ l2) = length l1 + length l2). Avec let rec (@) l1 l2 = match l1 with [] -> l2 | x :: r -> x :: (r @ l2) et length définie de même.[] @ l2 = l2, longueur length l2 = 0 + length l2. Cas x :: r : (x :: r) @ l2 = x :: (r @ l2), de longueur 1 + length (r @ l2) = 1 + length r + length l2 (hypothèse) = length (x :: r) + length l2.fold_left f acc l s’exécute en espace de pile O(1) ; fold_right (et @, map naïfs) en O(|l|).let rec fold_left f acc = function [] -> acc | x :: r -> fold_left f (f acc x) r : l’appel récursif est la dernière opération, sans calcul en attente ; le compilateur le remplace par un saut (optimisation garantie en OCaml), le cadre de pile est réutilisé. Dans fold_right f l acc = match l with [] -> acc | x :: r -> f x (fold_right f r acc), après le retour de l’appel récursif il reste à appliquer f : chaque niveau garde un cadre, d’où O(|l|) cadres et un débordement de pile pour |l| ~ 10⁶. Remède : accumulateur puis List.rev.if (x + 1 > x) avec x de type int peut être compilé en if (1).Fiche de cours · Méthodes
Méthodes et pièges
-Wall -Wextra -Werror -g -fsanitize=address,undefined. Chaque malloc a son free dans la même « couche ». Chaque accès tableau a une borne vérifiée. Chaque chaîne a son \0 (préférer snprintf, strncpy n’est pas la solution). Initialiser toutes les variables. Utiliser size_t pour les tailles, int32_t/uint8_t pour les tailles fixes (embarqué).match) en couvrant tous les cas. (3) Cas de base, puis cas récursif qui appelle la fonction sur une sous-structure. (4) Si la liste peut être longue, accumulateur + récursion terminale. (5) Laisser l’inférence vérifier ; lire l’erreur de type comme une preuve ratée.for x in L par for (i = 0; i < n; i++).Pièges C : = au lieu de == ; sizeof(t) sur un paramètre (c’est un pointeur : 8) ; renvoyer l’adresse d’une variable locale ; char *s = "abc"; s[0] = 'x' (littéral en lecture seule) ; for (unsigned i = n − 1; i >= 0; …) boucle infinie. Pièges OCaml : = (structurel) vs == (physique) ; + pour int et +. pour float ; oublier rec ; l @ [x] en boucle (O(n²)).
Fiche de cours · Exercices corrigés
Exercices corrigés
int *cree_tableau(int n) { int t[n]; for (int i = 0; i < n; i++) t[i] = i; return t; }t vit sur la pile de cree_tableau : à son retour, la mémoire est libérée et l’adresse renvoyée pointe vers une zone réutilisée par les appels suivants — comportement indéfini (le compilateur avertit : « function returns address of local variable »). Correction : int *t = malloc(n * sizeof *t); if (!t) return NULL; …; return t; et l’appelant fait free(t). Ou : l’appelant fournit le tableau (void remplir(int *t, int n)), le plus courant en embarqué (pas d’allocation dynamique).rev : 'a list -> 'a list en récursion terminale et prouver rev (rev l) = l.let rev l = let rec aux acc = function [] -> acc | x :: r -> aux (x :: acc) r in aux [] l. Lemme : aux acc l = (rev l) @ acc par récurrence structurelle sur l (cas [] : acc = [] @ acc ; cas x :: r : aux acc (x :: r) = aux (x :: acc) r = rev r @ (x :: acc) = (rev r @ [x]) @ acc = rev (x :: r) @ acc, en utilisant l’associativité de @ et la définition naïve rev (x :: r) = rev r @ [x]). Puis rev (rev l) = l : récurrence sur l avec le lemme rev (l1 @ l2) = rev l2 @ rev l1 (lui-même par récurrence sur l1) : rev (rev (x :: r)) = rev (rev r @ [x]) = rev [x] @ rev (rev r) = [x] @ r = x :: r.uint8_t a = 200, b = 100; uint8_t c = a + b; donne c = 44 sans comportement indéfini, alors que int x = INT_MAX; x = x + 1; est indéfini.a + b, les uint8_t sont d’abord promus en int : la somme vaut 300, calculée sans débordement. L’affectation à un uint8_t convertit un entier vers un type non signé : la norme définit cette conversion comme modulo 2⁸, donc 300 mod 256 = 44. Défini. Pour int, le débordement du calcul lui-même (pas d’une conversion) est indéfini par la norme : le compilateur peut supposer qu’il ne se produit pas (Théorème 4). Pour faire du modulo 2³² proprement : uint32_t.01 / C : compiler
Du texte au binaire
/* bonjour.c */
#include <stdio.h>
int main(void) {
int n = 5;
double moyenne = 17.0 / 3;
printf("n = %d, moyenne = %.2f\n", n, moyenne);
for (int i = 0; i < n; i++) {
printf("%d² = %d\n", i, i * i);
}
return 0; /* 0 = tout s'est bien passé */
}$ gcc -Wall -Wextra -O2 -o bonjour bonjour.c
$ ./bonjour
n = 5, moyenne = 5.67
0² = 0
...Les étapes que gcc enchaîne : préprocesseur (#include, #define : substitution de texte), compilation (C → assembleur), assemblage (→ code machine), édition de liens (avec la bibliothèque standard → exécutable). gcc -S montre l’assembleur ; gcc -c s’arrête au fichier objet.
-Wall -Wextra : toujours. Le compilateur C se tait par défaut sur des choses que Python refuserait ; les avertissements sont vos tests gratuits. -O2 optimise ; -g ajoute les infos de débogage pour gdb et valgrind.
Différences avec Python : tout est typé statiquement et déclaré ; les entiers ont une taille fixe (int 32 bits : −2 147 483 648 à 2 147 483 647, et débordement silencieux au-delà) ; la division entière tronque vers zéro (-7 / 2 == -3) ; il n’y a ni exceptions, ni ramasse-miettes, ni listes : que des tableaux de taille fixe et des pointeurs.
01 / C : compiler
Types, tailles, débordements : ce que Python vous cache
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
int main(void) {
printf("char %zu, int %zu, long %zu, double %zu octets\n",
sizeof(char), sizeof(int), sizeof(long), sizeof(double));
int x = INT_MAX;
x = x + 1; /* débordement : comportement indéfini ! */
printf("INT_MAX + 1 = %d\n", x); /* souvent -2147483648 */
uint8_t octet = 250;
octet += 10; /* non signé : modulo 256, bien défini */
printf("250 + 10 sur 8 bits = %u\n", octet); /* 4 */
printf("7 / 2 = %d, -7 / 2 = %d, -7 %% 2 = %d\n", 7 / 2, -7 / 2, -7 % 2);
printf("0.1 + 0.2 == 0.3 ? %d\n", 0.1 + 0.2 == 0.3);
return 0;
}Les types entiers à taille fixe
Sur microcontrôleur, on n’écrit jamais int mais uint8_t, int16_t, uint32_t (<stdint.h>) : la taille est alors garantie. Un compteur de millisecondes en uint32_t déborde après 49,7 jours — bug célèbre (Boeing 787, patch de redémarrage tous les 248 jours). Les soustractions non signées maintenant - avant restent correctes au débordement grâce à l’arithmétique modulo 232 : c’est la bonne façon de mesurer une durée.
02 / C : mémoire
Pointeurs : une variable qui contient une adresse
#include <stdio.h>
void echanger(int *a, int *b) { /* reçoit des ADRESSES */
int t = *a; /* *a : la valeur à l'adresse a */
*a = *b;
*b = t;
}
int main(void) {
int x = 3, y = 7;
int *p = &x; /* &x : l'adresse de x */
printf("x = %d, p = %p, *p = %d\n", x, (void *)p, *p);
*p = 10; /* modifie x à travers p */
printf("x = %d\n", x);
echanger(&x, &y);
printf("x = %d, y = %d\n", x, y);
int t[5] = {10, 20, 30, 40, 50};
int *q = t; /* un tableau « est » l'adresse de son premier élément */
printf("%d %d %d\n", t[2], *(q + 2), q[2]); /* trois écritures identiques */
return 0;
}En Python, chaque nom est déjà une référence vers un objet : a = b fait pointer a et b vers le même objet, et une fonction reçoit la référence (d’où liste.append qui modifie l’original). Le C rend cela explicite : sans &, une fonction reçoit une copie et echanger ne ferait rien.
Les trois bugs classiques : pointeur non initialisé (adresse au hasard), déréférencer NULL (plantage « segmentation fault »), dépasser la taille du tableau (corruption silencieuse : la faille de sécurité n°1 depuis 40 ans).
02 / C : mémoire
Pile et tas : où vivent les variables
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int *creer_tableau(int n) {
int *t = malloc(n * sizeof(int)); /* sur le TAS : survit à la fonction */
if (t == NULL) { perror("malloc"); exit(1); }
for (int i = 0; i < n; i++) t[i] = i * i;
return t;
}
int *creer_tableau_faux(int n) {
int t[100]; /* sur la PILE : détruit au return ! */
for (int i = 0; i < n; i++) t[i] = i * i;
return t; /* pointeur pendouillant : bug */
}
int main(void) {
int *carres = creer_tableau(10);
printf("%d\n", carres[7]);
carres = realloc(carres, 20 * sizeof(int)); /* agrandir (comme la list Python) */
free(carres); /* obligatoire, sinon fuite */
/* carres[0] = 1; ← utilisation après libération : bug */
char nom[32];
strncpy(nom, "robot explorateur", sizeof nom - 1); nom[sizeof nom - 1] = '\0';
printf("%s (%zu caractères)\n", nom, strlen(nom)); /* chaîne = tableau de char terminé par 0 */
return 0;
}La règle et l’outil
Chaque malloc a exactement un free. Ni zéro (fuite : la mémoire grossit jusqu’au plantage), ni deux (double free : corruption). Ne jamais renvoyer l’adresse d’une variable locale. valgrind ./prog détecte fuites, lectures hors limites et utilisations après libération : c’est l’outil indispensable ; gcc -fsanitize=address fait de même à la compilation. Python fait tout cela pour vous avec un compteur de références et un ramasse-miettes ; le prix est la vitesse et l’imprévisibilité du moment de libération — inacceptable sur un robot temps réel, d’où le C (et Rust, qui garantit ces règles à la compilation).
02 / C : mémoire
Structures et liste chaînée en C
#include <stdio.h>
#include <stdlib.h>
typedef struct Maillon {
double valeur;
struct Maillon *suivant;
} Maillon;
Maillon *inserer_tete(Maillon *tete, double v) {
Maillon *m = malloc(sizeof *m);
m->valeur = v; /* m->x ≡ (*m).x */
m->suivant = tete;
return m;
}
double somme(const Maillon *m) {
double s = 0;
for (; m != NULL; m = m->suivant) s += m->valeur;
return s;
}
void liberer(Maillon *m) {
while (m) { Maillon *s = m->suivant; free(m); m = s; }
}
int main(void) {
Maillon *l = NULL;
for (int i = 1; i <= 5; i++) l = inserer_tete(l, i * 1.5);
printf("somme = %.1f\n", somme(l));
liberer(l);
return 0;
}const Maillon *m : promesse de ne pas modifier ce qui est pointé ; le compilateur vérifie. typedef évite d’écrire struct Maillon partout. Une struct est l’ancêtre de la classe : des champs, sans méthodes ; les « méthodes » sont des fonctions qui reçoivent un pointeur vers la structure — exactement le self de Python.
02 / C : mémoire
Le C au niveau du matériel : bits, registres, volatile
#include <stdint.h>
/* Registre matériel : une adresse fixe où lire/écrire change l'état d'une broche (exemple STM32) */
#define GPIOA_ODR (*(volatile uint32_t *)0x40020014)
void allumer_led(void) { GPIOA_ODR |= (1u << 5); } /* mettre le bit 5 à 1 */
void eteindre_led(void) { GPIOA_ODR &= ~(1u << 5); } /* le mettre à 0 */
void basculer_led(void) { GPIOA_ODR ^= (1u << 5); } /* l'inverser */
int led_allumee(void) { return (GPIOA_ODR >> 5) & 1; }
/* Compacter des drapeaux : un octet = 8 booléens */
typedef enum { OBSTACLE = 1 << 0, BATTERIE_FAIBLE = 1 << 1, EN_MOUVEMENT = 1 << 2 } Etat;
uint8_t etat = OBSTACLE | EN_MOUVEMENT;
int bloque = (etat & OBSTACLE) && (etat & EN_MOUVEMENT);
/* Interruption : appelée par le matériel, pas par votre programme */
volatile uint32_t ticks = 0; /* volatile : « peut changer sans que le code le fasse » */
void SysTick_Handler(void) { ticks++; } /* toutes les millisecondes */
uint32_t millis(void) { return ticks; }volatile interdit au compilateur de mettre la variable en cache dans un registre : sans lui, while (ticks < 100); pourrait être optimisé en boucle infinie. Ce mot-clé n’existe pas en Python parce que Python n’a pas d’optimiseur qui raisonne ainsi. Module L18 pour la suite.
03 / OCaml
OCaml : tout est expression, tout est typé
(* Dans l'interpréteur : ocaml, ou utop *)
let x = 3 + 4 ;; (* val x : int = 7 *)
let pi = 3.14159 ;; (* val pi : float *)
let aire r = pi *. r *. r ;; (* val aire : float -> float ; *. pour les flottants *)
aire 2.0 ;; (* - : float = 12.56636 *)
let rec factorielle n =
if n = 0 then 1 else n * factorielle (n - 1) ;;
(* val factorielle : int -> int *)
let max a b = if a > b then a else b ;; (* val max : 'a -> 'a -> 'a : polymorphe ! *)
max 3 5 ;; max "a" "b" ;; max 2.5 1.0 ;;
(* Les fonctions sont des valeurs *)
let appliquer_deux_fois f x = f (f x) ;; (* ('a -> 'a) -> 'a -> 'a *)
appliquer_deux_fois (fun n -> n * 3) 2 ;; (* 18 *)
let carre = fun x -> x * x in appliquer_deux_fois carre 3 ;; (* 81 *)OCaml infère les types : vous n’écrivez rien, il déduit int -> int et refuse à la compilation factorielle 2.5 ou aire 2 (entier au lieu de flottant). Une fois que ça compile, une classe entière de bugs est impossible. 'a se lit « alpha » : n’importe quel type, tant que c’est le même partout.
Pas de return, pas de boucle for dans un premier temps, pas de variable qu’on modifie : un programme OCaml est une expression qu’on évalue. C’est déroutant une semaine, puis on trouve Python bavard.
03 / OCaml
Listes et filtrage de motifs : la récursion structurelle
let l = [3; 1; 4; 1; 5] ;; (* int list ; [] liste vide ; x :: reste *)
let rec longueur = function
| [] -> 0
| _ :: reste -> 1 + longueur reste ;;
let rec somme = function
| [] -> 0
| x :: reste -> x + somme reste ;;
let rec map f = function
| [] -> []
| x :: reste -> f x :: map f reste ;;
let rec filtre p = function
| [] -> []
| x :: reste -> if p x then x :: filtre p reste else filtre p reste ;;
(* Récursion terminale : l'accumulateur évite de faire grossir la pile *)
let somme_term l =
let rec aux acc = function
| [] -> acc
| x :: reste -> aux (acc + x) reste
in aux 0 l ;;
(* Tri par insertion *)
let rec insere x = function
| [] -> [x]
| y :: reste when x <= y -> x :: y :: reste
| y :: reste -> y :: insere x reste ;;
let rec tri = function
| [] -> []
| x :: reste -> insere x (tri reste) ;;
tri l ;; (* [1; 1; 3; 4; 5] *)
List.map (fun x -> x * x) l ;; (* la bibliothèque standard *)Une liste OCaml est soit vide, soit un élément suivi d’une liste : une liste chaînée immuable. Le filtrage (match … with / function) suit exactement cette définition : un cas par constructeur. Le compilateur signale les cas oubliés (« this pattern-matching is not exhaustive ») — impossible d’oublier la liste vide.
La récursion terminale (appel récursif en dernière position) est transformée en boucle par le compilateur OCaml : pas de limite de pile. Python ne le fait pas.
03 / OCaml
Types algébriques : décrire exactement les données
(* Type somme : une valeur est l'un des cas *)
type forme =
| Cercle of float
| Rectangle of float * float
| Triangle of float * float * float ;;
let aire = function
| Cercle r -> 3.14159 *. r *. r
| Rectangle (l, h) -> l *. h
| Triangle (a, b, c) ->
let s = (a +. b +. c) /. 2. in
sqrt (s *. (s -. a) *. (s -. b) *. (s -. c)) ;;
(* Option : la valeur peut manquer, et le type vous FORCE à traiter ce cas *)
let rec cherche cle = function
| [] -> None
| (k, v) :: reste -> if k = cle then Some v else cherche cle reste ;;
match cherche "b" [("a", 1); ("b", 2)] with
| Some v -> Printf.printf "trouvé %d\n" v
| None -> print_endline "absent" ;;
(* Arbre binaire : type récursif *)
type 'a arbre = Feuille | Noeud of 'a arbre * 'a * 'a arbre ;;
let rec inserer x = function
| Feuille -> Noeud (Feuille, x, Feuille)
| Noeud (g, v, d) as n ->
if x < v then Noeud (inserer x g, v, d)
else if x > v then Noeud (g, v, inserer x d) else n ;;
let rec infixe = function
| Feuille -> []
| Noeud (g, v, d) -> infixe g @ [v] @ infixe d ;;
infixe (List.fold_left (fun a x -> inserer x a) Feuille [5; 2; 8; 1; 9]) ;;
(* [1; 2; 5; 8; 9] *)Un type somme dit « une forme est un cercle OU un rectangle OU un triangle », et le filtrage traite chaque cas — le compilateur vérifie qu’aucun n’est oublié. C’est le polymorphisme du module L02 sans classes : on ajoute un cas au type et le compilateur liste tous les endroits à mettre à jour.
option remplace le None de Python et le NULL du C, mais avec une différence décisive : on ne peut pas oublier de tester. Le « milliard de dollars » de bugs liés à NULL (Tony Hoare) disparaît par construction. Rust, Swift, Kotlin ont repris cette idée.
03 / OCaml
Références, tableaux et modules : OCaml n’est pas que fonctionnel
(* Mutabilité explicite quand elle est utile *)
let compteur = ref 0 ;;
incr compteur ; compteur := !compteur + 10 ;; (* !r : lire ; r := v : écrire *)
let tri_insertion t = (* tableau mutable, boucles impératives *)
for i = 1 to Array.length t - 1 do
let x = t.(i) in
let j = ref (i - 1) in
while !j >= 0 && t.(!j) > x do
t.(!j + 1) <- t.(!j); decr j
done;
t.(!j + 1) <- x
done ;;
let t = [| 5; 2; 9; 1 |] in tri_insertion t; t ;; (* [|1; 2; 5; 9|] *)
(* Modules : regrouper types et fonctions derrière une signature *)
module Pile : sig
type 'a t
val vide : 'a t
val empiler : 'a -> 'a t -> 'a t
val depiler : 'a t -> ('a * 'a t) option
end = struct
type 'a t = 'a list
let vide = []
let empiler x p = x :: p
let depiler = function [] -> None | x :: r -> Some (x, r)
end ;;
(* L'utilisateur du module ne peut PAS savoir que c'est une liste : encapsulation garantie par le typeur *)
Hashtbl.create 16 ;; Queue.create () ;; (* la bibliothèque standard : tables de hachage, files, tas via Set/Map *)Le programme MPI utilise les tableaux, les références et les boucles autant que la récursion : l’idée est de choisir le style adapté, en gardant la mutabilité visible dans les types (ref, array). Compiler : ocamlfind ocamlopt -package str -o prog prog.ml, ou simplement dune build dans un projet.
04 / Comparer
Un algorithme, trois langages : la dichotomie
/* C */
int dichotomie(const int *t, int n, int x) {
int bas = 0, haut = n; /* invariant : x ∈ t[bas..haut[ si présent */
while (bas < haut) {
int m = bas + (haut - bas) / 2; /* pas (bas+haut)/2 : débordement possible ! */
if (t[m] < x) bas = m + 1;
else if (t[m] > x) haut = m;
else return m;
}
return -1;
}(* OCaml *)
let dichotomie t x =
let rec aux bas haut =
if bas >= haut then None
else
let m = bas + (haut - bas) / 2 in
if t.(m) < x then aux (m + 1) haut
else if t.(m) > x then aux bas m
else Some m
in aux 0 (Array.length t)| C | OCaml | Python | |
|---|---|---|---|
| Typage | statique, faible | statique, fort, inféré | dynamique, fort |
| Mémoire | manuelle | ramasse-miettes | ramasse-miettes |
| Absence de valeur | −1 / NULL (piège) | option (sûr) | None (non vérifié) |
| Vitesse (boucle) | 1× | ≈ 1,5× | ≈ 50-100× |
| Usage | embarqué, OS, perfs | compilateurs, preuve, finance | prototypage, science, IA |
Le bug de débordement dans (bas + haut) / 2 est resté 9 ans dans la bibliothèque Java (Bloch, 2006). Python en est immunisé ; C et OCaml (entiers 63 bits) non.
Cours
Cours 1 — Le modèle mémoire du C : ce qu’il faut avoir en tête à chaque ligne
| Zone | Qui y vit | Durée de vie | Erreur typique |
|---|---|---|---|
| Segment de code | Les instructions | Programme | — |
| Données statiques | Variables globales, static, chaînes littérales | Programme | Modifier une chaîne littérale (char *s = "abc"; s[0] = 'x'; → plantage) |
| Pile | Variables locales, paramètres, adresses de retour | L’appel de fonction | Renvoyer l’adresse d’une locale ; récursion trop profonde ; tableau local géant |
| Tas | Ce que malloc renvoie | Jusqu’au free | Fuite, double free, utilisation après free |
Déclarations, à lire de l’intérieur vers l’extérieur. int *p : p est un pointeur vers int. int t[10] : tableau de 10 int. int *t[10] : tableau de 10 pointeurs vers int. int (*p)[10] : pointeur vers un tableau de 10 int. const int *p : pointeur vers un int constant (on ne modifie pas *p) ; int *const p : pointeur constant (on ne modifie pas p). char **argv : pointeur vers pointeur vers char — le tableau des arguments de la ligne de commande.
Arithmétique des pointeurs. p + 1 avance de sizeof(*p) octets, pas de 1 : c’est ce qui rend t[i] ≡ *(t + i) correct pour tout type. p2 − p1 donne un nombre d’éléments. Comparer deux pointeurs n’a de sens que dans le même tableau.
Cours
Cours 2 — Le cycle de vie d’un programme C : préprocesseur, compilation, édition de liens, exécution
$ gcc -E prog.c -o prog.i # 1. préprocesseur : #include collés, #define substitués, #ifdef résolus
$ gcc -S prog.i -o prog.s # 2. compilation : C → assembleur (lisez-le : c'est instructif)
$ gcc -c prog.s -o prog.o # 3. assemblage : → code machine relogeable (fichier objet)
$ gcc prog.o util.o -lm -o prog # 4. édition de liens : objets + bibliothèques (libm : math) → exécutable
$ ./prog ; echo $? # 5. exécution ; code de retour de main| Erreur | Étape | Message typique | Cause |
|---|---|---|---|
| Fichier d’en-tête introuvable | 1 | fatal error: foo.h: No such file | Chemin d’include manquant (-I) |
| Erreur de syntaxe / type | 2 | error: expected ';', incompatible pointer type | Le code |
| Symbole non défini | 4 | undefined reference to 'sqrt' | Bibliothèque non liée (-lm) ou fonction jamais définie |
| Segmentation fault | 5 | Segmentation fault (core dumped) | Accès mémoire invalide : gdb / valgrind |
Un projet à plusieurs fichiers. Chaque .c est compilé séparément ; les .h déclarent ce que les autres fichiers peuvent utiliser (prototypes, struct, #define) avec une garde #ifndef CAPTEUR_H / #define CAPTEUR_H / … / #endif. make ne recompile que ce qui a changé :
# Makefile
CC = gcc
CFLAGS = -Wall -Wextra -O2 -g -std=c17
OBJ = main.o capteur.o filtre.o
robot: $(OBJ)
$(CC) $(CFLAGS) -o $@ $(OBJ) -lm
%.o: %.c capteur.h filtre.h
$(CC) $(CFLAGS) -c $<
clean:
rm -f $(OBJ) robot
# (les retraits sont des TABULATIONS, pas des espaces)Cours
Cours 3 — OCaml : évaluation, types, récursion — ce que le typeur vérifie pour vous
| Concept | Syntaxe | Ce qu’il faut savoir |
|---|---|---|
| Liaison | let x = e in corps | x n’est pas une variable : sa valeur ne change jamais. let imbriqués = portée lexicale. |
| Fonction | let f x y = … ; fun x -> … | Curryfiée : f 1 est une fonction (application partielle). Type int -> int -> int. |
| Récursion | let rec f n = … f (n-1) … | rec obligatoire ; récursion terminale optimisée. |
| Tuples / listes | (1, "a") ; [1; 2; 3] ; x :: l | Tuple hétérogène de taille fixe ; liste homogène immuable. |
| Filtrage | match e with | motif -> … | _ -> … | Exhaustivité vérifiée ; when pour une garde ; as pour nommer. |
| Types somme / produit | type t = A | B of int ; {x : float; y : float} | Constructeurs en majuscule ; enregistrements à champs nommés. |
| Polymorphisme | 'a list -> int | Une fonction qui ne regarde pas les éléments marche pour tout type. |
| Exceptions | exception Vide ; raise Vide ; try … with Vide -> … | Préférer option/result quand l’absence est normale. |
| Mutabilité | ref, array, mutable dans un enregistrement | Explicite et visible dans les types. |
(* Exemple travaillé : une pile persistante et son type abstrait, avec la signature qui cache la représentation *)
module type PILE = sig
type 'a t
val vide : 'a t
val empiler : 'a -> 'a t -> 'a t
val depiler : 'a t -> ('a * 'a t) option
val taille : 'a t -> int
end
module Pile : PILE = struct
type 'a t = 'a list
let vide = []
let empiler x p = x :: p
let depiler = function [] -> None | x :: r -> Some (x, r)
let taille = List.length
end
let () =
let p = Pile.(vide |> empiler 1 |> empiler 2) in (* |> : application inversée, lisible de gauche à droite *)
match Pile.depiler p with
| Some (x, _) -> Printf.printf "sommet %d, taille %d\n" x (Pile.taille p)
| None -> print_endline "vide"TP guidé
TP 1 — Compiler, déboguer, mesurer en C (sur PC, 2 h)
- Installer. Linux :
sudo apt install build-essential gdb valgrind. macOS :xcode-select --install(valgrind indisponible : utiliser-fsanitize=address). Windows : WSL2 + Ubuntu (recommandé), ou MSYS2. Vérifier :gcc --version. - Premier programme et premier avertissement.
stats.c: lit des entiers sur l’entrée standard (scanf("%d", &x) == 1) dans un tableau dynamique (doublement de capacité), affiche min, max, moyenne. Compilez avecgcc -Wall -Wextra -O2 -g -o stats stats.c; corrigez jusqu’à zéro avertissement. Test :seq 1 100 | ./stats→ moyenne 50.5. - Provoquer et diagnostiquer trois bugs. (a) Retirez le
free:valgrind --leak-check=full ./stats < nombres.txt→ « definitely lost ». (b) Lisezt[n]au lieu det[n−1]: valgrind « Invalid read of size 4 » avec la ligne exacte. (c) Déréférencez NULL :gdb ./stats,run < nombres.txt,bt(pile d’appels),print p,frame 1,list. Notez pour chaque bug : symptôme, outil, message, ligne. - Liste chaînée en C.
liste.h/liste.c(types,inserer_tete,longueur,inverser,liberer) +test_liste.cavec desassert(<assert.h>) + Makefile.make && ./test_liste && valgrind ./test_liste: « All heap blocks were freed ». - Mesurer C vs Python. Crible d’Ératosthène jusqu’à 10⁸ en C (tableau d’octets,
calloc) et en Python (bytearray) :time ./criblevstime python crible.py. Puis-O0vs-O2vs-O3 -march=native. Remplissez un tableau des temps. - Lire l’assembleur.
gcc -O2 -S -fverbose-asm boucle.csur une boucle qui somme un tableau : repérez la boucle, l’instruction d’addition, et avec-O3les instructions vectorielles (paddd,vpaddd). Vous venez de voir ce que « optimisation » veut dire. - Livrable. Dépôt avec Makefile, tests,
RESULTATS.md(tableau des bugs diagnostiqués, temps C/Python/-O).
TP guidé
TP 2 — OCaml : de l’interpréteur au projet compilé (sur PC, 2 h)
- Installer.
Windows : via WSL2. Éditeur : VS Code + extension « OCaml Platform ».bash -c "sh <(curl -fsSL https://opam.ocaml.org/install.sh)" # opam, le gestionnaire de paquets opam init -y && eval $(opam env) opam install -y utop dune ocaml-lsp-server ounit2 ocaml -version && utop -version - Interpréteur. Lancez
utop; tapez les exemples du cours 3 ; observez le type inféré de chaque définition ; provoquez trois erreurs de type (1 + 2.0,"a" ^ 1, unmatchnon exhaustif) et lisez les messages : ils indiquent toujours ce qui était attendu et ce qui a été trouvé. - Projet dune.
dune init project robotml && cd robotml # bin/main.ml, lib/robotml.ml, test/test_robotml.ml ; dune-project à la racine dune build 2>&1 | head # zéro erreur attendue dune exec robotml dune test - Bibliothèque. Dans
lib/: un moduleListes(longueur, map, filter, fold, inverser terminale, tri par insertion, tri fusion — sans utiliserList.) ; un moduleArbre(ABR polymorphe : insérer, contient, infixe, hauteur) ; un moduleExpr(type algébrique, eval, dérivée, affichage). Chaque fonction : sa signature dans un.mli(dunele lit ; c’est le contrat). - Tests. Avec OUnit2 : au moins 15 cas, dont la comparaison de votre tri à
List.sortsur 100 listes aléatoires (Random.int).dune testdoit être vert. - Performance et récursion terminale.
longueurnaïve sur une liste de 10⁶ éléments :Stack_overflow? Réécrivez-la avec accumulateur : plus de débordement. Mesurez avecSys.time ()le tri fusion sur 10⁶ entiers en OCaml natif (dune build --profile release) et comparez à Pythonsortedet à votre tri fusion Python. - Livrable. Le projet dune (build et tests verts), les
.mlicommentés,RESULTATS.mdavec les temps et les trois messages d’erreur de type expliqués.
Exercices
Exercices auto-corrigés — prédire ce que fait le C (simulé en Python)
Exercice 1 — Entiers et débordements
Pour chaque expression C, écrivez la valeur attendue (types : int 32 bits signé, unsigned 32 bits, uint8_t). La cellule simule la sémantique C et vérifie vos réponses.
Correction
−2147483648 (débordement signé, indéfini mais en pratique le tour complet) ; 4294967295 ; 44 ; −3 (troncature vers zéro, contrairement à Python −4) ; −1 (le reste a le signe du dividende ; Python : 1) ; 0 (5 − 7 en non signé vaut 4294967294, jamais négatif : bug classique dans for (unsigned i = n - 1; i >= 0; i--), boucle infinie) ; −2147483648 affiché.
Exercice 2 — Pointeurs et tableaux
Même principe : prédisez.
Correction
4 ; 1 ; 3 (différence en éléments) ; 5 (mais 1 ou 2 si t est un paramètre : il est alors un pointeur, sizeof vaut 8 — piège classique) ; 6 ; 5 ; « segfault » (déréférencer NULL : SIGSEGV).
Exercices
Exercices auto-corrigés — penser en OCaml (prototypé en Python, puis à écrire en OCaml)
Exercice 3 — Récursion structurelle sur listes
Écrivez en Python, dans le style OCaml (récursion sur [] / x :: reste, sans boucle ni méthode de liste autre que l’indexation l[0], l[1:]) : somme, map, filtre, replier_gauche(f, acc, l) (fold_left), aplatir (liste de listes → liste), compresser (runs consécutifs → (valeur, nombre)). Puis traduisez chacune en OCaml dans utop et notez le type inféré en commentaire.
Correction (Python, puis OCaml)
def somme(l): return 0 if not l else l[0] + somme(l[1:])
def map(f, l): return [] if not l else [f(l[0])] + map(f, l[1:])
def filtre(p, l): return [] if not l else ([l[0]] if p(l[0]) else []) + filtre(p, l[1:])
def replier_gauche(f, acc, l): return acc if not l else replier_gauche(f, f(acc, l[0]), l[1:])
def aplatir(ll): return [] if not ll else ll[0] + aplatir(ll[1:])
def compresser(l):
if not l: return []
r = compresser(l[1:])
return [(l[0], r[0][1] + 1)] + r[1:] if r and r[0][0] == l[0] else [(l[0], 1)] + r
(* OCaml *)
let rec somme = function [] -> 0 | x :: r -> x + somme r (* int list -> int *)
let rec map f = function [] -> [] | x :: r -> f x :: map f r (* ('a -> 'b) -> 'a list -> 'b list *)
let rec filtre p = function [] -> [] | x :: r -> if p x then x :: filtre p r else filtre p r
let rec fold_left f acc = function [] -> acc | x :: r -> fold_left f (f acc x) r (* ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a *)
let rec aplatir = function [] -> [] | l :: r -> l @ aplatir r
let rec compresser = function
| [] -> []
| x :: r -> (match compresser r with (y, n) :: q when y = x -> (x, n + 1) :: q | q -> (x, 1) :: q)05 / Défis
Défi ★ — Trois versions
Consigne (PC pour C et OCaml)
Écrivez en C, OCaml et Python : 1) pgcd(a, b) par Euclide ; 2) inverser une chaîne (en C : en place, dans le tableau de char) ; 3) est_premier(n) en O(√n). Compilez avec -Wall -Wextra et sans avertissement ; en OCaml, notez le type inféré de chaque fonction. Comparez le temps pour compter les premiers jusqu’à 107 dans les trois langages.
Correction C (extrait)
int pgcd(int a, int b) { while (b) { int r = a % b; a = b; b = r; } return a; }
void inverser(char *s) {
for (size_t i = 0, j = strlen(s) - 1; i < j; i++, j--) { char t = s[i]; s[i] = s[j]; s[j] = t; }
}
int est_premier(long n) {
if (n < 2) return 0;
if (n % 2 == 0) return n == 2;
for (long d = 3; d * d <= n; d += 2) if (n % d == 0) return 0;
return 1;
}Attention à strlen(s) - 1 quand la chaîne est vide : size_t est non signé, 0 − 1 = 4 294 967 295. Testez ce cas.
05 / Défis
Défi ★★ — Un tableau dynamique en C
Consigne (PC)
Implémentez la list de Python en C : struct Vecteur { int *donnees; size_t taille, capacite; } avec vec_creer, vec_ajouter (doublement de capacité quand plein), vec_obtenir (avec vérification des bornes), vec_liberer. Ajoutez 10 millions d’entiers, mesurez, vérifiez avec valgrind qu’il n’y a aucune fuite. Question : pourquoi doubler et non ajouter 100 cases à chaque fois ? (Calculez le coût total des recopies dans les deux cas.)
Correction (extrait)
typedef struct { int *donnees; size_t taille, capacite; } Vecteur;
Vecteur vec_creer(void) { return (Vecteur){ NULL, 0, 0 }; }
void vec_ajouter(Vecteur *v, int x) {
if (v->taille == v->capacite) {
size_t nc = v->capacite ? 2 * v->capacite : 8;
int *nd = realloc(v->donnees, nc * sizeof *nd);
if (!nd) { perror("realloc"); exit(1); }
v->donnees = nd; v->capacite = nc;
}
v->donnees[v->taille++] = x;
}
int vec_obtenir(const Vecteur *v, size_t i) {
if (i >= v->taille) { fprintf(stderr, "indice %zu hors limites\n", i); exit(1); }
return v->donnees[i];
}
void vec_liberer(Vecteur *v) { free(v->donnees); *v = vec_creer(); }Doublement : les recopies totalisent 8 + 16 + … + n < 2n : O(1) amorti par ajout. Ajouter 100 cases : n/100 recopies de taille croissante, soit n²/200 opérations : O(n) amorti — 10 millions d’ajouts prendraient des heures.
05 / Défis
Défi ★★★ — Esprit prépa : un évaluateur d’expressions en OCaml
Consigne (PC)
1) Type expr = Const of float | Var of string | Add of expr * expr | Mul of expr * expr | Neg of expr. 2) eval : (string * float) list -> expr -> float. 3) derive : string -> expr -> expr. 4) simplifie avec au moins 6 règles. 5) Un analyseur syntaxique parse : string -> expr pour « 3*x*x + 2*x + 1 » par descente récursive (grammaire : expr → terme (+ terme)* ; terme → facteur (* facteur)* ; facteur → nombre | variable | ( expr ) | - facteur). Comparez à la version Python du défi ★★★ du module L02 : qu’est-ce que le typage vous a évité ?
Correction (extrait : eval, derive, parse)
type expr = Const of float | Var of string | Add of expr * expr | Mul of expr * expr | Neg of expr
let rec eval env = function
| Const c -> c
| Var x -> List.assoc x env
| Add (a, b) -> eval env a +. eval env b
| Mul (a, b) -> eval env a *. eval env b
| Neg a -> -. (eval env a)
let rec derive x = function
| Const _ -> Const 0.
| Var y -> Const (if x = y then 1. else 0.)
| Add (a, b) -> Add (derive x a, derive x b)
| Mul (a, b) -> Add (Mul (derive x a, b), Mul (a, derive x b))
| Neg a -> Neg (derive x a)
(* Analyseur : les jetons sont une liste de chaînes ; chaque fonction rend (expr, reste) *)
let rec parse_expr toks =
let t, r = parse_terme toks in boucle_add t r
and boucle_add acc = function
| "+" :: r -> let t, r' = parse_terme r in boucle_add (Add (acc, t)) r'
| r -> acc, r
and parse_terme toks =
let f, r = parse_facteur toks in boucle_mul f r
and boucle_mul acc = function
| "*" :: r -> let f, r' = parse_facteur r in boucle_mul (Mul (acc, f)) r'
| r -> acc, r
and parse_facteur = function
| "(" :: r -> let e, r' = parse_expr r in (match r' with ")" :: r'' -> e, r'' | _ -> failwith ") attendu")
| "-" :: r -> let f, r' = parse_facteur r in Neg f, r'
| tok :: r -> (match float_of_string_opt tok with Some c -> Const c, r | None -> Var tok, r)
| [] -> failwith "expression incomplète"La découpe en jetons (lexer) : Str.split (Str.regexp "[ ]+") après avoir entouré les opérateurs d’espaces, ou un automate à la main. Le typage garantit que chaque constructeur est traité dans chaque fonction ; en Python, oublier le cas Neg dans simplifier ne se verrait qu’à l’exécution, sur une entrée particulière. C’est exactement pour cela que les compilateurs (dont ceux de Rust et… d’OCaml lui-même) sont écrits en OCaml.
06 / Vérification
En C, int *f(void) { int x = 42; return &x; } :
Deux questions supplémentaires
1. Que signifie le type OCaml 'a list -> int ? Une fonction qui prend une liste d’éléments de n’importe quel type et renvoie un entier (comme longueur).
2. Pourquoi uint32_t maintenant - avant reste-t-il correct après débordement du compteur ? L’arithmétique non signée est modulo 232 : la différence est exacte tant que la durée est inférieure à 232 ms.
Référence
Les mots à retenir
| Mot | Définition |
|---|---|
| Compilation | Traduction du source en code machine avant exécution. |
| Pointeur | Variable contenant une adresse ; & prend l’adresse, * déréférence. |
| Pile / tas | Mémoire automatique des fonctions / mémoire allouée par malloc. |
| Comportement indéfini | Le standard C ne dit rien : tout peut arriver (débordement signé, hors limites). |
| volatile | Variable modifiable hors du flot du programme (matériel, interruption). |
| Inférence de types | OCaml déduit les types sans annotation. |
| Filtrage de motifs | match : décomposer une valeur selon ses constructeurs. |
| Type algébrique | Type somme (ou) et produit (et) : Cercle of float | …. |
| option | Type qui représente une valeur possiblement absente, à traiter obligatoirement. |
| Récursion terminale | Appel récursif en dernière position, compilé en boucle. |
| Module / signature | Unité d’encapsulation d’OCaml ; l’interface cache l’implémentation. |
Pour continuer
Vous parlez trois langages
Module suivant : les bases de données et le web — SQL exécuté dans la page, modèle relationnel, HTTP, une API pour votre robot.
À faire chez soi
- Installer gcc (Linux :
sudo apt install build-essential valgrind; Windows : WSL ou MSYS2 ; macOS : Xcode CLT) et OCaml (opam, puisopam install utop dune). - Faire les problèmes 1-4 de CS50 en C.
- Lire les chapitres 1-4 d’OCaml from the very beginning et réécrire tous les exercices de listes du module L03 en OCaml.
- Lire le programme officiel MP2I d’informatique : repérer ce que vous savez déjà faire.