gl/ripetizioni

Funzioni e passaggio dei parametri: valore e riferimento

Funzioni, parametri formali e attuali, passaggio per valore e per riferimento in C++ e Python: teoria, errori tipici e tre esercizi svolti.

di Gaetano Livornese

  • #informatica
  • #terza superiore
  • #liceo scientifico
  • #funzioni
  • #passaggio dei parametri
  • #programmazione

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:

  1. 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.
  2. 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:

PassoChiamataVariabili localiCosa succede al chiamante
1main() iniziax=3, y=7—
2scambia(x, y) chiamataa e b sono alias di x, ynessuna copia creata
3temp = atemp=3—
4a = bx diventa 7modifica diretta
5b = tempy diventa 3modifica diretta
6funzione 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/.

Continua a leggere

Post correlati.

Vuoi applicare quello che hai letto?

Parliamone 30 minuti gratis: guardiamo insieme dove sei bloccato e ti dico onestamente come posso aiutarti.

Oppure scrivimi su WhatsApp.