Skip to content

Latest commit

 

History

History
326 lines (215 loc) · 16.2 KB

File metadata and controls

326 lines (215 loc) · 16.2 KB

🔝 Retour au Sommaire

3.2.1 — Entiers : int, long, short, int32_t, int64_t

Chapitre 3 — Types, Variables et Opérateurs · Section 3.2 · Sous-section 1 sur 3
Prérequis : 3.2 — Types primitifs, tailles et représentation mémoire


Introduction

Les types entiers constituent la famille la plus utilisée et la plus diverse des types primitifs en C++. Compteur de boucle, indice de tableau, code de retour, identifiant, masque de bits, port réseau, taille de fichier — les entiers sont partout. Et pourtant, choisir le bon type entier est loin d'être anodin : un choix inadapté peut entraîner des dépassements silencieux, des bugs de portabilité, des comparaisons incohérentes entre types signés et non signés, ou des pertes de données lors de conversions implicites.

Cette sous-section passe en revue les types entiers du langage, les garanties de la norme, les types à largeur fixe de <cstdint>, et les pièges les plus courants de l'arithmétique entière en C++.


Les types entiers fondamentaux

Le C++ hérite du C un ensemble de types entiers définis par des combinaisons de mots-clés. Chaque type existe en version signée (par défaut) et non signée (préfixe unsigned).

Types signés

Les types entiers signés, du plus petit au plus grand, sont :

signed char        sc = -128;        // Au moins 8 bits  
short              s  = -32'000;     // Au moins 16 bits  
int                i  = -2'000'000;  // Au moins 16 bits (32 en pratique)  
long               l  = -1'000'000;  // Au moins 32 bits  
long long          ll = -9'000'000'000'000'000'000LL; // Au moins 64 bits  

Le mot-clé signed est implicite pour short, int, long et long long. Écrire signed int est équivalent à int. La seule exception est char, dont le signe n'est pas spécifié par la norme — il peut être signé ou non signé selon l'implémentation. C'est pourquoi signed char et unsigned char existent comme types distincts de char.

💡 Les séparateurs de chiffres ' (apostrophes) sont disponibles depuis C++14. Ils n'ont aucun effet sémantique mais améliorent considérablement la lisibilité des grands nombres : 1'000'000 est plus facile à lire que 1000000.

Types non signés

Chaque type signé possède un équivalent non signé :

unsigned char        uc  = 255;  
unsigned short       us  = 65'535;  
unsigned int         ui  = 4'000'000'000U;  
unsigned long        ul  = 4'000'000'000UL;  
unsigned long long   ull = 18'000'000'000'000'000'000ULL;  

Les types non signés ne peuvent représenter que des valeurs positives ou nulles. En contrepartie, ils offrent une plage positive deux fois plus grande que leur équivalent signé pour le même nombre de bits.


Tailles sur les plateformes courantes

La norme impose des tailles minimales, pas des tailles exactes. Cependant, sur les plateformes les plus courantes en 2026, les tailles de fait sont les suivantes :

Linux x86_64 (GCC / Clang)

Type Taille Plage signée Plage non signée
char 1 octet (8 bits) −128 à 127 0 à 255
short 2 octets (16 bits) −32 768 à 32 767 0 à 65 535
int 4 octets (32 bits) −2 147 483 648 à 2 147 483 647 0 à 4 294 967 295
long 8 octets (64 bits) −9,2 × 10¹⁸ à 9,2 × 10¹⁸ 0 à 1,8 × 10¹⁹
long long 8 octets (64 bits) −9,2 × 10¹⁸ à 9,2 × 10¹⁸ 0 à 1,8 × 10¹⁹

Windows x86_64 (MSVC)

Type Taille Différence avec Linux
char 1 octet
short 2 octets
int 4 octets
long 4 octets ⚠️ 4 octets au lieu de 8
long long 8 octets

La différence critique est long : 8 octets sous Linux, 4 octets sous Windows. C'est la source de bugs de portabilité la plus fréquente avec les types entiers. Un code qui stocke une valeur supérieure à 2 milliards dans un long fonctionnera sous Linux et provoquera un dépassement silencieux sous Windows.

C'est précisément pour éliminer ce type d'ambiguïté que les types à largeur fixe existent.


Les types à largeur fixe : <cstdint>

L'en-tête <cstdint> (introduit en C++11, hérité de C99) définit des alias de types dont la taille est garantie par la norme, indépendamment de la plateforme :

Types à largeur exacte

#include <cstdint>

int8_t    a = -100;       // Exactement 8 bits, signé  
int16_t   b = -30'000;    // Exactement 16 bits, signé  
int32_t   c = -2'000'000; // Exactement 32 bits, signé  
int64_t   d = -9'000'000'000'000'000'000LL; // Exactement 64 bits, signé  

uint8_t   e = 200;        // Exactement 8 bits, non signé  
uint16_t  f = 60'000;     // Exactement 16 bits, non signé  
uint32_t  g = 4'000'000'000U;   // Exactement 32 bits, non signé  
uint64_t  h = 18'000'000'000'000'000'000ULL; // Exactement 64 bits, non signé  

Ces types sont des alias (typedef ou using) vers les types natifs de la plateforme. Sur Linux x86_64, int32_t est typiquement un alias de int, et int64_t un alias de long. Mais l'abstraction garantit que le code se comporte de manière identique partout.

⚠️ Les types à largeur exacte ne sont pas obligatoirement disponibles sur toutes les plateformes. La norme stipule qu'ils ne sont définis que si l'architecture propose un type natif de la taille correspondante. En pratique, sur toutes les plateformes desktop, serveur et embarquées modernes courantes, ils sont disponibles.

Types à largeur minimale

Pour les cas (rares) où un type à largeur exacte n'est pas disponible, <cstdint> fournit des types à largeur minimale :

int_least8_t   x;  // Au moins 8 bits (peut être plus)  
int_least16_t  y;  // Au moins 16 bits  
int_least32_t  z;  // Au moins 32 bits  
int_least64_t  w;  // Au moins 64 bits  
// Mêmes variantes en uint_leastN_t

Types les plus rapides

<cstdint> définit aussi des types « les plus rapides » (fastest), c'est-à-dire le type le plus performant sur la plateforme ayant au moins N bits :

int_fast8_t    x;  // Le type le plus rapide ayant au moins 8 bits  
int_fast16_t   y;  // Le type le plus rapide ayant au moins 16 bits  
int_fast32_t   z;  // Le type le plus rapide ayant au moins 32 bits  
int_fast64_t   w;  // Le type le plus rapide ayant au moins 64 bits  

Sur x86_64, int_fast16_t est souvent un alias de int64_t (le registre natif du processeur), car les opérations sur 64 bits sont les plus rapides. Ces types sont utiles dans du code intensif en calcul, mais rendent les tailles moins prévisibles. Leur usage reste marginal dans le code applicatif.

std::size_t et std::ptrdiff_t

Deux types méritent une mention spéciale car ils sont omniprésents dans la bibliothèque standard :

std::size_t est un entier non signé capable de représenter la taille de n'importe quel objet en mémoire. C'est le type retourné par sizeof et par les méthodes .size() de tous les conteneurs STL. Sur une plateforme 64 bits, il fait 8 octets.

std::vector<int> v = {1, 2, 3};  
std::size_t n = v.size();   // Type naturel pour une taille  

std::ptrdiff_t est un entier signé capable de représenter la différence entre deux pointeurs. C'est le type retourné par la soustraction de deux itérateurs ou pointeurs.

int arr[] = {10, 20, 30, 40};  
std::ptrdiff_t diff = &arr[3] - &arr[0]; // 3  

Littéraux entiers

Les littéraux entiers peuvent être exprimés dans quatre bases :

int decimal     = 42;        // Base 10 (par défaut)  
int octal       = 052;       // Base 8  (préfixe 0)  
int hexadecimal = 0x2A;      // Base 16 (préfixe 0x ou 0X)  
int binary      = 0b101010;  // Base 2  (préfixe 0b ou 0B, depuis C++14)  

⚠️ Le préfixe 0 pour l'octal est un piège classique. int x = 010; ne vaut pas 10 mais 8 (1 × 8¹ + 0 × 8⁰). C'est un héritage du C rarement souhaité. Si vous écrivez un zéro en tête « pour aligner », vous changez la base sans le vouloir.

Suffixes de type

Par défaut, un littéral entier est de type int s'il tient dans un int, long sinon, puis long long. Les suffixes permettent de forcer un type spécifique :

Suffixe Type Exemple
(aucun) int (ou plus grand si nécessaire) 42
U ou u unsigned int 42U
L ou l long 42L
LL ou ll long long 42LL
UL unsigned long 42UL
ULL unsigned long long 42ULL

💡 Évitez le suffixe l minuscule seul — il se confond visuellement avec le chiffre 1. Préférez toujours L majuscule : 42L plutôt que 42l.


Promotions et conversions entières

Lorsque des types entiers différents apparaissent dans une même expression, le compilateur applique des règles de promotion et de conversion pour unifier les types avant d'effectuer l'opération.

Promotion intégrale

Les types plus petits que int (char, short, bool, et leurs variantes) sont automatiquement promus en int (ou unsigned int si int ne peut pas représenter toutes les valeurs du type source) avant toute opération arithmétique :

short a = 10;  
short b = 20;  
auto result = a + b; // result est int, pas short  
                     // a et b sont promus en int avant l'addition

C'est contre-intuitif mais logique du point de vue du processeur : les ALU (unités arithmétiques et logiques) des processeurs modernes travaillent nativement avec des registres de 32 ou 64 bits. Opérer sur des types plus petits serait en réalité plus lent.

Conversions arithmétiques usuelles

Quand deux opérandes de types différents sont combinés, le compilateur convertit l'un vers l'autre selon une hiérarchie de « rang ». La règle simplifiée est la suivante :

  1. Si l'un des opérandes est long double, l'autre est converti en long double.
  2. Sinon, si l'un est double, l'autre est converti en double.
  3. Sinon, si l'un est float, l'autre est converti en float.
  4. Sinon, les promotions intégrales sont appliquées, puis le type de rang inférieur est converti vers le type de rang supérieur.
int i = 10;  
double d = 3.14;  
auto result = i + d; // i est converti en double → result est double  

Pièges de l'arithmétique entière

Piège n°1 : dépassement d'entier signé (undefined behavior)

Le dépassement d'un entier signé est un comportement indéfini (undefined behavior) en C++. Le compilateur est libre de faire n'importe quoi, y compris produire un résultat apparemment correct, optimiser de manière inattendue, ou provoquer un crash :

int32_t x = 2'147'483'647; // INT32_MAX  
x = x + 1;                 // ❌ Undefined behavior !  
// Le résultat N'EST PAS garanti d'être -2'147'483'648

Le compilateur peut exploiter l'hypothèse « les entiers signés ne dépassent jamais » pour optimiser le code. Par exemple, il peut supprimer une vérification de dépassement qu'il juge « impossible » selon les règles du langage, ce qui peut entraîner des failles de sécurité. Depuis C++20, la représentation en complément à deux est imposée, mais le dépassement signé reste un comportement indéfini.

Pour détecter ces dépassements, compilez avec -fsanitize=undefined (UBSan), couvert au chapitre 29.

Piège n°2 : arithmétique non signée et wrap-around

Contrairement aux entiers signés, le dépassement d'un entier non signé est défini par la norme : l'arithmétique est modulo 2^N. Cela signifie que les valeurs « bouclent » de manière prévisible :

uint32_t x = 0;  
x = x - 1; // Résultat : 4'294'967'295 (UINT32_MAX)  
            // Pas un bug du langage, mais souvent un bug logique

Ce wrap-around est bien défini, mais il est rarement souhaité. Un compteur de boucle qui passe silencieusement de 0 à 4 milliards est une source classique de boucles infinies.

Piège n°3 : comparaison signée/non signée

Comparer un entier signé et un entier non signé est techniquement valide, mais le résultat peut être contre-intuitif car le compilateur convertit le signé vers le non signé :

int a = -1;  
unsigned int b = 0;  

if (a < b) {
    std::print("attendu : -1 < 0\n");
} else {
    std::print("surprise : -1 >= 0 (?!)\n"); // ← C'est cette branche qui s'exécute
}

Ici, -1 est converti en unsigned int, ce qui donne 4'294'967'295 (sur 32 bits). La comparaison devient donc 4'294'967'295 < 0, qui est faux. Le compilateur émet un avertissement avec -Wall -Wextra (-Wsign-compare), mais le code compile sans erreur par défaut.

Ce piège est particulièrement fréquent avec les méthodes .size() des conteneurs STL, qui retournent un std::size_t (non signé) :

std::vector<int> v = {10, 20, 30};

for (int i = 0; i < v.size(); ++i) {
    // ⚠️ Warning : comparaison entre int (signé) et size_t (non signé)
}

Plusieurs solutions existent :

// Solution 1 : utiliser le bon type pour l'indice
for (std::size_t i = 0; i < v.size(); ++i) { /* ... */ }

// Solution 2 : std::ssize() retourne un ptrdiff_t signé (C++20)
for (auto i = 0; i < std::ssize(v); ++i) { /* ... */ }

// Solution 3 (recommandée) : range-based for quand l'indice n'est pas nécessaire
for (const auto& elem : v) { /* ... */ }

Piège n°4 : troncation lors des conversions

Affecter une valeur d'un type large vers un type étroit tronque silencieusement les bits de poids fort :

int64_t big = 5'000'000'000LL;  
int32_t small = big; // ⚠️ Troncation : small vaut 705'032'704  
                     //    (les 32 bits hauts sont perdus)

L'initialisation par accolades empêche cette troncation :

int32_t safe{big}; // ❌ Erreur de compilation : narrowing conversion

C'est l'un des avantages concrets de l'initialisation par accolades pour les types numériques.


Recommandations pratiques

Utilisez int pour les calculs courants. C'est le type « naturel » du processeur, celui sur lequel les opérations sont les plus efficaces. Pour un compteur de boucle, un accumulateur, un résultat intermédiaire — int convient parfaitement.

Utilisez les types à largeur fixe pour les interfaces et les données persistantes. Protocoles réseau, formats de fichiers, sérialisation binaire, communication inter-processus : dans tous ces contextes, la taille doit être exacte et portable. Utilisez int32_t, uint16_t, int64_t, etc.

Utilisez std::size_t pour les indices et les tailles lorsque vous interagissez avec la STL. Cela évite les avertissements signés/non signés et garantit que l'indice peut représenter n'importe quelle taille de conteneur.

Évitez unsigned pour la simple raison qu'une valeur « ne peut pas être négative ». Les C++ Core Guidelines (ES.106) déconseillent l'utilisation systématique de unsigned pour exprimer une contrainte de positivité. Le wrap-around silencieux des types non signés est plus dangereux qu'un nombre négatif facile à détecter avec un assert ou un contrat. Réservez unsigned aux cas où vous avez réellement besoin d'arithmétique modulo 2^N (masques de bits, hachage).

Activez -Wall -Wextra -Wpedantic pour que le compilateur vous signale les comparaisons signées/non signées, les conversions restrictives et les dépassements potentiels. Ajoutez -Werror en CI pour rendre ces avertissements bloquants.


En résumé

Les types entiers en C++ offrent un contrôle fin sur la représentation des données en mémoire, mais ce contrôle s'accompagne de responsabilités. Les garanties de taille minimale de la norme rendent les types fondamentaux (int, long) non portables entre plateformes. Les types à largeur fixe de <cstdint> résolvent ce problème et doivent être préférés dès que la taille a une importance sémantique. Enfin, les pièges de l'arithmétique entière — dépassement signé indéfini, wrap-around non signé, comparaisons mixtes, troncation — sont parmi les sources de bugs les plus fréquentes et les plus subtiles en C++. Le compilateur, correctement configuré, est votre meilleur allié pour les détecter.


⏭️ Flottants : float, double, long double