Parametri e argomenti
Due parole che si usano spesso come sinonimi ma che conviene distinguere:
- Un parametro è la variabile nella definizione della funzione:
int ninint square(int n). - Un argomento è il valore che fornisci nel punto della chiamata: il
4insquare(4).
Chiamare una funzione crea i suoi parametri come nuove variabili locali e vi copia dentro gli argomenti. Quella copia è l'argomento di tutta questa pagina.
Il C passa per valore, sempre
Questa è la regola, e il C non ha eccezioni: una funzione riceve copie dei suoi argomenti. Assegnare un valore a un parametro cambia solo la copia.
value vale ancora 42. La funzione ha ricevuto una copia chiamata x, l'ha azzerata, e la copia è sparita quando la funzione è terminata. Rinominare il parametro in value non cambia nulla: il nome non le collega, sono due variabili diverse.
Lo stesso vale per ogni tipo: int, double, char, un puntatore, perfino una struct intera (copiata campo per campo). Se vieni da Python, Java o JavaScript, tieni presente che quei linguaggi passano per valore i riferimenti agli oggetti, quindi modificare un oggetto dentro una funzione è visibile anche fuori. Il C non funziona così: copia l'oggetto stesso.
Il passaggio per valore è una caratteristica, non un limite. Una funzione non può corrompere per sbaglio i dati del chiamante, e questo rende il codice molto più facile da capire.
Il fallimento classico: swap
La dimostrazione da manuale è una funzione che prova a scambiare due valori:
La logica è corretta: scambia davvero a e b. Ma a e b sono copie, quindi il lavoro va perso nell'istante in cui la funzione termina.
Simulare il passaggio per riferimento con i puntatori
Per permettere a una funzione di cambiare una variabile del chiamante, passale l'indirizzo della variabile invece del suo valore. Anche quell'indirizzo viene passato per valore (il puntatore viene copiato), ma la copia punta ancora all'oggetto originale, quindi scrivere attraverso di essa raggiunge la memoria del chiamante.
Tre elementi di sintassi reggono tutta l'idea:
int *anella lista dei parametri dichiaraacome "un puntatore a unint".&xnel punto della chiamata produce l'indirizzo dix.*adentro la funzione significa "l'inta cui puntaa", che si può leggere e assegnare.
Questo è ciò che i programmatori C intendono con "passaggio per riferimento", ed è bene essere precisi: si tratta sempre di passaggio per valore. Viene copiato il puntatore. Assegnare un valore ad a stesso (a = NULL;) cambierebbe solo la copia; assegnare a *a cambia la variabile del chiamante. La pagina sui puntatori spiega il modello completo.
Il prezzo è che la chiamata non ti dice più se una variabile può cambiare, ed è proprio per questo che serve &. swap(x, y) non compila; devi scrivere swap(&x, &y), e quel & è il segnale visibile che questa chiamata può modificare x.
Restituire più risultati
return produce un solo valore, quindi gli output aggiuntivi viaggiano attraverso parametri puntatore. La forma tipica è un codice di stato come valore di ritorno e i veri risultati scritti attraverso i puntatori:
Nota che q e r mantengono i valori precedenti quando la chiamata fallisce: la funzione è uscita prima di scrivere qualsiasi cosa. È il contratto che il chiamante deve rispettare, ed è per questo che il controllo dello stato viene prima dell'uso degli output.
L'alternativa è restituire una struct con entrambi i campi, che evita del tutto i puntatori ed è spesso la soluzione più pulita quando i valori stanno davvero insieme.
Gli array sono diversi
Gli array non seguono la regola della copia, e questo fa inciampare quasi tutti. Quando passi un array, decade a un puntatore al suo primo elemento, quindi la funzione riceve un indirizzo, non una copia dei dati.
L'array del chiamante è cambiato davvero, senza nessun &, perché in questo contesto data è già un indirizzo. Tre conseguenze:
int a[] e int *a significano esattamente la stessa cosa in una lista di parametri. Anche int a[100]: la dimensione viene ignorata. Scrivere int a[] documenta l'intenzione, ma non leggerlo come "un array viene copiato".
sizeof dentro la funzione è sbagliato. Ecco il bug:
In main, sizeof(data) vale 20 byte e la lunghezza risulta 5. Dentro la funzione sizeof(a) è la dimensione di un puntatore, 8 su un sistema a 64 bit, quindi la "lunghezza" è 2. La dimensione dell'array lì semplicemente non è disponibile. Passa sempre la lunghezza come parametro separato. I compilatori moderni avvisano di questo errore con -Wall.
Usa const quando non devi scrivere. const int a[] dice che la funzione legge soltanto, cosa che il compilatore fa rispettare e su cui chi legge il codice può contare:
int sum(const int a[], int n); /* promette di non modificare l'array */
Anche le stringhe sono array, quindi vale lo stesso: void greet(const char *name) è la firma standard per una funzione che legge una stringa senza modificarla.
E il costo?
Passare una struct grande per valore ne copia ogni byte a ogni chiamata. Per una struct con pochi campi va benissimo, e spesso è più veloce dell'indirezione che aggiunge un puntatore. Per una struct grande, passa un puntatore, e marcalo const se la funzione legge soltanto:
struct Config { char name[64]; int flags[32]; double weights[128]; };
void applyConfig(const struct Config *cfg); /* niente copia da 512 byte a ogni chiamata */
const struct Config *cfg significa "un puntatore a una Config che non modificherò", e ottiene l'efficienza di un puntatore con la sicurezza di una copia.
Errori comuni
- Aspettarsi che un parametro normale cambi la variabile del chiamante. Non può. Passa un puntatore.
- Dimenticare
&nel punto della chiamata.swap(x, y)quandoswapvuole dei puntatori è un errore di tipo: il compilatore lo intercetta, ma solo se c'è un prototipo visibile. - Dimenticare
*dentro la funzione.a = b;su due parametri puntatore scambia le copie locali, non i valori. Quello che vuoi è*a = *b;. - Usare
sizeofsu un parametro array. È sempre sbagliato. Passa la lunghezza. - Restituire un puntatore a una variabile locale. La variabile locale muore quando la funzione termina, quindi il chiamante si ritrova con un puntatore pendente, e usarlo è comportamento indefinito. Restituisci per valore, scrivi in un buffer fornito dal chiamante oppure alloca con
malloc. - Non controllare se un parametro puntatore è
NULL. Una funzione che dereferenzia qualunque cosa le venga passata andrà in crash con un argomento nullo.
Domande frequenti
In C esiste il passaggio per riferimento?
No. Il C ha una sola modalità: il passaggio per valore. Una funzione riceve sempre una copia di ogni argomento, quindi assegnare un valore a un parametro non tocca mai la variabile del chiamante. Quello che sembra un passaggio per riferimento è il passaggio per valore di un puntatore: il puntatore viene copiato, ma punta ancora all'oggetto del chiamante, quindi la funzione può modificarlo attraverso il puntatore.
Come si passa un array a una funzione in C?
Scrivi il parametro come int a[] oppure int *a (significano la stessa cosa) e passa la lunghezza come parametro separato: int sum(const int a[], int n). L'array non viene copiato; la funzione riceve un puntatore al suo primo elemento, quindi le modifiche ad a[i] sono visibili al chiamante.
Perché sizeof non funziona su un parametro array in C?
Perché il parametro in realtà è un puntatore. sizeof(a) dentro la funzione restituisce la dimensione di un puntatore (di solito 8 byte), non quella dell'array, quindi sizeof(a)/sizeof(a[0]) dà qualcosa come 2 invece del numero di elementi. Passa sempre la lunghezza in modo esplicito.
Come può una funzione C restituire più di un valore?
Passa dei puntatori per i risultati aggiuntivi e scrivi attraverso di essi: int divide(int a, int b, int *quotient, int *remainder) restituisce un indicatore di successo e riempie entrambi gli output. L'alternativa è restituire una struct che raggruppa i valori.