Perché i tipi contano in C++
Quando dichiari un valore con un tipo esplicito come int age = 30;, quel tipo non è solo un'etichetta: dice al compilatore quanti byte riservare, come interpretarli e quali operazioni sono consentite. Se sbagli tipo puoi perdere precisione in silenzio, andare in overflow o provocare un comportamento indefinito.
Il C++ raggruppa i suoi tipi predefiniti in alcune famiglie: interi, numeri a virgola mobile, un tipo carattere e un booleano. Vediamoli uno per uno, poi passiamo alle regole che mettono in difficoltà.
I tipi fondamentali
Ecco un esempio di ogni tipo principale in un unico programma. Nota i suffissi dei letterali (L, f, u) e gli apici singoli per char:
Di default un bool si stampa come 1 o 0, non come true/false. Un char usa gli apici singoli: 'A' è un carattere, mentre "A" (virgolette doppie) è un letterale stringa, un tipo completamente diverso. All'inizio questi due errori sono comunissimi.
Le dimensioni non sono fisse
È la sorpresa più grande per chi arriva da linguaggi come Java. Lo standard C++ garantisce solo dimensioni minime e un ordine relativo (short ≤ int ≤ long ≤ long long). La dimensione effettiva dipende dal compilatore e dalla piattaforma. Verificala sempre con sizeof:
In una tipica compilazione Linux a 64 bit vedrai int = 4, long = 8. Su Windows a 64 bit, però, long è di soli 4 byte. Questa differenza di portabilità è proprio il motivo per cui non dovresti scrivere codice che presume che long sia a 64 bit.
Quando ti serve una larghezza esatta, usa i tipi interi a larghezza fissa di <cstdint>:
Usa int32_t/int64_t per formati di file, protocolli di rete o qualsiasi cosa debba comportarsi in modo identico su macchine diverse. Nota il cast (int)a: inviare a uno stream un tipo a 8 bit lo stampa come carattere, non come numero, quindi fai prima il cast.
Signed e unsigned
Ogni tipo intero esiste in due varianti. Un tipo signed può contenere numeri negativi; un tipo unsigned no, e in cambio dell'intervallo negativo ottiene un massimo positivo più alto. Un semplice int è signed di default.
Sottrarre da uno 0 unsigned fa ripartire il valore da un numero positivo enorme invece di andare in negativo. Questo colpisce di continuo, soprattutto con size_t (un tipo unsigned) restituito da .size():
vector<int> v = {1, 2, 3};
// PERICOLO: v.size() è unsigned. Se v è vuoto, v.size() - 1 ricomincia
// da un numero gigantesco e il ciclo gira quasi all'infinito.
for (size_t i = 0; i <= v.size() - 1; i++) { /* ... */ }
Preferisci i < v.size() (mai <= size() - 1), oppure evita del tutto il problema con un ciclo for basato su range.
L'overflow degli interi è comportamento indefinito
A differenza del ritorno a capo degli unsigned (che è ben definito), l'overflow degli interi signed è comportamento indefinito in C++. Il compilatore può fare qualsiasi cosa: restituire spazzatura, eliminare il controllo durante l'ottimizzazione o far crashare il programma.
La soluzione è la stessa della trappola dell'overflow in qualsiasi linguaggio: fai i calcoli in un tipo più largo. Converti un operando in long long prima del +, così la somma avviene a 64 bit. Convertire il risultato dopo è troppo tardi: l'overflow è già avvenuto.
Scegliere il tipo giusto
Per la maggior parte del codice i default vanno benissimo: int per i numeri interi, double per i decimali. Passa a qualcos'altro solo quando hai un motivo.
| Tipo | Dimensione tipica | Quando usarlo |
|---|---|---|
int | 32 bit | Il default per i numeri interi |
long long | 64 bit | Valori oltre i 2 miliardi circa: timestamp, contatori grandi |
double | 64 bit | Il default per i decimali, buona precisione |
float | 32 bit | Array in cui la memoria è scarsa e si può sacrificare precisione |
bool | 1 byte | Un flag vero/falso |
int32_t / int64_t | esatta | Formati multipiattaforma, protocolli, manipolazione di bit |
Un paio di insidie da tenere a mente. float ha solo circa 7 cifre decimali significative, quindi 0.1f + 0.2f non è esattamente 0.3: preferisci double a meno che tu non debba davvero risparmiare memoria. E char può essere signed o unsigned a seconda della piattaforma, quindi se fai calcoli su byte grezzi scrivi esplicitamente signed char o unsigned char.
Prossimo: La parola chiave auto
Scrivere il tipo ogni volta diventa noioso, e a volte il tipo è lungo o difficile da nominare. Il C++ permette al compilatore di dedurlo per te con la parola chiave auto: auto x = 42; rende x un int, e auto it = v.begin(); ti evita di scrivere un tipo di iteratore prolisso. La prossima pagina spiega quando auto rende il codice più chiaro e quando nasconde troppo.
Domande frequenti
Quali sono i tipi di dato di base in C++?
I tipi fondamentali sono gli interi (short, int, long, long long), i tipi a virgola mobile (float, double, long double), il tipo carattere char e il tipo booleano bool. Ogni tipo intero può anche essere signed o unsigned. Tutto il resto, come std::string, gli array e le tue classi, è costruito sopra questi tipi.
Qual è la differenza tra int e long in C++?
Entrambi memorizzano numeri interi, ma long è garantito largo almeno quanto int (spesso 64 bit sulle piattaforme a 64 bit, ma solo 32 bit su Windows). Lo standard fissa solo le dimensioni minime, quindi per una larghezza garantita usa i tipi a larghezza fissa di <cstdint> come int32_t e int64_t.
Quanto è grande un int in C++?
Lo standard garantisce solo che int sia almeno di 16 bit, ma praticamente su ogni desktop e server moderno è di 32 bit. Dato che le dimensioni dipendono dalla piattaforma, non dare mai nulla per scontato: stampa sizeof(int) per verificarlo, oppure usa i tipi di <cstdint> come int32_t quando ti serve una larghezza esatta.