Né en 1972 pour écrire le système Unix, le C est toujours le langage dans lequel sont écrits les noyaux des systèmes, les interpréteurs des autres langages et les programmes des objets embarqués. Cinq planches pour comprendre pourquoi : il parle presque directement à la machine.
En 1969, aux laboratoires Bell, Ken Thompson écrit Unix en assembleur, puis invente un petit langage, B. Dennis Ritchie le transforme en 1972 : il lui ajoute des types (entier, caractère, nombre à virgule) et des structures, et l'appelle C. En 1973, le noyau d'Unix est réécrit en C : pour la première fois, un système d'exploitation peut être transporté d'un ordinateur à un autre en recompilant son code. Le livre de Kernighan et Ritchie (1978), puis les normes ANSI (1989) et ISO (C99, C11, C17, C23), fixent le langage.
Sa philosophie tient en une phrase : faire confiance au programmeur. Le C donne accès aux adresses de la mémoire, ne vérifie presque rien à l'exécution, et produit un code très rapide. C'est sa force et sa difficulté.
| Famille | Langage ou extension | Année | Ce qu'il ajoute au C |
|---|---|---|---|
| Objets | Objective-C | 1984 | Les messages entre objets de Smalltalk ; langage historique d'Apple. |
| C++ (Bjarne Stroustrup) | 1985 | Classes, héritage, fonctions virtuelles, modèles génériques ; presque tout programme C reste valide. | |
| Parallélisme | OpenMP | 1997 | Des directives #pragma qui répartissent une boucle sur plusieurs cœurs. |
| CUDA C (Nvidia) | 2007 | Des fonctions exécutées par des milliers de fils sur la carte graphique. | |
| OpenCL | 2009 | La même idée, pour des processeurs de toutes marques. | |
| Héritiers de la syntaxe | Java, C#, JavaScript, Go, Rust… | 1995 – | Accolades, point-virgule, for (i = 0; i < n; i++) : la grammaire du C est devenue une langue commune. |
Un programme C passe par quatre traducteurs successifs. Le préprocesseur recopie les fichiers inclus et remplace les macros, comme un traitement de texte. Le compilateur traduit le C en assembleur. L'assembleur le traduit en octets. L'éditeur de liens réunit ces octets avec ceux des bibliothèques (ici, la fonction printf) pour former un fichier exécutable. Tout ce qui suit a été réellement produit par le compilateur GCC 13.
La ligne #define CARRE(x) x * x n'est pas une fonction : le préprocesseur recopie le texte sans parenthèses. CARRE(n + 1) devient n + 1 * n + 1, que la priorité des opérations lit comme n + n + 1.
Le petit simulateur ci-dessous exécute, instruction par instruction, les 15 instructions x86 produites par GCC pour somme_carres : les registres edi (n), eax (le compteur i), ecx et edx (la somme s).
stdio.h est recopié en entier), puis 15 instructions et 41 octets de code machine pour la fonction, et enfin un exécutable de 16 008 octets, presque tout entier occupé par la mécanique de démarrage et les liens vers la bibliothèque. Le compilateur a transformé la boucle for en un test, un saut et six instructions répétées n fois : la boucle C se lit presque ligne à ligne dans le code machine. C'est ce qu'on appelle un langage « proche de la machine ». Quant à la macro, elle affiche 21 au lieu de 121 pour n = 10 : une raison pour laquelle le C moderne préfère les fonctions inline.Pour le C, la mémoire est une longue suite d'octets numérotés : le numéro d'un octet est son adresse. Une variable occupe quelques octets consécutifs : 4 pour un int, 1 pour un char. Un pointeur est une variable qui contient une adresse. L'opérateur & donne l'adresse d'une variable, l'opérateur * va lire ou écrire à l'adresse contenue dans un pointeur. Suivez ce petit programme ligne par ligne (machine à adresses de 32 bits).
—
p = t, le pointeur désigne le premier élément du tableau : en C, un tableau et l'adresse de son premier élément se confondent. Surtout, p + 2 n'ajoute pas 2 à l'adresse mais 2 × 4 = 8, la taille de deux int : c'est l'arithmétique des pointeurs, et t[2] n'est qu'une autre écriture de *(t + 2). Cet accès direct aux adresses rend le C irremplaçable pour piloter un circuit, dont les registres sont à des adresses fixes ; il impose aussi au programmeur de ne jamais sortir des zones qui lui appartiennent, car le langage ne le vérifie pas.Quand on ne sait pas d'avance combien de données on aura, on demande de la mémoire au fur et à mesure dans une zone appelée le tas, avec la fonction malloc, et on la rend avec free. Une liste chaînée en est l'exemple type : chaque maillon contient une valeur et l'adresse du maillon suivant. En C, rien ne rend la mémoire automatiquement : un maillon oublié reste réservé jusqu'à la fin du programme, c'est une fuite de mémoire.
struct maillon { int valeur; struct maillon *suivant; }; /* 4 + 4 = 8 octets */
struct maillon *m = malloc(sizeof *m); /* demander 8 octets au tas */
m->valeur = v; m->suivant = tete; tete = m; /* ajouter en tête */
suivant permet de les retrouver. Retirez des maillons au milieu de nombreux ajouts : la mémoire libre se morcelle (la fragmentation). Et si l'on perd le pointeur de tête sans appeler free, toute la chaîne reste réservée mais devient inaccessible. Les langages à ramasse-miettes (Lisp, Java, Python) récupèrent ces blocs automatiquement ; C++ (avec ses destructeurs) et Rust (avec son système de propriété) les libèrent au bon moment sans ramasse-miettes.Le C a été étendu dans deux directions. Vers les objets d'abord : C++ ajoute des classes, et ce qu'il ajoute peut s'écrire en C pur avec des pointeurs de fonctions, c'est d'ailleurs ainsi que fonctionnait le premier compilateur C++, qui produisait du C. Vers le parallélisme ensuite : quand les processeurs ont cessé d'accélérer vers 2005, il a fallu répartir les calculs sur plusieurs cœurs, puis sur les milliers de petits processeurs des cartes graphiques.
// C++ (1985)
struct Forme {
virtual double aire() = 0;
};
struct Cercle : Forme {
double r;
double aire() { return 3.14159265 * r * r; }
};
Forme *f = new Cercle{2.0};
f->aire(); // quelle fonction ?
/* ce que le compilateur en fait, en C */
struct vtable { double (*aire)(void *); };
struct Cercle { struct vtable *vptr; double r; };
double aire_cercle(void *o) {
struct Cercle *c = o;
return 3.14159265 * c->r * c->r;
}
/* f->aire() devient : */
f->vptr->aire(f);
/* OpenMP (1997) : une seule ligne ajoutée suffit à répartir la boucle */
#pragma omp parallel for reduction(+:s)
for (int i = 0; i < N; i++)
s += f(i);
Depuis 2017, les navigateurs savent exécuter le WebAssembly, un code machine portable conçu pour recevoir des programmes compilés depuis le C, le C++ ou Rust. Le programme ci-dessous, le crible d'Ératosthène, a été compilé par Clang 18 en un module WebAssembly de 672 octets, inclus tel quel dans cette page. Il compte les nombres premiers jusqu'à n ; la même fonction, réécrite en JavaScript, sert de témoin.
Les noyaux d'Unix, de Linux, et une large part de ceux de Windows et de macOS ; les pilotes de périphériques et les micrologiciels de démarrage.
L'interpréteur officiel de Python (CPython), ceux de Lua, de Ruby et de PHP, et les machines virtuelles de nombreux autres langages sont écrits en C.
SQLite, la base de données la plus répandue, zlib pour la compression, OpenSSL pour le chiffrement, Git pour les versions : du C, appelé depuis tous les langages.
Microcontrôleurs des voitures, appareils ménagers, capteurs, satellites : là où la mémoire se compte en kilo-octets, le C reste la langue d'usage.
Quatre étapes transforment un texte en octets ; le C se lit presque mot à mot dans le code machine produit.
Pointeurs et arithmétique des pointeurs donnent un accès direct aux octets de la mémoire, sans filet.
malloc et free rendent le programmeur responsable de la mémoire : souplesse, mais risque de fuites.
Écrit une fois, le C se compile pour tous les processeurs, jusqu'au WebAssembly des navigateurs.
Les textes de la planche 1 sont la sortie réelle de GCC 13.3 (options -O1) sur une machine x86-64 sous Linux ; le module WebAssembly de la planche 5 a été produit par Clang 18 (options -O2) et est inclus en base 64 dans le code de cette page.