Le langage C
Le nom C n’est pas un sigle. Il succède au langage B, lui-même issu de BCPL.
Le langage qui a rendu Unix portable
Date d’apparition : 1972
- Milieu des années 1960 : Martin Richards conçoit BCPL, un langage compact destiné notamment à la programmation de systèmes.
- 1969–1970 : Ken Thompson crée le langage B à partir de BCPL pour l’environnement Unix naissant.
- 1971 : Dennis Ritchie commence à ajouter des types à B et développe une étape intermédiaire brièvement appelée NB, pour New B.
- 1972 : la période la plus créative de la mise au point du C aboutit aux éléments essentiels du nouveau langage.
- Été 1973 : le noyau d’Unix est réécrit en C sur le DEC PDP-11.
- 1978 : Brian Kernighan et Dennis Ritchie publient The C Programming Language, bientôt désigné par leurs initiales : le K&R.
- 1989 : le comité ANSI X3J11 publie la première norme officielle du langage.
Dennis Ritchie situe la formation du C entre 1969 et 1973, avec un moment décisif en 1972. Il est donc plus exact de parler d’une évolution progressive que d’une invention survenue en une seule journée. Le C se construit au rythme des besoins d’Unix, des progrès de son compilateur et des possibilités offertes par le PDP-11.
À quel besoin répond le langage C ?
Le C répond d’abord à une question très concrète : comment écrire Unix autrement qu’en assembleur sans perdre trop de performances ?
L’assembleur est rapide et précis, mais chaque famille de machines possède son propre jeu d’instructions. Un programme écrit pour le PDP-7 ne peut pas être simplement recompilé pour le PDP-11. Un langage de plus haut niveau facilite la lecture et la modification du programme, mais son compilateur doit produire un code suffisamment compact et rapide pour un système d’exploitation.
Les premières versions de B utilisées sur le PDP-7 produisent du code interprété, sensiblement plus lent que l’assembleur. Elles ne semblent donc pas adaptées à la réécriture complète d’Unix.
Dennis Ritchie fait alors évoluer à la fois le langage et son compilateur. Le nouveau compilateur génère directement des instructions pour le PDP-11. Le C peut ainsi rivaliser avec l’assembleur pour de nombreuses tâches tout en offrant une expression plus structurée du programme.
Avant le C : BCPL et B
La filiation du langage peut être résumée ainsi :
BCPL → B → C
BCPL, conçu par Martin Richards, est un langage volontairement simple. B, développé principalement par Ken Thompson, en reprend l’esprit en l’adaptant aux moyens très limités disponibles pour le premier Unix.
BCPL et B ne distinguent pas fortement les types de données. Une cellule mémoire contient un mot, et c’est l’opération effectuée qui détermine la manière d’interpréter son contenu. Le même motif de bits peut donc servir d’entier, de caractère ou d’adresse.
Cette simplicité convient au PDP-7, mais elle devient gênante sur le PDP-11. Cette nouvelle machine travaille avec des mots de 16 bits tout en permettant l’adressage séparé des octets. Un caractère, un entier et une adresse ne se manipulent donc plus exactement de la même manière. Le langage doit connaître la nature de la donnée afin de produire les bonnes instructions.
Ritchie introduit alors des types comme char et int, puis des pointeurs associés à ces types. La première version enrichie de B est brièvement appelée NB, pour New B. Lorsque le langage devient suffisamment différent, Ritchie le nomme C, dans la continuité de B. Contrairement à BASIC ou FORTRAN, C n’est donc pas un acronyme.
Un langage compilé, impératif et structuré
Un programme C décrit une suite d’opérations regroupées dans des fonctions.
Le programmeur utilise des variables, des tests et des boucles pour indiquer ce que la machine doit faire.
Voici un exemple simple :
#include <stdio.h>
int main(void)
{
int i;
int somme = 0;
for (i = 1; i <= 10; i++) {
somme = somme + i;
}
printf("Somme : %d\n", somme);
return 0;
}
Ce programme additionne les nombres de 1 à 10, puis affiche le résultat.
Quelques éléments caractéristiques apparaissent immédiatement :
intindique qu’une variable ou qu’une fonction utilise un entier ;mainest la fonction par laquelle commence l’exécution du programme ;fordécrit une boucle ;- les accolades regroupent plusieurs instructions ;
- le point-virgule termine la plupart des instructions ;
printfappartient à la bibliothèque standard et permet ici d’afficher le résultat.
Le fichier source n’est pas directement exécuté par le processeur. Le préprocesseur traite notamment les directives comme #include, le compilateur traduit le C, puis l’éditeur de liens réunit le programme et les bibliothèques nécessaires pour produire un exécutable.
L’introduction des types constitue une différence essentielle avec B. Le programmeur peut indiquer la nature des données :
char lettre = 'C';
int compteur = 10;
float temperature = 21.5f;
Le type informe le compilateur sur la représentation de la valeur et sur les opérations possibles. Les premières versions du C restent toutefois moins strictes que les compilateurs modernes et vérifient imparfaitement certains usages. Le contrôle des types se renforcera progressivement.
Les pointeurs
Le pointeur est l’un des mécanismes les plus caractéristiques du C. Il s’agit d’une variable dont la valeur désigne l’adresse d’une autre donnée.
int valeur = 42;
int *p = &valeur;
*p = 50;
Dans cet exemple :
&valeurobtient l’adresse de la variablevaleur;pconserve cette adresse ;*ppermet d’accéder à la donnée située à cette adresse ;- l’instruction
*p = 50modifie donc directementvaleur.
Pourquoi ne pas inscrire directement une adresse précise dans le programme ? Parce que l’emplacement d’une variable peut changer d’une exécution ou d’une machine à l’autre. Le pointeur permet de travailler avec une adresse obtenue au moment approprié, sans connaître à l’avance sa valeur numérique.
Cette indirection sert notamment à construire des structures dynamiques, parcourir des zones mémoire ou modifier une donnée depuis une fonction. Elle offre une grande puissance, mais impose aussi de la prudence : une adresse invalide peut provoquer une corruption de mémoire ou l’arrêt du programme.
Tableaux et pointeurs : une relation étroite
En C, les tableaux et les pointeurs sont étroitement liés. Dans la plupart des expressions, le nom d’un tableau est converti en pointeur vers son premier élément.
int notes[3] = {12, 15, 18};
printf("%d\n", notes[1]);
printf("%d\n", *(notes + 1));
Les deux affichages donnent ici la même valeur : 15. L’écriture notes[1] est une notation plus lisible pour accéder au deuxième élément, tandis que *(notes + 1) montre le calcul d’adresse sous-jacent.
Ce choix permet de décrire et de parcourir efficacement les tableaux avec peu de règles. En contrepartie, la frontière entre le tableau et l’adresse de ses données peut sembler déroutante. Ritchie reconnaît lui-même que cette relation est à la fois une force du langage et l’une de ses caractéristiques les plus critiquées.
Une chaîne de caractères est un tableau
Le C d’origine ne possède pas de type spécifique pour représenter une chaîne de caractères. Une chaîne est un tableau de caractères terminé par une valeur nulle :
char nom[] = "UNIX";
En mémoire, ce tableau contient :
'U' 'N' 'I' 'X' '\0'
Le caractère '\0' marque la fin de la chaîne. Les fonctions de la bibliothèque peuvent ainsi parcourir les caractères jusqu’à rencontrer ce marqueur.
Cette représentation est simple, mais le programmeur doit prévoir assez de mémoire pour les caractères et pour le marqueur final. Si une fonction écrit au-delà de la zone réservée, elle peut endommager d’autres données. Une partie des erreurs classiques du C vient de cette gestion très directe de la mémoire.
Les structures : décrire les données d’un système
Pour écrire un système d’exploitation, il faut représenter des fichiers, des processus ou des périphériques. Le mot-clé struct permet de regrouper plusieurs données :
struct fichier {
int numero;
char nom[14];
};
Une telle structure peut représenter, de manière simplifiée, un numéro de fichier et son nom. Le langage peut ainsi organiser logiquement les informations tout en conservant une représentation concrète et prévisible en mémoire.
Les opérateurs ++ et —
Les opérateurs ++ et -- augmentent ou diminuent une valeur d’une unité :
i++;
j--;
Une explication souvent répétée affirme qu’ils auraient été créés pour exploiter les modes d’adressage avec auto-incrémentation du PDP-11.
Dennis Ritchie indique que cette histoire est impossible : ces opérateurs existaient déjà dans B avant l’arrivée du PDP-11.
Le PDP-7 possédait en revanche quelques cellules mémoire particulières capables de s’incrémenter lors d’un adressage indirect. Elles ont peut-être inspiré Ken Thompson. Ritchie estime également que la forme ++x permettait de produire un code plus compact que x = x + 1 dans l’environnement très contraint de l’époque.
1973 : Unix est réécrit en C
À partir de 1972, le langage et son compilateur deviennent suffisamment efficaces pour envisager une tâche beaucoup plus ambitieuse. Durant l’été 1973, le noyau d’Unix pour le PDP-11 est réécrit en C.
Ce passage constitue un tournant. Un système d’exploitation n’est plus prisonnier de l’assembleur d’une seule machine. La plus grande partie du code peut être conservée, puis recompilée après adaptation du compilateur et de quelques éléments dépendant du matériel.
La portabilité n’est pourtant pas l’objectif principal des toutes premières versions du C. Ritchie explique que cette préoccupation prend davantage d’importance par la suite. Le portage d’Unix vers l’Interdata 8/32, puis vers d’autres architectures, démontre concrètement que le même système peut changer de machine sans être entièrement réécrit.
Les succès d’Unix et du C se renforcent alors mutuellement. Unix fournit au C un environnement réel et une vaste collection d’outils ; le C rend Unix plus lisible, modifiable et transportable.
Un langage volontairement compact
Le C lui-même reste relativement petit. Des fonctions essentielles comme l’affichage, la lecture d’un fichier ou la manipulation d’une chaîne ne sont pas des instructions fondamentales du langage. Elles sont fournies par des bibliothèques.
Le préprocesseur complète également le compilateur. Deux directives deviennent particulièrement importantes :
#includeinsère les déclarations contenues dans un fichier d’en-tête ;#definepermet notamment de définir des constantes symboliques ou des macros.
Les premières versions du préprocesseur sont très simples. Il est ensuite enrichi avec des macros comportant des paramètres et avec la compilation conditionnelle.
Cette séparation participe à la philosophie du C : le cœur du langage fournit des mécanismes généraux, tandis que la bibliothèque prend en charge l’interaction avec l’environnement.
Du livre K&R à la norme ANSI
En 1978, Brian W. Kernighan et Dennis M. Ritchie publient The C Programming Language. L’ouvrage présente le langage de manière concise, avec de nombreux exemples.
Pendant plusieurs années, ce livre sert de définition de fait du C. On parle alors couramment de C K&R. Mais le langage continue d’évoluer et les compilateurs ne se comportent pas toujours de manière identique. En 1983, l’ANSI crée donc le comité X3J11. Celui-ci cherche surtout à préciser les pratiques existantes et à renforcer la portabilité sans transformer radicalement le langage.
La norme publiée en 1989, souvent appelée ANSI C ou C89, apporte notamment :
- des prototypes de fonctions indiquant le type des paramètres ;
- les qualificatifs
constetvolatile; - des règles de conversion plus précises ;
- une description complète du préprocesseur ;
- une bibliothèque standard commune aux implémentations conformes.
La norme est reprise au niveau international sous la référence ISO/IEC 9899:1990. D’autres révisions suivront, notamment C99, C11, C17 et C23, sans modifier l’identité générale du langage.
Pourquoi le C s’est-il autant diffusé ?
Selon Ritchie, le succès d’Unix constitue le premier moteur de la diffusion du C. Mais cette association n’explique pas tout. Le langage répond aussi à plusieurs besoins durables :
- efficacité : le compilateur peut produire un code proche des possibilités réelles du processeur ;
- portabilité : un même programme peut être adapté à différentes architectures sans réécriture complète ;
- compacité : le langage comporte relativement peu de mécanismes fondamentaux ;
- contrôle : le programmeur maîtrise la représentation des données et l’utilisation de la mémoire ;
- stabilité : malgré ses évolutions, le cœur du C reste reconnaissable d’une époque à l’autre.
Le compilateur portable pcc, développé par Stephen C. Johnson, facilite l’adaptation du langage à de nouvelles machines. Dans les années 1980, les compilateurs C deviennent disponibles sur une grande variété d’ordinateurs et de systèmes, y compris les micro-ordinateurs.
Les limites : le prix du contrôle
La proximité avec la machine donne au C sa puissance, mais aussi une grande partie de ses difficultés.
Le langage laisse au programmeur la responsabilité de nombreuses opérations :
- vérifier qu’un pointeur désigne une adresse valide ;
- respecter les limites d’un tableau ;
- réserver puis libérer correctement la mémoire dynamique ;
- éviter les conversions de types dangereuses ;
- organiser lui-même les grands programmes en modules cohérents.
Une erreur peut donc dépasser le simple résultat incorrect. Elle peut modifier une zone mémoire voisine, provoquer un arrêt brutal ou créer une vulnérabilité de sécurité.
Ritchie reconnaît également que certaines déclarations sont difficiles à lire et que la relation entre tableaux et pointeurs déroute souvent les débutants. Plusieurs de ces complications viennent de l’histoire du langage : le C a dû évoluer tout en continuant à accepter une grande quantité de code plus ancien.
Un héritage visible dans de nombreux langages
Le C donne naissance à plusieurs descendants directs. Objective-C lui ajoute notamment des mécanismes orientés objet, tandis que C++, développé par Bjarne Stroustrup aux Bell Labs, l’étend profondément.
Son influence se retrouve aussi dans la syntaxe de langages plus éloignés comme Java, C#, JavaScript ou PHP : accolades, points-virgules, opérateurs ++ et --, tests avec if et boucles avec for ou while. Ces langages ne sont pas tous des descendants directs du C, mais ils reprennent une partie de ses conventions.
Le C reste particulièrement adapté aux domaines où la maîtrise des ressources est essentielle : systèmes d’exploitation, pilotes, logiciels embarqués, compilateurs, bibliothèques et programmes devant dialoguer étroitement avec le matériel.
Un langage entre l’assembleur et l’abstraction
La contribution décisive du C consiste à avoir trouvé un équilibre permettant de remplacer une grande partie de l’assembleur sans éloigner complètement le programmeur de la machine. Il montre également qu’un système d’exploitation important peut être écrit presque entièrement dans un langage compilé de haut niveau.
Dennis Ritchie résume lui-même le C comme un langage étrange, imparfait et pourtant extraordinairement réussi. Ses défauts sont inséparables d’un choix fondamental : donner au programmeur des mécanismes simples, généraux et proches de la représentation concrète des données.
L’héritage du C tient donc à trois idées :
- remplacer l’assembleur pour une grande partie de la programmation système ;
- rendre le logiciel plus portable d’une architecture à une autre ;
- conserver un contrôle précis sur la mémoire et sur les ressources de la machine.
Né pour améliorer un environnement de programmation très limité, le C devient ainsi l’un des langages fondamentaux de l’informatique moderne.
Sources principales
- Dennis M. Ritchie, The Development of the C Language, article présenté à la conférence HOPL-II, 1993.
- Dennis M. Ritchie, The Evolution of the Unix Time-sharing System, Bell Laboratories Technical Journal, 1984.
- Dennis M. Ritchie, Primeval C: two very early compilers, archives Bell Labs.
- Brian W. Kernighan et Dennis M. Ritchie, The C Programming Language, Prentice-Hall, première édition, 1978.
- ISO/IEC JTC 1/SC 22/WG14, groupe de travail chargé de la normalisation du langage C.
- Computer History Museum, Dennis Ritchie.
