Des premiers concepts jusqu'au code de la factorielle — tout ce qu'un élève ingénieur devrait savoir pour démarrer.
Informatique vient de la contraction d'information et d'automatique. Ce n'est pas l'étude des ordinateurs — c'est l'étude de la représentation, du traitement et de la transmission de l'information de manière automatique.
Un ordinateur n'est qu'un outil. L'informatique, c'est :
| Année | Quoi | Pourquoi c'est important |
|---|---|---|
| ~250 av. J.-C. | Algorithme d'Euclide (PGCD) | Un des plus vieux algorithmes encore utilisés |
| 1843 | Ada Lovelace écrit le 1er programme | Pour la machine analytique de Babbage — avant même que la machine existe |
| 1936 | Turing invente la machine de Turing | Fonde théoriquement l'informatique. Aussi : il a cassé Enigma. |
| 1945 | Von Neumann propose l'architecture stockée-programme | Le programme et les données dans la même mémoire — encore utilisé aujourd'hui |
| 1972 | Dennis Ritchie crée le C | Le langage qui a écrit UNIX, Linux, Windows… et votre factorielle |
| 1980–90 | PC, Internet, Web | L'informatique sort des labos. Fin de la préhistoire. |
Un ordinateur ne comprend que deux états : 0 et 1 — c'est le binaire. Chaque 0 ou 1 est un bit (Binary digit). 8 bits forment un octet (byte).
1 bit → 2 valeurs : 0 ou 1 8 bits → 256 valeurs : 0 à 255 32 bits → 4 294 967 296 valeurs 64 bits → 18 446 744 073 709 551 616 valeurs
On compte en base 2 (binaire) comme on compte en base 10 (décimal) :
| Binaire | Décimal | Hexadécimal (base 16) |
|---|---|---|
| 0000 | 0 | 0x0 |
| 0001 | 1 | 0x1 |
| 0010 | 2 | 0x2 |
| 0101 | 5 | 0x5 |
| 1010 | 10 | 0xA |
| 1111 | 15 | 0xF |
| 1 0000 | 16 | 0x10 |
En hexadécimal (base 16), on utilise 0–9 puis A–F pour représenter les valeurs 10 à 15. C'est très utilisé en programmation pour lire des octets : 0x7F est plus lisible que 01111111.
Pour représenter des nombres négatifs, on utilise le complément à deux : le bit de poids fort (le plus à gauche) indique le signe (1 = négatif).
Avec long long (64 bits signés), la plage est :
−9 223 372 036 854 775 808 à +9 223 372 036 854 775 807
Notre 20! = 2 432 902 008 176 640 000 tient encore. 21! dépasse, et le C ne vérifie pas — c'est le débordement (overflow). Le résultat devient faux silencieusement.
Algorithme : une suite finie d'instructions non-ambiguës qui, partant d'entrées données, produit une sortie et se termine en temps fini.
Un algorithme doit être :
// Algorithme : préparer un café (version express)
Données : eau, café moulu, machine
Résultat : une tasse de café potable
1. Remplir le réservoir d'eau
2. Mettre le café dans le filtre
3. Placer la tasse sous la sortie
4. Allumer la machine
5. Attendre que la tasse soit pleine
6. Servir
Si l'une des étapes est ambiguë (« mettre un peu de café »), le résultat varie selon l'exécutant. En programmation, c'est pareil — la machine ne devine pas. Elle exécute exactement ce qui est écrit.
Pour calculer n! (produit de 1 à n), on peut écrire :
Entrée : un entier n ≥ 0
Sortie : n! (le produit 1 × 2 × 3 × … × n)
Si n = 0 → retourner 1
Sinon :
résultat = 1
Pour i de 1 à n :
résultat = résultat × i
Retourner résultat
On initialise un accumulateur, on le multiplie par chaque entier, on rend le résultat. C'est une approche itérative — on répète une opération un nombre connu de fois.
Le pseudo-code est un langage informel pour décrire un algorithme sans se soucier de la syntaxe d'un vrai langage. On en trouve partout : dans les livres, sur les tableaux blancs, et dans les copies d'étudiants le matin de l'examen.
Pseudo-code
Fonction factorielle(n):
Si n < 0 alors
retourner -1
Fin Si
res ← 1
Pour i ← 2 à n faire
res ← res × i
Fin Pour
retourner res
C réel long long factorielle(int n) { if (n < 0) return -1; long long res = 1; for (int i = 2; i <= n; i++) { res *= i; } return res; }
Les grandes étapes pour transformer un problème en code qui marche :
Le C est un langage compilé, impératif, créé en 1972 par Dennis Ritchie chez Bell Labs. Il a écrit UNIX, qui a écrit Linux, qui fait tourner Internet et les téléphones Android. Bref : le C, c'est le socle.
Tout programme C commence par main() :
int main() { return 0; }
Syntaxe : ensemble des règles d'écriture du langage. Comme en français, chaque phrase a une structure. Ici :
{ } délimitent un bloc;return 0; signifie « tout s'est bien passé »Pour communiquer avec l'utilisateur, on utilise printf
(déclarée dans <stdio.h>) :
#include <stdio.h> int main() { printf("Bonjour les ingenieurs !\n"); return 0; }
#include : directive qui insère le contenu d'un fichier
d'en-tête. Permet d'utiliser des fonctions sans les réécrire.\n : caractère d'échappement qui représente un
saut à la ligne (newline). Sans lui, tout reste sur la même ligne.
Une variable est une boîte qui contient une valeur. Le type dit quelle sorte de valeur peut aller dans la boîte :
| Type | Taille | Exemple | Usage |
|---|---|---|---|
int | 32 bits | int n = 5; | Entier signé, −2³¹ à 2³¹−1 |
long long | 64 bits | long long x = 120LL; | Très grand entier signé |
float | 32 bits | float pi = 3.14f; | Nombre à virgule (simple précision) |
double | 64 bits | double pi = 3.1415926535; | Nombre à virgule (double précision) |
char | 8 bits | char c = 'A'; | Un caractère (lettre, chiffre, symbole) |
long long s'écrit avec
deux mots. Pour les constantes littérales, on ajoute LL
(ex: 2432902008176640000LL) pour dire au compilateur
« oui, je sais que c'est un très grand nombre, merci de ne pas râler ».
printf utilise des spécificateurs de format
pour afficher des variables :
| Spécificateur | Type | Exemple |
|---|---|---|
%d | int | printf("%d", n); |
%lld | long long | printf("%lld", res); |
%f | double / float | printf("%f", pi); |
%c | char | printf("%c", c); |
%s | chaîne de caractères | printf("%s", "coucou"); |
%x | int (hexa) | printf("%x", 255); → ff |
printf("factorielle(%d) = %lld\n", n, res); // Affiche : factorielle(5) = 120
printf est le couteau suisse de l'affichage en C.
Il y a 50 façons de formater un nombre. Vous en utiliserez 4.
Les 46 autres, vous les chercherez sur Internet — et c'est normal.
Les opérateurs permettent de combiner des valeurs pour en produire de nouvelles.
| Opérateur | Opération | Exemple | Résultat |
|---|---|---|---|
+ | Addition | 10 + 3 | 13 |
- | Soustraction | 10 - 3 | 7 |
* | Multiplication | 10 * 3 | 30 |
/ | Division entière | 10 / 3 | 3 (troncature) |
% | Modulo (reste) | 10 % 3 | 1 |
++ | Incémentation | i++ | i += 1 |
-- | Décrémentation | i-- | i -= 1 |
Attention : la division entre deux entiers donne un entier
(troncature, pas d'arrondi). 10 / 3 = 3, pas 3,333…
int a = 10 / 3; // a = 3 (division entière) double b = 10.0 / 3; // b = 3.333333 (division réelle)
Les opérateurs abrégés : res *= i équivaut à res = res * i.
Ça marche pour +=, -=, /=, %= aussi.
| Opérateur | Signification | Exemple vrai |
|---|---|---|
== | Égal à | 5 == 5 |
!= | Différent de | 5 != 3 |
< | Plus petit que | 3 < 5 |
> | Plus grand que | 5 > 3 |
<= | Plus petit ou égal | 5 <= 5 |
>= | Plus grand ou égal | 5 >= 3 |
= (affectation)
et == (comparaison). if (n = 5) est toujours vrai
(ça met 5 dans n, et 5 est non-nul, donc « vrai »). Le compilateur vous
avertira avec -Wall. Activez toujours -Wall.
Pour combiner des conditions :
| Opérateur | Nom | Exemple |
|---|---|---|
&& | ET logique | if (n > 0 && n <= 20) |
|| | OU logique | if (n == 0 || n == 1) |
! | NON logique | if (!erreur) |
&& et || sont paresseux (short-circuit) :
si la première partie suffit à décider, la seconde n'est pas évaluée.
C'est la flemme au niveau processeur. Et ça évite des bugs du genre
if (ptr != NULL && ptr->value == 42).
Un programme linéaire s'arrête vite. Pour faire des choix et répéter des actions, on a les structures de contrôle.
Exécute un bloc si la condition est vraie, un autre sinon :
if (n < 0) { return -1; // pas de factorielle pour les negatifs } else { // continuer le calcul }
Condition : toute expression qui vaut 0 (faux)
ou ≠ 0 (vrai). En C, il n'y a pas de type booléen dédié avant C99
(stdbool.h apporte bool). Avant, on utilise int.
if (x) y = 1; z = 2;
— la deuxième ligne n'est pas dans le if.
Mettez toujours { }, même pour une ligne.
Votre futur vous remerciera.
La boucle for est parfaite quand on sait à l'avance
combien de fois répéter :
// Structure : for (initialisation ; condition ; incrément) for (int i = 2; i <= n; i++) { res *= i; }
Les trois parties :
int i = 2 — exécutée une fois au débuti <= n — testée avant chaque tour ; si fausse, on sorti++ — exécuté à la fin de chaque tour// Parcourir un tableau d'éléments for (int i = 0; i < nb_tests; i++) { ... } // Compter à rebours for (int i = n; i >= 1; i--) { ... } // Pas de 2 for (int i = 0; i < 100; i += 2) { ... }
Quand on ne sait pas à l'avance combien de tours, on utilise
while :
while (condition) { // corps } // Exemple : tant que l'utilisateur n'a pas donné une valeur valide int n; printf("Entrez n (0-20) : "); scanf("%d", &n); while (n < 0 || n > 20) { printf("Valeur invalide. Recommencez : "); scanf("%d", &n); }
Notre factorielle utilise for car on connaît exactement
le nombre d'itérations (n−1 multiplications).
for et while ?
for c'est « je sais combien de fois » (tour de manège).
while c'est « je ne sais pas quand ça s'arrête »
(attente du bus). Les deux peuvent faire les mêmes choses, mais
choisir le bon rend le code plus lisible.
Une fonction est une boîte noire qui prend des paramètres en entrée et produit une valeur de retour. Avantages :
// Sans fonction : tout dans main int main() { int n = 5; long long res = 1; for (int i = 2; i <= n; i++) res *= i; printf("%lld\n", res); return 0; } // Avec fonction : factorielle est réutilisable, testable long long factorielle(int n) { long long res = 1; for (int i = 2; i <= n; i++) res *= i; return res; } int main() { printf("%lld\n", factorielle(5)); return 0; }
type_retour nom_de_la_fonction(type_param1 nom1, type_param2 nom2) { // corps return valeur; // doit correspondre au type_retour }
| Terme | Définition | Notre exemple |
|---|---|---|
| Signature | Nom + paramètres de la fonction | factorielle(int n) |
| Paramètre (formel) | Variable reçue par la fonction | n |
| Argument (réel) | Valeur passée lors de l'appel | 5 dans factorielle(5) |
| Valeur de retour | Résultat renvoyé par return | 120 |
| Variable locale | Variable déclarée dans la fonction | res, i |
Une variable déclarée à l'intérieur d'une fonction n'existe que dans cette fonction. On dit que sa portée (scope) est locale.
int factorielle(int n) { long long res = 1; // locale : n'existe que ici for (int i = 2; i <= n; i++) { res *= i; // i aussi est local à la boucle (C99) } return res; } // res et n disparaissent ici int main() { printf("%d", n); // ERREUR : n n'existe pas ici return 0; }
Commençons par l'essentiel. On veut une fonction qui calcule n! :
long long factorielle(int n) { if (n < 0) return -1; // protection : pas de factorielle négative long long res = 1; // accumulateur : 0! = 1, on part de 1 for (int i = 2; i <= n; i++) { // on commence à 2 (×1 ne change rien) res *= i; // accumulateur : res = res × i } return res; // retour du résultat }
| Entrée | Déroulement | Sortie |
|---|---|---|
| n = 0 | la boucle ne s'exécute pas (2 ≤ 0 faux) | 1 |
| n = 1 | la boucle ne s'exécute pas (2 ≤ 1 faux) | 1 |
| n = 5 | res = 1×2×3×4×5 | 120 |
| n = −3 | return -1 immédiat | −1 |
Une fonction, ça se teste. On ajoute un main() avec
une batterie de cas :
int main() { struct { int n; long long attendu; } tests[] = { {0, 1}, {1, 1}, {2, 2}, {3, 6}, {5, 120}, {10, 3628800}, {12, 479001600}, {20, 2432902008176640000LL}, }; int nb_tests = sizeof(tests) / sizeof(tests[0]); int passes = 0, echoues = 0; for (int i = 0; i < nb_tests; i++) { long long res = factorielle(tests[i].n); if (res == tests[i].attendu) { printf("OK factorielle(%d) = %lld\n", tests[i].n, res); passes++; } else { printf("FAIL factorielle(%d) = %lld (attendu %lld)\n", tests[i].n, res, tests[i].attendu); echoues++; } } printf("\n%d passes, %d echoues sur %d tests\n", passes, echoues, nb_tests); return echoues > 0 ? 1 : 0; }
Quelques nouveautés dans ce code :
struct) : permet de regrouper plusieurs valeurs de types différents dans une même entité. Ici, on couple n (l'entrée) et attendu (le résultat attendu).tests[]) : une collection d'éléments de même type, indexée par un entier.sizeof(tests) / sizeof(tests[0]) : astuce pour calculer le nombre d'éléments d'un tableau à la compilation.cond ? a : b : si cond est vraie, vaut a, sinon b. C'est un if/else compressé.Voici d'autres façons d'écrire la même chose — chacune avec ses avantages et inconvénients :
// Version récursive (élégante, risquée pour n grand) long long factorielle_rec(int n) { if (n < 0) return -1; if (n <= 1) return 1; return n * factorielle_rec(n - 1); } // Version avec while (équivalente, style différent) long long factorielle_while(int n) { if (n < 0) return -1; long long res = 1; int i = 2; while (i <= n) { res *= i; i++; } return res; } // Version décroissante (moins efficace, mais instructive) long long factorielle_desc(int n) { if (n < 0) return -1; long long res = 1; for (int i = n; i > 1; i--) { res *= i; } return res; }
La version itérative (notre version) est la meilleure pour ce cas : O(n) en temps, O(1) en mémoire, pas de risque de débordement de pile, et le compilateur l'optimise très bien (elle est courte).
Un programme non testé est un programme qui a tort — mais qui ne le sait pas encore. Tester permet de :
| Type de test | Exemple | Notre programme |
|---|---|---|
| Cas nominal | Une entrée normale | factorielle(5) → 120 |
| Cas limite | Aux bornes du domaine | factorielle(0) → 1, factorielle(20) → 20! |
| Cas d'erreur | Entrée invalide | factorielle(-1) → -1 |
| Cas de dépassement | Valeur qui fait planter | factorielle(21) → overflow silencieux (non testé ici) |
Notre batterie de 8 tests couvre les cas nominaux (5, 10, 12) et les cas limites (0, 1, 20). Le cas d'erreur (n négatif) n'est pas testé automatiquement — on pourrait l'ajouter.
Comment passe-t-on du code C lisible… au binaire que le processeur exécute ? Voici les étapes, visibles avec notre Makefile :
| Étape | Fait quoi ? | Commande | Fichier produit |
|---|---|---|---|
| 1. Préprocesseur | Remplace #include, #define, etc. | gcc -E | — (code C développé) |
| 2. Compilation | Traduit le C en assembleur | gcc -S | .s (assembleur) |
| 3. Assemblage | Traduit l'assembleur en code machine (obj) | gcc -c ou as | .o (objet) |
| 4. Édition de liens | Assemble les .o + bibliothèques → exécutable | gcc (sans -c) | factorielle |
# 1. Générer l'assembleur (.s) gcc -Wall -Wextra -std=c99 -S -o factorielle.s factorielle.c # 2. Assembler le .s en exécutable gcc -Wall -Wextra -std=c99 -o factorielle-asm factorielle.s # 3. Tout d'un coup gcc -Wall -Wextra -std=c99 -o factorielle factorielle.c
Le fichier factorielle.s est un fichier texte en
assembleur (AT&T, le style par défaut
chez GCC). Chaque ligne correspond à une instruction processeur :
movq, addl, imulq, etc.
-Wall comme vous lavez les légumes.
Ce cours pose les fondations. Les documents suivants approfondissent chaque aspect avec le même style et la même rigueur :
| Doc | Contenu | Public |
|---|---|---|
| 📐 factorielle-doc.html | Tout sur le programme factorielle : code, tests, overflow, variantes | Ceux qui veulent les détails de l'implémentation |
| 📗 asm-doc.html | Les instructions assembleur une par une, avec humour | Ceux qui veulent voir ce que le compilateur a fabriqué |
| 🧠 processor-doc.html | Anatomie du processeur : ALU, registres, cache, pipeline… | Ceux qui veulent comprendre ce qui se passe sous le capot |
factorielle.c,
regardez comment l'assembleur change avec make asm.
Comparez -O0 et -O2 avec make perf.make debug lance gdb avec
un point d'arrêt. Utilisez next, step,
print n pour suivre l'exécution pas à pas.factorielle.s
(généré par make asm) et retrouvez la boucle.
C'est comme une version « hardcore » de votre code.factorielle(21) ? Pourquoi ? Comment détecter l'overflow ?