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 più gestibili.
Immagina una funzione come una scatola nera: dall'esterno vedi solo cosa entra e cosa esce — non come funziona internamente. Può ricevere zero o più valori in ingresso (i parametri) e può produrre output in due modi: tramite il valore di ritorno (al massimo uno, dichiarato prima del nome) oppure tramite parametri passati per riferimento usati come canali di uscita. Grazie a quest'ultimo meccanismo una funzione può di fatto restituire più risultati contemporaneamente.
// DEFINIZIONE: tipo_ritorno nome(parametri) { corpo } int somma(int a, int b) { // riceve due int, restituisce un int return a + b; // valore restituito al chiamante } int main() { int ris = somma(3, 5); // CHIAMATA: passa 3 e 5 come argomenti cout << ris; // stampa 8 return 0; }
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] << "\t"; 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.
Prototipi di funzione
Il compilatore C++ deve conoscere una funzione prima di incontrarne la chiamata. La soluzione più semplice è scrivere la definizione sopra il main. Quando invece si preferisce avere il main in cima al file — più leggibile per chi studia il programma — si usa il prototipo: una sola riga che dichiara nome, tipo di ritorno e tipi dei parametri, senza il corpo.
// ── PROTOTIPI ───────────────────────────────────────────── // Il compilatore conosce le funzioni ancora prima di vederle int somma(int a, int b); // tipo ritorno + nome + tipi parametri void saluta(string nome); // i nomi parametro sono opzionali // ── MAIN ────────────────────────────────────────────────── int main() { int ris = somma(4, 7); // OK: somma è già dichiarata saluta("Mario"); // OK: saluta è già dichiarata cout << "Somma: " << ris << endl; return 0; } // ── DEFINIZIONI ─────────────────────────────────────────── // Devono corrispondere esattamente al prototipo int somma(int a, int b) { return a + b; } void saluta(string nome) { cout << "Ciao, " << nome << "!" << endl; }
Tipo di ritorno e tipi dei parametri devono essere identici. I nomi dei parametri nel prototipo sono opzionali: int somma(int, int); è equivalente a int somma(int a, int b); — ma scriverli rende il codice più leggibile.
Nelle applicazioni più grandi il codice viene diviso in più file sorgente. I prototipi vengono raccolti in un file header (.h), le definizioni nel file sorgente (.cpp). Ogni file che usa le funzioni include l'header con #include "miefunzioni.h". È un argomento che affronteremo in una pagina dedicata alla compilazione separata.
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 |
Tutti e cinque i casi in una pagina sola — pronta per la stampa o il salvataggio in PDF dal dialogo di stampa del browser.
🔧 Simulatore passaggio parametri
Imposta un valore iniziale, scegli la modalità e osserva cosa succede alla variabile nel chiamante prima, durante e dopo la chiamata.