Types, valeurs et le système de types
Explorez les types fondamentaux du C++, les entiers de taille fixe, les formes d'initialisation, la déduction avec auto, et le système const qui garantit la correction à la compilation.
Le système de types : votre première ligne de défense
Le C++ est un langage à typage statique — chaque expression a un type connu à la compilation. Le système de types n'est pas juste de la comptabilité ; c'est votre outil le plus puissant pour détecter les bogues avant même que votre code ne s'exécute. Un programme C++ bien typé porte ses invariants dans le système de types lui-même, rendant des classes entières d'erreurs impossibles.
Le type d'une variable détermine trois choses : combien de mémoire elle occupe, quelles valeurs elle peut représenter, et quelles opérations sont valides sur elle. Bien choisir les types est le fondement d'un code C++ correct.
Types fondamentaux
Le C++ fournit un ensemble de types intégrés qui correspondent directement aux capacités du matériel. Leurs tailles exactes varient selon la plateforme, mais le standard garantit des plages minimales :
#include <iostream>
#include <cstdint> // Fixed-width integer types
#include <limits> // std::numeric_limits
int main() {
// Boolean
bool is_valid = true; // true or false, typically 1 byte
// Character types
char letter = 'A'; // At least 8 bits, holds ASCII
char8_t u8ch = u8'A'; // C++20: exactly 8 bits, UTF-8
char16_t u16ch = u'\u00E9'; // At least 16 bits, UTF-16
char32_t u32ch = U'\U0001F600'; // At least 32 bits, UTF-32
// Integer types (sizes are minimums, typical x86-64 shown)
short s = 32000; // At least 16 bits (16)
int i = 2'000'000; // At least 16 bits (32) — digit separator C++14
long l = 100'000L; // At least 32 bits (64 on Linux, 32 on Windows)
long long ll = 9'000'000'000'000'000'000LL; // At least 64 bits
// Unsigned variants
unsigned int ui = 42u; // Cannot be negative
// Floating-point types
float f = 3.14f; // ~7 decimal digits precision
double d = 3.141592653589793; // ~15 decimal digits precision
long double ld = 3.14159265358979323846L;
// Fixed-width types from <cstdint> — use these when size matters
std::int32_t exact = 42; // Exactly 32 bits, guaranteed
std::uint64_t big = 18'446'744'073'709'551'615ULL; // Exactly 64 bits
std::int_fast32_t fast = 42; // At least 32 bits, fastest available
std::size_t index = 0; // Unsigned, suitable for sizes and indices
std::cout << "int: " << sizeof(int) << " bytes, max = "
<< std::numeric_limits<int>::max() << '\n';
}Cliquez sur Exécuter pour compiler et lancer cet exemple.
L'initialisation : la bonne méthode
Le C++ a fait évoluer plusieurs syntaxes d'initialisation. L'initialisation uniforme (par accolades) {} est préférée en C++ moderne car elle empêche les conversions rétrécissantes :
#include <iostream>
#include <string>
int main() {
// Direct initialization
int a(42); // OK but can be confused with function declaration
// Copy initialization
int b = 42; // OK, familiar from C
// Uniform (brace) initialization — preferred in modern C++
int c{42}; // Direct-list-initialization
int d = {42}; // Copy-list-initialization
// Brace initialization PREVENTS narrowing conversions
// double to int loses precision — the compiler will ERROR:
// int narrow{3.14}; // ERROR: narrowing conversion
int ok = 3.14; // Compiles! Silently truncates to 3 (dangerous)
// Value initialization (zero for built-in types)
int zero{}; // Guaranteed to be 0
double dzero{}; // Guaranteed to be 0.0
bool bfalse{}; // Guaranteed to be false
// auto deduction
auto x = 42; // int
auto y = 3.14; // double
auto z = 'A'; // char
auto s = std::string{"hello"}; // std::string (not const char*!)
// decltype — queries the type of an expression
decltype(x) another = 100; // same type as x → int
decltype(auto) ref = (x); // int& because (x) is an lvalue expression
std::cout << "zero-initialized int: " << zero << '\n';
std::cout << "auto-deduced string: " << s << '\n';
}Cliquez sur Exécuter pour compiler et lancer cet exemple.
En C, le pointeur nul est représenté par la macro NULL, qui se développe généralement en 0 ou (void*)0. Cela cause une ambiguïté en C++ car 0 est aussi un int valide. Considérez une fonction surchargée pour un type int et un type pointeur — passer NULL (qui est 0) appellerait la surcharge int, pas celle du pointeur.
C++11 a introduit nullptr, un mot-clé de type std::nullptr_t qui est implicitement convertible vers n'importe quel type pointeur mais pas vers les entiers. Utilisez toujours nullptr au lieu de NULL ou 0 pour les pointeurs nuls.
De même, préférez '\0' (le caractère nul) quand vous voulez dire le caractère de valeur zéro, plutôt que 0, pour communiquer clairement votre intention.
- Utilisez toujours nullptr au lieu de NULL ou 0 pour les pointeurs nuls
- nullptr a son propre type (std::nullptr_t) et évite l'ambiguïté de surcharge
- NULL est une macro C qui se développe en 0 — c'est un entier, pas un pointeur
- Utilisez '\0' pour le caractère nul afin de le distinguer de l'entier 0
const et les qualificateurs de type
Le qualificateur const indique au compilateur (et aux lecteurs humains) qu'une valeur ne doit pas être modifiée après l'initialisation. C'est l'un des outils les plus importants en C++ pour écrire du code sûr et auto-documenté.
const se lit de droite à gauche : const int* est un pointeur vers un int constant (l'int ne peut pas être changé, le pointeur peut l'être). int* const est un pointeur constant vers un int (le pointeur ne peut pas être changé, l'int peut l'être). const int* const est un pointeur constant vers un int constant.
constexpr (C++11) va plus loin : cela signifie que la valeur doit pouvoir être calculée à la compilation. Cela permet au compilateur d'évaluer les expressions pendant la compilation, éliminant entièrement le coût à l'exécution. Utilisez constexpr pour les constantes véritablement connues à la compilation.
consteval (C++20) est encore plus strict : il marque une fonction qui doit être évaluée à la compilation — l'appeler à l'exécution est une erreur de compilation.
Le qualificateur volatile indique au compilateur que la valeur d'une variable peut changer en dehors du contrôle du programme (par ex., registres matériels, E/S mappées en mémoire). Il empêche certaines optimisations. N'utilisez pas volatile pour le multithreading — utilisez std::atomic à la place.
L'un des bogues les plus insidieux en C et en ancien C++ provient des conversions rétrécissantes — des conversions implicites qui perdent de l'information. Par exemple, assigner un double à un int tronque silencieusement la partie fractionnaire. Assigner un grand int à un short provoque silencieusement un débordement.
L'initialisation par accolades {} empêche les conversions rétrécissantes à la compilation. C'est l'une des raisons les plus fortes de la préférer à l'initialisation par =. Si vous écrivez int x{3.14};, le compilateur émettra une erreur. Avec int x = 3.14;, il tronque silencieusement à 3.
De même, mélanger des types signés et non signés en arithmétique provoque des conversions implicites qui peuvent produire des résultats surprenants. Un int négatif comparé à un unsigned int sera d'abord converti en non signé, devenant potentiellement un très grand nombre positif.
- L'initialisation par accolades {} détecte les conversions rétrécissantes à la compilation
- L'initialisation par assignation (=) autorise silencieusement le rétrécissement — des données sont perdues sans avertissement
- Mélanger des types signés et non signés provoque une conversion implicite vers le non signé
- Un int négatif converti en non signé devient un très grand nombre positif
Alias de types
Les alias de types améliorent la lisibilité et facilitent le changement de types ultérieurement. Préférez using à typedef en C++ moderne :
#include <cstdint>
#include <vector>
#include <string>
// Modern style (using) — preferred
using Byte = std::uint8_t;
using StudentId = std::int32_t;
using Names = std::vector<std::string>;
// Old style (typedef) — equivalent but harder to read with complex types
typedef std::uint8_t OldByte;
typedef std::vector<std::string> OldNames;
// using works with templates; typedef does not easily
template <typename T>
using Vec = std::vector<T>;
int main() {
Byte b{255};
StudentId sid{12345};
Names names{"Alice", "Bob", "Charlie"};
Vec<double> values{1.0, 2.0, 3.0};
}Cliquez sur Exécuter pour compiler et lancer cet exemple.
- Chaque expression C++ a un type connu à la compilation — le type détermine la taille, les valeurs valides et les opérations valides
- Utilisez les types de taille fixe de
(int32_t, uint64_t) quand la taille exacte importe - Préférez l'initialisation par accolades {} — elle empêche les conversions rétrécissantes dangereuses
- Utilisez nullptr (pas NULL ni 0) pour les pointeurs nuls afin d'éviter l'ambiguïté de surcharge
- auto déduit le type depuis l'initialiseur ; decltype interroge le type d'une expression sans l'évaluer
- Utilisez constexpr pour les constantes de compilation et 'using' pour les alias de types
Quiz — Testez vos connaissances
(15 XP)1. Quel est l'avantage de l'initialisation par accolades `int x{42};` par rapport à l'initialisation par assignation `int x = 42;` ?
2. Pourquoi devriez-vous utiliser `nullptr` au lieu de `NULL` en C++ ?
3. Quel type `auto x = 42;` déduit-il pour `x` ?