Dans le chapitre sur l’allocation dynamique, nous avons effleuré la notion de représentation en parlant de celle d’objet. Pour rappel, la représentation d’un type correspond à la manière dont les données sont réparties en mémoire, plus précisément comment les multiplets et les bits les composant sont agencés et utilisés.
Dans ce chapitre, nous allons plonger au cœur de ce concept et vous exposer la représentation des types.
- La représentation des entiers
- La représentations des flottants
- La représentation des pointeurs
- Ordre des multiplets et des bits
- Les fonctions memset, memcpy, memmove et memcmp
La représentation des entiers
Les entiers non signés
La représentation des entiers non signés étant la plus simple, nous allons commencer par celle-ci.
Dans un entier non signé, les différents bits correspondent à une puissance de deux. Plus précisément, le premier correspond à 20, le second à 21, le troisième à 22 et ainsi de suite jusqu’au dernier bit composant le type utilisé. Pour calculer la valeur d’un entier non signé, il suffit donc d’additionner les puissances de deux correspondant aux bits à 1.
Pour illustrer notre propos, voici un tableau comprenant la représentation de plusieurs nombres au sein d’un objet de type unsigned char
(ici sur un octet).
Nombre | Représentation | |||||||
---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | |
20 = 1 | ||||||||
3 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | |
20 + 21 = 3 | ||||||||
42 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | |
21 + 23 + 25 = 42 | ||||||||
255 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | |
20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 = 255 |
Notez que nous écrivons ici les nombres binaires par puissance de deux décroissantes. Nous suivons en fait la même logique que pour les nombres en base dix. En effet, 195 par exemple se décompose en une suite décroissante de puissances de 10 : 1×102 + 9×101 + 5×100.
Les entiers signés
Les choses se compliquent un peu avec les entiers signés. En effet, il nous faut représenter une information supplémentaire : le signe de la valeur.
La représentation en signe et magnitude
La première solution qui vous est peut-être venue à l’esprit serait de réserver un bit, par exemple le dernier, pour représenter le signe. Ainsi, la représentation est identique à celle des entiers non signés si ce n’est que le dernier bit est réservé pour le signe (ce qui diminue donc en conséquence la valeur maximale représentable). Cette méthode est appelée représentation en signe et magnitude.
Nombre | Représentation en signe et magnitude | |||||||
---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
+ | 26 | 25 | 24 | 23 | 22 | 21 | 20 | |
20 = 1 | ||||||||
-1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
- | 26 | 25 | 24 | 23 | 22 | 21 | 20 | |
-20 = -1 |
Cependant, si la représentation en signe et magnitude paraît simple, elle n’est en vérité pas facile à mettre en œuvre au sein d’un processeur car elle implique plusieurs vérifications (notamment au niveau du signe) qui se traduisent par des circuits supplémentaires et un surcoût en calcul. De plus, elle laisse deux représentations possibles pour le zéro (0000 0000
et 1000 0000
), ce qui est gênant pour l’évaluation des conditions.
La représentation en complément à un
Dès lors, comment pourrions-nous faire pour simplifier nos calculs en évitant des vérifications liées au signe ? Eh bien, sachant que le maximum représentable dans notre exemple est 255
(soit 1111 1111
), il nous est possible de représenter chaque nombre négatif comme la différence entre le maximum et sa valeur absolue. Par exemple -1
sera représenté par 255 - 1
, soit 254
(1111 1110
). Cette représentation est appelée représentation en complément à un.
Ainsi, si nous additionnons 1
et -1
, nous n’avons pas de vérifications à faire et obtenons le maximum, 255
. Toutefois, ceci implique, comme pour la représentation en signe et magnitude, qu’il existe deux représentations pour le zéro : 0000 0000
et 1111 1111
.
Nombre | Représentation en complément à un | |||||||
---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
+ | 26 | 25 | 24 | 23 | 22 | 21 | 20 | |
20 = 1 | ||||||||
-1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
-/27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | |
-(255 - 27 - 26 - 25 - 24 - 23 - 22 - 21) = -1 |
Notez qu’en fait, cette représentation revient à inverser tous les bits d’un nombre positif pour obtenir son équivalent négatif. Par exemple, si nous inversons 1
(0000 0001
), nous obtenons bien -1
(1111 1110
) comme ci-dessus.
Par ailleurs, il subsiste un second problème : dans le cas où deux nombres négatifs sont additionnés, le résultat obtenu n’est pas valide. En effet, -1 + -1
nous donne 1111 1110 + 1111 1110
soit 1 1111 1100
; autrement dit, comme nous travaillons sur huit bits pour nos exemples, -3
… Mince !
Pour résoudre ce problème, il nous faut reporter la dernière retenue (soit ici le dernier bit que nous avons ignoré) au début (ce qui revient à ajouter un) ce qui nous permet d’obtenir 1111 1101
, soit -2
.
La représentation en complément à deux
Ainsi est apparue une troisième représentation : celle en complément à deux. Celle-ci conserve les qualités de la représentation en complément à un, mais lui corrige certains défauts. En fait, elle représente les nombres négatifs de la même manière que la représentation en complément à un, si ce n’est que la soustraction s’effectue entre le maximum augmenté de un (soit 256
dans notre cas) et non le maximum.
Ainsi, par exemple, la représentation en complément à deux de -1
est 256 - 1
, soit 255
(1111 1111
).
Nombre | Représentation en complément à deux | |||||||
---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
+ | 26 | 25 | 24 | 23 | 22 | 21 | 20 | |
20 = 1 | ||||||||
-1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
-/27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | |
-(256 - 27 - 26 - 25 - 24 - 23 - 22 - 21 - 20) = -1 |
Remarquez que cette représentation revient finalement à inverser tous les bits d’un nombre positif et à augmenter le résultat de un en vue d’obtenir son équivalent négatif. Par exemple, si nous inversons les bits du nombre 1
nous obtenons 1111 1110
et si nous lui ajoutons un, nous avons bien 1111 1111
.
Cette fois ci, l’objectif recherché est atteint !
En effet, si nous additionnons 1
et -1
(soit 0000 0001
et 1111 1111
) et que nous ignorons la retenue, nous obtenons bien zéro. De plus, il n’y a plus de cas particulier de retenue à déplacer comme en complément à un, puisque, par exemple, la somme -1 + -2
, soit 1111 1111 + 1111 1110
donne 1 1111 1101
, autrement dit -3
sans la retenue. Enfin, nous n’avons plus qu’une seule représentation pour le zéro.
La norme du langage C ne précise pas quelle représentation doit être utilisée pour les entiers signés1. Elle impose uniquement qu’il s’agisse d’une de ces trois. Cependant, dans les faits, il s’agit presque toujours de la représentation en complément à deux.
Notez que chacune de ces représentations dispose toutefois d’une suite de bits qui est susceptible de ne pas représenter une valeur et de produire une erreur en cas d’utilisation2.
- Pour les représentations en signe et magnitude et en complément à deux, il s’agit de la suite où tous les bits sont à zéro et le bit de signe à un :
1000 0000
; - Pour la représentation en complément à un, il s’agit de la suite où tous les bits sont à un, y compris le bit de signe :
1111 1111
.
Dans le cas des représentations en signe et magnitude et en complément à un, il s’agit des représentations possibles pour le « zéro négatif ». Pour la représentation en complément à deux, cette suite est le plus souvent utilisée pour représenter un nombre négatif supplémentaire (dans le cas de 1000 0000
, il s’agira de -128
). Toutefois, même si ces représentations peuvent être utilisées pour représenter une valeur valide, ce n’est pas forcément le cas.
Néanmoins, rassurez-vous, ces valeurs ne peuvent être produites dans le cas de calculs ordinaires, sauf survenance d’une condition exceptionnelle comme un dépassement de capacité (nous en parlerons bientôt). Vous n’avez donc pas de vérifications supplémentaires à effectuer à leur égard. Évitez simplement d’en produire une délibérément, par exemple en l’affectant directement à une variable.
Les bits de bourrage
Il est important de préciser que tous les bits composant un type entier ne sont pas forcément utilisés pour représenter la valeur qui y est stockée. Nous l’avons vu avec le bit de signe, qui ne correspond pas à une valeur. La norme prévoit également qu’il peut exister des bits de bourrage, et ce aussi bien pour les entiers signés que pour les entiers non signés à l’exception du type char
. Ceux-ci peuvent par exemple être employés pour maintenir d’autres informations (par exemple : le type de la donnée stockée, ou encore un bit de parité pour vérifier l’intégrité de celle-ci).
Par conséquent, il n’y a pas forcément une corrélation parfaite entre le nombres de bits composant un type entier et la valeur maximale qui peut y être stockée.
Sachez toutefois que les bits de bourrage au sein des types entiers sont assez rares, les architectures les plus courantes n’en emploient pas.
Les entiers de taille fixe
Face à ces incertitudes quant à la représentation des entiers, la norme C99 a introduit de nouveaux types entiers : les entiers de taille fixe. À la différence des autres types entiers, leur représentation est plus stricte :
- Ces types ne comprennent pas de bits de bourrage ;
- Comme leur nom l’indique, leur taille est un nombre de bits fixe ;
- La représentation en complément à deux est utilisée pour les nombres négatifs.
Ces types sont définis dans l’en-tête <stdint.h>
et sont les suivants.
Type | Minimum | Maximum |
---|---|---|
int8_t | -128 | 127 |
INT8_MIN | INT8_MAX | |
uint8_t | 0 | 255 |
UINT8_MAX | ||
int16_t | -32.768 | 32.767 |
INT16_MIN | INT16_MAX | |
uint16_t | 0 | 65.535 |
UINT16_MAX | ||
int32_t | -2.147.483.648 | 2.147.483.647 |
INT32_MIN | INT32_MAX | |
uint32_t | 0 | 4.294.967.295 |
UINT32_MAX | ||
int64_t | -9.223.372.036.854.775.808 | 9.223.372.036.854.775.807 |
INT64_MIN | INT64_MAX | |
uint64_t | 0 | 18.446.744.073.709.551.615 |
UINT64_MAX |
Pour le reste, ces types s’utilisent comme les autres types entiers, si ce n’est qu’ils n’ont pas d’indicateur de conversion spécifique. À la place, il est nécessaire de recourir à des macroconstantes qui sont remplacées par une chaîne de caractères comportant le bon indicateur de conversion. Ces dernières sont définies dans l’en-tête <inttypes.h>
et sont les suivantes.
Type | Macroconstantes |
---|---|
int8_t | PRId8 (ou PRIi8) et SCNd8 (ou SCNi8) |
uint8_t | PRIu8 (ou PRIo8, PRIx8, PRIX8) et SCNu8 (ou SCNo8, SCNx8) |
int16_t | PRId16 (ou PRIi16) et SCNd16 (ou SCNi16) |
uint16_t | PRIu16 (ou PRIo16, PRIx16, PRIX16) et SCNu16 (ou SCNo16, SCNx16) |
int32_t | PRId32 (ou PRIi32) et SCNd32 (ou SCNi32) |
uint32_t | PRIu32 (ou PRIo32, PRIx32, PRIX32) et SCNu32 (ou SCNo32, SCNx32) |
int64_t | PRId64 (ou PRIi64) et SCNd64 (ou SCNi64) |
uint64_t | PRIu64 (ou PRIo64, PRIx64, PRIX64) et SCNu64 (ou SCNo64, SCNx64) |
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
int
main(void)
{
int16_t n = INT16_MIN;
uint64_t m = UINT64_MAX;
printf("n = %" PRId16 ", m = %" PRIu64 "\n", n, m);
return 0;
}
n = -32768, m = 18446744073709551615
Le support de ces types est facultatif, il est donc possible qu’ils ne soient pas disponibles, bien que cela soit assez peu probable.
La représentations des flottants
La représentation des types flottants amène deux difficultés supplémentaires :
- la gestion de nombres réels, c’est-à-dire potentiellement composés d’une partie entière et d’une suite de décimales ;
- la possibilité de stocker des nombres de différents ordres de grandeur, entre 10-37 et 1037.
Première approche
Avec des entiers
Une première solution consisterait à garantir une précision à 10-37 en utilisant deux nombres entiers : un, signé, pour la partie entière et un, non signé, pour stocker la suite de décimales.
Cependant, si cette approche a le mérite d’être simple, elle a le désavantage d’utiliser beaucoup de mémoire. En effet, pour stocker un entier de l’ordre de 1037, il serait nécessaire d’utiliser un peu moins de 128 bits, soit 16 octets (et donc environ 32 octets pour la représentation globale). Un tel coût est inconcevable, même à l’époque actuelle.
Une autre limite provient de la difficulté d’effectuer des calculs sur les nombres flottants avec une telle représentation : il faudrait tenir compte du passage des décimales vers la partie entière et inversement.
Avec des puissances de deux négatives
Une autre représentation possible consiste à attribuer des puissances de deux négatives à une partie des bits. Les calculs sur les nombres flottants obtenus de cette manière sont similaires à ceux sur les nombres entiers. Le tableau ci-dessous illustre ce concept en divisant un octet en deux : quatre bits pour la partie entière et quatre bits pour la partie fractionnaire.
Nombre | Représentation | |||||||
---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
23 | 22 | 21 | 20 | 2-1 | 2-2 | 2-3 | 2-4 | |
20 = 1 | ||||||||
1,5 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 |
23 | 22 | 21 | 20 | 2-1 | 2-2 | 2-3 | 2-4 | |
2-1 + 20 = 1,5 | ||||||||
0,875 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
23 | 22 | 21 | 20 | 2-1 | 2-2 | 2-3 | 2-4 | |
2-3 + 2-2 + 2-1 = 0,875 |
Toutefois, notre problème de capacité persiste : il nous faudra toujours une grande quantité de mémoire pour pouvoir stocker des nombres d’ordre de grandeur aussi différents.
Une histoire de virgule flottante
Par conséquent, la solution qui a été retenue historiquement consiste à réserver quelques bits qui contiendront la valeur d’un exposant. Celui-ci sera ensuite utilisé pour déterminer à quelles puissances de deux correspondent les bits restants. Ainsi, la virgule séparant la partie entière de sa suite décimales est dite « flottante », car sa position est ajustée par l’exposant. Toutefois, comme nous allons le voir, ce gain en mémoire ne se réalise pas sans sacrifice : la précision des calculs va en pâtir.
Le tableau suivant utilise deux octets : un pour stocker un exposant sur sept bits avec un bit de signe ; un autre pour stocker la valeur du nombre. L’exposant est lui aussi signé et est représenté en signe et magnitude (par souci de facilité). Par ailleurs, cet exposant est attribué au premier bit du deuxième octet ; les autres correspondent à une puissance de deux chaque fois inférieure d’une unité.
Nombre | Signe | Exposant | Bits du nombre | |||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+ | + | 25 | 24 | 23 | 22 | 21 | 20 | 20 | 2-1 | 2-2 | 2-3 | 2-4 | 2-5 | 2-6 | 2-7 | |
+ | 0 | 20 = 1 | ||||||||||||||
-1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
− | + | 25 | 24 | 23 | 22 | 21 | 20 | 20 | 2-1 | 2-2 | 2-3 | 2-4 | 2-5 | 2-6 | 2-7 | |
− | 0 | -20 = -1 | ||||||||||||||
0,5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
+ | + | 25 | 24 | 23 | 22 | 21 | 20 | 20 | 2-1 | 2-2 | 2-3 | 2-4 | 2-5 | 2-6 | 2-7 | |
+ | 0 | 2-1 = 0,5 | ||||||||||||||
10 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
+ | + | 25 | 24 | 23 | 22 | 21 | 20 | 23 | 22 | 21 | 20 | 2-1 | 2-2 | 2-3 | 2-4 | |
+ | 21 + 20 = 3 | 23 + 21 = 10 | ||||||||||||||
0,00001 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 |
+ | − | 25 | 24 | 23 | 22 | 21 | 20 | 2-17 | 2-18 | 2-19 | 2-20 | 2-21 | 2-22 | 2-23 | 2-24 | |
+ | -21 + -24 = -17 | 2-24 + 2-23 + 2-22 + 2-20 + 2-19 + 2-17 = ~0,00001 | ||||||||||||||
32769 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+ | + | 25 | 24 | 23 | 22 | 21 | 20 | 215 | 214 | 213 | 212 | 211 | 210 | 29 | 28 | |
+ | 20 + 21 + 22 + 23 = 15 | 215 = 32768 |
Les quatre premiers exemples n’amènent normalement pas de commentaires particuliers ; néanmoins, les deux derniers illustrent deux problèmes posés par les nombres flottants.
Les approximations
Les nombres réels mettent en évidence la difficulté (voir l’impossibilité) de représenter une suite de décimales à l’aide d’une somme de puissances de deux. En effet, le plus souvent, les valeurs ne peuvent être qu’approchées et obtenues suite à des arrondis. Dans le cas de 0,00001, la somme 2-24 + 2-23 + 2-22 + 2-20 + 2-19 + 2-17 donne en vérité 0,000010907649993896484375 qui, une fois arrondie, donnera bien 0,00001.
Les pertes de précision
Dans le cas où la valeur à représenter ne peut l’être entièrement avec le nombre de bits disponibles (autrement dit, le nombre de puissances de deux disponibles), la partie la moins significative sera abandonnée et, corrélativement, de la précision.
Ainsi, dans notre exemple, si nous souhaitons représenter le nombre 32769, nous avons besoin d’un exposant de 15 afin d’obtenir 32768. Toutefois, vu que nous ne possédons que de 8 bits pour représenter notre nombre, il nous est impossible d’attribuer l’exposant 0 à un des bits. Cette information est donc perdue et seule la valeur la plus significative (ici 32768) est conservée.
Formalisation
L’exemple simplifié que nous vous avons montré illustre dans les grandes lignes la représentation des nombres flottants. Cependant, vous vous en doutez, la réalité est un peu plus complexe. De manière plus générale, un nombre flottant est représenté à l’aide : d’un signe, d’un exposant et d’une mantisse qui est en fait la suite de puissances qui sera additionnée.
Par ailleurs, nous avons utilisé une suite de puissances de deux, mais il est parfaitement possible d’utiliser une autre base. Beaucoup de calculatrices utilisent par exemple des suites de puissances de dix et non de deux.
À l’heure actuelle, les nombres flottants sont presque toujours représentés suivant la norme IEEE 754 qui utilise une représentation en base deux. Si vous souhaitez en apprendre davantage, nous vous invitons à lire le tutoriel Introduction à l’arithmétique flottante d’Aabu
La représentation des pointeurs
Cet extrait sera relativement court : la représentation des pointeurs est indéterminée. Sur la plupart des architectures, il s’agit en vérité d’entiers non signés, mais il n’y a absolument aucune garantie à ce sujet.
Ordre des multiplets et des bits
Jusqu’à présent, nous vous avons montré les représentations binaires en ordonnant les bits et les multiplets par puissance de deux décroissantes (de la plus grande puissance de deux à la plus petite). Cependant, s’il s’agit d’une représentation possible, elle n’est pas la seule. En vérité, l’ordre des multiplets et des bits peut varier d’une machine à l’autre.
Ordre des multiplets
Dans le cas où un type est composé de plus d’un multiplet (ce qui, à l’exception du type char
, est pour ainsi dire toujours le cas), ceux-ci peuvent être agencés de différentes manières. L’ordre des multiplets d’une machine est appelé son boutisme (endianness en anglais).
Il en existe principalement deux : le gros-boutisme et le petit-boutisme.
Le gros-boutisme
Sur une architecture gros-boutiste, les multiplets sont ordonnés par poids décroissant.
Le poids d’un bit se détermine suivant la puissance de deux qu’il représente : plus elle est élevée, plus le bit a un poids important. Le bit représentant la puissance de deux la plus basse est appelé de bit de poids faible et celui correspondant à la puissance la plus grande est nommé bit de poids fort. Pour les multiplets, le raisonnement est identique : son poids est déterminé par celui des bits le composant.
Autrement dit, une machine gros-boutiste place les multiplets comprenant les puissances de deux les plus élevées en premier (nos exemples précédents utilisaient donc cette représentation).
Le petit-boutisme
Une architecture petit-boutiste fonctionne de manière inverse : les multiplets sont ordonnés par poids croissant.
Ainsi, si nous souhaitons stocker le nombre 1
dans une variable de type unsigned short
(qui sera, pour notre exemple, d’une taille de deux octets), nous aurons les deux résultats suivants, selon le boutisme de la machine.
Nombre | Représentation gros-boutiste | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | Octet de poids fort | Octet de poids faible | ||||||||||||||
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | |
215 | 214 | 213 | 212 | 211 | 210 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | |
20 = 1 |
Nombre | Représentation petit-boutiste | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | Octet de poids faible | Octet de poids fort | ||||||||||||||
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 215 | 214 | 213 | 212 | 211 | 210 | 29 | 28 | |
20 = 1 |
Le boutisme est relativement transparent du point de vue du programmeur, puisqu’il s’agit d’une propriété de la mémoire. En pratique, de tels problèmes ne se posent que lorsqu’on accède à la mémoire à travers des types différents de celui de l’objet stocké initialement (par exemple via un pointeur sur char
). En particulier, la communication entre plusieurs machines doit les prendre en compte.
Ordre des bits
Cependant, ce serait trop simple si le problème en restait là…
Malheureusement, l’ordre des bits varie également d’une machine à l’autre et ce, indépendamment de l’ordre des multiplets. Comme pour les multiplets, il existe différentes possibilités, mais les plus courantes sont le gros-boutisme et le petit-boutisme. Ainsi, il est par exemple possible que la représentation des multiplets soit gros-boutiste, mais que celle des bits soit petit-boutiste (et inversement).
Nombre | Représentation gros-boutiste pour les multiplets et petit-boutiste pour les bits | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | Octet de poids fort | Octet de poids faible | ||||||||||||||
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
28 | 29 | 210 | 211 | 212 | 213 | 214 | 215 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | |
20 = 1 |
Nombre | Représentation petit-boutiste pour les multiplets et petit-boutiste pour les bits | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | Octet de poids faible | Octet de poids fort | ||||||||||||||
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 210 | 211 | 212 | 213 | 214 | 215 | |
20 = 1 |
Notez toutefois que, le plus souvent, une représentation petit-boutiste ou gros-boutiste s’applique aussi bien aux octets qu’aux bits.
Par ailleurs, sachez qu’à quelques exceptions près, l’ordre du stockage des bits n’apparaît pas en C. En particulier, les opérateurs de manipulation de bits, vus au chapitre suivant, n’en dépendent pas.
Applications
Connaître le boutisme d’une machine
Le plus souvent, il est possible de connaître le boutisme employé pour les multiplets à l’aide du code suivant. Ce dernier stocke la valeur 1
dans une variable de type unsigned short
, et accède à son premier octet à l’aide d’un pointeur sur unsigned char
. Si celui-ci est nul, c’est que la machine est gros-boutiste, sinon, c’est qu’elle est petit-boutiste.
#include <stdio.h>
int main(void)
{
unsigned short n = 1;
unsigned char *p = (unsigned char *)&n;
if (*p != 0)
printf("Je suis petit-boutiste.\n");
else
printf("Je suis gros-boutiste.\n");
return 0;
}
Notez bien que cette technique n’est pas entièrement fiable. En effet, rappelez-vous : d’une part, les deux boutismes présentés, s’ils sont les plus fréquents, ne sont pas les seuls et, d’autre part, la présence de bits de bourrage au sein du type entier, bien que rare, pourrait fausser le résultat.
Le boutisme des constantes octales et hexadécimales
Suivant ce qui vient de vous être présenté, peut-être vous êtes vous posé la question suivante : si les boutismes varient d’une machine à l’autre, alors que vaut finalement la constante 0x01
? En effet, suivant les boutismes employés, celle-ci peut valoir 1 ou 16.
Heureusement, cette question est réglée par la norme1 : le boutisme ne change rien à l’écriture des constantes octales et hexadécimales, elle est toujours réalisée suivant la convention classique des chiffres de poids fort en premier. Ainsi, la constante 0x01
vaut 1 et la constante 0x8000
vaut 32768 (en non signé).
De même, les indicateurs de conversion x
, X
et o
affichent toujours leurs résultats suivant cette écriture.
printf("%02x\n", 1);
01
- ISO/IEC 9899:201x, doc. N1570, § 6.4.4.1, Integer constants, al. 4, p. 63.↩
Les fonctions memset, memcpy, memmove et memcmp
Pour terminer ce chapitre, il nous reste à vous présenter quatre fonctions que nous avions passées sous silence lors de notre présentation de l’en-tête <string.h>
: memset()
, memcpy()
, memmove()
et memcmp()
. Bien qu’elles soient définies dans cet en-tête, ces fonctions ne sont pas véritablement liées aux chaînes de caractères et opèrent en vérité sur les multiplets composant les objets. De telles modifications impliquant la représentation des types, nous avons attendu ce chapitre pour vous en parler.
La fonction memset
void *memset(void *obj, int val, size_t taille);
La fonction memset()
initialise les taille
premiers multiplets de l’objet référencé par obj
avec la valeur val
(qui est convertie vers le type unsigned char
). Cette fonction est très utile pour (ré)initialiser les différents éléments d’un aggrégat sans devoir recourir à une boucle.
#include <stdio.h>
#include <string.h>
int main(void)
{
int tab[] = { 10, 20, 30, 40, 50 };
memset(tab, 0, sizeof tab);
for (size_t i = 0; i < sizeof tab / sizeof tab[0]; ++i)
printf("%zu : %d\n", i, tab[i]);
return 0;
}
0 : 0
1 : 0
2 : 0
3 : 0
4 : 0
Faites attention ! Manipuler ainsi les objets byte par byte nécessite de connaître leur représentation. Dans un souci de portabilité, on ne devrait donc pas utiliser memset
sur des pointeurs ou des flottants. De même, pour éviter d’obtenir les représentations problématiques dans le cas d’entiers signés, il est conseillé de n’employer cette fonction que pour mettre ces derniers à zéro.
Les fonctions memcpy et memmove
void *memcpy(void *destination, void *source, size_t taille);
void *memmove(void *destination, void *source, size_t taille);
Les fonctions memcpy()
et memmove()
copient toutes deux les taille
premiers multiplets de l’objet pointé par source
vers les premiers multiplets de l’objet référencé par destination
. La différence entre ces deux fonctions est que memcpy()
ne doit être utilisée qu’avec deux objets qui ne se chevauchent pas (autrement dit, les deux pointeurs ne doivent pas accéder ou modifier une même zone mémoire ou une partie d’une même zone mémoire) alors que memmove()
ne souffre pas de cette restriction.
#include <stdio.h>
#include <string.h>
int main(void)
{
int n = 10;
int m;
memcpy(&m , &n, sizeof n);
printf("m = n = %d\n", m);
memmove(&n, ((unsigned char *)&n) + 1, sizeof n - 1);
printf("n = %d\n", n);
return 0;
}
m = n = 10
n = 0
L’utilisation de memmove()
amène quelques précisions : nous copions ici les trois derniers multiplets de l’objet n
vers ses trois premiers multiplets. Étant donné que notre machine est petit-boutiste, les trois derniers multiplets sont à zéro et la variable n
a donc pour valeur finale zéro.
La fonction memcmp
int memcmp(void *obj1, void *obj2, size_t taille);
La fonction memcmp()
compare les multiplets des objets référencés par obj1
et obj2
. Si tous leurs multiplets sont égaux, elle retourne zéro. Dans le cas contraire, elle s’arrête à la première paire de multiplets qui diffère et retourne un nombre négatif ou positif suivant que la valeur du multiplet comparé de obj1
est inférieure ou supérieure à celle du multiplet comparé de obj2
(pour la comparaison, les deux valeurs sont converties en unsigned char
).
#include <stdio.h>
#include <string.h>
int main(void)
{
int n = 10;
int m = 10;
int o = 20;
if (memcmp(&n, &m, sizeof n) == 0)
printf("n et m sont égaux\n");
if (memcmp(&n, &o, sizeof n) != 0)
printf("n et o ne sont pas égaux\n");
return 0;
}
n et m sont égaux
n et o ne sont pas égaux
Dit autrement, la fonction memcmp()
retourne zéro si les deux portions comparées ont la même représentation.
Étant donné que memcmp()
compare deux objets multiplet par multiplet, ceci inclut leurs éventuels bits ou multiplets de bourrages. Il est donc parfaitement possible, par exemple, que memcmp()
considère deux structures ou deux entiers comme différents simplement parce que leurs bits ou multiplets de bourrages sont différents.
En résumé
- les entiers non signés sont représentés sous forme d’une somme de puissance de deux ;
- il existe trois représentations possibles pour les entiers signés, mais la plus fréquente est celle en complément à deux ;
- les types entiers peuvent contenir des bits de bourrage sauf le type
char
; - les types flottants sont représentés sous forme d’un signe, d’un exposant et d’une mantisse, mais sont susceptibles d’engendrer des approximations et des pertes de précision ;
- la représentation des pointeurs est indéterminée ;
- le boutisme utilisé dépend de la machine cible ;
- les constantes sont toujours écrites suivant la représentation gros-boutiste.
Liens externes
Si vous souhaitez quelques exemples d’architectures exotiques, nous vous invitons à lire l’article « C portability » de Joe Nelson.