Classi di Storage in Linguaggio C
In questa lezione esploreremo le classi di storage in linguaggio C, un concetto fondamentale per comprendere come le variabili vengono gestite in memoria.
Le classi di storage permettono di definire la durata di vita, la visibilità o scope e il collegamento o linkage delle variabili, influenzando il loro comportamento durante l'esecuzione del programma.
Approfondiremo le quattro principali classi di storage: auto
, static
, extern
e register
, concentrandoci al caso delle variabili. In seguito, vedremo come queste classi si applicano anche alle funzioni. L'ultima classe di storage, thread_local
sarà trattata in una lezione successiva.
Proprietà delle variabili
Prima di addentrarci nello studio delle classi di storage, è necessario fare una premessa sulle proprietà delle variabili in C. Ogni variabile in C ha tre proprietà fondamentali:
-
Durata di vita:
Chiamata anche Durata di Storage, determina quando la memoria necessaria per la variabile viene allocata e quando viene rilasciata. Le variabili possono avere durata automatica, statica, dinamica o di thread.
Quando una variabile ha durata di vita automatica significa che la variabile viene allocata quando il programma entra nel blocco di codice in cui è dichiarata e rilasciata quando il programma esce dal blocco di codice. Tutte le variabili locali hanno sempre durata automatica. Questo perché, come abbiamo visto, esse vengono allocate sullo stack delle funzioni.
Quando, invece, una variabile ha durata di vita statica, la variabile viene allocata quando il programma inizia la sua esecuzione e rilasciata quando il programma termina. Le variabili globali, cioè le variabili dichiarate con il modificatore
static
, hanno durata statica.Una variabile può essere allocata anche dinamicamente sullo heap di un processo. In tal caso la durata di vita della variabile è dinamica ed è a carico del programmatore sia quando creare la variabile sia quando rilasciarla. In tal caso si dice che la variabile ha una durata di vita dinamica.
Nello standard C11, dal momento che è stato introdotto il supporto ai thread, è possibile dichiarare variabili con durata di vita di thread. Queste variabili vengono allocate quando il thread viene creato e rilasciate quando il thread termina. Studieremo questo argomento nelle prossime lezioni.
-
Lo scope o visibilità di una variabile è quella porzione di programma in cui la variabile è accessibile o comunque referenziabile.
In linguaggio C, una variabile può essere visibile a livello di blocco di codice oppure può essere visibile a livello di file e quindi una variabile globale.
-
Linkage o Collegamento:
Il linkage o collegamento di una variabile determina come la variabile è accessibile da altri file sorgente o porzioni di programma.
Una variabile può essere dichiarata con:
- linkage esterno: la variabile è accessibile da altri file sorgente dello stesso programma;
-
linkage interno: la variabile è accessibile solo all'interno del file sorgente in cui è dichiarata;
In tal caso la variabile è visibile solo all'interno del file sorgente e può essere utilizzata in tutte le funzioni di quel file. Un altro file sorgente può avere una variabile con linkage interno e con lo stesso nome. In questo caso, la seconda variabile verrà trattata come una variabile del tutto differente.
-
Linkage Nullo: la variabile appartiene ad una singola funzione o blocco di codice e non è accessibile da altre parti del programma.
In generale, le tre proprietà viste sopra dipendono dal punto in cui una variabile è dichiarata. Di default valgono le seguenti regole:
-
Se una variabile è dichiarata all'interno di un blocco di codice, compreso il corpo di una funzione, essa avrà le seguenti proprietà:
- Durata di vita automatica;
- Visibilità limitata al blocco di codice in cui è dichiarata;
- Linkage nullo.
-
Se una variabile è dichiarata, invece, all'esterno di qualunque blocco di codice, ossia al livello più esterno del programma, essa avrà le seguenti proprietà:
- Durata di vita statica;
- Visibilità a livello di file;
- Linkage esterno.
Prendiamo l'esempio seguente:
int x;
void f(void) {
int y;
}
In questo esempio, abbiamo la variabile y
che ha durata automatica, visibilità limitata alla funzione f
e linkage nullo. La variabile x
, invece, ha durata statica, visibilità a livello di file e linkage esterno.
Nella maggior parte dei casi, questo comportamento di default va bene. Tuttavia, in alcuni casi, potrebbe essere necessario specificare manualmente le proprietà di una variabile. Per fare ciò, C mette a disposizione le classi di storage. Esaminiamole nel dettaglio.
Classe di Storage auto
La classe di storage auto
è legale solo per variabili che vengono dichiarate all'interno di un blocco di codice.
Classe di Storage auto
In linguaggio C, una variabile dichiarata con classe di storage auto
ha le seguenti proprietà:
- Durata di vita automatica: la variabile viene allocata quando il programma entra nel blocco di codice in cui è dichiarata e rilasciata quando il programma esce dal blocco di codice;
- Visibilità limitata al blocco di codice in cui è dichiarata;
- Linkage nullo.
Dal momento che le variabili locali in C hanno già di default queste proprietà, l'utilizzo della classe di storage auto
è superfluo, infatti viene sempre omessa. Tuttavia, è possibile utilizzarla per rendere esplicito il comportamento di default.
void f(void) {
// In tal caso auto è superfluo
auto int x;
}
Dal momento che il comportamento di auto
è quello di default per le variabili locali, spesso le variabili locali vengono chiamate anche Variabili Automatiche.
Classe di Storage static
La classe di storage static
è legale sia per variabili locali che per variabili globali. Tuttavia, il suo effetto cambia a seconda del punto in cui la variabile è dichiarata.
- Se la variabile è dichiarata all'esterno di qualunque blocco di codice, quindi a livello di file, la classe
static
specifica che la variabile ha Linkage Interno. In altre parole, la variabile è visibile solo a livello del file in cui è dichiarata. - Se la variabile è dichiarata all'interno di un blocco di codice, la classe
static
specifica che la variabile ha Durata di Vita Statica. In altre parole, la variabile viene allocata quando il programma inizia la sua esecuzione e rilasciata quando il programma termina.
Vediamo un esempio:
static int x;
void f(void) {
static int y;
}
Quando dichiariamo una variabile al di fuori di qualunque blocco come static (nell'esempio la variabile x
), stiamo in sostanza nascondendo la variabile rispetto agli altri file sorgente. Solo le funzioni che appaiono nello stesso file, come nel caso della funzione f
, possono accedervi.
Ad esempio:
static int x;
void f1(void) {
/* f1 può accedere ad x */
}
void f2(void) {
/* f2 può accedere ad x */
}
Quando una variabile viene dichiarata all'interno di un blocco di codice come static
, la variabile ha durata statica, ossia risiederà sempre nella stessa locazione di memoria per tutta la durata del programma. Per cui, a differenza delle variabili automatiche, che perdono il loro valore ogni volta che il programma esce dal blocco in cui sono contenute, una variabile static
manterrà il suo valore indefinitamente.
Le variabili static
hanno alcune proprietà importanti:
- Una variabile
static
definita all'interno di un blocco viene inizializzata una volta sola, prima dell'inizio dell'esecuzione del programma. Viceversa, una variabile automatica viene inizializzata ogni volta che il blocco di codice in cui è contenuta viene eseguito a patto, ovviamente, di avere un inizializzatore. - Ogni volta che una funzione invoca se stessa in maniera ricorsiva, viene creato un nuovo insieme di variabili locali o automatiche. Se la funzione, però, ha una variabile
static
, questa verrà condivisa tra tutte le chiamate ricorsive. - Abbiamo visto che le funzioni non possono restituire un puntatore ad una variabile locale o automatica. Tuttavia, una funzione può restituire un puntatore ad una variabile
static
.
La potenza delle variabili static
sta nel fatto che possono essere utilizzate per mantenere lo stato di una funzione tra diverse chiamate. In altre parole, una funzione può salvare dei dati in una zona nascosta della memoria e recuperarli alla chiamata successiva. Questa zona non è accessibile da altre parti del programma.
Spesso le variabili static
sono adoperate per rendere i programmi più efficienti.
Prendiamo un esempio. Supponiamo di voler realizzare una funzione che prende in ingresso un valore compreso tra 0 e 15 e ne restituisca il corrispondente valore esadecimale. Una possibile implementazione è la seguente:
char cifra_esadecimale(int n) {
const char cifre[] = "0123456789ABCDEF";
return cifre[n];
}
In questa funzione, la cifra passata come argomento viene trasformata nell'equivalente esadecimale sfruttando un array di caratteri. Tuttavia, l'array cifre
viene creato ad ogni chiamata della funzione. Questo comporta uno spreco di risorse, in quanto l'array è sempre lo stesso e non cambia. Possiamo migliorare l'implementazione utilizzando una variabile static
:
char cifra_esadecimale(int n) {
static const char cifre[] = "0123456789ABCDEF";
return cifre[n];
}
Dal momento che le variabili static
vengono inizializzate una sola volta, l'array cifre
verrà creato una sola volta e verrà condiviso tra tutte le chiamate della funzione.
Ricapitolando:
Classe di Storage static
In linguaggio C, una variabile dichiarata con classe di storage static
ha le seguenti proprietà:
- Durata di vita statica: la variabile viene allocata quando il programma inizia la sua esecuzione e rilasciata quando il programma termina;
- Visibilità limitata al blocco di codice in cui è dichiarata se la variabile è dichiarata all'interno di un blocco di codice, oppure visibilità a livello di file se la variabile è dichiarata all'esterno di un blocco di codice;
- Linkage interno se la variabile è dichiarata all'esterno di un blocco di codice, oppure linkage nullo se la variabile è dichiarata all'interno di un blocco di codice.
Classe di Storage extern
La class di storage extern
permette a più file sorgente di condividere la stessa variabile.
Abbiamo già affrontato l'utilizzo di extern
quando abbiamo visto come adoperare i file header per condividere variabili globali.
In generale, bisogna ricordare che quando dichiariamo una variabile in questo modo:
extern int x;
stiamo dicendo al compilatore che la variabile x
è di tipo intero ma che la sua allocazione di memoria avviene in un altro file sorgente. Tecnicamente si tratta di una dichiarazione ma non di una definizione. La definizione vera e propria della variabile x
avviene in un altro file sorgente.
Una variabile, infatti, può avere pià dichiarazioni ma una sola definizione. La definizione è quella che effettivamente riserva la memoria per la variabile. Le dichiarazioni, invece, informano il compilatore dell'esistenza della variabile.
A questa regola, però, esiste un'eccezione. Se una variabile è dichiarata come extern
e allo stesso tempo inizializzata, allora la dichiarazione diventa anche una definizione. In tal caso, la variabile x
viene allocata nello stesso file sorgente in cui è dichiarata.
extern int x = 10;
La riga di sopra è a tutti gli effetti identica alla riga seguente:
int x = 10;
Questa regola è stata introdotta allo scopo di evitare che più dichiarazioni extern
possano inizializzare una variabile allo stesso tempo.
Una variabile dichiarata come extern
ha sempre durata di vita statica.
Lo scope o visibilità di una variabile extern
dipende dal punto in cui è dichiarata:
- Se è dichiarata all'interno di un blocco di codice, la variabile è visibile solo all'interno del blocco di codice stesso;
- Se è dichiarata all'esterno di un blocco di codice, la variabile è visibile a livello di file.
/* Durata di vita statica */
/* Visibilità a livello di file */
extern int a;
void f(void) {
/* Durata di vita statica */
/* Visibilità limitata al blocco di codice */
extern int b;
}
Per quanto riguarda, invece, il linkage le regole sono leggermente più complesse.
- Se la variabile è stata precedentemente dichiarata nel file come
static
all'esterno di qualunque funzione, allora essa avrà linkage interno; - Altrimenti la variabile avrà linkage esterno. Questo è il comportamento di default.
Ricapitolando:
Classe di Storage extern
In linguaggio C, una variabile dichiarata con classe di storage extern
ha le seguenti proprietà:
- Durata di vita statica;
- Visibilità limitata al blocco di codice in cui è dichiarata se la variabile è dichiarata all'interno di un blocco di codice, oppure visibilità a livello di file se la variabile è dichiarata all'esterno di un blocco di codice;
- Linkage interno se la variabile è dichiarata all'esterno di un blocco di codice e precedentemente dichiarata come
static
, oppure linkage esterno se la variabile è dichiarata all'esterno di un blocco di codice e non è stata dichiarata comestatic
.
Classe di Storage register
Se dichiariamo una variabile con la classe di storage register
, stiamo dicendo al compilatore che la variabile verrà utilizzata molto frequentemente e che, quindi, è conveniente allocarla in un registro della CPU.
register int x;
Infatti, un registro è una piccola area di memoria del processore che consente di memorizzare dati e valori temporanei. Dal momento che l'accesso alla memoria RAM è un'operazione molto lenta rispetto all'accesso ai registri, l'utilizzo di variabili register
può rendere il programma più veloce.
Bisogna, però, tener presente che l'uso della classe register
è solo una richiesta al compilatore e non un comando. In altre parole, il compilatore, per motivi di ottimizzazione o quant'altro, potrebbe ignorare la richiesta di allocare la variabile in un registro e decidere di allocarla in memoria.
La classe di storage register
può essere adoperata solo per le variabili locali definite all'interno di un blocco di codice. Non è possibile dichiarare variabili globali come register
. Per questo motivo, le variabili register
hanno le stesse proprietà delle variabili auto
:
- Durata di vita automatica;
- Visibilità limitata al blocco di codice in cui sono dichiarate;
- Linkage nullo.
Tuttavia, le variabili register
hanno una differenza fondamentale rispetto ad altre variabili: non è possibile ottenere l'indirizzo di una variabile register
. Questo perché, come abbiamo detto, le variabili register
sono allocate nei registri della CPU e non in memoria. Per cui, un'operazione del genere è illegale:
register int x;
// ERRORE: non è possibile ottenere
// l'indirizzo di una variabile register
int *p = &x;
Questa restrizione vale anche nel caso in cui il compilatore abbia scelto di memorizzare la variabile in memoria.
La classe register
è molto utile nei casi in cui una variabile viene acceduta o modificata con una frequenza molto elevata. Ad esempio, un uso tipico delle variabili register
è all'interno dei cicli for
. Volendo scrivere una funzione che effettua la media di un array di double
, possiamo scrivere:
double media(const double *array, size_t n) {
register double sum = 0.0;
register size_t i;
for (i = 0; i < n; i++) {
sum += array[i];
}
return sum / n;
}
In questo caso, sia la variabile sum
che la variabile i
vengono allocate nei registri della CPU. Questo rende il programma più veloce rispetto al caso in cui le variabili venissero allocate in memoria.
Ricapitolando:
Classe di Storage register
In linguaggio C, una variabile dichiarata con classe di storage register
ha le seguenti proprietà:
- Durata di vita automatica;
- Visibilità limitata al blocco di codice in cui è dichiarata;
- Linkage nullo.
Si possono dichiarare solo variabili locali come register
.
Inoltre, non è possibile ottenere l'indirizzo di una variabile register
.
Oggigiorno, l'utilizzo della classe di storage register
è molto limitato. I moderni compilatori sono in grado di ottimizzare il codice in maniera molto più efficace rispetto a quanto si potesse fare in passato. Per questo motivo, spesso è meglio lasciare al compilatore la scelta di allocare le variabili nei registri o in memoria.
Tuttavia, usare la classe register
comunque fornisce informazioni utili che il compilatore può sfruttare per ottimizzare il codice. Per cui, se si è sicuri che una variabile verrà utilizzata molto frequentemente, è possibile utilizzare la classe register
per dare una suggerimento al compilatore. Inoltre, con la classe register
si può impedire che l'indirizzo di una variabile venga preso e quindi che il valore venga modificato attraverso un puntatore. In questo senso, register
è molto simile al qualificatore restrict
applicato ai puntatori che abbiamo già studiato in precedenza.
In Sintesi
In questa lezione abbiamo visto che le variabili in linguaggio C hanno tre proprietà fondamentali:
- Durata di vita;
- Visibilità o Scope;
- Linkage o Collegamento.
Per modificare queste proprietà, C mette a disposizione quattro classi di storage principali:
auto
: per variabili locali con durata automatica;static
: per variabili con durata statica e visibilità limitata;extern
: per variabili con durata statica e visibilità globale;register
: per variabili locali allocate nei registri della CPU.
Ognuna di queste classi di storage ha delle proprietà specifiche che permettono di personalizzare il comportamento di una variabile.
Esiste un'ulteriore classe di storage introdotta a partire dallo standard C11: thread_local
. Questa classe permette di dichiarare variabili con durata di vita di thread. Per poterla studiare nel dettaglio, però, dobbiamo studiare i thread in C11, cosa che faremo nelle prossime lezioni.
Nella prossima lezione, invece, studieremo le classi di storage applicate alle funzioni.