^ [[inference_de_type|Chapitre précédent]] ^ [[programmez_avec_le_langage_c|Sommaire principal]] ^ [[informations_sur_les_types|Chapitre suivant]] ^
====== Manipuler les types ======
===== Taille des variables en mémoire =====
Dans le chapitre [[logique_et_calcul_booleen]], vous avez vu que lorsque vous affichez l'inverse d'un nombre entier représenté en binaire, la valeur affichée est représentée par 8 chiffres hexadécimaux (ce qui est équivalent à 32 bits ou 4 octets).
Pour rappel, 1 chiffre hexadécimal est équivalent à 4 bits, 2 chiffres hexadécimaux sont équivalents à 8 bits ou 1 octets, 4 chiffres hexadécimaux à 16 bits ou 2 octets, et ainsi de suite.
#include
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 à de se souvenir 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 écrites 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''.
Quel que soit le nombre de chiffres que vous utilisez pour écrire la littérale entière, la valeur inverse est affichée avec 8 chiffres hexadécimaux (selon la plateforme). La raison est que le compilateur crée une variable temporaire en mémoire de 32 bits puis calcule l'inverse aussi sur 32 bits. L'affichage correspond a cette variable temporaire.
Le nombre de bits utilisé pour représenter en mémoire 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 diminue la mémoire disponible et ralentit les programmes.
Dans ce cas, pourquoi un booléen est représenté sur 8 bits (1 octet) et pas sur un simple bit ? Parce que les processeurs sont optimisés pour travailler avec certaines tailles, en général des multiples de 8 bits (par exemple 8, 16, 32 ou 64 bits, voire plus). 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é pour représenter un type ou une variable, en utilisant l'opérateur ''sizeof'', qui prend en paramètre la variable ou le type. Par exemple, pour utiliser ''sizeof'' avec des types :
#include
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
int main() {
int const a { 123 };
double const d { 12.34 };
bool const b { true };
char const c { 'a' };
std::cout << "sizeof(a) = " << sizeof(a) << std::endl;
std::cout << "sizeof(d) = " << sizeof(d) << std::endl;
std::cout << "sizeof(b) = " << sizeof(b) << std::endl;
std::cout << "sizeof(c) = " << sizeof(c) << std::endl;
}
Ce qui affichera (selon le contexte d'exécution) :
sizeof(a) = 4
sizeof(d) = 8
sizeof(b) = 1
sizeof(c) = 1
Pour les types non fondamentaux, en particulier pour les chaînes de caractères ''std::string'' (mais ça sera aussi le 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
#include
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
#include
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 :
* avec un type fondamental (''int'', ''double'', etc.), ''sizeof'' retourne la taille en mémoire ;
* avec un type complexe (par exemple les classes de la bibliothèque standard), ''sizeof'' ne retourne pas forcément la taille réellement occupée en mémoire par la classe.
===== Les types entiers =====
==== Les modificateurs de type ====
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 se limite pas aux types par défaut, vous avez beaucoup de liberté pour manipuler la mémoire. Il est par exemple 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éfinis 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
int main() {
std::cout << "sizeof(char) = " << sizeof(char) << std::endl;
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(char) = 1
sizeof(short int) = 2
sizeof(int) = 4
sizeof(long int) = 8
sizeof(long long int) = 8
Vous pouvez remarquer que ''long'' et ''long long int'' ont la même taille en mémoire dans ce test. Mais il est possible que vous n'ayez pas les mêmes valeurs.
C'est une particularité des types du C++ : la norme ne définit pas une taille fixe pour ces types, simplement des contraintes par rapport à leur taille respective. Ces règles sont simples :
* la taille de ''char'' est 1 ;
* chaque type de la liste précédente a une taille supérieure ou égale au type précédent.
sizeof(char) == 1 <= sizeof(short int) <= sizeof(int) <= sizeof(long int) <= sizeof(long long int)
**Type abrégé**
Il est possible de ne pas indiquer ''int'' dans les types précédent. Si un modificateur est utilisé sans préciser le type, ça sera ''int'' par défaut. Il est donc possible d'écrire ''short'', ''long'' et ''long long''.
Dans ce cours, le type ''int'' sera toujours indiqué.
Si vous souhaitez absolument des types avec une taille fixée, il existe également les types suivants : ''int8_t'', ''int16_t'', ''int32_t'', ''int64_t'' et leur équivalent non signé (voir en dessous) ''uint8_t'', ''uint16_t'', ''uint32_t'', ''uint64_t'' ("u" pour "unsigned"). Voir [[http://en.cppreference.com/w/cpp/types/integer|Fixed width integer types]].
Ces types imposent des contraintes plus fortes, il est préférable de les utiliser uniquement si vous en avez besoin.
==== Limites des nombres ====
Ce problème est un effet direct de la représentation des nombres par un nombre fini de bits dans un ordinateur. Comme un nombre possède une valeur maximale (selon le type de ce nombre), réaliser un calcul qui dépasse cette valeur maximale produira des comportements non attendus (le comportement exacte dépendra du type).
Par exemple, si vous écrivez l'assertion suivante :
assert(i < (i+1));
C'est-à-dire que vous vérifiez qu'un nombre est inférieur au nombre suivant, vous pourriez vous attendre à ce que cette assertion soit toujours vraie.
Mais si vous écrivez, par exemple pour un nombre entier de type ''int'' :
#include
#include
int main() {
const int i { std::numeric_limits::max() }; // valeur maximale de int
assert(i < (i+1)); // probleme
}
Dans ce cas, le comportement n'est pas définie dans la norme C++, mais généralement, l'assertion sera fausse. Il faut donc bien vérifier ses conditions avant de réaliser des calculs, pour etre sur que le resultat obtenu soit celui attendu.
**Floating-point environment**
Il est possible de gérer plus finalement le comportement lors de problème de calculs, mais cela sort du cadre de ce cours. Pour plus de details, consulter la documentation : [[http://en.cppreference.com/w/cpp/numeric/fenv/FE_exceptions|Floating-point environment]]
==== Signature des nombres entiers ====
Par défaut, les nombres entiers peuvent être positifs ou négatifs (on dit qu'ils 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 type entier qui ne peut représenter que des valeurs positives.
**Signature de char**
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é.
Le cas d'utilisation le plus important est avec ''char'' : la norme C++ ne définit pas la signature de ''char'', il peut être signé ou non selon le système. Il est donc recommandé, lorsque vous utilisez ''char'' pour représenter un entier et non un caractère, de préciser systématiquement la signature avec ''unsigned'' ou ''signed''.
Un entier non signé permet de représenter des nombres positifs 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 1 bit pourra prendre 2 valeurs, un nombre représenté par 2 bits pourra prendre 2x2 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 2N 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
0 1 2 3
4 5 6 7
Les valeurs possibles vont donc de -2N-1 à 2N-1-1. La valeur négative (8) est en valeur absolue plus grande que la valeur positive (7), du fait qu'il faut représenter la valeur 0. (On peut bien sûr représenter n'importe quelle plage de valeurs, ce n'est qu'une question de choix. Cette plage de valeurs est celle utilisée en général, pour des raisons d'efficacité).
Si on ne représente que des valeurs positives ou nulles, on va pouvoir représenter par exemple :
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15
La plage de valeurs devient donc 0 à 2N-1. La valeur maximale des nombres non signés est donc bien supérieure à celle des nombres signés.
==== Vérifier les erreurs de conversion ====
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 initialisez une variable avec une valeur hors limite, une erreur sera signalée.
#include
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''.
main.cpp:4:27: error: constant expression evaluates to 255 which cannot be narrowed to
type 'signed char' [-Wc++11-narrowing]
const signed char x { 255 };
^~~
main.cpp:4:27: note: insert an explicit cast to silence this issue
const signed char x { 255 };
^~~
static_cast( )
On peut également vérifier que la taille en mémoire ne change pas :
#include
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érifie pas que les valeurs passées sont bien positives. Si vous entrez une valeur négative, le comportement sera différent :
#include
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
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( )
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
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
==== Comparer des nombres signés et non signés ====
Comparer un nombre non signé avec un nombre signe peut produire des comportements inattendus. Par exemple, le code suivant compare les valeurs -1 et 1, vous pourriez vous attendre a ce que -1 soit inferieur a 1. Cependant, le fait que ce code compare un nombre signé avec un nombre non signé retourne le mauvais résultat.
#include
int main() {
const signed int a { -1 };
const unsigned int b { 1 };
std::cout << std::boolalpha << (a < b) << std::endl;
}
affiche :
main.cpp: In function 'int main()':
main.cpp:7:39: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
std::cout << std::boolalpha << (a < b) << std::endl;
~~^~~
false
Ce comportement s'explique par les règles de conversion implicite du C++, mais sera, dans la majorité des cas, contre intuitif. Ces règles sont un peu complexes et il n'est pas nécessaires de connaître les détails (la valeur non signée -1 devient une très grande valeur après conversion). Dans ce cours, vous allez simplement suivre des règles simples pour éviter ce problème.
1. activez les avertissements du compilateur. Ces avertissements sont destinés à vous aider, ca serait dommage de s'en passer.
2. ne comparez JAMAIS des nombres signés et non signes. Convertissez explicitement les nombres non signés avant de les comparer aux nombres signes.
a < static_cast(b)
3. utilisez des entiers signés par défaut, en particulier pour tous les calculs. Utilisez les nombres non signés que pour représenter des tailles (le plus souvent ''std::size'', comme pour la taille des collections de la bibliothèque standard).
===== Les nombres réels =====
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
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.
===== Les modificateurs de littérales =====
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
int main() {
char const c { 12 }; // conversion de "int" vers "char"
unsigned int const i { 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 les majuscules (donc les suffixes suivants sont acceptés : ''ul'', ''UL'', ''ull'' ou ''ULL'').
#include
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
int main() {
float const f { 123.456f };
double const d { 123.456 };
long double const ld { 123.456l };
}
Les modificateurs de littérales prennent tout leur sens avec la déduction de type. Lorsque vous écrivez :
const int i { 123 };
Vous indiquez deux fois que vous souhaitez manipuler un entier de type ''int'' : dans la littérale et dans la déclaration de la variable. Si vous modifiez le type de la littérale (par exemple pour utiliser un réel au lieu d'un entier), il faut penser à modifier aussi le type de la variable.
La déduction de type permet d'éviter de spécifier deux fois la même information :
const auto i = 123;
Cette syntaxe est donc plus simple à maintenir, elle sera préférable. Cependant, il n'est pas possible de créer certains types de littérales. Par exemple, il n'est pas possible de créer une littérale de type ''short int''. Dans ce cas, il est possible d'utiliser un conversion implicite avec ''static_cast'' (vous verrez plus en détail cela par la suite) :
const auto i = static_cast(123);
Mais cette syntaxe commence à être un peu lourde, il est dans ce cas probablement préférable de ne pas utiliser la déduction de type :
const short int i { 123 };
(Il existe des arguments en faveur de la syntaxe avec ''static_cast'', mais ce sont plus des considérations de style. Retenez simplement que certains développeurs C++ préfèrent une syntaxe ou l'autre... et que les développeurs C++ sont parfois des gens compliqués. :-))
^ [[inference_de_type|Chapitre précédent]] ^ [[programmez_avec_le_langage_c|Sommaire principal]] ^ [[informations_sur_les_types|Chapitre suivant]] ^