Due domande su ogni variabile
Ogni variabile in C ha due proprietà distinte, e confonderle è l'origine della maggior parte delle sorprese di questa pagina:
- Scope: dove si può usare il nome. Una questione di compilazione.
- Durata: quanto a lungo esiste la memoria. Una questione di esecuzione.
Di solito vanno di pari passo (una normale variabile locale è visibile nel suo blocco e vive esattamente quanto quel blocco), ma static le separa, ed è per questo che è la parola chiave da capire bene qui.
Scope di blocco
Una variabile dichiarata tra parentesi graffe è visibile dalla sua dichiarazione fino alla graffa di chiusura, e da nessun'altra parte.
Il blocco interno vede outer, perché è annidato al suo interno. Il blocco esterno non vede inner. Vale per ogni coppia di graffe: corpi di funzione, corpi di if, corpi di ciclo o un blocco isolato come quello qui sopra.
I cicli lo rendono concreto. Un contatore dichiarato nell'intestazione di un for appartiene al ciclo:
for (int i = 0; i < 5; i++) {
/* i vive qui */
}
/* qui i non esiste */
Ed è esattamente ciò che vuoi: due cicli nella stessa funzione possono usare i ciascuno senza interferenze, e il contatore non può essere letto per sbaglio dopo il ciclo. Se il valore ti serve dopo, per esempio l'indice in cui si è fermata una ricerca, dichiaralo prima del ciclo.
I parametri di una funzione hanno lo scope del corpo della funzione, quindi si comportano come variabili locali dichiarate al suo inizio.
C99 permette una dichiarazione in qualsiasi punto di un blocco, non solo all'inizio. Dichiara le variabili dove ti servono per la prima volta: una variabile con uno scope breve ha meno modi di andare storta.
Durata: memoria automatica
Una normale variabile locale ha memoria automatica: nasce quando il controllo entra nel suo blocco e viene distrutta quando il controllo ne esce. La sua memoria sta sullo stack.
Stampa 1 tre volte. Ogni chiamata riceve un count nuovo di zecca, inizializzato a 0 e scartato al ritorno. È anche il motivo per cui ogni frame di una funzione ricorsiva ha la propria copia di ogni variabile locale.
Due conseguenze da dire chiaramente. Una locale non inizializzata contiene spazzatura, non zero: i byte che si trovavano sullo stack. E restituire un puntatore a una locale è comportamento indefinito, perché la memoria sparisce nel momento in cui la funzione restituisce:
/* BUG: buf non esiste dopo il return */
char *broken(void) {
char buf[64] = "hello";
return buf;
}
Locali static: memoria tra una chiamata e l'altra
Metti static su una locale e la sua durata diventa l'intero programma, mentre il suo scope resta esattamente lo stesso.
Ora il contatore stampa 1, 2, 3. Il = 0 viene eseguito una sola volta, prima che parta main, non a ogni chiamata.
Tre regole per le locali static:
- L'inizializzatore deve essere un'espressione costante, perché viene applicato all'avvio del programma, non a runtime.
- Senza inizializzatore, una
staticviene inizializzata a zero (a differenza di una locale automatica, che contiene spazzatura). - La variabile resta privata della funzione. Nessun altro codice può nominarla.
Quest'ultimo punto è tutto il suo fascino: una funzione può ricordare qualcosa tra una chiamata e l'altra senza esporre una globale che chiunque potrebbe modificare. Usala per contatori di chiamate, tabelle di lookup costruite al primo utilizzo e flag di inizializzazione una tantum.
Il prezzo è che la funzione non è più una semplice corrispondenza tra input e output: gli stessi argomenti possono dare risposte diverse, e questo la rende più difficile da testare e non sicura da chiamare da più thread contemporaneamente senza protezione. Usa le locali static in modo consapevole, non per comodità.
Scope di file: le globali
Una variabile dichiarata fuori da ogni funzione ha scope di file. È visibile dalla sua dichiarazione fino alla fine del file, in ogni funzione che segue, e la sua durata è l'intero programma.
Le globali sono inizializzate a zero per default, quindi totalOperations parte da 0 anche senza = 0.
Per default una globale ha anche linkage esterno: altri file .c dello stesso programma possono raggiungerla dichiarandola extern:
/* in stats.c */
int totalOperations = 0; /* la definizione: esattamente una nel programma */
/* in main.c */
extern int totalOperations; /* una dichiarazione: "esiste da qualche parte" */
Metti la dichiarazione extern in un file header, così che tutti vedano la stessa. Nota l'asimmetria: la dichiarazione extern dice che la variabile esiste, e un solo file .c deve definirla davvero.
Perché le globali mordono
Le globali sono il modo più facile per condividere dati e il modo più facile per creare bug impossibili da circoscrivere. Quattro problemi concreti:
Chiunque può modificarle. Quando totalOperations contiene un valore sbagliato, il colpevole può essere qualsiasi riga di qualsiasi file. Con un parametro, i sospettati sono i punti di chiamata che hai sotto gli occhi.
Le funzioni diventano impossibili da testare. Una funzione che legge una globale non si può chiamare in isolamento: devi prima preparare lo stato globale e ricordarti di ripristinarlo dopo.
Il nome vale per tutto il programma. Una globale chiamata count o buffer prima o poi entrerà in collisione con quella di qualcun altro.
I loro inizializzatori devono essere costanti di compilazione. In C una globale si può inizializzare solo con un'espressione costante: int limit = readConfig(); non compila, e nemmeno int b = a * 2; a livello di file. Tutto ciò che va calcolato deve essere assegnato a runtime da qualche funzione di setup, e ogni file che tocca la globale prima di quella chiamata vede uno 0 silenzioso.
Le alternative sono quasi sempre disponibili: passa i valori come parametri, restituisci i risultati e raggruppa lo stato correlato in una struct passata esplicitamente. Le eccezioni vere esistono, come un oggetto di configurazione per tutto il programma o un handle di logging, e anche quelle stanno meglio come static a livello di file con funzioni di accesso, che è l'argomento della prossima sezione.
static a livello di file: privacy
Su una variabile globale o su una funzione, static significa qualcosa di completamente diverso da ciò che significa su una locale: linkage interno. Il nome diventa privato del proprio file .c e non si può raggiungere da nessun altro.
/* counter.c */
#include "counter.h"
static int count = 0; /* privata di questo file: nessun altro file puo toccarla */
static void validate(void) { /* una funzione di supporto privata, fuori dall'interfaccia */
if (count < 0) count = 0;
}
void increment(void) { /* pubblica: dichiarata in counter.h */
count++;
validate();
}
int getCount(void) { /* pubblica */
return count;
}
Un altro file può chiamare increment e getCount, ma non può vedere count né chiamare validate: il linker non risolverà quei nomi. È il sistema di moduli del C, per quello che vale: un header che dichiara le funzioni pubbliche e static su tutto il resto.
Due vantaggi pratici oltre all'ordine. Due file possono avere ciascuno una static void validate(void) senza collisioni, cosa impossibile se le funzioni fossero pubbliche. E il compilatore sa che una funzione static non ha chiamanti fuori dal file, quindi può metterla inline in modo più aggressivo o avvisarti che non è usata.
Quindi la parola chiave ha due significati distinti, decisi da dove compare:
static su una variabile LOCALE -> la durata diventa permanente (scope invariato)
static a livello di FILE -> il linkage diventa interno (durata invariata)
Shadowing
Dichiarare in uno scope interno un nome che esiste già in uno scope esterno oscura (shadowing) il nome esterno: dentro lo scope interno il nome si riferisce alla nuova variabile e quella esterna è irraggiungibile.
È lecito, e a volte voluto. Più spesso è un incidente, e costoso: una funzione che doveva aggiornare una globale aggiorna invece una locale con lo stesso nome, e la globale non cambia mai. Lo stesso succede quando una locale oscura un parametro, e l'assegnazione che volevi fare sull'input finisce nel nulla.
gcc -Wshadow segnala ogni caso. Non è incluso in -Wall, quindi attivalo esplicitamente:
gcc -Wall -Wextra -Wshadow program.c -o program
L'abitudine che evita del tutto il problema è dare i nomi in base al ruolo e non al tipo: totalScore e itemScore non possono oscurarsi a vicenda, mentre due variabili chiamate entrambe n prima o poi lo faranno.
Riepilogo rapido
dichiarata dentro un blocco scope di blocco, durata automatica, spazzatura se non inizializzata
parametro di funzione scope del corpo della funzione, una copia dell'argomento
static dentro una funzione scope di blocco, durata del PROGRAMMA, a zero, conservata tra le chiamate
dichiarata fuori da ogni funzione scope di file, durata del programma, a zero, visibile ad altri file
static fuori da ogni funzione scope di file, durata del programma, PRIVATA di questo file .c
dichiarazione extern indica una variabile definita in un altro file
Domande frequenti
Cos'è lo scope in C?
La porzione di codice in cui un nome è visibile. Una variabile dichiarata dentro un blocco (una coppia di parentesi graffe) è visibile solo in quel blocco: questo è lo scope di blocco. Una dichiarata fuori da ogni funzione ha scope di file ed è visibile dalla sua dichiarazione fino alla fine del file.
Cosa fa static a una variabile locale in C?
Cambia la durata della variabile senza cambiarne lo scope. Una locale static viene creata una volta, inizializzata una volta e mantiene il suo valore tra una chiamata e l'altra, ma resta visibile solo dentro la sua funzione. È il modo per dare memoria a una funzione senza usare una globale.
Qual è la differenza tra static e globale in C?
Una globale normale è visibile a tutti i file del programma, che possono raggiungerla con extern. Una variabile o funzione static a livello di file è privata di quel singolo file .c: gli altri file non possono collegarsi a essa. static a livello di file serve a nascondere; static su una locale riguarda la durata.
Perché le variabili globali sono una cattiva idea in C?
Qualsiasi funzione può modificarle, quindi un bug può nascere da qualunque punto del programma; le funzioni che le leggono non si possono testare in isolamento; e il nome occupa l'intero programma, favorendo le collisioni. Passa i valori come parametri e restituisci i risultati, e dove uno stato condiviso serve davvero, rendilo static a livello di file così che un solo file possa toccarlo.