Aller au contenu principal

Un langage qui ne cache rien

Ce que ce chapitre apporte5 points
  • Écrire, exécuter et lire un programme C minimal.
  • Nommer les trois choses que tout programme C possède : un point d'entrée, des types, une valeur de retour.
  • Distinguer un langage compilé d'un langage interprété, et dire ce que chacun décide.
  • Afficher une valeur avec printf et choisir la conversion qui correspond au type.
  • Lire un message d'erreur et le relier à la ligne fautive.

Python cache la mémoire, et c'est ce qui le rend agréable. Une liste grandit toute seule, un entier n'a pas de taille, une variable oubliée finit par disparaître sans qu'on s'en occupe. Ce confort a un prix : rien de ce qui se passe réellement dans la machine n'est visible, et le jour où un programme est trop lent, trop gros, ou destiné à une carte sans système d'exploitation, ce voile devient un obstacle.

Le C ne cache rien. Chaque variable occupe un nombre d'octets qu'on peut afficher, chaque tableau est une adresse et une taille qu'il faut retenir soi-même, et la mémoire prise doit être rendue à la main. Ce parcours prend ce langage pour ce qu'il est : non pas un Python moins commode, mais l'outil qui rend la machine lisible.

Le plus petit programme qui tourne

C

Quatre choses s'y trouvent, et aucune n'est décorative.

La ligne #include <stdio.h> demande au préprocesseur d'insérer les déclarations des fonctions d'entrée et de sortie, printf en tête. Sans elle, le compilateur ne saurait pas ce que printf attend.

La fonction main est le point d'entrée : l'exécution commence là, toujours, et nulle part ailleurs. Son nom n'est pas un choix, c'est une convention que le système attend.

Le int devant main annonce que la fonction rend un entier, et le return 0 le rend. Cette valeur est le code de retour du programme : zéro signifie que tout s'est bien passé, et c'est elle que le système d'exploitation examine.

Le void entre parenthèses dit que main ne prend aucun paramètre.

À modifier soi-même
Le bloc ci-dessus s'exécute et se modifie. Remplacer le texte affiché, puis relancer. Retirer ensuite le \n à la fin et relancer : la sortie ne va plus à la ligne, ce qui se voit dès qu'on affiche deux fois.
Retirer enfin le point-virgule après le printf : l'interpréteur refuse le programme et dit ce qu'il attendait. Un point-virgule manquant est la première erreur de tout débutant en C, et le message qu'elle produit vaut la peine d'être vu une fois volontairement.

Compilé, et ce que cela change

Un programme Python est lu ligne par ligne par un interpréteur, pendant l'exécution. Un programme C est traduit entièrement en instructions machine avant de s'exécuter, par un compilateur.

InterprétéCompilé
Quand les erreurs de syntaxe apparaissentà l'exécution de la ligneavant toute exécution
Ce qui est livréle sourceun exécutable, illisible
Vitesse d'exécutionplus lenteproche du maximum de la machine
Portabilitéle même fichier partoutun exécutable par machine cible
La conséquence qui compte pour apprendre
Un compilateur voit tout le programme avant de le lancer, donc il peut refuser ce qui n'a pas de sens sans jamais l'exécuter : un type incohérent, une variable jamais déclarée, une fonction appelée avec le mauvais nombre d'arguments.
C'est ce qui rend le C exigeant à l'écriture et sûr à l'exécution, exactement à l'inverse d'un langage interprété où une faute dort tranquillement dans une branche jamais empruntée.
Ce que l'interpréteur de ces pages fait, et ne fait pas
Les blocs de ce parcours s'exécutent dans le navigateur, grâce à un interpréteur écrit pour l'occasion. Il couvre le sous-ensemble du langage que ce parcours enseigne, et il refuse en le nommant ce qu'il ne sait pas faire plutôt que de l'ignorer.
Il rend même un service qu'aucun compilateur ne rend : il vérifie chaque accès mémoire et signale une lecture hors d'un tableau à l'endroit exact. Un vrai programme C ne dirait rien, lirait une valeur quelconque, et planterait plus tard ailleurs. C'est commode pour apprendre, et il faut savoir que la vraie machine, elle, ne prévient pas.

Afficher, et choisir la bonne conversion

printf prend un format, c'est-à-dire un texte où certains signes commençant par % seront remplacés par les valeurs qui suivent.

C
ConversionPour quel type
%dun entier
%cun caractère
%fun flottant, %.2f pour deux décimales
%sune chaîne
%pune adresse
Un format qui ment ne provoque aucune erreur
Écrire printf("%d", 3.5) ne fait pas planter un vrai programme C : il affiche un nombre quelconque, pris en lisant les octets d'un flottant comme s'ils étaient un entier. Le programme continue, et le chiffre faux se promène dans un rapport.
C'est le premier exemple de ce que ce langage exige : le compilateur fait confiance au format, et personne ne vérifie qu'il correspond aux arguments.
À manipuler
Dans le bloc ci-dessus, remplacer %.2f par %d et relancer : la taille s'affiche comme 1, sans le moindre avertissement. L'interpréteur de ces pages tronque, une vraie machine afficherait un nombre sans rapport, et ni l'un ni l'autre ne signale quoi que ce soit.
Ajouter ensuite une quatrième valeur à la dernière ligne sans ajouter de % dans le format. Ce cas-là, l'interpréteur le refuse en disant combien d'arguments il a reçus et combien le format en consomme, parce que c'est une faute qu'il peut voir.

À calculer soi-même

Un programme C se raisonne en octets avant de se raisonner en valeurs.

Les tailles, dès le premier programme

  • 1.

    Combien d'octets un int occupe-t-il sur une machine ordinaire ?

  • 2.

    Combien d'octets un char occupe-t-il ?

  • 3.

    Combien d'octets un double occupe-t-il ?

  • 4.

    Combien de valeurs entières distinctes un int de 4 octets peut-il représenter ?

  • 5.

    Quelle est la plus grande valeur qu'un int SIGNÉ de 4 octets peut porter ?

  • 6.

    Un tableau de 100 int occupe combien d'octets ?

La dernière réponse est le genre de calcul qu'un programme Python ne demande jamais et qu'un programme C demande tout le temps. Réserver de la place suppose de savoir combien, et savoir combien suppose de connaître la taille des types.

Vérification

Vérification rapideon peut se reprendre

1.Par quelle fonction l'exécution d'un programme C commence-t-elle ?

2.Que vaut le return 0 à la fin de main ?

3.Que se passe-t-il si le format de printf ne correspond pas aux arguments ?

4.Quelle est la différence essentielle entre compiler et interpréter ?

Exercices type

Pourquoi faut-il écrire #include <stdio.h> pour employer printf ?

Parce que le compilateur doit connaître la signature de printf avant de voir un appel : combien d'arguments elle prend, de quels types, et ce qu'elle rend. Le fichier stdio.h contient cette déclaration.

Sans elle, le compilateur devine, et ses suppositions sont souvent fausses. C'est la différence entre un langage où tout est connu d'avance et un langage qui découvre les fonctions en chemin.

Que se passe-t-il si main ne contient pas de return ?

Depuis la norme de 1999, main rend implicitement zéro dans ce cas, et c'est la seule fonction du langage à bénéficier de cette exception.

Pour toutes les autres fonctions déclarées avec un type de retour, l'absence de return donne une valeur indéterminée, ce qui est une des sources de bogue les plus difficiles à voir : le programme fonctionne, et rend parfois n'importe quoi.

Pourquoi le C demande-t-il de déclarer le type de chaque variable ?

Parce que le type décide de la place occupée et de l'interprétation des octets. Les mêmes quatre octets valent un entier, un flottant tronqué ou quatre caractères selon ce qu'on en dit.

Un langage qui gère la mémoire tout seul peut se permettre de retenir le type avec la valeur. Un langage qui ne cache rien laisse cette information à la charge du programme, et c'est ce qui permet de savoir, à l'avance, combien il occupera.

La méthode

  1. Commencer par le squelette : les inclusions, int main(void), return 0. Il ne change jamais.
  2. Déclarer chaque variable avec son type, au plus près de son premier usage.
  3. Choisir la conversion de printf d'après le type, et non d'après ce qu'on espère afficher.
  4. Relire le message d'erreur en entier, et commencer par la ligne qu'il nomme. Une erreur de compilation en entraîne souvent d'autres : seule la première est fiable.
  5. Exécuter tôt et souvent, plutôt que d'écrire cinquante lignes avant le premier essai.

Synthèse

  • Tout programme C commence par main, et rend un code de retour que le système examine.
  • #include apporte les déclarations dont le compilateur a besoin, avant tout appel.
  • Un langage compilé traduit tout avant d'exécuter : les erreurs de syntaxe apparaissent avant l'exécution, et ce qui est livré est un exécutable.
  • printf prend un format dont les conversions doivent correspondre aux types. Personne ne vérifie cette correspondance, et une erreur y est silencieuse.
  • Chaque type occupe un nombre d'octets connu : 1 pour un char, 4 pour un int, 8 pour un double. C'est ce qui permet de savoir ce qu'un programme occupera.

Et ensuite

Les tailles viennent d'être annoncées ; le chapitre suivant les met à l'épreuve, avec ce qu'elles impliquent de débordements et de conversions silencieuses. Les types et leur taille montre ce qui arrive quand un entier ne tient plus dans ses quatre octets.

Mettre en pratique