Fine settembre 2026: nelle terze del liceo scientifico, opzione scienze applicate, il programma di Informatica ha appena chiuso il ripasso di selezione e cicli e sta entrando in ottobre nel capitolo delle funzioni e dei parametri. È lo stesso punto del programma su cui arrivano, nello stesso periodo, le terze degli istituti tecnici articolazione Informatica — cambia il contesto della scuola, non l’argomento. La prima verifica pratica in laboratorio su questo tema cade tipicamente fra sei e otto settimane da qui, quindi a novembre: il tempo per costruire un modello mentale solido, invece di imparare la sintassi a memoria, c’è.
Le funzioni sono il primo vero salto di astrazione dopo cicli e array: non più “un programma che fa una cosa”, ma “un programma diviso in pezzi che si richiamano a vicenda”. Ed è proprio nel passaggio dei dati fra questi pezzi — i parametri — che si annida l’errore più insidioso di tutto il capitolo: credere che una funzione possa sempre modificare quello che riceve, e scoprire a verifica che non è così.
Perché le funzioni: scomporre il problema
Una funzione nasce per due motivi, ed è bene tenerli distinti fin da subito perché la verifica li chiede entrambi:
- Riutilizzo: scrivi una volta il codice che calcola, per dire, il massimo di un array, e lo richiami ogni volta che ti serve, senza ricopiarlo.
- Scomposizione: un problema complesso si legge meglio (e si scrive con meno errori) se lo spezzi in sotto-problemi più piccoli, ciascuno con un nome e un compito preciso.
Una funzione ha una firma (o prototipo): tipo del valore restituito, nome, e l’elenco dei parametri formali — i “segnaposto” che compaiono nella definizione. Quando la funzione viene chiamata, le si passano i parametri attuali (o argomenti): i valori concreti di quella specifica chiamata.
// parametro formale: n
int quadrato(int n) {
return n * n;
}
int main() {
int risultato = quadrato(5); // 5 è il parametro attuale
std::cout << risultato << std::endl;
return 0;
}
L’errore più comune: return contro stampa a video
La prima cosa che una verifica pratica controlla, spesso senza dirlo esplicitamente, è se capisci la differenza fra una funzione che restituisce un valore con return e una che si limita a stamparlo a video con printf/std::cout (o print() in Python). Sono due cose diverse:
int quadrato(int n) {
return n * n; // restituisce un valore: il chiamante lo può usare
}
void stampaQuadrato(int n) {
std::cout << n * n << std::endl; // stampa e basta: non restituisce nulla
}
Se scrivi int x = stampaQuadrato(5); il compilatore protesta, perché una funzione void non produce un valore da assegnare. È un errore da compilazione, quindi in teoria “facile da beccare” — ma nella pratica capita spesso il contrario: una funzione dichiarata int che dimentica il return in qualche ramo del codice, e il compilatore in certi casi non se ne accorge subito, mentre lo scritto teorico sì.
Visibilità delle variabili: locali e globali
Ogni variabile ha uno scope, cioè una porzione di codice in cui esiste ed è visibile. Le variabili dichiarate dentro una funzione (compresi i parametri formali) sono locali: nascono quando la funzione viene chiamata e muoiono quando finisce. Una variabile dichiarata fuori da ogni funzione è globale: esiste per tutta la durata del programma ed è visibile ovunque.
int contatore = 0; // globale
void incrementa() {
contatore++; // modifica la variabile globale
int locale = 10; // esiste solo dentro incrementa()
}
La regola pratica che do sempre: usa variabili globali il meno possibile. Una funzione che legge e scrive variabili globali è difficile da testare da sola e nasconde effetti collaterali che lo scritto teorico ama chiedere di individuare (“cosa vale contatore dopo tre chiamate a incrementa()?”).
Passaggio per valore: la funzione lavora su una copia
In C++, per default, un parametro viene passato per valore: la funzione riceve una copia del dato, non il dato originale. Qualunque modifica dentro la funzione resta locale a quella copia.
void scambia_non_funziona(int a, int b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 3, y = 7;
scambia_non_funziona(x, y);
std::cout << x << " " << y << std::endl; // stampa ancora 3 7
return 0;
}
Lo scambio avviene sì, ma solo sulle copie locali a e b: quando la funzione finisce, quelle copie vengono distrutte e x, y nel chiamante restano invariate. È l’errore concettuale numero uno del capitolo, e viene chiesto quasi sempre in una domanda del tipo “cosa stampa questo programma?”.
Passaggio per riferimento: la funzione lavora sull’originale
Per far sì che una funzione modifichi davvero la variabile del chiamante, in C++ si usa il riferimento, con la & nella dichiarazione del parametro:
void scambia(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 3, y = 7;
scambia(x, y);
std::cout << x << " " << y << std::endl; // stampa 7 3
return 0;
}
int &a significa “a è un altro nome per la stessa variabile che mi hai passato”: non è una copia, è la variabile del chiamante vista con un nome diverso. Ogni modifica ad a dentro la funzione è una modifica diretta a x fuori. Confrontare i due esempi — scambia_non_funziona e scambia — fianco a fianco, e capire esattamente perché uno stampa 3 7 e l’altro 7 3, è l’esercizio mentale che sblocca davvero l’argomento.
Array passati a funzione: un caso a parte
Con gli array in C++ la situazione è diversa da un int singolo, ed è una fonte comune di confusione: un array passato come parametro non viene copiato. In C++ (come in C) il nome di un array decade all’indirizzo del suo primo elemento, quindi la funzione riceve di fatto un puntatore all’array originale — e può modificarlo, anche senza scrivere &:
void raddoppia_tutti(int arr[], int lunghezza) {
for (int i = 0; i < lunghezza; i++) {
arr[i] = arr[i] * 2;
}
}
int main() {
int voti[4] = {5, 6, 7, 8};
raddoppia_tutti(voti, 4);
// ora voti = {10, 12, 14, 16}: modificato per davvero
return 0;
}
Qui non c’è bisogno di &: l’array è già, di per sé, passato “per riferimento” nella sostanza. È il motivo per cui lo stesso identico problema — “questa funzione modifica il chiamante o no?” — ha risposta diversa a seconda che il parametro sia un int semplice o un array, e la verifica ama chiedere di distinguerli nello stesso compito.
E in Python? Non “per valore” né “per riferimento” puro
In Python la distinzione classica valore/riferimento non si applica allo stesso modo: si parla di passaggio per riferimento all’oggetto (o by object reference). La chiave è se l’oggetto passato è mutabile o immutabile:
def incrementa(n):
n = n + 1 # crea un nuovo intero, non modifica l'originale
def aggiungi_elemento(lista):
lista.append(0) # modifica la lista originale, sul posto
x = 5
incrementa(x)
print(x) # stampa ancora 5: gli int sono immutabili
voti = [6, 7, 8]
aggiungi_elemento(voti)
print(voti) # stampa [6, 7, 8, 0]: le liste sono mutabili
Un intero (int) in Python è immutabile: n = n + 1 non modifica l’oggetto puntato da n, ne crea uno nuovo e lo assegna al nome locale n dentro la funzione — l’originale fuori resta intatto. Una lista è mutabile: lista.append(0) modifica l’oggetto stesso a cui punta lista, e siccome sia il parametro dentro la funzione sia la variabile fuori puntano allo stesso oggetto in memoria, la modifica si vede anche fuori. Lo stesso identico ragionamento — “questo dato può essere modificato sul posto?” — vale per array in C++ e liste in Python: cambia il meccanismo, non la domanda da porsi.
Tracciare a mano lo stack delle chiamate
Il modo più affidabile per non sbagliare “cosa stampa questo programma” è tracciare la sequenza delle chiamate in una tabella, riga per riga:
| Passo | Chiamata | Variabili locali | Cosa succede al chiamante |
|---|---|---|---|
| 1 | main() inizia | x=3, y=7 | — |
| 2 | scambia(x, y) chiamata | a e b sono alias di x, y | nessuna copia creata |
| 3 | temp = a | temp=3 | — |
| 4 | a = b | x diventa 7 | modifica diretta |
| 5 | b = temp | y diventa 3 | modifica diretta |
| 6 | funzione finisce | — | x=7, y=3 |
Fare questa tabella su carta, prima di lanciare il programma al PC, è esattamente il metodo che uso a lezione con chi si blocca su questo argomento: costringe a decidere, riga per riga, se si sta creando una copia o si sta toccando l’originale.
Tre esercizi svolti
Esercizio 1 — scambio di due valori.
In C++, con riferimenti (vedi sopra: scambia(int &a, int &b) scambia x=3, y=7 in x=7, y=3). In Python lo stesso risultato si ottiene senza funzione dedicata, sfruttando l’assegnazione multipla — non serve passaggio per riferimento perché non serve nessuna funzione:
x, y = 3, 7
x, y = y, x
print(x, y) # 7 3
Esercizio 2 — funzione che modifica un array (o una lista).
Scrivere una funzione che azzera tutti gli elementi negativi di un array di interi.
void azzera_negativi(int arr[], int lunghezza) {
for (int i = 0; i < lunghezza; i++) {
if (arr[i] < 0) {
arr[i] = 0;
}
}
}
In Python, stessa logica su una lista:
def azzera_negativi(lista):
for i in range(len(lista)):
if lista[i] < 0:
lista[i] = 0
voti = [5, -2, 8, -1, 3]
azzera_negativi(voti)
print(voti) # [5, 0, 8, 0, 3]
Esercizio 3 — funzione che restituisce due valori.
In C++ una funzione restituisce un solo valore con return, quindi per “restituirne due” si usano parametri passati per riferimento come canali di uscita:
void minMax(int arr[], int lunghezza, int &minimo, int &massimo) {
minimo = arr[0];
massimo = arr[0];
for (int i = 1; i < lunghezza; i++) {
if (arr[i] < minimo) minimo = arr[i];
if (arr[i] > massimo) massimo = arr[i];
}
}
int main() {
int voti[5] = {6, 8, 5, 9, 7};
int min, max;
minMax(voti, 5, min, max);
std::cout << min << " " << max << std::endl; // 5 9
return 0;
}
In Python il problema non esiste: una funzione può restituire direttamente una tupla con più valori.
def min_max(lista):
return min(lista), max(lista)
voti = [6, 8, 5, 9, 7]
minimo, massimo = min_max(voti)
print(minimo, massimo) # 5 9
Vale la pena notare questa differenza a lezione: quello che in C++ richiede riferimenti come “uscite aggiuntive” della funzione, in Python è semplicemente il caso più comune di return multiplo — non è che Python sia “più furbo”, è che il linguaggio tratta diversamente il concetto di valore restituito.
Dove porta dopo
Il passo successivo naturale, che il tuo programma affronterà più avanti, è la ricorsione — una funzione che richiama se stessa: qui non ce ne siamo occupati apposta, perché il post sui cicli annidati e gli array in terza informatica la introduce già insieme al resto del programma di terza. Se invece il tuo indirizzo lavora soprattutto in Python e vuoi una visione più ampia del linguaggio, la roadmap Python in 6 settimane copre funzioni, liste e applicazioni a matematica e fisica. E se in futuro affronterai il C “puro” all’università, i puntatori spiegati bene riprendono esattamente questo discorso di indirizzi e riferimenti, un livello più in profondità.
Come lavoro su questo argomento in lezione
Con chi arriva bloccato sui parametri, il primo passo non è rispiegare la sintassi di &: è far tracciare a mano, come nella tabella qui sopra, due o tre chiamate di funzione — una per valore, una per riferimento, una con un array — finché non diventa ovvio da solo perché una modifica “si vede” fuori e l’altra no. Una volta che il modello mentale è a posto, la sintassi si impara in dieci minuti.
Per allenarti su questo e sugli argomenti vicini, in /simulazioni/liceo/ trovi le verifiche organizzate per materia: su funzioni e parametri di informatica non abbiamo ancora una simulazione dedicata, e te lo dico apertamente invece di rimandarti a qualcosa che non c’entra.
Le lezioni di informatica sono online e uno a uno: si parte da dove ti sei bloccato — spesso proprio “perché questa funzione non modifica quello che le passo” — e si costruisce da lì. Se vuoi parlarne prima, siamo su /contatti/.