Aller au contenu principal

Les types et leur taille

Ce que ce chapitre apporte5 points
  • Donner la taille de chaque type de base, et la lire avec sizeof.
  • Prévoir ce que devient un entier qui dépasse sa capacité.
  • Distinguer la division entière de la division réelle, et choisir celle qu'on veut.
  • Traiter un caractère pour ce qu'il est : un petit entier.
  • Prévoir ce que perd une valeur qui passe d'un type à un autre.

En Python, un entier n'a pas de limite : il grandit tant que la mémoire suit, et 2 ** 200 s'écrit comme n'importe quel calcul. C'est commode, et c'est une illusion entretenue à grands frais par l'interpréteur.

En C, un entier occupe quatre octets, ni plus ni moins. Ces quatre octets portent un peu plus de quatre milliards de valeurs distinctes, et quand un calcul sort de cet intervalle, le résultat ne devient pas faux avec un message : il devient faux en silence, et souvent négatif. Ce chapitre montre où sont les bords, ce qui se passe quand on les franchit, et pourquoi diviser deux entiers ne donne pas ce qu'on attend.

Ce que sizeof révèle

sizeof rend le nombre d'octets qu'occupe un type. C'est un des rares opérateurs du langage qui donne une réponse sur la machine plutôt que sur le calcul.

C
Une taille dépend de la machine, et celle-ci en est une
Les trois premières lignes donneraient le même résultat sur presque toutes les machines actuelles. La quatrième, non : l'interpréteur de ces pages adresse la mémoire sur quatre octets, alors qu'un ordinateur 64 bits en emploie huit pour chaque pointeur.
C'est précisément pour cela que sizeof existe. Écrire 4 à la place de sizeof(int) dans un programme revient à parier sur la machine qui l'exécutera, et ce pari finit par se perdre.

Ce qui arrive au bord

Un int signé de quatre octets porte les valeurs de 231-2^{31} à 23112^{31} - 1, soit de 2147483648-2\,147\,483\,648 à 21474836472\,147\,483\,647. Ajouter un au plus grand ne donne pas le suivant.

C

Le compteur repart du plus petit, comme un compteur kilométrique qui repasse à zéro. Les bits de poids fort qui ne tiennent plus sont simplement perdus, et le bit qui porte le signe se retrouve allumé.

À manipuler
Dans le bloc ci-dessus, remplacer grand + 1 par grand * 2, puis par grand + 2, et relire chaque fois le résultat. Le débordement n'est pas un cas particulier de l'addition de un : c'est ce qui arrive à toute opération dont le résultat ne tient plus.
Changer ensuite char petit = 127 en char petit = 200 : la valeur stockée n'est déjà plus celle qui était écrite, parce qu'un char signé s'arrête à 127.
Le débordement ne se voit pas, il se déduit
Aucun message, aucune ligne fautive, aucune exception. Un total qui devait croître devient négatif, une durée en millisecondes recommence à zéro au bout de 24,8 jours, une taille de fichier se met à mentir au-delà de deux gigaoctets.
La parade ne consiste pas à surveiller le résultat après coup, puisqu'il est déjà faux. Elle consiste à choisir un type assez large dès le départ, ou à vérifier avant l'opération que le résultat tiendra.

Diviser deux entiers

En C, l'opération dépend du type des opérandes, pas du résultat attendu.

C

La dernière ligne est la faute la plus fréquente du langage. La division est calculée entre deux entiers, donc elle donne 3, et ce 3 est ensuite rangé dans un double qui affiche fidèlement 3.00. Le type de la variable qui reçoit n'a aucune influence sur le calcul qui la remplit.

La règle à garder
Si les deux opérandes sont entiers, l'opération est entière, et le résultat est tronqué vers zéro.
Pour obtenir une division réelle, il faut qu'au moins un des deux opérandes soit réel : écrire 2.0 plutôt que 2, ou multiplier d'abord par 1.0.

Un caractère est un nombre

Le type char ne contient pas de lettre : il contient un entier sur un octet, et l'affichage avec %c l'interprète comme le code d'un caractère.

C

La dernière ligne est la conversion que tout programme C finit par écrire : les codes des dix chiffres se suivent, donc retrancher le code de '0' donne la valeur du chiffre. Aucune fonction n'est nécessaire, et c'est bien une soustraction d'entiers.

Ce que perd une conversion

Ranger une valeur dans un type plus étroit ne provoque aucune erreur, et perd ce qui ne tient pas.

C

La première conversion tronque, elle n'arrondit pas : 19,99 devient 19, et 19,99 euros deviennent 19 euros. La seconde garde le dernier octet de 300, c'est-à-dire 44, et le nombre affiché n'a plus aucun rapport avec celui qui était écrit.

À calculer soi-même

Le bord d'un type se calcule avant d'être rencontré.

Les bords, les restes et les conversions

  • 1.

    Quelle est la plus grande valeur d'un int signé de 4 octets ?

  • 2.

    Quelle est la plus grande valeur d'un char signé de 1 octet ?

  • 3.

    Combien vaut la division entière 17 / 5 en C ?

  • 4.

    Combien vaut 17 % 5 ?

  • 5.

    Le caractère 'A' vaut 65. Combien vaut le caractère 'E' ?

  • 6.

    Une valeur 300 rangée dans un char signé : quelle valeur reste-t-il ?

  • 7.

    Un compteur de millisecondes sur un int signé déborde au bout de combien de jours ?

Le dernier résultat est une date célèbre : un système qui compte ses millisecondes dans un int signé se détraque au bout de vingt-cinq jours environ, ce qui est bien assez long pour que personne ne s'en aperçoive avant la mise en service.

Vérification

Vérification rapideon peut se reprendre

1.Que vaut sizeof(int) sur une machine ordinaire ?

2.Que vaut l'expression 9 / 2 en C, si 9 et 2 sont des int ?

3.Que se passe-t-il quand un int dépasse sa plus grande valeur ?

4.Pourquoi double moyenne = 7 / 2; affiche-t-il 3.00 ?

5.Que vaut l'expression '7' - '0' ?

Exercices type

Pourquoi écrire sizeof(int) plutôt que 4 dans un programme ?

Parce que 4 est une réponse vraie sur la machine où le programme a été écrit, et une supposition partout ailleurs. Un int fait deux octets sur certains microcontrôleurs, et un pointeur en fait huit sur toute machine 64 bits.

Un programme qui réserve n * sizeof(int) octets réserve toujours ce qu'il faut. Un programme qui réserve n * 4 réserve la moitié de ce qu'il faut le jour où il est recompilé ailleurs, et il écrase la mémoire qui suit.

Comment calculer une moyenne correcte à partir d'un total et d'un nombre, tous deux entiers ?

En rendant réel au moins un des deux opérandes de la division :

double moyenne = total / (double) nombre;

ou, sans écriture de conversion, en multipliant d'abord par 1.0 :

double moyenne = 1.0 * total / nombre;

Ce qui ne marche pas, c'est de déclarer la variable de réception en double : la division a déjà eu lieu, entre entiers, et le résultat tronqué est arrivé avant que le type de la variable n'ait son mot à dire.

Un capteur rend des valeurs entre 0 et 1000, et le programme les accumule dans un int. Au bout de combien de mesures un débordement devient-il possible ?

Dans le pire des cas, chaque mesure vaut 1000. Le total dépasse 23112^{31} - 1 quand le nombre de mesures dépasse 2147483647/10002\,147\,483\,647 / 1000, soit environ 2,1 millions de mesures.

À une mesure par seconde, cela laisse vingt-quatre jours. À cent mesures par seconde, cinq heures et demie. Le calcul se fait avant d'écrire le programme, et il décide du type : un long long, ou une moyenne glissante qui ne cumule jamais.

La méthode

  1. Choisir le type d'après l'intervalle des valeurs, et non d'après l'habitude.
  2. Calculer le pire cas avant d'accumuler : la valeur maximale multipliée par le nombre de termes.
  3. Regarder le type des opérandes avant de lire une division, pas celui de la variable qui reçoit.
  4. Écrire sizeof(type) partout où une taille est demandée, jamais le nombre d'octets.
  5. Se méfier des conversions vers un type plus étroit : elles ne préviennent pas, et elles tronquent.

Synthèse

  • Un char occupe 1 octet, un int 4, un double 8. sizeof donne la réponse sur la machine qui exécute, et c'est la seule réponse fiable.
  • Un int signé porte les valeurs de 231-2^{31} à 23112^{31} - 1. Au-delà, il repart du plus petit, sans message.
  • Une division entre deux entiers est entière. Le type de la variable qui reçoit ne change rien.
  • Un char est un entier sur un octet : '7' - '0' vaut 7, et c'est une soustraction.
  • Une conversion vers un type plus étroit tronque ou perd des octets, toujours en silence.

Et ensuite

Les valeurs ont maintenant une taille et des bords. Décider et répéter met ces valeurs dans des conditions et des boucles, et montre les deux fautes que le compilateur laisse passer sans rien dire.

Mettre en pratique

Les bords d'un int, la division entière, le caractère qui est un nombre, et la conversion qui tronque.

Tous les exercices sur les types et leur taille