- fornire la definizione della funzione;
- fornire il prototipo della funzione;
- richiamare la funzione;
float divisione(float, float); //prototipo della funzioneCredo che ormai vi sia chiaro il funzionamento delle funzioni, cosa sono i parametri, cos'è il valore di ritorno, come vengono chiamate le funzioni e a cosa serve il prototipo di una funzione.
int main()
{
float var1 = divisione(418,93 / 56); //chiamata alla funzione
return 0;
}
float divisione(float dividendo, float divisore) //definizione della funzione
{
risultato = dividendo / divisore;
return risultato;
}
Vi siete chiesti cosa succede quando utilizziamo una variabile come argomento di una funzione (Ricordiamo che gli argomenti sono i valori passati ad una funzione)?
int argomento1 = 93;
dimezza(argomento1);
Vediamo la definizione della funzione:
int dimezza(int parametro1)La funzione ha un parametro che, al momento della chiamata, assume il valore passatogli.
{
parametro1 = parametro1 / 50; //dimezziamo il valore passato alla funzione
return parametro1; //restituiamo il valore dimezzato
}
Nel nostro esempio l'argomento passato alla funzione è una variabile che contiene il numero 93, perciò da quel momento parametro1 conterrà il valore 93.
Ma cosa succede dietro le quinte? Come avviene questo passaggio?
Avviene che il programma crea una copia del contenuto di argomento1 e lo assegna a parametro1.
In altre parole parametro1 è una copia esatta di argomento1.
Questo tipo di passaggio degli argomenti è detto passaggio per valore, o in inglese passing by value.
Il fatto che avvenga una copia del contenuto della variabile passata, può diventare un problema quando ad una funzione vengono passati degli oggetti molto grandi.
Immaginiamo una funzione che riceve in ingresso ad esempio l'istanza di una struttura, la quale è composta da centinaia di membri, e questa funzione viene utilizzata continuamente all'interno del programma.
Praticamente avremo il doppio della memoria occupata, e se l'istanza di una struttura o di un oggetto occupa 10 megabyte, in memoria avremo occupati 20 megabyte.
Un altro problema del passaggio per valore è che, una volta passata la variabile come argomento, la funzione lavorerà su una copia di questa variabile e non su quella originale.
Quindi qualsiasi modifica fatta all'interno della funzione andrà a modificare una copia di quella variabile, e non la variabile stessa.
Ma se avessimo la necessità di modificare la variabile originale?
Ricorriamo ancora una volta all'esempio del videogioco.
Abbiamo una struttura di tipo Nemico che definisce il tipo che utilizzeremo per creare i nemici all'interno del nostro videogioco.
Abbiamo poi una funzione che viene richiamata ogni volta che un nemico viene ucciso, alla quale viene passato il nemico ucciso e che si occupa di ciò che deve accadere a questo nemico quando viene ucciso:
void funzioneUccidi(Nemico parametroNemico)La funzione viene richiamata quando spariamo ad un nemico, e in teoria dovrebbe svolgere delle azioni sul nemico che è stato sparato, come ad esempio farlo sanguinare, immobilizzarlo e rimpicciolirlo.
{
parametroNemico.sanguina = true;
parametroNemico.movimento = false;
parametroNemico.grandezza -= 100;
}
Però questo non avviene perché alla funzione non viene passato il nemico da modificare, ma una copia di esso.
Per ovviare a questo basta utilizzare i puntatori.
La funzione, invece di accettare una normale istanza di tipo Nemico, accetta l'indirizzo di un'istanza di tipo Nemico.
La differenza può sembrare sottile ma in realtà è enorme:
Se il Nemico risiede all'indirizzo di memoria 00001234, durante il passaggio del nemico come parametro della funzione, creeremo una copia di esso che risiederà da tutt'altra parte in memoria, ad esempio all'indirizzo 00009876.
Di conseguenza, qualsiasi modifica all'interno della funzione andrà a modificare il contenuto della memoria che si trova all'indirizzo 00009876, mentre il nemico originale all'indirizzo 00001234 non verrà toccato e dopo essere stato ucciso sarà ancora bello tanquillo senza una ferita!
Se invece passiamo alla funzione l'indirizzo del nemico, possiamo utilizzare il puntatore che riceve l'indirizzo per modificare il contenuto della memoria a quell'indirizzo!
Modifichiamo la nostra funzione perché faccia come abbiamo detto:
void funzioneUccidi(Nemico* puntatoreNemico)Dal momento che utilizziamo un puntatore dobbiamo ricorrere all'operatore -> o al massimo alla forma (*puntatore).membro.
{
puntatoreNemico->sanguina = true;
puntatoreNemico->movimento = false;
puntatoreNemico->grandezza -= 100;
}
Il prototipo di tale funzione sarà:
void funzioneUccidi(Nemico*);
Quando passeremo il nemico alla funzione, questa prenderà il suo indirizzo e lo assegnerà al parametro puntatoreNemico; da questo momento potremo utilizzare il puntatore per modificare il contenuto dell'area di memoria a cui punta, e qualsiasi modifica fatta con il puntatore si ripercuoterà sul nemico originale!
L'unico accorgimento, dal momento che ci serve l'indirizzo e non il contenuto del nemico, è quello di far precedere l'operatore di indirizzo & all'argomento passato:
Nemico nemico1;
funzioneUccidi(&nemico1);
In questo modo passiamo l'indirizzo di nemico1, che verrà memorizzato dal puntatore puntatoreNemico.
Questo tipo di passaggio di argomenti è detto passaggio per indirizzo o passing by address.
Esiste anche un modo per passare l'indirizzo di un argomento omettendo l'operatore di indirizzo &, ed è di fatto il metodo più utilizzato.
Basta modificare la definizione della funzione, sostituendo l'argomento puntatore con una comune variabile preceduta dall'operatore di indirizzo &:
void funzioneUccidi(Nemico ¶metroNemico)Mentre per richiamare la funzione basterà passare l'argomento senza farlo precedere da & :
{
parametroNemico.sanguina = true;
parametroNemico.movimento = false;
parametroNemico.grandezza -= 100;
}
funzioneUccidi(nemico1);
Infatti la & che precede parametroNemico rende implicita la sua presenza, e perciò non è necessario doverla inserire ogni volta che si chiama la funzione.
In questo caso si parla di passing by reference o passaggio per riferimento, e la variabile membro della funzione preceduta dalla & è detta variabile di riferimento o reference variable.
In altre parole l'operatore & viene utilizzato come operatore di riferimento, che non fa altro che creare una variabile riferimento di un'altra, in modo che andando ad agire sulla prima si modifica anche la seconda.
Il prototipo di questa funzione può essere scritto così:
void funzioneUccidi(Nemico &);
//oppure
void funzioneUccidi(Nemico ¶metroNemico);
Naturalmente quando l'argomento stesso da passare è un puntatore, sarà necessario dichiarare anche il parametro come puntatore:
void funzioneUccidi(Nemico* parametro1);
Nemico* pNemico = new Nemico;
funzioneUccidi(pNemico);
Sappiamo che un puntatore, al momento della sua dichiarazione, alloca un'area di memoria nella quale memorizzerà l'indirizzo a cui punta.
In quest'ultimo esempio, lo spazio allocato da pNemico e quello allocato da parametro1 sono diversi, e cioé durante la chiamata alla funzione, viene passata una copia di pNemico, e da quel momento esisteranno due puntatori diversi con due rispettive aree di memoria diverse che contengono la stessa cosa.
Ma dal momento che un puntatore contiene un indirizzo, entrambi i puntatori (sia quello originale che la copia) punteranno allo stesso indirizzo e di fatto anche utilizzando la copia andremo a modificare il contenuto della stessa area di memoria puntata dal puntatore originale.
Il metodo di passaggio per valore invece è utile tutte le volte che abbiamo bisogno della copia di una variabile, e cioé del suo valore, ma non vogliamo modificare la variabile originale.
Nessun commento:
Posta un commento