Tipi Primitivi in C++

I tipi sono fondamentali per qualsiasi programma: ci dicono che cosa significano i nostri dati e quali operazioni possiamo eseguire su di essi.

C++ offre un ampio supporto per i tipi. Il linguaggio definisce diversi tipi primitivi (caratteri, interi, numeri in virgola mobile, ecc.) e fornisce meccanismi che ci consentono di definire i nostri tipi di dati.

Iniziamo lo studio dei tipi in C++ esaminando i tipi primitivi definiti dal linguaggio.

Concetti Chiave
  • C++ definisce diversi tipi primitivi, tra cui tipi aritmetici e il tipo speciale void.
  • I tipi aritmetici sono suddivisi in tipi integrali (inclusi caratteri e booleani) e tipi in virgola mobile.
  • La dimensione dei tipi aritmetici varia tra macchine e compilatori, ma lo standard garantisce dimensioni minime.
  • I tipi integrali possono essere firmati o non firmati, e la loro dimensione minima è garantita dallo standard.
  • I tipi in virgola mobile rappresentano valori decimali con diverse precisioni.
  • I tipi primitivi hanno rappresentazioni a livello macchina che influenzano il modo in cui i dati sono memorizzati e manipolati.
  • I letterali in C++ possono essere di vari tipi, inclusi interi, in virgola mobile, caratteri, booleani e puntatori.
  • I suffissi nei letterali interi (come U, L, LL) specificano il tipo esatto del letterale.
  • I letterali booleani sono rappresentati da true e false, mentre nullptr è un letterale puntatore.

Tipi Primitivi o Built-in

Il linguaggio C++ definisce un insieme di tipi primitivi che includono i tipi aritmetici e un tipo speciale chiamato void.

I tipi aritmetici rappresentano caratteri, interi, valori booleani e numeri in virgola mobile. Il tipo void non ha valori associati e può essere usato solo in poche circostanze, più comunemente come tipo di ritorno per funzioni che non restituiscono un valore.

Tipi Aritmetici

I tipi aritmetici sono divisi in due categorie: tipi integrali o interi (che includono i tipi carattere e booleani) e tipi in virgola mobile.

La dimensione, cioè il numero di bit—dei tipi aritmetici, varia da macchina a macchina e da compilatore a compilatore.

Lo standard garantisce dimensioni minime come elencato nella tabella sottostante. Tuttavia, i compilatori possono usare dimensioni maggiori per questi tipi. Poiché il numero di bit varia, anche il valore massimo (o minimo) che un tipo può rappresentare varia.

Tipo Significato Dimensione Minima
bool booleano NA
char carattere 8 bit
wchar_t carattere wide 16 bit
char16_t carattere Unicode 16 bit
char32_t carattere Unicode 32 bit
short intero short 16 bit
int intero 16 bit
long intero long 32 bit
long long intero long 64 bit
float virgola mobile singola precisione 6 cifre significative
double virgola mobile doppia precisione 10 cifre significative
long double virgola mobile estesa precisione 10 cifre significative
Tabella 1: Tipi aritmetici in C++

Il tipo bool rappresenta i valori di verità true e false.

Esistono diversi tipi carattere, la maggior parte dei quali esiste per supportare l'internazionalizzazione. Il tipo carattere di base è char. Un char è garantito essere abbastanza grande da contenere valori numerici corrispondenti ai caratteri del set di caratteri di base della macchina. Cioè, un char ha la stessa dimensione di un singolo byte della macchina.

I restanti tipi carattere, ossia wchar_t, char16_t e char32_t, sono usati per set di caratteri estesi. Il tipo wchar_t è garantito essere abbastanza grande da contenere qualsiasi carattere del set di caratteri esteso più grande della macchina. I tipi char16_t e char32_t sono destinati ai caratteri Unicode. (Unicode è uno standard per rappresentare caratteri usati in praticamente qualsiasi lingua naturale.)

I restanti tipi integrali rappresentano valori interi di dimensioni (potenzialmente) diverse. Il linguaggio garantisce che un int sarà almeno grande quanto uno short, un long almeno grande quanto un int, e un long long almeno grande quanto un long. Il tipo long long è stato introdotto dallo standard C++11 ma era già fornito da molti compilatori prima che venisse standardizzato.

I tipi in virgola mobile rappresentano valori decimali, ossia con parte frazionaria, a precisione singola, doppia ed estesa. Lo standard specifica un numero minimo di cifre significative. La maggior parte dei compilatori fornisce più precisione del minimo specificato. Tipicamente, i float sono rappresentati in una word (32 bit), i double in due word (64 bit), e i long double in tre o quattro word (96 o 128 bit). I tipi float e double tipicamente forniscono circa 7 e 16 cifre significative, rispettivamente. Il tipo long double è spesso usato come modo per supportare hardware a virgola mobile specializzato; la sua precisione è più probabile che vari da un'implementazione all'altra.

Definizione

Rappresentazione a livello macchina dei Tipi Aritmetici

I computer memorizzano i dati come una sequenza di bit, ciascuno contenente uno 0 o un 1, come ad esempio:

00011011011100010110010000111011

La maggior parte dei computer gestisce la memoria come blocchi di bit di dimensioni che sono potenze di 2.

Il blocco più piccolo di memoria indirizzabile è chiamato "byte". L'unità base di memorizzazione, di solito un piccolo numero di byte, è chiamata "word". In C++ un byte ha almeno tanti bit quanti ne servono per contenere un carattere nel set di caratteri di base della macchina. Sulla maggior parte delle macchine un byte contiene 8 bit e una word è di solito 32 o 64 bit, cioè 4 o 8 byte.

La maggior parte dei computer associa un numero (chiamato "indirizzo") a ciascun byte in memoria. Su una macchina con byte da 8 bit e word da 32 bit, potremmo vedere una word di memoria come segue:

\begin{array}{|c|c|} \hline \texttt{736424} & \texttt{00111011} \\ \hline \texttt{736425} & \texttt{00011011} \\ \hline \texttt{736426} & \texttt{00111011} \\ \hline \texttt{736427} & \texttt{00011011} \\ \hline \end{array}

Qui, l'indirizzo del byte è a sinistra, con gli 8 bit del byte che seguono l'indirizzo.

Possiamo usare un indirizzo per riferirci a qualsiasi delle varie collezioni di bit di dimensioni diverse che iniziano a quell'indirizzo. È possibile parlare della word all'indirizzo 736424 o del byte all'indirizzo 736427. Per dare significato alla memoria a un dato indirizzo, dobbiamo conoscere il tipo del valore memorizzato lì. Il tipo determina quanti bit sono usati e come interpretare quei bit.

Se l'oggetto alla posizione 736424 ha tipo float e se i float su questa macchina sono memorizzati in 32 bit, allora sappiamo che l'oggetto a quell'indirizzo occupa l'intera word. Il valore di quel float dipende dai dettagli di come la macchina memorizza i numeri in virgola mobile. In alternativa, se l'oggetto alla posizione 736424 è un unsigned char su una macchina che usa il set di caratteri ISO-Latin-1, allora il byte a quell'indirizzo rappresenta un punto e virgola.

Per maggiori informazioni su come i tipi aritmetici sono rappresentati a livello macchina, vedi le lezioni di reti logiche presenti sul sito.

Tipi con Segno e Senza Segno

Tranne che per bool e i tipi carattere estesi, i tipi integrali possono essere signed o unsigned. Un tipo signed rappresenta numeri negativi o positivi (incluso lo zero); un tipo unsigned rappresenta solo valori maggiori o uguali a zero.

I tipi int, short, long e long long sono tutti signed. Otteniamo il tipo unsigned corrispondente aggiungendo unsigned al tipo, come unsigned long. Il tipo unsigned int può essere abbreviato come unsigned.

A differenza degli altri tipi interi, ci sono tre distinti tipi carattere di base: char, signed char e unsigned char. In particolare, char non è lo stesso tipo di signed char. Sebbene ci siano tre tipi carattere, ci sono solo due rappresentazioni: signed e unsigned. Il tipo char (semplice) usa una di queste rappresentazioni. Quale delle altre due rappresentazioni carattere è equivalente a char dipende dal compilatore.

In un tipo unsigned, tutti i bit rappresentano il valore. Per esempio, un unsigned char da 8 bit può contenere valori da 0 a 255 inclusi.

Lo standard non definisce come i tipi signed sono rappresentati, ma specifica che l'intervallo dovrebbe essere equamente diviso tra valori positivi e negativi. Quindi, un signed char da 8 bit è garantito poter contenere valori da –127 a 127; la maggior parte delle macchine moderne usa rappresentazioni che permettono valori da –128 a 127.

Consiglio

Come scegliere un tipo aritmetico da usare

Il linguaggio C++, come il linguaggio C da cui deriva, è progettato per permettere ai programmi di avvicinarsi all'hardware quando necessario.

I tipi aritmetici sono definiti per adattarsi alle peculiarità di vari tipi di hardware. Di conseguenza, il numero di tipi aritmetici in C++ può essere sconcertante. La maggior parte dei programmatori può (e dovrebbe) ignorare queste complessità limitando i tipi che usa. Alcune regole pratiche possono essere utili per decidere quale tipo usare:

  • Usa un tipo unsigned quando sai che i valori non possono essere negativi.
  • Usa int per l'aritmetica intera. short è di solito troppo piccolo e, in pratica, long spesso ha la stessa dimensione di int. Se i tuoi dati sono più grandi della dimensione minima garantita di un int, allora usa long long.
  • Non usare char semplice o bool in espressioni aritmetiche. Usali solo per contenere caratteri o valori di verità. I calcoli con char sono particolarmente problematici perché char è signed su alcune macchine e unsigned su altre. Se hai bisogno di un intero piccolo, specifica esplicitamente signed char o unsigned char.
  • Usa double per i calcoli in virgola mobile; float di solito non ha abbastanza precisione, e il costo dei calcoli in doppia precisione rispetto a quelli in singola precisione è trascurabile. Infatti, su alcune macchine, le operazioni in doppia precisione sono più veloci di quelle in singola. La precisione offerta da long double di solito non è necessaria e spesso comporta un notevole costo a runtime.

Esercizi

  • Quali sono le differenze tra int, long, long long e short? Tra un tipo unsigned e uno signed? Tra un float e un double?

    Soluzione:

    • short è garantito essere almeno 16 bit, int almeno 16 bit, long almeno 32 bit, e long long almeno 64 bit. Un tipo signed può rappresentare sia numeri positivi che negativi, mentre un tipo unsigned può rappresentare solo numeri positivi (incluso lo zero). Un float è un numero in virgola mobile a precisione singola, mentre un double è un numero in virgola mobile a precisione doppia.
  • Per calcolare una rata di mutuo, quali tipi useresti per tasso, capitale e rata? Spiega perché hai scelto ciascun tipo.

    Soluzione:

    • Per il tasso, userei double perché i tassi di interesse spesso includono cifre decimali e richiedono una precisione elevata. Per il capitale, userei long long se il mutuo è molto grande, altrimenti int potrebbe essere sufficiente, dato che il capitale è un valore intero. Per la rata, userei double perché la rata mensile può includere cifre decimali e richiede precisione.

Conversioni di Tipo

Il tipo di un oggetto definisce i dati che un oggetto può contenere e quali operazioni quell'oggetto può eseguire. Tra le operazioni che molti tipi supportano c'è la possibilità di convertire oggetti di un dato tipo in altri tipi correlati.

Le conversioni di tipo avvengono automaticamente quando usiamo un oggetto di un tipo dove ci si aspetta un oggetto di un altro tipo. Torneremo sulle conversioni di tipo nelle prossime lezioni, ma per ora è utile capire cosa succede quando assegniamo un valore di un tipo a un oggetto di un altro tipo.

Quando assegniamo un tipo aritmetico a un altro:

bool b = 42;             // b è true
int i = b;               // i ha valore 1
i = 3.14;                // i ha valore 3
double pi = i;           // pi ha valore 3.0
unsigned char c = -1;    // supponendo char da 8 bit, c ha valore 255
signed char c2 = 256;    // supponendo char da 8 bit, il valore di c2 è indefinito

cosa succede dipende dall'intervallo di valori che i tipi permettono:

  • Quando assegniamo uno dei tipi aritmetici non bool a un oggetto bool, il risultato è false se il valore è 0 e true altrimenti.
  • Quando assegniamo un bool a uno degli altri tipi aritmetici, il valore risultante è 1 se il bool è true e 0 se il bool è false.
  • Quando assegniamo un valore in virgola mobile a un oggetto di tipo integrale, il valore viene troncato. Il valore memorizzato è la parte prima del punto decimale.
  • Quando assegniamo un valore integrale a un oggetto di tipo in virgola mobile, la parte frazionaria è zero. La precisione può andare persa se l'intero ha più bit di quanti il tipo in virgola mobile possa contenere.
  • Se assegniamo un valore fuori intervallo a un oggetto di tipo unsigned, il risultato è il resto del valore modulo il numero di valori che il tipo di destinazione può contenere. Per esempio, un unsigned char da 8 bit può contenere valori da 0 a 255, inclusi. Se assegniamo un valore fuori da questo intervallo, il compilatore assegna il resto di quel valore modulo 256. Quindi, assegnare –1 a un unsigned char da 8 bit dà a quell'oggetto il valore 255.
  • Se assegniamo un valore fuori intervallo a un oggetto di tipo signed, il risultato è indefinito. Il programma potrebbe sembrare funzionare, potrebbe andare in crash, o potrebbe produrre valori spazzatura.

Il compilatore applica queste stesse conversioni di tipo quando usiamo un valore di un tipo aritmetico dove ci si aspetta un valore di un altro tipo aritmetico. Per esempio, quando usiamo un valore non bool come condizione, il valore aritmetico viene convertito in bool nello stesso modo in cui sarebbe convertito se lo avessimo assegnato a una variabile bool:

int intero = 42;

// la condizione sarà valutata come true
if (intero)
    intero = 0;

Se il valore è 0, allora la condizione è false; tutti gli altri valori (diversi da zero) producono true.

Allo stesso modo, quando usiamo un bool in un'espressione aritmetica, il suo valore viene sempre convertito in 0 o 1. Di conseguenza, usare un bool in un'espressione aritmetica è quasi sicuramente scorretto.

Consiglio

Evitare Comportamenti Indefiniti e Dipendenti dall'Implementazione

Il comportamento indefinito deriva da errori che il compilatore non è tenuto (e a volte non è in grado) a rilevare. Anche se il codice compila, un programma che esegue un'espressione indefinita è errato.

Sfortunatamente, i programmi che contengono comportamenti indefiniti possono sembrare funzionare correttamente in alcune circostanze e/o su alcuni compilatori. Non c'è garanzia che lo stesso programma, compilato con un compilatore diverso o anche una versione successiva dello stesso compilatore, continuerà a funzionare correttamente. Né c'è garanzia che ciò che funziona con un insieme di input funzionerà con un altro.

Allo stesso modo, i programmi di solito dovrebbero evitare comportamenti dipendenti dall'implementazione, come assumere che la dimensione di un int sia un valore fisso e noto. Tali programmi sono detti non portabili.

Quando il programma viene spostato su un'altra macchina, il codice che si basava su comportamenti dipendenti dall'implementazione potrebbe fallire. Individuare questi tipi di problemi in programmi precedentemente funzionanti è, per usare un eufemismo, spiacevole.

Espressioni che Coinvolgono Tipi unsigned

Anche se è improbabile che assegniamo intenzionalmente un valore negativo a un oggetto di tipo unsigned, possiamo (troppo facilmente) scrivere codice che lo fa implicitamente. Per esempio, se usiamo sia valori unsigned che int in un'espressione aritmetica, il valore int viene normalmente convertito in unsigned. Convertire un int in unsigned avviene nello stesso modo in cui assegneremmo l'int a un oggetto unsigned:

unsigned u = 10;
int i = -42;

// stampa -84
std::cout << i + i << std::endl;

// se int a 32 bit, stampa 4294967264
std::cout << u + i << std::endl;

Nella prima espressione, sommiamo due valori int (negativi) e otteniamo il risultato atteso. Nella seconda espressione, il valore int, -42, viene convertito in unsigned prima che l'addizione venga eseguita. Convertire un numero negativo in unsigned si comporta esattamente come se avessimo tentato di assegnare quel valore negativo a un oggetto unsigned. Il valore "va in overflow" come descritto sopra.

Indipendentemente dal fatto che uno o entrambi gli operandi siano unsigned, se sottraiamo un valore da un unsigned, dobbiamo essere sicuri che il risultato non possa essere negativo:

unsigned u1 = 42, u2 = 10; 

// OK: risultato è 32
std::cout << u1 - u2 << std::endl;

// OK: ma il risultato va in overflow
std::cout << u2 - u1 << std::endl;

Il fatto che un unsigned non possa essere minore di zero influisce anche su come scriviamo i cicli. Ad esempio, consideriamo il seguente ciclo che stampa i numeri da 10 a 0:

for (int i = 10; i >= 0; --i)
    std::cout << i << std::endl;

Potremmo pensare di poter riscrivere questo ciclo usando un unsigned. Dopotutto, non intendiamo stampare numeri negativi. Tuttavia, questo semplice cambiamento di tipo significa che il nostro ciclo non terminerà mai:

// ERRATO: u non può mai essere minore di 0;
// la condizione sarà sempre vera!!!
for (unsigned u = 10; u >= 0; --u)
    std::cout << u << std::endl;

Considera cosa succede quando u è 0. In quella iterazione, stamperemo 0 e poi eseguiremo l'espressione nel ciclo for. Quell'espressione, --u, sottrae 1 da u. Quel risultato, -1, non può essere rappresentato in un unsigned. Come per qualsiasi altro valore fuori intervallo, -1 verrà trasformato in un valore unsigned. Supponendo int a 32 bit, il risultato di --u, quando u è 0, è 4294967295.

Un modo per scrivere questo ciclo è usare un while invece di un for. Usare un while ci permette di decrementare prima (anziché dopo) aver stampato il valore:

// inizia il ciclo uno oltre il primo elemento che vogliamo stampare
unsigned u = 11;

while (u > 0) {
    // decrementa prima, così che l'ultima iterazione stamperà 0
    --u;
    std::cout << u << std::endl;
}

Questo ciclo inizia decrementando il valore della variabile di controllo del ciclo. Nell'ultima iterazione, u sarà 1 all'ingresso del ciclo. Decrementeremo quel valore, il che significa che stamperemo 0 in questa iterazione. Quando testeremo nuovamente u nella condizione del while, il suo valore sarà 0 e il ciclo terminerà. Poiché iniziamo decrementando u, dobbiamo inizializzare u a un valore uno maggiore del primo valore che vogliamo stampare. Quindi, inizializziamo u a 11, così che il primo valore stampato sia 10.

Nota

Attenzione a mescolare tipi signed e unsigned nelle espressioni

Espressioni che mescolano valori signed e unsigned possono produrre risultati sorprendenti quando il valore signed è negativo.

È essenziale ricordare che i valori signed sono automaticamente convertiti in unsigned. Per esempio, in un'espressione come a * b, se a è -1 e b è 1, allora se entrambi a e b sono int, il valore è, come atteso, -1. Tuttavia, se a è int e b è unsigned, allora il valore di questa espressione dipende da quanti bit ha un int sulla particolare macchina. Su una macchina a 32 bit, il valore di a viene convertito in unsigned, producendo 4294967295. Il risultato dell'espressione è quindi 4294967295.

Esercizi

  • Quale output produrrà il seguente codice?

    unsigned u = 10, u2 = 42;
    std::cout << u2 - u << std::endl;
    std::cout << u - u2 << std::endl;
    int i = 10, i2 = 42;
    std::cout << i2 - i << std::endl;
    std::cout << i - i2 << std::endl;
    std::cout << i - u << std::endl;
    std::cout << u - i << std::endl;
    

    Soluzione:

    Il codice produrrà il seguente output:

    32
    4294967264
    32
    -32
    0
    0
    

    Spiegazione:

    • u2 - u = 42 - 10 = 32
    • u - u2 = 10 - 42 = (in unsigned) 4294967264 (se int è a 32 bit)
    • i2 - i = 42 - 10 = 32
    • i - i2 = 10 - 42 = -32
    • i - u = 10 - 10 = 0
    • u - i = 10 - 10 = 0

Letterali

Un valore, come 42, è noto come letterale perché il suo valore è auto-evidente. Inoltre, il valore non può essere cambiato e viene fissato nel programma. I letterali sono anche chiamati costanti letterali o valori letterali.

Ogni letterale ha un tipo. La forma e il valore di un letterale determinano il suo tipo. Vediamo i tipi di letterali più comuni.

Letterali Interi e in Virgola Mobile

Possiamo scrivere un letterale intero usando la notazione decimale, ottale o esadecimale.

I letterali interi che iniziano con 0 (zero) sono interpretati come ottali. Quelli che iniziano con 0x o 0X sono interpretati come esadecimali. Per esempio, possiamo scrivere il valore 20 in uno qualsiasi dei seguenti tre modi:

20   /* decimale */
024  /* ottale */
0x14 /* esadecimale */

Il tipo di un letterale intero dipende dal suo valore e dalla notazione. Di default, i letterali decimali sono signed mentre i letterali ottali ed esadecimali possono essere tipi signed o unsigned.

Un letterale decimale ha il tipo più piccolo tra int, long o long long (cioè, il primo tipo in questa lista) che riesce a contenere il valore del letterale.

I letterali ottali ed esadecimali hanno il tipo più piccolo tra int, unsigned int, long, unsigned long, long long o unsigned long long che riesce a contenere il valore del letterale.

È un errore usare un letterale troppo grande per entrare nel tipo più grande correlato. Non esistono letterali di tipo short. Vedremo tra poco che possiamo forzare questi comportamenti di default usando un suffisso.

Sebbene i letterali interi possano essere memorizzati in tipi signed, tecnicamente parlando, il valore di un letterale decimale non è mai un numero negativo. Se scriviamo quello che sembra essere un letterale decimale negativo, per esempio, -42, il segno meno non fa parte del letterale. Il segno meno è un operatore che nega il valore del suo operando (letterale).

I letterali in virgola mobile includono un punto decimale o un esponente specificato usando la notazione scientifica. Usando la notazione scientifica, l'esponente è indicato da E o e:

3.14159
3.14159E0
0.
0e0
.001

Di default, i letterali in virgola mobile hanno tipo double. Possiamo forzare il tipo usando un suffisso come mostrato più avanti.

Letterali Carattere e Stringa di Caratteri

Un carattere racchiuso tra apici singoli è un letterale di tipo char. Zero o più caratteri racchiusi tra virgolette doppie sono un letterale stringa:

// letterale carattere
'a'

// letterale stringa
"Ciao, come stai?"

Il tipo di un letterale stringa è array di char costanti, un tipo che discuteremo nelle prossime lezioni. Il compilatore aggiunge un carattere terminatore ('\0') a ogni letterale stringa. Quindi, la dimensione effettiva di un letterale stringa è uno in più rispetto alla sua dimensione apparente. Per esempio, il letterale 'A' rappresenta il singolo carattere A, mentre il letterale stringa "A" rappresenta un array di due caratteri, la lettera A e il carattere terminatore.

Due letterali stringa che appaiono adiacenti tra loro e che sono separati solo da spazi, tabulazioni o nuove linee sono concatenati in un unico letterale. Usiamo questa forma di letterale quando dobbiamo scrivere un letterale che altrimenti sarebbe troppo grande per stare comodamente su una sola riga:

// letterale stringa su più righe
std::cout << "una stringa letterale davvero, davvero lunga "
             "che si estende su due righe" << std::endl;

Sequenze di Escape

Alcuni caratteri, come il backspace o i caratteri di controllo, non hanno una rappresentazione visibile. Tali caratteri sono non stampabili. Altri caratteri (apici singoli e doppi, punto interrogativo e backslash) hanno un significato speciale nel linguaggio. I nostri programmi non possono usare direttamente nessuno di questi caratteri. Invece, usiamo una sequenza di escape per rappresentare tali caratteri. Una sequenza di escape inizia con un backslash. Il linguaggio definisce diverse sequenze di escape:

Sequenza di Escape Significato
\n Nuova riga
\t Tab orizzontale
\a Campanella (bell)
\v Tab verticale
\b Backspace
\" Doppio apice
\\ Backslash
\? Punto interrogativo
\' Apice singolo
\r Ritorno carrello
\f Formfeed
Tabella 2: Sequenze di Escape Definite dal Linguaggio C++

Usiamo una sequenza di escape come se fosse un singolo carattere:

// stampa una nuova linea
std::cout << '\n';

// stampa una tabulazione seguita da "Ciao!" e una nuova linea
std::cout << "\tCiao!\n";

Possiamo anche scrivere una sequenza di escape generalizzata, che è composta da \x seguita da una o più cifre esadecimali o un backslash \ seguita da una, due o tre cifre ottali. Il valore rappresenta il valore numerico del carattere. Alcuni esempi (supponendo il set di caratteri Latin-1):

'\7'          // campanella (bell), valore ottale 7
'\x7'         // campanella (bell), valore esadecimale 7
'\12'         // nuova linea, valore ottale 10
'\xA'         // nuova linea, valore esadecimale A
'\115'        // lettera M, valore ottale 77
'\x4D'        // lettera M, valore esadecimale 4D

Come con una sequenza di escape definita dal linguaggio, usiamo queste sequenze di escape come faremmo con qualsiasi altro carattere:

// stampa Ciao MONDO! seguito da una nuova linea
std::cout << "Ciao \x4dO\x4eDO!\n";

// stampa M seguito da una nuova linea
std::cout << '\115' << '\n';

Nota che se un backslash \ è seguita da più di tre cifre ottali, solo le prime tre sono associate alla \. Per esempio:

"\1234"

rappresenta due caratteri: il carattere rappresentato dal valore ottale 123 e il carattere 4. Al contrario, \x usa tutte le cifre esadecimali che seguono:

"\x1234"

rappresenta un singolo carattere a 16 bit composto dai bit corrispondenti a queste quattro cifre esadecimali. Poiché la maggior parte delle macchine ha char da 8 bit, tali valori sono improbabili da usare. Di solito, caratteri esadecimali con più di 8 bit sono usati con set di caratteri estesi usando uno dei prefissi che vedremo.

Specificare il Tipo di un Letterale

Possiamo sovrascrivere il tipo predefinito di un letterale intero, in virgola mobile o carattere fornendo un suffisso o prefisso come elencato nelle tabelle che seguono:

Prefisso Significato Tipo
u letterale carattere Unicode a 16 bit char16_t
U letterale carattere Unicode a 32 bit char32_t
L letterale carattere wide wchar_t
u8 letterale stringa UTF-8 array di char costanti
Tabella 3: Prefissi per Letterali Carattere e Stringa
Suffisso Significato Tipo
u o U letterale intero unsigned unsigned int, unsigned long, o unsigned long long
l o L letterale intero long long o long long
ll o LL letterale intero long long long long
f o F letterale in virgola mobile singola precisione float
l o L letterale in virgola mobile estesa precisione long double
Tabella 4: Suffissi per Letterali Interi e in Virgola Mobile

Ad esempio:

L'a'        // letterale carattere wide, tipo è wchar_t
u8"ciao"    // letterale stringa utf-8 (utf-8 codifica un carattere Unicode in 8 bit)
42ULL       // letterale intero unsigned, tipo è unsigned long long
1E-3F       // letterale virgola mobile singola precisione, tipo è float
3.14159L    // letterale virgola mobile estesa precisione, tipo è long double

Possiamo specificare indipendentemente il segno e la dimensione di un letterale intero. Se il suffisso contiene una U, allora il letterale ha un tipo unsigned, quindi un letterale decimale, ottale o esadecimale con suffisso U ha il tipo più piccolo tra unsigned int, unsigned long o unsigned long long in cui il valore del letterale entra. Se il suffisso contiene una L, allora il tipo del letterale sarà almeno long; se il suffisso contiene LL, allora il tipo del letterale sarà long long o unsigned long long. Possiamo combinare U con L o LL. Per esempio, un letterale con suffisso UL sarà unsigned long o unsigned long long, a seconda che il suo valore entri in unsigned long.

Consiglio

Usare il suffisso L anziché l

Quando specifichiamo il tipo di un letterale intero long, conviene sempre usare il suffisso L maiuscolo piuttosto che l minuscolo. La lettera minuscola l può essere facilmente confusa con il numero uno (1), specialmente in alcuni tipi di font usati negli editor del codice.

Letterali Booleani e Letterale Puntatore

Le parole true e false sono letterali di tipo bool:

bool test = false;

La parola nullptr è un letterale puntatore. Diremo di più su puntatori e nullptr nelle prossime lezioni.

Esercizi

  • Determina il tipo di ciascuno dei seguenti letterali. Spiega le differenze tra i letterali in ciascuno dei quattro esempi:

    'a', L'a', "a", L"a"
    10, 10u, 10L, 10uL, 012, 0xC
    3.14, 3.14f, 3.14L
    10, 10u, 10., 10e-2
    

    Soluzione:

    • 'a' è di tipo char, L'a' è di tipo wchar_t, "a" è di tipo array di char costanti, e L"a" è di tipo array di wchar_t costanti.
    • 10 è di tipo int, 10u è di tipo unsigned int, 10L è di tipo long, 10uL è di tipo unsigned long, 012 è di tipo int (valore ottale 10), e 0xC è di tipo int (valore esadecimale 12).
    • 3.14 è di tipo double, 3.14f è di tipo float, e 3.14L è di tipo long double.
    • 10 è di tipo int, 10u è di tipo unsigned int, 10. è di tipo double, e 10e-2 è di tipo double.
  • Quali, se presenti, sono le differenze tra le seguenti definizioni:

    int mese = 9, giorno = 7; 
    int mese = 09, giorno = 07;
    

    Soluzione:

    La prima definizione è corretta e assegna i valori decimali 9 e 7 alle variabili mese e giorno. La seconda definizione è errata perché 09 e 07 sono interpretati come numeri ottali (a causa del prefisso 0), ma 9 non è un numero ottale valido. Quindi, la seconda definizione causerà un errore di compilazione.

  • Quali valori rappresentano questi letterali? Che tipo ha ciascuno?

    "Ciao \x4dONDO!"
    3.14e1L
    1024f
    3.14L
    

    Soluzione:

    • "Ciao \x4dONDO!" rappresenta la stringa "Ciao MONDO!" e il suo tipo è array di char costanti.
    • 3.14e1L rappresenta il valore 31.4 e il suo tipo è long double.
    • 1024f rappresenta il valore 1024.0 e il suo tipo è float.
    • 3.14L rappresenta il valore 3.14 e il suo tipo è long double.
  • Usando sequenze di escape, scrivi un programma che stampi 2M seguito da una nuova linea. Modifica il programma per stampare 2, poi una tabulazione, poi una M, seguita da una nuova linea.

    Soluzione:

    #include <iostream>
    
    int main() {
        std::cout << "\x32\x4d\n";
        std::cout << "\x32\t\x4d\n";
        return 0;
    }