Outils d'utilisateurs

Outils du Site


types_en_detail

Ceci est une ancienne révision du document !


Typage fort et faible ? de même type, notion de typage fort (on ne mélange pas les types). Mais conversion possible

limite des nombres réels

manque une partie sur les string

Manipuler les types

Taille des variables

Dans le chapitre Logique binaire et calcul booléen, vous avez vu que lorsque vous afficher l'inverse d'un nombre booléen, la valeur est affichée correspond à un nombre codé sur 32 bits (pour rappel, 32 bits = 4 octets = 8 chiffres hexadécimaux).

main.cpp
#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

Quelque soit le nombre de chiffres hexadécimaux que l'on utilise pour écrire la littérale booléenne, la valeur inverse est toujours affichée sur 32 bits. La raison est que dans tous les cas, le compilateur créé une variable temporaire de 32 bits puis calcul l'inverse dessus. Le résultat est donc toujours sur 32 bits.

Plus généralement, un processeur est optimisé pour travailler avec certaines tailles de variable, en général 8, 16, 32 ou 64 bits. Le langage C++ permet d'utiliser des variables de différentes tailles, que vous pourrez utiliser selon vos besoins.

Voyons pour commencer comment connaître la taille en mémoire d'une variable ou d'un type. Dans les deux cas, vous devez utiliser la fonction sizeof et lui passer un paramètre la variable ou le type :

main.cpp
#include <iostream>
#include <string>
 
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(std::string) = " << sizeof(std::string) << std::endl;
    std::cout << "sizeof(char) = " << sizeof(char) << std::endl;
}

Le code précédent va par exemple afficher les tailles suivantes, en octets. La taille peut dépendre du système d'exploitation et du processeur.

sizeof(int) = 4
sizeof(double) = 8
sizeof(bool) = 1
sizeof(std::string) = 8
sizeof(char) = 1

De la même façon, pour connaître la taille en mémoire d'une variable, vous pouvez écrire :

main.cpp
#include <iostream>
#include <string>
 
int main() {
    int const x { 123 };
    double const d { 12.34 };
    bool const b { true };
    std::string const s { "hello, world!" };
    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(std::string) = " << sizeof(s) << 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(std::string) = 8
sizeof(char) = 1

Il faut bien faire attention à la taille en mémoire et la taille du contenu d'une variable. Si on prend par exemple une chaîne de caractères de type string, on peut remarquer que la taille retournée par sizeof sera toujours la même, quelque soit le nombre de caractères.

main.cpp
#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 bien comprendre pourquoi sizeof donne ce résultat, il faut voir 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 ;

main.cpp
#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

Les modificateurs de type

Dans la majorité des ordinateurs actuels, la mémoire disponible se compte en plusieurs Giga-octets et la taille mémoire des données ne sera pas critiques (sauf dans des contextes particulier, 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 remarqué 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 pourrait 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.

Les nombres entiers

La taille mémoire des types est paramétrable en utilisant des modificateurs de types, qui s'ajoute à un type de base. Pour les entiers, les modificateurs disponibles sont : short (court), 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 :

main.cpp
#include <iostream>
 
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

Remarque : le type char a été ajouté dans le code précédent. La raison est que à la base, ce type est un type entier, similaire à int et qui se manipule de la même façon. La seule différence est que cout affichera un char en convertissant la valeur entière en un caractère en utilisant le code ASCII. Ainsi, il est possible d'utiliser un char en écrivant directement une littérale de type entier et de réaliser des calculs dessus :

main.cpp
#include <iostream>
 
int main() { 
    char const c = 65; // correspond au caractère 'A'
    std::cout << "char(65) = " << c << std::endl;
}

affiche (voir la table de conversion ASCII) :

char(65) = A

Par défaut, les nombres entiers peuvent être positif ou négatif. Lorsque l'on n'a pas besoin de gérer des valeurs négatives, il est possible d'utiliser le mot-clé unsigned. De cette façon, au lieu que les nombres puissent prendre une valeur comprise entre -Max à +Max (Max représentant la valeur maximal qu'un type peut prendre), un type unsigned pourra prendre une valeur compris entre 0 et +2*Max.

main.cpp
#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

Vérification des valeurs

Attention cependant, le compilateur ne vérifiera pas que les valeurs passées soit bien positives. Si vous entrez une valeur négative, le comportement sera différent :

main.cpp
#include <iostream>
 
int main() { 
    unsigned int i {};
    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 crochets :

main.cpp
#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é.

main.cpp
#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 const i [ -1 };       // erreur, "-1" est négatif

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).

main.cpp
#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 à donc était appelé float (“flottant”) en rapport à “nombre à virgule flottante”. Par la suite, lorsque les nombres réels ont été codé sur 64 bits, le nouveau type a été appelé double puisque c'était le double d'un type float.

Les modificateurs de littérales

De la même manière que les variables, les littérales (c'est-à-dire les valeurs constantes entrée 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 convertie dans le type de la variable.

main.cpp
#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 long int et le suffixe ll ou LL indique une littérale 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).

main.cpp
#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 permet de créer une littérale de type float et le suffixe l ou L permet de créer une littérale long double.

main.cpp
#include <iostream>
 
int main() {
    float const f { 123.456f };
    double const d { 123.456 };
    long double const ld { 123.456l };
}

Obtenir des informations sur les types

digits ? digits10 ? max_digits10 ? radix ?

  • is_signed
  • is_integer
  • is_exact
  • min
  • max
  • lowest
  • epsilon

Tester les valeurs

  • is_nan
  • is_finite
  • is_infinite

Limites des types numériques

max, min, epsilon, etc

Créer ses types

Définir des types

Partir d'un code simple :

int x {};
int y {};
int z {};
int a {};
int b {};
int c {};

On décide de changer d'avis et d'utiliser des réels. Nécessite de tout remplacer

double x {};
double y {};
double z {};
double a {};
double b {};
double c {};

Même problème que pour les variables : nécessite modification de plusieurs lignes, code non facilement maintenable, non respect qualité logiciel.

Pour éviter cela, définir un type, le nommer et l'utiliser.

using local_type = int;
local_type x {};
local_type y {};
local_type z {};
local_type a {};
local_type b {};
local_type c {};

Pour changer de type, besoin que de changer une ligne. Code plus facilement maintenable

Nom des types = même règle que pour les noms des variables. Souvent, pour montrer que c'est un type, on ajoute _t a la fin (C++, STL, boost) ou on commence par une majuscule (Qt)

Type et sémantique

sémantique simple : donner un sens aux types.

ex:

int temp1 { 12 }; // heure
int temp2 { 40 }; // minutes
temps1 + temps2; // n'a pas de sens

Nommer les types :

using heure = int;
using minute = int;
heure temp1 { 12 };
minute temp2 { 40 };

Le type exprimer à quoi il correspond, c'est plus précis que “int”. Mais pas suffisant, on peut toujours écrire :

temps1 + temps2; // n'a pas de sens

idéallement, faudrait que compilateur préviennent. Pourquoi fait-il pas ? Parce que “heure” et “minute” ont un sens pour lecteur. Mais pour compilateur, ce sont des int, donc pas d'interdiction d'addition. Sémantique incomplète

Dans la suite, apprendre à créer ses types et créer une sémantique (des “règles” d'utilisation).

Représentation des nombres

représentation des nombres entier négatif

limitation de représentation d'un nombre “réel” par une représentation limité en mémoire

types_en_detail.1401929584.txt.gz · Dernière modification: 2014/06/05 02:53 par gbdivers