comunicazioni fra programma e funzioni
L'ultima volta a lezione abbiamo visto ripassato i due modi principali di passare un parametro ad una funzione: per valore e per riferimento. Con questo post vorrei riepilogare questi due metodi e accostarli ad altri (sottili variazioni di quelli). E, visto che siamo in tema, aggiungere anche qualche parola sui differenti modi di restituire un valore che si può prescrivere a una funzione.
[I] Partiamo dal passaggio di parametri (userò, nei prototipi, procedure, ovvero funzioni void).
viene creata una variabile locale, a, che viene inizializzata col valore passato al momento della chiamata della funzione. Tale valore può essere fornito esplicitamente (tramite una cosiddetta "costante letterale" o litteral in inglese) oppure tramite una variabile (che sarà usata solo come "portatrice" del valore con cui inizializzare la variabile locale a). Come tutte le variabili locali, a scomparirà al termine dell'esecuzione della funzione.
è una piccola variante del caso precedente: semplicemente si specifica che la variabile locale a non potrà essere modificata dopo essere stata inizializzata col valore passato al momento della chiamata della funzione. Eventuali tentativi di modificarne il valore sono segnalati al momento della compilazione (può essere utile, dunque, per accorgersi di eventuali errori nella programmazione...). Inoltre l'informazione che a non potrà essere modificata può essere usata dal compilatore per ottimizzare l'esecuzione del codice (può essere utile, dunque, per migliorare le prestazioni del programma).
non viene creata alcuna variabile locale, ma al contrario all'interno della funzione l'identificatore a sarà usato per riferirsi direttamente alla variabile che viene passata al momento della chiamata della funzione (a sarà, come si suol dire, un alias di quella variabile). Questo implica ovviamente che al momento della chiamata non si potrà passare un valore (esplicito). Altrettanto ovviamente, quindi, tutte le modifiche che verranno fatte all'interno della funzione usando il nome a sopravviveranno alla funzione stessa, perchè la variabile a cui a faceva riferimento non è una variabile locale.
è una piccola variante del caso precedente: semplicemente si specifica che la funzione non potrà modificare il valore della variabile che viene passata al momento della chiamata della funzione. Notate bene che questa notazione non presuppone che venga passata alla funzione una costante, bensì si limita solo a specificare che verrà trattata come una costante all'interno della funzione. Di più: un riferimento costante di questo tipo può anche essere inizializzato con non-l-value, cioè anche con un litteral, cioè con un valore esplicito: in questo caso il programma crea una variabile temporanea che inizializza col valore fornito, tale variabile ha evidentemente durata limitata al perdurare della funzione stessa e, in quanto const, non può essere modificata nel corpo della funzione.
L'unico caso di una certa frequenza in cui non è possibile usare quest'ultima variante è quello in cui si desidera modificare la variabile passata come parametro (ed è sufficiente, dunque, togliere lo specificatore const). Il caso estremo (in cui si desidera modificare il parametro dentro la funzione ma non intaccare l'eventuale variabile esterna usata come argomento) è decisamente raro e usato in casi del tutto specifici e speciali. In tale situazione si opta dunque per la prima variante.
[II] Veniamo, infine, alla restituzione del valore da parte di una funzione non-void.
Questo è il caso tipico.
Dal punto di vista interno alla funzione, quando il controllo torna dalla funzione al programma chiamante, tramite l'istruzione:
il programma costruisce una copia del valore calcolato dell'espressione (che "muore" appena termina la funzione), creando un valore locale nell'ambito del programma chiamante. Se dunque, ad esempio, espressione è una variabile (locale), la funzione non restituisce quella variabile (che, in quanto locale, muore appena la funzione termina), ma una sua copia, che sopravvive alla funzione e diventa un valore locale (temporaneo, cioè non identificato da un nome) del programma chiamante.
Dal punto di vista del programma chiamante, invece, quel che succede è che viene eseguita la chiamata alla funzione (con precedenza rispetto alle altre operazioni) e al suo posto viene sostituito il valore di ritorno della funzione stessa.
Questa volta, invece, il valore è passato per riferimento, cioè non ne viene costruita una copia, ma nel programma chiamante viene utilizzato direttamente il riferimento al valore restituito dalla funzione. Questo implica diverse cose:
a) l'espressione di return non può essere seguita da un'espressione, ma necessariamente da una variabile (del tipo restituito) il cui riferimento dovrà essere restituito.
b) inoltre tale variabile dovrà necessariamente essere di un tipo che sopravviva alla funzione stessa (altrimenti ci si ritrova con un errore perchè si sta tentando di restituire una variabile che non esiste più...). Siccome abbiamo visto che è fortemente sconsigliato usare variabili globali all'interno delle funzioni, l'unica altra possibilità che ci rimane è che la variabile che vogliamo restituire sia stata essa stessa passata alla funzione per riferimento, come un suo argomento.
c) siccome quel che restituiamo non è un valore, ma una variabile, questo è l'unico caso in cui possiamo chiamare la funzione a sinistra di un'operazione di assegnazione!!!
[I] Partiamo dal passaggio di parametri (userò, nei prototipi, procedure, ovvero funzioni void).
- Passaggio per valore:
void funzione(int a);
viene creata una variabile locale, a, che viene inizializzata col valore passato al momento della chiamata della funzione. Tale valore può essere fornito esplicitamente (tramite una cosiddetta "costante letterale" o litteral in inglese) oppure tramite una variabile (che sarà usata solo come "portatrice" del valore con cui inizializzare la variabile locale a). Come tutte le variabili locali, a scomparirà al termine dell'esecuzione della funzione.
- Passaggio per valore costante:
void funzione(const int a);
è una piccola variante del caso precedente: semplicemente si specifica che la variabile locale a non potrà essere modificata dopo essere stata inizializzata col valore passato al momento della chiamata della funzione. Eventuali tentativi di modificarne il valore sono segnalati al momento della compilazione (può essere utile, dunque, per accorgersi di eventuali errori nella programmazione...). Inoltre l'informazione che a non potrà essere modificata può essere usata dal compilatore per ottimizzare l'esecuzione del codice (può essere utile, dunque, per migliorare le prestazioni del programma).
- Passaggio per riferimento:
void funzione(int &a);
non viene creata alcuna variabile locale, ma al contrario all'interno della funzione l'identificatore a sarà usato per riferirsi direttamente alla variabile che viene passata al momento della chiamata della funzione (a sarà, come si suol dire, un alias di quella variabile). Questo implica ovviamente che al momento della chiamata non si potrà passare un valore (esplicito). Altrettanto ovviamente, quindi, tutte le modifiche che verranno fatte all'interno della funzione usando il nome a sopravviveranno alla funzione stessa, perchè la variabile a cui a faceva riferimento non è una variabile locale.
- Passaggio per riferimento costante (o "a sola lettura"):
void funzione(const int &a);
è una piccola variante del caso precedente: semplicemente si specifica che la funzione non potrà modificare il valore della variabile che viene passata al momento della chiamata della funzione. Notate bene che questa notazione non presuppone che venga passata alla funzione una costante, bensì si limita solo a specificare che verrà trattata come una costante all'interno della funzione. Di più: un riferimento costante di questo tipo può anche essere inizializzato con non-l-value, cioè anche con un litteral, cioè con un valore esplicito: in questo caso il programma crea una variabile temporanea che inizializza col valore fornito, tale variabile ha evidentemente durata limitata al perdurare della funzione stessa e, in quanto const, non può essere modificata nel corpo della funzione.
Per tutte queste proprietà, quest'ultima è, di fatto, la più usata delle quattro varianti qui riportate. Il motivo è evidente: nel caso si chiami la funzione con valori espliciti, essa ricade automaticamente nella seconda variante, mentre se si chiama la funzione usando delle variabili, essa offre l'efficienza di non creare alcuna variabile temporanea. E questo dettaglio che può sembrare banale diventa invece notevolmente importante quando si ha a che fare non con tipi elementari (quali float, int, double...), bensì con oggetti (istanze di classi) molto complessi, la creazione dei quali può magari implicare il ricorso a un operatore di costruttore molto dispendioso.
L'unico caso di una certa frequenza in cui non è possibile usare quest'ultima variante è quello in cui si desidera modificare la variabile passata come parametro (ed è sufficiente, dunque, togliere lo specificatore const). Il caso estremo (in cui si desidera modificare il parametro dentro la funzione ma non intaccare l'eventuale variabile esterna usata come argomento) è decisamente raro e usato in casi del tutto specifici e speciali. In tale situazione si opta dunque per la prima variante.
[II] Veniamo, infine, alla restituzione del valore da parte di una funzione non-void.
- Passaggio per valore:
float funzione(int &a);
Questo è il caso tipico.
Dal punto di vista interno alla funzione, quando il controllo torna dalla funzione al programma chiamante, tramite l'istruzione:
return espressione;
il programma costruisce una copia del valore calcolato dell'espressione (che "muore" appena termina la funzione), creando un valore locale nell'ambito del programma chiamante. Se dunque, ad esempio, espressione è una variabile (locale), la funzione non restituisce quella variabile (che, in quanto locale, muore appena la funzione termina), ma una sua copia, che sopravvive alla funzione e diventa un valore locale (temporaneo, cioè non identificato da un nome) del programma chiamante.
Dal punto di vista del programma chiamante, invece, quel che succede è che viene eseguita la chiamata alla funzione (con precedenza rispetto alle altre operazioni) e al suo posto viene sostituito il valore di ritorno della funzione stessa.
- Passaggio per riferimento:
float& funzione(int &a);
Questa volta, invece, il valore è passato per riferimento, cioè non ne viene costruita una copia, ma nel programma chiamante viene utilizzato direttamente il riferimento al valore restituito dalla funzione. Questo implica diverse cose:
a) l'espressione di return non può essere seguita da un'espressione, ma necessariamente da una variabile (del tipo restituito) il cui riferimento dovrà essere restituito.
b) inoltre tale variabile dovrà necessariamente essere di un tipo che sopravviva alla funzione stessa (altrimenti ci si ritrova con un errore perchè si sta tentando di restituire una variabile che non esiste più...). Siccome abbiamo visto che è fortemente sconsigliato usare variabili globali all'interno delle funzioni, l'unica altra possibilità che ci rimane è che la variabile che vogliamo restituire sia stata essa stessa passata alla funzione per riferimento, come un suo argomento.
c) siccome quel che restituiamo non è un valore, ma una variabile, questo è l'unico caso in cui possiamo chiamare la funzione a sinistra di un'operazione di assegnazione!!!
Nessun commento:
Posta un commento