3 Livello 1
Il presente capitolo introduce i costrutti fondamentali per la gestione dei dati nel linguaggio C. Vengono trattati in modo sistematico il concetto di variabile e la sua rappresentazione in memoria, le strategie per la definizione delle costanti, l’utilizzo degli operatori aritmetici e le caratteristiche dei tipi di dato semplici.
3.1 Le variabili
Ora sei pronto a fare un passo avanti e utilizzare le variabili. Per capire cosa siano, immaginale semplicemente come dei contenitori destinati a ospitare i dati che il tuo programma dovrà usare. Puoi fare riferimento a questo contenitore (un piccolo blocco della memoria RAM) usando un nome scelto da te. Sei libero di chiamare una variabile quasi come preferisci (ci sono solo pochissime regole sintattiche da rispettare), ma il consiglio è di scegliere sempre nomi significativi, che facciano capire immediatamente cosa c’è dentro.
In C prima di poter usare una variabile, devi obbligatoriamente dichiararla. La dichiarazione è un passaggio cruciale perché associa alla tua variabile un tipo. Quando assegni un tipo a una variabile, stai definendo due cose fondamentali:
- La dimensione del contenitore (quanti byte di memoria deve riservare a quella variabile).
- Le operazioni consentite (cosa puoi fare con quel dato).
Ricorda che la dimensione esatta in byte associata ad un tipo di dato non è fissa a priori, ma può dipendere dall’architettura hardware della macchina su cui viene eseguito il programma (ad esempio, se la CPU è a 32 o a 64 bit). Ecco un esempio di come si presenta una dichiarazione di variabile in C:
int number; // variabile di tipo intero di nome è numberCon questa istruzione stai dichiarando una variabile chiamata number come un numero intero (int). In sostanza viene riservato un piccolo blocco di memoria RAM per questa variabile (quasi certamente 4 byte) e, da questo momento in poi potrai accedere al contenuto di quel blocco di RAM semplicemente usando il nome della variabile.
In Figura 3.1, gli effetti in RAM della dichiarazione della variabile number.
number. Il ? indica il fatto che il valore contenuto nei 4 byte non è noto.
Il linguaggio C è case sensitive, significa che è sensibile alla differenza tra lettere maiuscole e lettere minuscole. I programmatori usano delle convenzioni standard per dare i nomi alle variabili. In C la più diffusa è lo snake_case, in cui le parole sono scritte in minuscolo e separate dal carattere underscore. In altri linguaggi è molto comune anche il camelCase (chiamato così perché le maiuscole in mezzo alla parola ricordano le gobbe di un cammello).
3.1.1 Variabili e dimensione
Quanto è grande questo blocco di memoria? Nella maggior parte delle architetture moderne la risposta è 4 byte, ma puoi verificarlo tu stesso direttamente sul tuo computer con un piccolo test:
#include <stdio.h>
int main(void) {
int number; // variabile di tipo intero
printf("\nUn intero occupa %zu byte", sizeof(number));
// In alternativa:
// printf("\nUn intero occupa %zu byte", sizeof(int));
return 0;
}In questo blocco di codice, sizeof ha il compito di determinare la dimensione fisica in byte occupata in memoria RAM dalla variabile number o, nell’alternativa commentata, direttamente dal tipo di dato int. Come puoi vedere, la funzione printf può essere usata anche per stampare il risultato di un’espressione. Per farlo, devi semplicemente indicare al C dove vuoi che il valore appaia all’interno della stringa di testo. Questo si fa inserendo uno specificatore di formato (%zu fa riferimento al tipo size_t intero senza segno definito dallo standard C per rappresentare la dimensione in byte di qualsiasi oggetto in memoria), dopodiché, ti basta indicare l’espressione o la variabile subito dopo la virgola.
3.1.2 Variabili e contenuto
Ma se number fa riferimento ad un blocco della RAM, qual è il suo contenuto? Proviamo a verificarlo.
#include <stdio.h>
int main(void) {
int number;
printf("Contenuto della variabile number: %d", number);
// %d è il segnaposto del valore contenuto nella variabile number
return 0;
}Se provi a compilare ed eseguire questo codice, potresti rimanere sorpreso. Il compilatore non si bloccherà e sul terminale potrebbe apparire un numero apparentemente senza senso (si genera quello che formalmente si chiama comportamento indefinito). Per capire cosa è successo, dobbiamo guardare di nuovo sotto il cofano. Quando hai scritto int number; è stata cercata nella RAM una porzione libera (da 4 byte) ed è stata usata per la variabile number. Quel numero che vedi a schermo non è altro che l’interpretazione come numero intero della sequenza di bit che già si trovavano in quella porzione di RAM. Questi valori “spazzatura” sono residui lasciati in quella zona di memoria dal tuo stesso programma.
Posso ovviamente accedere a quel blocco di RAM e modificarne il contenuto:
#include <stdio.h>
int main(void) {
int number = 111;
printf("Contenuto della variabile number: %d", number);
return 0;
}L’istruzione =, nota come assegnazione, consente di sovrascrivere (e quindi perdere definitivamente) il contenuto precedente. Da questo momento la variabile number contiene il valore intero 111, o meglio, il blocco di RAM di 4 byte che tu chiami number conterrà una sequenza di bit (32 bit) associabile a quel numero intero.
È di fondamentale importanza non confondere il simbolo = con il simbolo ==.
=(assegnamento): viene usato per inserire (e sovrascrivere) un valore dentro una variabile.==(confronto): viene usato per verificare un’uguaglianza, cioè per controllare se due variabili o espressioni hanno lo stesso valore.
Se usi per errore il simbolo = dove invece volevi usare ==, il compilatore non segnalerà alcun errore di sintassi. Il programma verrà generato ed eseguito normalmente, ma si comporterà in modo completamente diverso da ciò che ti aspetti.
3.1.3 Variabili e indirizzo
È possibile conoscere l’indirizzo di RAM che ospita il valore associato ad una particolare variabile? Partiamo da questo frammento di codice:
#include <stdio.h>
int main(void) {
int x, y;
x = 99;
printf("\n%d",x);
printf("\n%p",&x);
x = x + 1;
printf("\n%d",x);
printf("\n%p",&x);
y = x;
printf("\n%d",y);
printf("\n%p",&y);
return 0;
}Questo programma ci permette di “fotografare” la RAM in tre momenti diversi, svelando la differenza fondamentale tra cosa contiene una variabile e dove si trova fisicamente. Per farlo, introduciamo un nuovo strumento:
- L’operatore
&(address-of) anteposto al nome di una variabile, non legge il suo valore, ma chiede di restituire l’indirizzo di memoria.
Lo specificatore %p indica alla funzione printf che vogliamo stampare un indirizzo di memoria, che verrà mostrato sullo schermo in formato esadecimale (una sequenza di numeri e lettere, ad esempio 0x16b87f098).
Lo standard richiederebbe di convertire l’indirizzo al tipo generico void * prima di stamparlo, ovvero printf("%p", (void *)&x);. I compilatori comuni accettano anche la forma semplificata, ma è bene conoscere quella formalmente corretta.
Analizziamo cosa succede durante l’esecuzione. All’inizio dichiariamo due variabili, x e y. Il compilatore riserva due spazi distinti nella RAM. Quando scriviamo x = 99;, riempiamo il primo contenitore. Le prime due printf mostreranno 99 (il valore memorizzato) e 0x… (l’indirizzo fisico di x). L’istruzione x = x + 1; incrementa il valore di x, che diventa 100. Questa è un’operazione distruttiva e il vecchio 99 è perso per sempre. Le successive due printf ci mostrano che il valore è ora 100 e l’indirizzo di memoria è identico a quello di prima. Le variabili in C hanno un domicilio fisso. Nel corso del programma puoi cambiare il valore, ma il contenitore non si sposta mai dalla cella di memoria che gli è stata assegnata. L’assegnamento y = x crea una copia. Sappiamo che x in questo momento vale 100. Cosa succede nella RAM? Le ultime printf rivelano che la variabile y stampa il valore 100 e l’indirizzo di memoria di y è completamente diverso da quello di x. In sostanza, “il computer” è andato all’indirizzo di x, ha preso i 32 bit che componevano il numero 100, ed è andato a copiarli nell’indirizzo di y. Da questo momento in poi, x e y vivono vite separate. Se modifichi y, il valore di x non cambierà.
Il Python le cose sono molto diverse. A differenza del C, in Python una variabile è semplicemente un etichetta attaccata a un oggetto. In Python, le variabili non hanno un tipo, sono gli oggetti ad averlo. Questo cambio di filosofia spiega perché in Python non esiste la necessità di dichiarare le variabili e perché si ha la sensazione che una variabile possa cambiare tipo nello stesso programma.
In Python è perfettamente legale scrivere x = 1 e subito dopo x = "ciao". A seguito della prima istruzione viene creato nella RAM un oggetto di tipo intero che contiene il valore 1 e poi l’etichetta x viene attaccata a quell’oggetto. A seguito della seconda istruzione viene creato un nuovo oggetto, questa volta di tipo stringa, contenente il testo “ciao” e a questo punto, viene attaccata al nuovo oggetto di tipo stringa. L’etichetta x è rimasta identica ma è cambiato l’oggetto a cui fa riferimento.
3.2 Le costanti
Fino a questo momento abbiamo parlato di variabili, il cui valore può cambiare nel tempo. Spesso sorge l’esigenza di utilizzare valori che devono rimanere immutabili per tutta la durata del programma. In C esistono due strategie diverse per definire una costante: la parola chiave const e la direttiva al preprocessore #define.
L’utilizzo di const qualifica una variabile “in sola lettura”. Per esempio con const float PiGreco = 3.14; il valore viene assegnato in modo permanente e il compilatore impedirà qualsiasi tentativo di modifica. Dal punto di vista della memoria, si comportano come normali variabili in quanto hanno un tipo associato e rispettano le regole di visibilità (si dice che sono “scope-controlled”) che vedremo in seguito.
La direttiva al preprocessore #define è diversa, non definisce una variabile, ma è un’istruzione rivolta al preprocessore (ovvero a quel componente che analizza il codice sorgente prima che inizi la compilazione vera e propria). Per esempio con #define N 100 il preprocessore esegue un lavoro di “trova e sostituisci” su tutto il file di testo (ogni volta che incontra i simbolo N scrive al suo posto 100).
3.3 Operatori aritmetici
Il C mette a disposizione i classici operatori per effettuare calcoli numerici sulle variabili e sulle espressioni. Di seguito sono elencati gli operatori fondamentali:
+Addizione: calcola la somma tra due operandi.-Sottrazione: calcola la differenza tra due operandi.*Moltiplicazione: calcola il prodotto tra due operandi./Divisione: calcola il quoziente tra due operandi.%Modulo: calcola il resto della divisione intera.
Il comportamento dell’operatore di divisione / dipende dal tipo di dato degli operandi. Se entrambi gli operandi sono interi esegue una divisione intera troncando la parte decimale. Se almeno uno degli operandi è un numero a virgola mobile esegue una divisione reale.
L’operatore modulo % richiede invece operandi interi.
3.4 Tipi di dato
Torniamo ora al legame tra dichiarazioni e tipi. In C esistono pochi tipi fondamentali (chiamati anche tipi base). Più avanti vedremo che potrai definire dei nuovi tipi personalizzati, ma per adesso i mattoncini di partenza sono quattro:
int: utilizzato per i numeri interi (es. 10, -5, 0).float: utilizzato per i numeri reali a singola precisione (numeri con la virgola, es. 3.14).double: utilizzato per i numeri reali a doppia precisione (più precisi e capienti dei float).char: utilizzato per i singoli caratteri di testo (es. ‘A’, ‘b’, ‘?’).
La scelta di quale tipo assegnare a una variabile dipenderà esclusivamente dal contesto e dagli obiettivi del tuo programma. Ricorda che da questa decisione dipenderanno sempre due aspetti cruciali: le operazioni che potrai effettuare su quella variabile e la dimensione del blocco di memoria che il computer riserverà.
A ciascun tipo di dato è associato un preciso intervallo di valori rappresentabili. Questo limite è una conseguenza della quantità di memoria fisica che il compilatore alloca per quel tipo, la quale determina il numero massimo di configurazioni binarie distinte che l’hardware può assumere. La dimensione effettiva e l’intervallo associato dipendono dall’architettura del processore.
Per fissare le idee, ecco dimensioni su un moderno PC a 64 bit:
| Tipo | Dimensione tipica |
|---|---|
char |
1 byte |
int |
4 byte |
float |
4 byte |
double |
8 byte |
Esistono inoltre dei qualificatori che permettono di modificare dimensione e intervallo dei tipi interi: short e long agiscono sulla dimensione (es. short int, long int), mentre unsigned rinuncia ai numeri negativi per raddoppiare il massimo valore rappresentabile. I valori esatti validi per la tua piattaforma sono definiti negli header di sistema <limits.h> (per gli interi) e <float.h> (per i tipi a virgola mobile).