Lezione 4 di 7 · 20 min di lettura
Variabili e tipi
Come un programma ricorda le cose. Numeri interi, decimali, vero/falso, testo, e le regole per dare i nomi.
Una scatola con un’etichetta
Un programma che stampa sempre la stessa frase non è molto utile. Per fare qualcosa di interessante deve ricordare dei valori e lavorarci sopra. Il posto dove un programma ricorda le cose si chiama variabile.
Immagina una scatola con un’etichetta. L’etichetta è il nome, dentro c’è il valore, e la forma della scatola è il tipo: una scatola per numeri interi non può contenere del testo.
int age = 34;Leggila così: “creo una scatola di tipo int (numero intero), la chiamo age, e ci metto dentro 34”. Da questo momento, ogni volta che scrivi age nel codice, il programma usa quel valore.
import std::io;
fn void main()
{
int age = 34;
io::printn(age);
age = 35; // change the box's contents
io::printn(age);
}34
35Nota la seconda assegnazione: age = 35; senza int davanti. Il tipo si dichiara una volta sola, quando la variabile nasce. Dopo, si usa solo il nome. (Scrivere int age = 35; una seconda volta sarebbe un errore: “variabile già dichiarata”.)
Dettagli nerd Dove sta, fisicamente, la scatola?
Nella RAM, la memoria di lavoro del computer. Immaginala come una lunghissima fila di caselle numerate, ognuna grande un byte, cioè 8 bit (8 cifre binarie, ognuna 0 o 1). Il numero di ogni casella è il suo indirizzo. Un int occupa 4 byte consecutivi: quando scrivi int age = 34;, il compilatore riserva 4 caselle e si annota che age è il nome di quelle caselle. Il nome esiste solo nel tuo codice: nell’eseguibile restano soltanto indirizzi e numeri.
La RAM è veloce ma volatile: spegni il computer e tutto sparisce. Quello che deve sopravvivere va scritto su disco, in un file.
Zero di default
Cosa c’è in una variabile appena creata, se non le assegni niente?
int counter;
io::printn(counter);0In C3 le variabili nascono azzerate. Sempre. Sembra ovvio, ma non lo è affatto: in C una variabile locale non inizializzata contiene spazzatura (qualsiasi cosa ci fosse in quella zona di memoria prima), ed è la fonte di una quantità impressionante di bug. C3 ha chiuso la porta a questa categoria di errori.
Dettagli nerd Cos'è un buffer?
Un buffer è una zona di memoria riservata per parcheggiare temporaneamente dei dati mentre passano da un posto a un altro: i byte letti da un file prima di elaborarli, il testo da stampare prima di spedirlo al terminale, i campioni audio prima di mandarli alle casse. int[1000] buffer qui sopra è proprio questo: 1000 interi uno dopo l’altro (4000 byte), pronti a essere riempiti.
Il nome viene dall’inglese buffer, “cuscinetto”: fa da ammortizzatore tra chi produce i dati e chi li consuma, che raramente vanno alla stessa velocità. Il video in streaming che “carica” qualche secondo in anticipo? Sta riempiendo un buffer. E azzerare 4000 byte costa poco, ma non zero: se stai per sovrascriverli tutti comunque, @noinit ti risparmia un lavoro inutile.
I tipi numerici interi
int è il tipo intero “normale”: 32 bit, quindi va da circa −2 miliardi a +2 miliardi. Basta quasi sempre. Quando non basta, ci sono i suoi fratelli:
| Tipo | Bit | Intervallo | Quando usarlo |
|---|---|---|---|
int | 32 | −2 147 483 648 … 2 147 483 647 | Il default. Contatori, indici, quantità. |
long | 64 | ±9,2 trilioni | Numeri grandi: timestamp, byte di un disco, popolazione mondiale al quadrato. |
short | 16 | −32 768 … 32 767 | Raro. Quando la memoria conta davvero. |
ichar | 8 | −128 … 127 | Raro. Un byte con segno. |
int128 | 128 | Enorme | Crittografia, matematica esotica. |
E per ogni tipo con segno esiste il gemello senza segno (solo positivi, ma con il doppio dello spazio in alto): uint, ulong, ushort, char, uint128.
Dettagli nerd Perché proprio −2 147 483 648?
Con 32 bit si possono formare 2³² combinazioni diverse di 0 e 1, cioè 4 294 967 296 valori. Un uint le usa tutte per i numeri da 0 a 4 294 967 295. Un int deve fare posto anche ai negativi e le divide a metà: 2³¹ valori negativi (da −2 147 483 648 a −1) e 2³¹ valori da zero in su (da 0 a 2 147 483 647). Ecco perché il lato negativo ha un numero in più: lo zero “ruba” un posto ai positivi.
Il trucco con cui i processori rappresentano i negativi si chiama complemento a due: il bit più a sinistra non vale “più 2³¹” ma “meno 2³¹”. Il vantaggio è enorme: somma e sottrazione usano lo stesso circuito, senza distinguere tra positivi e negativi. Lo rivedremo nella prossima lezione, quando un numero “traboccherà”.
Sì, hai letto bene: char in C3 è un intero senza segno a 8 bit (0…255). Si chiama così per tradizione, perché un byte è spesso usato per rappresentare un carattere, ma il compilatore lo tratta come un numero. Ci torniamo tra poco.
Scrivere i numeri
Un numero scritto nel codice si chiama letterale. C3 ne accetta parecchie forme:
int decimal = 1_000_000; // the underscore is ignored: it's just for readability
int hex = 0xFF; // 255, base 16
int binary = 0b1010; // 10, base 2
int octal = 0o755; // 493, base 8
long big = 3_000_000_000; // doesn't fit in an int, but does in a longI trattini bassi sono un regalo: 1_000_000 si legge al volo, 1000000 no.
Dettagli nerd Perché i programmatori amano l'esadecimale?
Il computer ragiona in binario, ma 0b11111111 è faticoso da leggere. L’esadecimale (base 16, con le cifre 0-9 e a-f) è il compromesso perfetto: una cifra esadecimale corrisponde esattamente a 4 bit, quindi un byte si scrive sempre con due cifre. 0xFF = 0b1111_1111 = 255, il valore più alto che sta in un byte.
Per questo lo vedrai ovunque si parli di memoria: indirizzi, contenuti dei file, colori delle pagine web. #2563eb, il blu del logo di C3, sono tre byte: rosso 25, verde 63, blu eb.
Quiz
Cosa succede compilando int x = 3_000_000_000;?
I numeri con la virgola
Per i numeri decimali (con la virgola, che nel codice è un punto) ci sono due tipi:
| Tipo | Bit | Precisione | Quando usarlo |
|---|---|---|---|
double | 64 | ~15 cifre significative | Il default. Quasi sempre. |
float | 32 | ~7 cifre significative | Grafica, audio, quando la memoria conta. |
double pi = 3.14159;
double avogadro = 6.022e23; // scientific notation: 6.022 × 10²³
float weight = 72.5f; // the f suffix says "this is a float"
double int_as_double = 7; // an int can become a double with no problemDettagli nerd Come si scrive 3,14 con solo 0 e 1?
Con una specie di notazione scientifica in base 2, definita da uno standard chiamato IEEE 754 che usano praticamente tutti i processori. Un double divide i suoi 64 bit in tre parti: 1 bit per il segno, 11 per l’esponente (di quanto spostare la virgola) e 52 per la mantissa (le cifre significative). È come scrivere 6,022 × 10²³, ma con le potenze di 2.
La conseguenza: molti numeri “tondi” in base 10 in base 2 hanno infinite cifre, esattamente come 1/3 = 0,333… in base 10. 0.1 è uno di questi, e il double ne conserva la migliore approssimazione possibile: stampato con 20 decimali, è 0.10000000000000000555. Nella lezione 6 vedremo quando questo ti morde.
Vero o falso: bool
Il tipo bool ha solo due valori possibili: true e false. Sembra poco, ma è il tipo con cui il programma decide: “l’utente ha vinto?”, “il file esiste?”, “il numero è pari?“.
bool is_raining = true;
bool has_umbrella = false;
io::printn(is_raining);trueVedremo nella prossima lezione che i confronti (5 > 3) producono proprio un bool, e nel Modulo 2 che if lo usa per scegliere quale strada prendere.
Testo: String
Il testo si tiene in una String:
String name = "Ada";
io::printn(name);
io::printn(name.len);Ada
3.len è la lunghezza della stringa, cioè quanti byte contiene. (Per il testo con accenti la faccenda è più sottile: "caffè" ha 6 byte, non 5, perché la è ne occupa due in UTF-8. Ci torneremo; per ora usa .len su testo semplice.)
Dettagli nerd Byte, caratteri e UTF-8
Un computer non conosce le lettere, solo i numeri. Una codifica è la tabella che dice quale numero corrisponde a quale carattere. La più antica ancora in uso, l’ASCII (1963), assegna i numeri da 0 a 127 alle lettere inglesi, alle cifre e alla punteggiatura: A è 65, a è 97, lo spazio è 32. Un byte per carattere, e tutto torna.
Ma l’ASCII non ha la è, né il €, né gli ideogrammi, né le emoji. UTF-8, la codifica usata oggi dal web e da C3, risolve con un’idea furba: i caratteri ASCII restano identici, un byte ciascuno, mentre tutti gli altri usano da 2 a 4 byte. La è diventa i due byte c3 a8 in esadecimale (e no, quel c3 è solo una coincidenza). Ecco perché "caffè".len fa 6.
Dentro le virgolette puoi usare delle sequenze di escape, che iniziano con \:
| Scrivi | Ottieni |
|---|---|
\n | a capo |
\t | tabulazione |
\" | una virgoletta doppia |
\\ | una barra rovesciata |
io::printn("Line one\nLine two");
io::printn("They said: \"hi\"");Line one
Line two
They said: "hi"Se gli escape ti danno fastidio (percorsi Windows, espressioni regolari), esistono le stringhe grezze tra apici inversi, dove \ è solo una barra:
String path = `C:\Users\ada\file.txt`;Un singolo carattere: char
Un carattere tra apici singoli, 'A', è un letterale di tipo char. E come abbiamo detto, char è un numero: 'A' vale 65, secondo la tabella ASCII che assegna un numero a ogni simbolo.
char letter = 'A';
io::printn(letter);
io::printfn("%c", letter);65
ALa seconda riga usa printfn con %c, che dice “trattalo come carattere, non come numero”. Impareremo printfn per bene nella lezione 6; per ora ti basta il concetto: un char è un byte, e come lo stampi dipende da te.
Quiz
Qual è la differenza tra 'A' e "A"?
Costanti: valori che non cambiano
Alcuni valori non devono mai cambiare durante l’esecuzione: il numero massimo di giocatori, la velocità della luce, l’IVA. Per questi c’è const:
const int MAX_PLAYERS = 4;
const double VAT = 0.22;
const GAME_NAME = "Briscola"; // the type can be omitted: it's inferred from the valueLe costanti si scrivono in MAIUSCOLO_CON_TRATTINI. Non è un vezzo: è una regola del linguaggio, e il compilatore rifiuta const int max = 4;. Il vantaggio è che, leggendo il codice, sai a colpo d’occhio cosa è una costante e cosa no.
Provare a modificarla è un errore di compilazione:
MAX_PLAYERS = 5; // error: you can't assign to a constantLe regole dei nomi
C3 è severo sui nomi, e questa severità è un dono: guardando un nome sai cos’è.
| Cosa | Regola | Esempi validi |
|---|---|---|
| Variabili, funzioni, parametri | Iniziano con minuscola | age, user_name, countPoints |
Tipi (struct, enum, …) | Iniziano con Maiuscola e contengono almeno una minuscola | Player, String, ShoppingList |
| Costanti | Tutto MAIUSCOLO | MAX_LIVES, PI |
| Moduli | Tutto minuscolo, con _ | std::io, my_game |
In tutti i casi: solo lettere a-z, A-Z, cifre 0-9 e _. Niente accenti, niente spazi, e non si può iniziare con una cifra.
Lo stile raccomandato (non obbligatorio, ma usato dalla libreria standard) è snake_case per variabili e funzioni: user_name, non userName. In questo corso seguiamo lo stile della libreria standard.
Quiz
Quale di queste dichiarazioni non compila?
Una dichiarazione alla volta
Un’ultima differenza dal C, piccola ma che evita malintesi: ogni variabile ha la sua riga.
int a, b = 4; // error in C3: what value does a have?int a;
int b = 4; // now it's clearMettiamo tutto insieme
import std::io;
const int CURRENT_YEAR = 2026;
fn void main()
{
String name = "Ada";
int birth_year = 1815;
double height = 1.65;
bool is_programmer = true;
char initial = 'A';
int age = CURRENT_YEAR - birth_year;
io::printn(name);
io::printn(age);
io::printn(height);
io::printn(is_programmer);
io::printfn("%c", initial);
}Ada
211
1.650000
true
AHai notato 1.650000? Quando stampi un double con printn, C3 usa sei cifre decimali. Nella lezione 6 vedremo come scegliere quante cifre mostrare.
Guarda la riga int age = CURRENT_YEAR - birth_year;: il valore iniziale di una variabile può essere un calcolo, non solo un numero scritto a mano. È il ponte verso la prossima lezione.
Esercizio · sul tuo computer
La tua scheda
Scrivi un programma profile.c3 con queste variabili, esattamente con questi valori e tipi:
- una
Stringanimalcon valore"Dog"; - un
intdog_agecon valore7; - un
inthuman_ageuguale adog_agemoltiplicato per7(l’operatore è*); - un
boolis_vaccinatedcon valoretrue.
Stampa ognuna delle quattro variabili con io::printn, una per riga, nell’ordine dato. Riporta qui l’output.
Mostra una soluzione (prima prova da solo!)
import std::io;
fn void main()
{
String animal = "Dog";
int dog_age = 7;
int human_age = dog_age * 7;
bool is_vaccinated = true;
io::printn(animal);
io::printn(dog_age);
io::printn(human_age);
io::printn(is_vaccinated);
}Ricapitolando
- Una variabile è una scatola con un nome, un tipo e un valore:
int age = 34;. - Le variabili nascono azzerate. Sempre.
- Interi:
int(default),long(grandi),char(un byte senza segno). Decimali:double(default),float. - Da intero a decimale si passa gratis; il contrario richiede un cast
(int). boolètrueofalse.Stringè testo tra virgolette.'A'è unchar, cioè un numero.constper i valori fissi, in MAIUSCOLO.- Nomi: minuscola per variabili e funzioni, Maiuscola per i tipi, MAIUSCOLO per le costanti.
Prossima lezione: facciamo i conti. Somme, divisioni (con una trappola famosa), confronti e le regole di precedenza che C3 ha cambiato rispetto al C.