Anatomia di una funzione
Una funzione è un blocco di codice con un nome che esegue un compito specifico e può essere richiamato più volte. Permette di evitare la duplicazione del codice e di suddividere un problema complesso in parti gestibili.
// PROTOTIPO (dichiarazione): annuncia l'esistenza della funzione al compilatore // Va messo prima del main se la definizione compare dopo int somma(int a, int b); int main() { int ris = somma(3, 5); // CHIAMATA: passa 3 e 5 come argomenti cout << ris; // stampa 8 return 0; } // DEFINIZIONE: il corpo effettivo della funzione int somma(int a, int b) { return a + b; // restituisce il risultato al chiamante }
Il prototipo dice al compilatore il nome, il tipo di ritorno e i tipi dei parametri — basta per poterla chiamare. La definizione contiene il corpo. Se la definizione precede il main, il prototipo è superfluo. Nei progetti multi-file il prototipo va nell'header (.h), la definizione nel sorgente (.cpp).
Funzioni void e return
// void: non restituisce nulla void saluta(string nome) { cout << "Ciao, " << nome << "!" << endl; // return; si usa solo per uscire anticipatamente (es. controllo errori) // in assenza di condizioni particolari, non va scritto } // con valore di ritorno: calcola e restituisce un risultato al chiamante float media(float a, float b) { return (a + b) / 2.0f; } int main() { saluta("Mario"); float m = media(7.5f, 8.0f); cout << "Media: " << m << endl; // 7.75 return 0; }
Passaggio di parametri
Quando si chiama una funzione si passano i valori su cui deve lavorare. Il C++ offre quattro modi, ognuno con effetti diversi sulla variabile originale nel chiamante.
Per valore
La funzione riceve una copia del valore. Qualsiasi modifica alla copia resta locale e non si riflette sulla variabile originale.
void prova(int x) { // x è una COPIA del parametro x = x * 2; // modifica solo la copia locale cout << "dentro: " << x << endl; } int main() { int a = 5; prova(a); cout << "fuori: " << a << endl; // stampa 5 — a NON è cambiato return 0; }
Per riferimento (&)
L'operatore & nella firma trasforma il parametro in un alias della variabile originale. Non viene creata nessuna copia: la funzione opera direttamente sulla variabile del chiamante.
void raddoppia(int& x) { // & = riferimento: x è un alias di chi viene passato x = x * 2; // modifica DIRETTAMENTE la variabile del chiamante } void scambia(int& a, int& b) { int tmp = a; a = b; b = tmp; } int main() { int n = 5; raddoppia(n); cout << n << endl; // 10 — n è cambiato int x = 3, y = 7; scambia(x, y); cout << x << " " << y; // 7 3 return 0; }
const riferimento (const tipo&)
Passare per riferimento evita di copiare il dato — utile con tipi grandi come string o struct. Aggiungere const impedisce alla funzione di modificare il valore: si ottiene efficienza senza rischio di modifiche accidentali.
// senza const: efficiente ma la funzione potrebbe modificare s void stampa(string& s) { cout << s; } // con const: zero copie + garanzia che s non venga modificata void stampa(const string& s) { cout << s << endl; // s = "altro"; // ❌ errore di compilazione: s è const } int main() { string nome = "Mario Rossi"; stampa(nome); // zero copie, nessuna modifica possibile return 0; }
Per tipi piccoli (int, float, char) il passaggio per valore è sufficiente — copiarli è istantaneo. Per tipi grandi (string, struct, array) usa const tipo& se non devi modificare il dato, e tipo& se devi modificarlo.
Per puntatore (*)
La funzione riceve l'indirizzo della variabile. L'effetto è lo stesso del riferimento — si modifica l'originale — ma la sintassi è diversa: nella chiamata si usa & per passare l'indirizzo, e dentro la funzione * per accedere al valore. È lo stile del C, ancora diffuso.
void raddoppia(int* p) { if (p == nullptr) return; // controlla sempre prima di dereferenziare *p = *p * 2; // * = vai all'indirizzo e modifica il valore } int main() { int n = 5; raddoppia(&n); // & = passa l'indirizzo di n cout << n << endl; // 10 return 0; }
Array come parametro
Array monodimensionali
Quando passi un array a una funzione, il compilatore lo tratta automaticamente come un puntatore al primo elemento (tecnicamente: array decay). L'array non viene copiato: la funzione lavora direttamente sull'originale. La dimensione deve essere passata come parametro separato.
// queste tre firme sono equivalenti per il compilatore: void stampa(int arr[], int n) // forma più leggibile — preferita void stampa(int* arr, int n) // equivalente: arr diventa puntatore void stampa(int arr[10], int n) // la dimensione [10] è ignorata // funzione che LEGGE l'array (const = nessuna modifica accidentale) void stampa(const int arr[], int n) { for (int i = 0; i < n; i++) cout << arr[i] << " "; cout << endl; } // funzione che MODIFICA l'array void raddoppia(int arr[], int n) { for (int i = 0; i < n; i++) arr[i] *= 2; // modifica l'array originale } int main() { int v[5] = {1, 2, 3, 4, 5}; stampa(v, 5); // 1 2 3 4 5 raddoppia(v, 5); stampa(v, 5); // 2 4 6 8 10 return 0; }
Dentro la funzione, sizeof(arr) restituisce la dimensione del puntatore (8 byte su 64 bit), non quella dell'array. La dimensione va sempre passata come parametro separato n.
Array bidimensionali
Con le matrici la regola cambia: la prima dimensione può essere omessa, la seconda è obbligatoria. Il compilatore ne ha bisogno per calcolare la posizione di ogni elemento in memoria: mat[i][j] corrisponde a *(mat + i * COL + j), quindi senza sapere quante colonne ha ogni riga non può fare i conti.
// prima dimensione [] omessa: ok // seconda dimensione [3]: OBBLIGATORIA void stampa(int mat[][3], int righe) { for (int i = 0; i < righe; i++) { for (int j = 0; j < 3; j++) cout << mat[i][j] << " "; cout << endl; } } void azzera(int mat[][3], int righe) { for (int i = 0; i < righe; i++) for (int j = 0; j < 3; j++) mat[i][j] = 0; // modifica la matrice originale } int main() { int m[2][3] = {{1,2,3}, {4,5,6}}; stampa(m, 2); // 1 2 3 / 4 5 6 azzera(m, 2); stampa(m, 2); // 0 0 0 / 0 0 0 return 0; }
Una funzione che accetta int mat[][3] accetta solo matrici con 3 colonne. Per matrici con numero di colonne variabile si usano altre tecniche (array di puntatori o allocazione dinamica) che vanno oltre questo livello.
Tabella comparativa
| Modalità | Sintassi firma | Chiamata | Modifica originale? | Quando usare |
|---|---|---|---|---|
| Per valore | void f(int x) | f(a) |
No — copia | Lettura, tipi piccoli |
| Per riferimento | void f(int& x) | f(a) |
Sì — alias | Output, swap, modifica |
| const riferimento | void f(const string& s) | f(a) |
No — protetto | Lettura di tipi grandi |
| Per puntatore | void f(int* p) | f(&a) |
Sì — via indirizzo | Stile C, puntatore nullable |
| Array 1D | void f(int arr[], int n) | f(v, 5) |
Sì — decay | Elaborazione array |
| Array 2D | void f(int m[][3], int r) | f(m, 2) |
Sì — decay | Elaborazione matrici |
🔧 Simulatore passaggio parametri
Imposta un valore iniziale, scegli la modalità e osserva cosa succede alla variabile nel chiamante prima, durante e dopo la chiamata.