Regole di visibilità delle variabili in C

Premi qui per scaricare il PDF

[adinserter block="7"]

In questa lezione analizzeremo le principali regole di visibilità e tempo di vita delle variabili. Studieremo le differenze tra variabili locali e variabili globali.

Blocco

E’ una porzione di codice C racchiusa tra parentesi graffe

Si compone di:

  • Dichiarazione di variabili (facoltativa).
  • una sequenza di istruzioni.

Due blocchi possono essere:

  • annidati (quando uno è all’interno dell’altro);
  • paralleli (entrambi interni a un terzo blocco).

Visibilità

  • Le variabili dichiarate all’interno di un blocco, sono visibili solo all’interno di quel blocco.
  • Le variabili dichiarate all’interno di un sottoprogramma sono visibili nell’intero sottoprogramma.
  • Variabili dichiarate a livello globale, sono visibili nell’intero file (main + sottoprogramma).

La dichiarazione di un elemento in una funzione o in un blocco, maschera le eventuali omonime più “esterne”.

Esempio

int g1, g2;
char g3;

int f1(int, int);
int f2(int, int);

int main()
{
	int a,b;
	{ //Inizio blocco 1
		char a, c;
		{ //Inizio blocco 2
			float a;
		}//Fine blocco 2
	}//Fine blocco 1
}//fine main

int f1(int par1, int par2)
{
	int d, g2;
	{ //Inizio blocco 3
		int e;
	} //Fine blocco 3
	{ //Inizio blocco 4
		int d;
	} //Fine blocco 4
}

int f2(int par3, int par4)
{
	int f;
	for (int idx = 0; idx < 10; idx++)
	{
		int a = 7; 
		printf("%d", a+idx)
	}
}
[adinserter block="7"]

Tempo di vita di una variabile

Va dalla creazione (allocazione della memoria)

Alla distruzione (rilascio della memoria allovata)

Due classi di variabile:

  • Automatiche

    • Sono utilizzabili quando il blocco funzione in cui sono state dichiarate, viene richiamato. Il flusso di esecuzione entra nel loro ambito di visibilità.
    • Sono distrutte quando il flusso di esecuzione esce da tale ambito.
    • Tutte le variabili dichiarate nei blocchi e nelle funzioni (anche i parametri formali).
  • Statiche

    • Create quando il flusso di esecuzione entra nel blocco dove sono dichiarate.
    • Distrutte quando il programma termina, può però essere utilizzata solo nel sottoprogramma dove è dichiarata.
    • Tutte le variabili di funzione o blocco dichiarate con modificatore: static.

    Esempio

    Variabile Statica (evita che alla chiusura del sottoprogramma, la variabile venga distrutta)

    void foo(void)
    {
    	static int cont = 0;
    	cont = cont + 1;
    	printf ("%d - ", cont);
    }
    
    int main()
    {
    	foo();
    	foo();
    	foo();
    }
    //Il codice stampa 1 - 2 - 3
    

    Variabile Automatica

    void foo(void)
    {
    	int cont = 0;
    	cont = cont + 1;
    	printf ("%d - ", cont);
    }
    
    int main()
    {
    	foo(); //Termina il sottoprogramma e cont viene distrutto
    	foo(); //Termina il sottoprogramma e cont viene distrutto
    	foo(); //Termina il sottoprogramma e cont viene distrutto
    }
    //Il codice stampa 1 - 1 - 1
    
[adinserter block="7"]

Variabile globale

Sono visibili solo nel documento sorgente (file sorgente) dove sono dichiarate.

Una variabile globale definita per es. in: fileB.c non è visibile nel file sorgente che contiene il main, per esempio: fileA.c (e viceversa).

Variabili globali

Una variabile globale definita in un altro file sorgente, per poter essere utilizzata, deve essere indicate come una variabile extern.

Le variabili globali statiche sono visibili (possono essere usate) nel codice del file sorgente dove sono dichiarate, ma non in codice esterno.

[adinserter block="7"]

Esempio Scorretto

typedef struct 
{ 
	int actualSize, contents[SIZE]; 
}tabella;
tabella myTab; 
void stampa(void);
int search(int k); 
int main() 
{
	int i, pos, val;
	printf("Dimensione tabella?( < %d) : ", SIZE); 
	scanf("%d", &myTab.actualSize); 
/*bisognerebbe controllare che sia < SIZE*/ 
	for( i=0; i<myTab.actualSize; i++ ) 
	{
		printf(“\\n dammi un valore della tabella : "); 
		scanf("%d", &myTab.contents[i]);
	}
	printf("\\n dammi valore da cercare in tabella : "); scanf("%d", &val);
	stampa();
	pos = search(val);

	if ( pos != -1 ) 
		printf ("\\n elemento trovato in posizione %d \\n", pos); 
	else 
		printf(("\\n valore non trovato in tabella\\n");
	return 0;
}

void stampa (void)
{
	int j;
	printf("\\n ecco la tabella:\\n");
	for (j = 0; j < myTab.actualSize; j++)
		printf("%d", myTab.contents[j]);
	printf("\\n");
}

int search( int k ) 
{
	for (int n=0; n<myTab.actualSize; n++ )
		if (myTab.contents[n]==k) 
			return n;
	return -1;
} 

Le «macchine» di stampa() e di search() possono accedere all’ambiente globale:

  • se lo modificano, possono generare ciò che si chiama un «effetto collaterale» (side effect) sul programma chiamante.
  • Effetto non previsto dalla “semantica naturale” delle due funzioni (intuitivamente, non dovrebbero modificare myTab).

Per garantire formalmente che non accada … la tabella deve essere usata come argomento della funzione, cioè passiamo la tabella per copia!

Esempio Corretto

typedef int array [SIZE]; 
typedef struct 
{ 
int actualSize;
array contents; 
} tabella; 
int search(tabella, int);

int main() 
{
	int i, pos, val;
	tabella myTab;
	printf("\\n Dimensione tabella?(<%d): ", SIZE);
	scanf("%d", &myTab.actualSize); 
	/*bisognerebbe controllare che sia < SIZE*/
	for (i=0; i<myTab.actualSize; i++) 
	{
		printf("\\n dammi un valore della tabella "); 
		scanf("%d", &myTab.contents[i]);
	} 
	printf("\\n dammi valore da cercare in tabella "); 
	scanf("%d", &val);
	
	pos = search(myTab, val);
	if (pos!=-1) 
		printf("\\n elemento trovato in posizione %d \\n", pos);
	else 
		printf("\\n valore non trovato in tabella\\n"); 
	
	return 0;
}

int search(tabella t, int k) 
{
	for( int n=0; n < t.actualSize; n++ )
		if( t.contents[n] == k ) 
			return n;
	return -1;
}
[adinserter block="7"]

Side effects

Non esistono se NON si utilizzano variabili globali.

I sottoprogrammi possono solo:

  • restituire un valore;
  • modificare l’ambiente globale;
  • passare i parametri per valore (una copia) ad altri sottoprogrammi;
  • Usare i puntatori ricevuti come parametri per modificare ambienti diversi da quello locale (quello del chiamante) → Causa side effects.

Funzioni: modello di esecuzione

Immaginiamo che esista un macchina dedicata al compito di eseguire il programma main().

Viene creata una nuova macchina dedicata all’atto della chiamata di ogni funzione. Le macchine dedicate hanno una loro memoria (detta ambiente della funzione)

  • Per le variabili locali;
  • Per i valori dei parametri che ricevono;
  • Per il risultato che restituiscono.

Al livello di astrazione del programmatore:

  • In seguito a una chiamata a sottoprogramma, il programma in corso viene sospeso e il controllo passa al sottoprogramma.

A livello della macchina C:

  • Salvataggio del program counter (posizione di memoria dell’istruzione che deve essere eseguita) e del contesto del programma chiamante → vengono salvati nello stack (area di memoria RAM);
  • Assegnazione al PC dell’indirizzo del codice del sottoprogramma;
  • Esecuzione del sottoprogramma;
  • Ritorno al programma chiamante con ripristino del suo contesto;

Record di attivazione

Insieme di tutte le variabili di cui il sottoprogramma fa uso.

Ogni funzione ha associato il record di attivazione che contiene:

  • Tutti i dati relativi all’ambiente locale del sottoprogramma;
  • Contesto del chiamante;
  • L’indirizzo di ritorno nel programma chiamante, cioè l’indirizzo di RAM dell’istruzione del chiamante che deve essere considerata al termine dell’esecuzione del sottoprogramma → valore del Program Counter;
  • Altri dati utili.

Il record di attivazione è il modello di quello che abbiamo chiamato ambiente. Per ogni attivazione di sottoprogramma, si crea un nuovo record di attivazione.

Esempio di Codice

int moltiplica (int x, int y)
{
	int r = x*y;
	return r;
}

int power (int b, int e)
{
	int i, p = 1;
	for (i = 1; i <= e; i++)
		p = moltiplica (p, b);
	return p;
}

int main()
{
	int i = 2, j = 3, k;
	k = moltiplica (i, j);
	k = power(i, j);
	return 0;
}

Record di attivazione di moltiplica (x, y, r)

Record di attivazione del main (i, j, k)

Record di attivazione di power (b, e, i, p)

[adinserter block="7"]

Stack Pointer

Puntatore alla pila, un registro che contiene l’indirizzo della parola di memoria da leggere nello stack.

È necessario poichè permette di eseguire del codice, un numero arbitrario di volte, senza prevedere a priori quant’è questo numero odi volte.

Ogni chiamata a una funzione richiede allocazione di memoria per le sue variabili (automatiche).

Le funzioni possono richiamare se stesse (ricorsione)

  • Possono quindi esistere più “istanze” di una stessa funzione, “addormentate” in attesa della terminazione di una “gemella” per riprendere l’esecuzione.

In questo ultimo caso il compilatore non può sapere quanto spazio di memoria il sistema operativo dovrà allocare per le variabili durante l’esecuzione del programma.

[adinserter block="7"]

Premi qui per scaricare il PDF