Tipi, Valori e il Sistema di Tipi
Esplora i tipi fondamentali del C++, gli interi a larghezza fissa, le forme di inizializzazione, la deduzione con auto e il sistema const che garantisce la correttezza a tempo di compilazione.
Il Sistema di Tipi: La Tua Prima Linea di Difesa
Il C++ è un linguaggio a tipizzazione statica — ogni espressione ha un tipo noto a tempo di compilazione. Il sistema di tipi non è solo contabilità; è il tuo strumento più potente per catturare bug prima che il codice venga eseguito. Un programma C++ ben tipizzato porta i suoi invarianti nel sistema di tipi stesso, rendendo intere classi di errori impossibili.
Il tipo di una variabile determina tre cose: quanta memoria occupa, quali valori può rappresentare, e quali operazioni sono valide su di essa. Definire correttamente i tipi è il fondamento di un codice C++ corretto.
Tipi Fondamentali
Il C++ fornisce un insieme di tipi integrati che si mappano direttamente sulle capacità dell'hardware. Le loro dimensioni esatte variano per piattaforma, ma lo standard garantisce intervalli minimi:
#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';
}Premi Esegui per compilare ed eseguire questo esempio.
Inizializzazione: Il Modo Corretto
Il C++ ha sviluppato diverse sintassi di inizializzazione. L'inizializzazione uniforme (con parentesi graffe) {} è preferita nel C++ moderno perché impedisce le conversioni con restringimento:
#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';
}Premi Esegui per compilare ed eseguire questo esempio.
In C, il puntatore null è rappresentato dalla macro NULL, che tipicamente si espande in 0 o (void*)0. Questo causa ambiguità in C++ perché 0 è anche un int valido. Considera una funzione con overload sia per int che per un tipo puntatore — passare NULL (che è 0) chiamerebbe l'overload int, non quello puntatore.
Il C++11 ha introdotto nullptr, una parola chiave di tipo std::nullptr_t che è implicitamente convertibile a qualsiasi tipo puntatore ma non a interi. Usa sempre nullptr invece di NULL o 0 per i puntatori null.
Analogamente, preferisci '\0' (il carattere null) quando intendi il carattere con valore zero, piuttosto che 0, per comunicare chiaramente l'intento.
- Usa sempre nullptr invece di NULL o 0 per i puntatori null
- nullptr ha il suo tipo (std::nullptr_t) ed evita l'ambiguità negli overload
- NULL è una macro C che si espande in 0 — è un intero, non un puntatore
- Usa '\0' per il carattere null per distinguerlo dall'intero 0
const e i Qualificatori di Tipo
Il qualificatore const indica al compilatore (e ai lettori umani) che un valore non deve essere modificato dopo l'inizializzazione. È uno degli strumenti più importanti in C++ per scrivere codice sicuro e auto-documentante.
const si legge da destra a sinistra: const int* è un puntatore a un int const (l'int non può essere cambiato, il puntatore sì). int* const è un puntatore const a int (il puntatore non può essere cambiato, l'int sì). const int* const è un puntatore const a un int const.
constexpr (C++11) va oltre: significa che il valore deve essere calcolabile a tempo di compilazione. Questo consente al compilatore di valutare le espressioni durante la compilazione, eliminando completamente il costo a runtime. Usa constexpr per le costanti veramente note a tempo di compilazione.
consteval (C++20) è ancora più rigoroso: contrassegna una funzione che deve essere valutata a tempo di compilazione — chiamarla a runtime è un errore di compilazione.
Il qualificatore volatile indica al compilatore che il valore di una variabile potrebbe cambiare al di fuori del controllo del programma (es. registri hardware, I/O mappato in memoria). Impedisce certe ottimizzazioni. Non usare volatile per il threading — usa std::atomic invece.
Uno dei bug più insidiosi nel C e nel C++ vecchio stile deriva dalle conversioni con restringimento — conversioni implicite che perdono informazioni. Per esempio, assegnare un double a un int tronca silenziosamente la parte frazionaria. Assegnare un int grande a uno short causa silenziosamente un overflow.
L'inizializzazione con parentesi graffe {} impedisce le conversioni con restringimento a tempo di compilazione. Questa è una delle ragioni più forti per preferirla all'inizializzazione con =. Se scrivi int x{3.14};, il compilatore emetterà un errore. Con int x = 3.14;, tronca silenziosamente a 3.
Analogamente, mescolare tipi con segno e senza segno nell'aritmetica causa conversioni implicite che possono produrre risultati sorprendenti. Un int negativo confrontato con un unsigned int verrà prima convertito in unsigned, diventando potenzialmente un numero positivo molto grande.
- L'inizializzazione con parentesi graffe {} cattura le conversioni con restringimento a tempo di compilazione
- L'inizializzazione con assegnamento (=) permette silenziosamente il restringimento — i dati vengono persi senza warning
- Mescolare tipi con segno e senza segno causa la conversione implicita a unsigned
- Un int negativo convertito a unsigned diventa un numero positivo molto grande
Alias di Tipo
Gli alias di tipo migliorano la leggibilità e rendono più facile cambiare i tipi in seguito. Preferisci using rispetto a typedef nel C++ moderno:
#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};
}Premi Esegui per compilare ed eseguire questo esempio.
- Ogni espressione C++ ha un tipo noto a tempo di compilazione — il tipo determina la dimensione, i valori validi e le operazioni valide
- Usa i tipi a larghezza fissa da
(int32_t, uint64_t) quando la dimensione esatta conta - Preferisci l'inizializzazione con parentesi graffe {} — impedisce le pericolose conversioni con restringimento
- Usa nullptr (non NULL o 0) per i puntatori null per evitare ambiguità negli overload
- auto deduce il tipo dall'inizializzatore; decltype interroga il tipo di un'espressione senza valutarla
- Usa constexpr per le costanti a tempo di compilazione e 'using' per gli alias di tipo
Quiz — Metti alla prova le tue conoscenze
(15 XP)1. Qual è il vantaggio dell'inizializzazione con parentesi graffe `int x{42};` rispetto all'inizializzazione con assegnamento `int x = 42;`?
2. Perché dovresti usare `nullptr` invece di `NULL` in C++?
3. A quale tipo viene dedotto `x` con `auto x = 42;`?