Ceci est une ancienne révision du document !
Dans le chapitre Logique binaire et calcul booléen, vous avez vu que lorsque vous affichez l'inverse d'un nombre binaire, la valeur affichée correspond à un nombre codé sur 32 bits (ou 64 bits selon le système. Pour rappel, 32 bits correspondent à 4 octets ou encore 8 chiffres hexadécimaux et 64 bits correspondent à 8 octets ou 16 chiffres hexadécimaux).
#include <iostream> int main() { std::cout << std::hex << std::showbase; std::cout << ~0b1 << std::endl; std::cout << ~0b001 << std::endl; std::cout << ~0b00001 << std::endl; }
affiche :
0xfffffffe 0xfffffffe 0xfffffffe
N'oubliez pas que dans les écritures précédentes, les préfixes 0b
et 0x
ne sont pas des chiffres, cela indique que les nombres affichés correspondent respectivement à une valeur écrite en binaire et une valeur écrite en hexadécimal.
Un autre point important à souvenir aussi est que la représentation d'un nombre dans un code peut être différente de la représentation de ce nombre en mémoire et de la représentation lors de l'affichage avec std::cout
. Par exemple ici, les littérales sont écrite en binaire et l'affichage est en hexadécimal.
Le modificateur std::hex
permet d'afficher les nombres en hexadécimal et le modificateur std::showbase
permet d'afficher le préfixe 0x
.
Quelque soit le nombre de bits que vous utilisez pour écrire la littérale booléenne, la valeur inverse est toujours affichée sur 32 bits (dans cet exemple). La raison est que le compilateur créé une variable temporaire de 32 bits puis calcule l'inverse. Le résultat est donc toujours sur 32 bits (dans cet exemple).
Le nombre de bits utilisé pour représenter un nombre est important, puisque cela impacte le nombre de valeurs qui seront représentables. Prenons par exemple un nombre représenté par deux bits. Ce nombre pourra donc prendre quatre valeurs possibles :
00 01 10 11
Si un nombre est représenté par quatre bits, il pourra alors prendre 16 valeurs possibles :
0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
Pourquoi ne pas utiliser dans ce cas le nombre de bits le plus grand possible dans tous les cas ? Tout simplement parce que la mémoire est limitée. Et utiliser plus de bits que nécessaire diminuera la mémoire disponible et ralentira les programmes.
Dans ce cas, pourquoi un booléen est représenté par 32 bits et pas un simple bit ? Parce que les processeurs sont optimisés pour travailler avec certaines tailles, en général des multiples de 8, comme 8, 16, 32 ou 64 bits. Dans l'exemple, 32 bits correspond au nombre de bits le plus naturel pour le processeur (et donc le plus efficace).
Il est possible de connaître le nombre de bits utilisés pour représenter un type ou une variable. Dans les deux cas, vous devez utiliser la fonction sizeof
, qui prend en paramètre la variable ou le type. Par exemple, pour utiliser sizeof
avec des types :
#include <iostream> int main() { std::cout << "sizeof(int) = " << sizeof(int) << std::endl; std::cout << "sizeof(double) = " << sizeof(double) << std::endl; std::cout << "sizeof(bool) = " << sizeof(bool) << std::endl; std::cout << "sizeof(char) = " << sizeof(char) << std::endl; }
Ce code affiche les tailles des types correspondants, en octets (pensez à multiplier par huit si vous souhaitez connaître le nombre de bits). La taille peut dépendre du système d'exploitation et du processeur.
sizeof(int) = 4 sizeof(double) = 8 sizeof(bool) = 1 sizeof(char) = 1
De la même façon, pour connaître la taille en mémoire d'une variable, vous pouvez écrire :
#include <iostream> int main() { int const x { 123 }; double const d { 12.34 }; bool const b { true }; char const c { 'a' }; std::cout << "sizeof(int) = " << sizeof(x) << std::endl; std::cout << "sizeof(double) = " << sizeof(d) << std::endl; std::cout << "sizeof(bool) = " << sizeof(b) << std::endl; std::cout << "sizeof(char) = " << sizeof(c) << std::endl; }
Ce qui affichera (selon le contexte d'exécution) :
sizeof(int) = 4 sizeof(double) = 8 sizeof(bool) = 1 sizeof(char) = 1
Pour les types non fondamentaux, en particulier pour les chaînes de caractères std::string
(mais ça sera aussi la cas pour la majorité des classes de la bibliothèque standard que vous verrez), la valeur retournée par sizeof
ne correspond pas au nombre d'éléments dans cette classe.
#include <iostream> #include <string> int main() { std::string const s1 { "hello, world!" }; std::string const s2 { "Bonjour tout le monde !" }; std::cout << "sizeof(s1) = " << sizeof(s1) << std::endl; std::cout << "sizeof(s2) = " << sizeof(s2) << std::endl; }
affiche :
sizeof(s1) = 8 sizeof(s2) = 8
Pour comprendre pourquoi sizeof
donne ce résultat, il faudra détailler le fonctionnement interne de la classe string
. Vous verrez cela dans les chapitres sur la création de nouvelle classe. Pour connaître la taille de la chaîne de caractères (c'est-à-dire le nombre de caractères), il faut utiliser la fonction membre size
;
#include <iostream> #include <string> int main() { std::string const s1 { "hello, world!" }; std::string const s2 { "Bonjour tout le monde !" }; std::cout << "s1.size() = " << s1.size() << std::endl; std::cout << "s2.size() = " << s2.size() << std::endl; }
affiche :
s1.size() = 13 s2.size() = 23
Donc, pour résumer :
int
, double
, etc.), sizeof
retourne la taille en mémoire ;sizeof
ne retourne pas forcement la taille réellement occupée en mémoire par la classe.Dans la majorité des ordinateurs actuels, la mémoire disponible se compte en giga-octets et la taille mémoire des données ne sera pas critique (sauf dans des contextes particuliers, par exemple sur les systèmes embarqués ou dans les applications réalisant de nombreux calculs numériques). Il sera alors possible, dans un grand nombre d'applications, d'utiliser les types par défaut présentés dans les chapitres précédents.
Dans d'autres situations, il pourra être intéressant d'optimiser la taille des données en fonction des besoins. En effet, si on regarde les valeurs retournées par sizeof
, on peut remarquer qu'un booléen, qui peut être codé sur 1 bit, est en réalité codé sur 8 bits (1 octet). Donc un surcoût mémoire d'un facteur de 8. De même, si on souhaite créer une variable qui permet de compter de 0 à 3, on pourrait n'utiliser que 2 bits (qui pourraient alors prendre les valeurs : 0b00
, 0b01
, 0b10
et 0b11
). Une variable de type int
prend 32 bits, soit un surcoût d'un facteur de 16.
Le C++ ne limite pas les types aux tailles par défaut, il est possible de modifier la taille de la représentation en mémoire d'un type fondamental. De plus, pour les types entiers, il existe des types prédéfinies avec des tailles fixées.
La taille mémoire des types est paramétrable en utilisant des modificateurs de types, qui s'ajoutent au type de base int
. Pour les entiers, les modificateurs disponibles sont : short
(“court” en français), long
et long long
. Le modificateur de type se place devant le type qu'il modifie. Il est facile d'écrire un code pour vérifier que la taille des types est modifiée en conséquence :
#include <iostream> int main() { std::cout << "sizeof(short int) = " << sizeof(short int) << std::endl; std::cout << "sizeof(int) = " << sizeof(int) << std::endl; std::cout << "sizeof(long int) = " << sizeof(long int) << std::endl; std::cout << "sizeof(long long int) = " << sizeof(long long int) << std::endl; }
affiche :
sizeof(short int) = 2 sizeof(int) = 4 sizeof(long int) = 8 sizeof(long long int) = 8
Par défaut, les nombres entiers peuvent être positifs ou négatifs (il sont signés). Lorsque vous n'avez pas besoin de représenter des valeurs négatives, il est possible d'utiliser le mot-clé unsigned
pour créer un entier qui ne peut représenter que des valeurs positives. (Il existe aussi le mot-clé signed
pour définir explicitement un type signé, mais comme les types sont signés par défaut, ce mot-clé n'est pas très utilisé).
Un entier non signé permet de représenter des nombres plus grands qu'un entier signé. Pour comprendre cela, reprenons l'exemple d'un entier codé sur quatre bits.
Chaque bit peut prendre deux valeurs possibles (que l'on peut représenter par 0 ou 1 par exemple). Donc un nombre représenté par un bit pourra prendre 2 valeurs, un nombre représenté par 2 bits pourra prendre 2×2 valeurs, un nombre de 3 bits 2x2x2 valeurs et ainsi de suite.
Pour un nombre représenté par N bits, il sera donc possible de représenter 2^N valeurs possibles. Un nombre de 4 bits pourra représenter 16 valeurs.
Pour un entier signé, cela signifie que l'on va pouvoir par exemple représenter les nombres suivants (en décimal) :
-8 -7 -6 -5 -4 -3 -2 -1 00 01 02 03 04 05 06 07
Les valeurs possibles vont donc de -(N/2) à (N/2-1). La valeur négative est plus petite que la valeur positive, en valeur absolue, du fait qu'il faut représenter la valeur 0. (On peut bien sûr représenter n'importe quelle plage de valeurs, c'est qu'une question de choix. Cette plage de valeur est celle utilisé en générale, pour des raisons d'efficacité).
Si on représente que des valeurs positives ou nulle, on va pouvoir représenter par exemple :
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16
La plage de valeur devient donc 0 à N/2. La valeur maximale des nombres non signés est donc bien supérieure à celle des nombres signés.
Un type signed char
est codé sur 8 bits et acceptera des valeurs allant de -128 à +127, alors que le type unsigned char
, codé aussi sur 8 bits, acceptera des valeurs allant de 0 à 255.
Si vous initialiser une variable avec une valeur hors limite, une erreur sera signalée.
#include <iostream> int main() { const signed char x { 255 }; const unsigned char y { 255 }; }
Ce code produit une erreur de conversion (narrowing) pour le signed char
, mais pas pour le unsigned char
.
On peut également vérifier que la taille en mémoire ne change pas :
#include <iostream> int main() { std::cout << "sizeof(char) = " << sizeof(char) << std::endl; std::cout << "sizeof(unsigned char) = " << sizeof(unsigned char) << std::endl; std::cout << "sizeof(short int) = " << sizeof(short int) << std::endl; std::cout << "sizeof(unsigned short int) = " << sizeof(unsigned short int) << std::endl; std::cout << "sizeof(int) = " << sizeof(int) << std::endl; std::cout << "sizeof(unsigned int) = " << sizeof(unsigned int) << std::endl; std::cout << "sizeof(long int) = " << sizeof(long int) << std::endl; std::cout << "sizeof(unsigned long int) = " << sizeof(unsigned long int) << std::endl; std::cout << "sizeof(long long int) = " << sizeof(long long int) << std::endl; std::cout << "sizeof(unsigned long long int) = " << sizeof(unsigned long long int) << std::endl; }
affiche
sizeof(char) = 1 sizeof(unsigned char) = 1 sizeof(short int) = 2 sizeof(unsigned short int) = 2 sizeof(int) = 4 sizeof(unsigned int) = 4 sizeof(long int) = 8 sizeof(unsigned long int) = 8 sizeof(long long int) = 8 sizeof(unsigned long long int) = 8
Attention cependant, le compilateur ne vérifiera pas que les valeurs passées soient bien positives. Si vous entrez une valeur négative, le comportement sera différent :
#include <iostream> int main() { unsigned int i {}; i = - 1; std::cout << "unsigned int i = " << i << std::endl; }
affiche :
unsigned int i = 4294967295
Une vérification est faite par le compilateur uniquement lorsque vous initialisez une variable unsigned
avec une valeur négative et en utilisant les accolades :
#include <iostream> int main() { unsigned int const i { -1 }; // erreur unsigned int const j = -1; // ok std::cout << "unsigned int i = " << i << std::endl; std::cout << "unsigned int j = " << j << std::endl; }
affiche le message d'erreur :
main.cpp:4:22: error: constant expression evaluates to -1 which cannot be narrowed to type 'unsigned int' [-Wc++11-narrowing] unsigned int i { -1 }; ^~ main.cpp:4:22: note: override this message by inserting an explicit cast unsigned int i { -1 }; ^~ static_cast<unsigned int>( ) 1 error generated.
Plus généralement, lorsque vous initialisez une variable en utilisant une littérale, le compilateur vérifie que votre littérale est compatible avec le type donné.
#include <iostream> int main() { char const c { 123456 }; // erreur, "123456" est trop grand pour "char" int const i { 1234567890123 }; // erreur, "1234567890123" est trop grand pour "int" float const c { 123.456e123 }; // erreur, "123.456e123" est trop grand pour "float" unsigned int const i { -1 }; // erreur, "-1" est négatif
Il est possible de modifier également les types réels, mais avec une syntaxe un peu différente. Vous avez vu le type de base double
sur 64 bits. Pour créer un type réel sur 32 bits, vous pouvez utiliser le type float
et pour un type réel sur 128 bits, le type long double
. Il n'existe pas d'autres formats de nombres réels (8 ou 16 bits).
#include <iostream> int main() { std::cout << "sizeof(float) = " << sizeof(float) << std::endl; std::cout << "sizeof(double) = " << sizeof(double) << std::endl; std::cout << "sizeof(long double) = " << sizeof(long double) << std::endl; }
affiche :
sizeof(float) = 4 sizeof(double) = 8 sizeof(long double) = 16
Pour des raisons historiques, les nombres réels étaient calculés par défaut sur 32 bits. Ce type a donc été appelé float
(“flottant”) en rapport à “nombre à virgule flottante”. Par la suite, lorsque les nombres réels ont été codés sur 64 bits, le nouveau type a été appelé double
puisque c'était le double d'un type float
.
Les nombres réels peuvent être positifs ou négatifs. Il n'existe pas de version unsigned
pour les nombres réels. Ils peuvent prendre une valeur comprise entre -max
et max
(max
représentant la valeur maximale qu'un type peut prendre).
De la même manière que les variables, les littérales (c'est-à-dire les valeurs constantes entrées directement dans le code) possèdent également un type.
123456; // type int 123.456; // type double 'a'; // type char true; // type bool
Lorsque l'on initialise une variable d'un type donné avec une littérale d'un autre type, une conversion est réalisée si possible. Lorsque la valeur d'une littérale est trop grande et ne peut être attribuée à un type, le compilateur signale une erreur d'arrondi (narrowing). S'il n'y a pas de problème de conversion, le type de la littérale est converti dans le type de la variable.
#include <iostream> int main() { char const c { 12 }; // conversion de "int" vers "char" unsigned int const ui { 123 }; // conversion de "int" vers "unsigned int" float const f { 123.456 }; // conversion de "double" vers "float" }
Il est possible d'ajouter un suffixe aux littérales pour modifier leur type. Pour les entiers, le suffixe u
ou U
indique une littérale non signée (unsigned
). Pour la taille mémoire, le suffixe l
ou L
indique une littérale de type long int
et le suffixe ll
ou LL
indique une littérale de type long long int
. Il est possible de combiner le suffixe pour le signe avec un suffixe pour la taille, mais sans mélanger les minuscules et la majuscules (donc les valeurs suivantes sont acceptées : ul
, UL
, ull
ou ULL
).
#include <iostream> int main() { int const i { 123 }; unsigned int const ui { 123u }; unsigned long int const uli { 123ul }; unsigned long long int const ulli { 123ull }; }
Pour les nombres réels, le suffixe f
ou F
indique une littérale de type float
et le suffixe l
ou L
indique une littérale long double
.
#include <iostream> int main() { float const f { 123.456f }; double const d { 123.456 }; long double const ld { 123.456l }; }