# Funzioni ## Introduzione alle Funzioni in C++ In C++, una **funzione** è un blocco di codice che esegue un compito specifico. Le funzioni sono una componente essenziale di qualsiasi programma, perché permettono di organizzare il codice in modo modulare e riutilizzabile. ### Vantaggi dell'uso delle funzioni: - **Modularità**: suddividendo il codice in funzioni, è possibile isolare le diverse parti logiche e semplificare la comprensione e la manutenzione del programma. - **Riutilizzabilità**: una funzione può essere chiamata più volte, evitando la duplicazione del codice. - **Testabilità**: è più facile testare e fare debug su una singola funzione piuttosto che su un programma monolitico. Ogni funzione ha un **nome** che la identifica e che viene utilizzato per invocarla. Può accettare **parametri** (dati in ingresso) e restituire un **valore**. ## Dichiarazione e Definizione di una Funzione In C++, le funzioni sono costituite da due parti principali: **dichiarazione** e **definizione**. - **Dichiarazione della funzione**: informa il compilatore dell'esistenza della funzione, specificando il nome, i parametri (se presenti) e il tipo di valore restituito. È spesso inserita nella parte iniziale del file. Esempio di dichiarazione: ```cpp int somma(int, int); // dichiara una funzione che accetta due interi e restituisce un intero ``` - **Definizione della funzione**: specifica il corpo della funzione, cioè l'implementazione vera e propria di ciò che fa la funzione. Esempio di definizione: ```cpp int somma(int a, int b) { return a + b; // restituisce la somma dei due numeri } ``` > **Nota**: È importante che i tipi dei parametri e il tipo di ritorno della funzione nella dichiarazione corrispondano esattamente a quelli nella definizione. ## Parametri delle Funzioni Le funzioni possono accettare **parametri** che servono come input per eseguire operazioni. I parametri sono variabili che vengono definite nella parentesi della dichiarazione della funzione. Questi parametri vengono utilizzati all'interno della funzione per svolgere calcoli o elaborazioni. Quando una funzione accetta parametri, il valore passato viene **copiato** all'interno della funzione, quindi eventuali modifiche a tale valore all'interno della funzione **non influenzeranno il valore originale**. Esempio: ```cpp void stampaQuadruplo(int numero) { numero = numero * 4; // Modifica la copia locale cout << "Il quadruplo è: " << numero << endl; } ``` Se la funzione viene chiamata con: ```cpp int valore = 5; stampaQuadruplo(valore); cout << valore; // Stampa ancora 5, perché la modifica avviene su una copia ``` > Questo meccanismo di copia dei valori avviene per tutti i parametri... **tranne quelli di tipo array** > > Quando un array viene passato a una funzione, non viene copiata una nuova versione dell’array. Al contrario, viene passato un **riferimento** all’array originale, quindi le modifiche effettuate alla variabile all'interno della funzione influenzeranno anche l’array esterno. > > Esempio: > ```cpp > void modificaArray(int arr[]) { > arr[0] = 100; // Modifica il primo elemento dell'array originale > } > > int main() { > int numeri[] = {1, 2, 3}; > modificaArray(numeri); > cout << numeri[0]; // Output: 100, quindi l’array originale è stato modificato > return 0; > } > ``` > Questa particolarità è dovuta al fatto che in C++ un array viene gestito come un indirizzo di memoria, quindi la funzione accede direttamente all’array originale e non a una sua copia. ## Tipi di Ritorno delle Funzioni Le funzioni possono restituire un valore utilizzando la parola chiave `return`. Il **tipo di ritorno** della funzione (definito prima del nome della funzione) specifica il tipo di dato che verrà restituito. Esempi di tipi di ritorno comuni: - `void`: la funzione non restituisce alcun valore. - Tipi primitivi: `int`, `double`, `char`, `bool`, ecc. - Tipi complessi come *puntatori*, o strutture (`struct`). Esempio: ```cpp double calcolaAreaCerchio(double raggio) { return 3.14159 * raggio * raggio; // Restituisce l'area del cerchio } ``` > **Nota**: una funzione può restituire **un solo** valore. Tuttavia, spesso è utile restituire più valori. Vedremo come fare questo nel capitolo seguente. ## Ambito delle Variabili (Scope) e Durata delle Variabili (Lifetime) Quando si lavora con le funzioni in C++, è importante comprendere come funzionano l'ambito e la durata delle variabili, perché influiscono direttamente sul comportamento e sulla gestione della memoria nel programma. ### Ambito delle Variabili (Scope) L’**ambito** di una variabile si riferisce alla porzione del codice in cui la variabile è visibile e utilizzabile. In C++, ci sono vari tipi di ambito: - **Ambito Locale**: - Le variabili dichiarate all'interno di una funzione (o di un blocco come un loop o un `if`) sono visibili solo all'interno di quella funzione o blocco. - Quando la funzione termina, queste variabili vengono automaticamente distrutte e la memoria viene liberata. Esempio: ```cpp void esempio() { int x = 10; // Variabile locale: visibile solo all'interno di questa funzione cout << x << endl; } // cout << x << endl; // ERRORE: 'x' non è visibile qui ``` - **Ambito Globale**: - Le variabili dichiarate al di fuori di tutte le funzioni sono visibili e accessibili da qualsiasi parte del programma. - Tuttavia, le variabili globali vanno usate con cautela, perché possono rendere il codice meno modulare e più difficile da gestire. Esempio: ```cpp int globale = 100; // Variabile globale void stampaGlobale() { cout << globale << endl; // Accesso a variabile globale } ``` ### Durata delle Variabili (Lifetime) La **durata** di una variabile si riferisce a quanto tempo una variabile esiste in memoria durante l'esecuzione del programma: - **Variabili Automatiche (Locali)**: - La maggior parte delle variabili locali ha una **durata automatica**: vengono allocate quando la funzione viene chiamata e deallocate quando la funzione termina. - Queste variabili sono allocate nello stack. Esempio: ```cpp void esempio() { int y = 5; // y è allocata e distrutta ogni volta che esempio() viene chiamata } ``` - **Variabili Globali**: Le variabili globali hanno una durata che coincide con l'intero ciclo di vita del programma, e possono essere visibili ovunque. ## Restituire Più Valori: Utilizzo delle `struct` Poiché una funzione in C++ può restituire solo un valore alla volta, una soluzione comune per restituire più valori è l'uso delle **`struct`**, una struttura che consente di raggruppare virtualmente più valori sotto un unico valore effettivo. ### Creare una `struct` per restituire più valori Ad esempio, supponiamo di voler creare una funzione che restituisce sia la somma che il prodotto di due numeri. Creiamo una `struct` chiamata `Risultati` che contiene entrambi i valori: ```cpp struct Risultati { int somma; int prodotto; }; ``` Ora, definiamo una funzione che utilizza questa `struct` come tipo di ritorno: ```cpp Risultati calcolaSommaEProdotto(int a, int b) { Risultati res; res.somma = a + b; res.prodotto = a * b; return res; } ``` La funzione `calcolaSommaEProdotto` accetta due interi e restituisce una `struct` contenente sia la somma che il prodotto di questi numeri. ### Esempio di utilizzo ```cpp int main() { int x = 4, y = 5; Risultati risultato = calcolaSommaEProdotto(x, y); cout << "Somma: " << risultato.somma << endl; // Output: Somma: 9 cout << "Prodotto: " << risultato.prodotto << endl; // Output: Prodotto: 20 return 0; } ``` In questo modo, possiamo restituire più valori da una funzione raggruppandoli in una struttura, rendendo il codice più organizzato e chiaro. ## Funzioni Ricorsive Le **funzioni ricorsive** sono funzioni che chiamano se stesse direttamente o indirettamente. La ricorsione è uno strumento potente che permette di risolvere problemi complessi suddividendoli in sottoproblemi più semplici dello stesso tipo. ### Struttura di una Funzione Ricorsiva Una funzione ricorsiva deve avere: - **Caso Base**: Una condizione che, quando soddisfatta, termina la ricorsione. Senza un caso base, la funzione continuerebbe a chiamarsi all'infinito, causando uno **stack overflow**. - **Passo Ricorsivo**: La parte della funzione che riduce il problema e chiama nuovamente la funzione con parametri modificati, avvicinandosi gradualmente al caso base. Esempio classico: il **fattoriale** di un numero (n!): ```cpp int fattoriale(int n) { if (n == 0) { // Caso base return 1; } else { // Passo ricorsivo return n * fattoriale(n - 1); } } ``` Chiamando `fattoriale(5)`, la funzione si chiamerà ripetutamente riducendo `n` finché `n` non raggiunge 0, momento in cui ritornerà `1` e risolverà le chiamate annidate. ### Vantaggi e Svantaggi della Ricorsione - **Vantaggi**: - La ricorsione può semplificare la scrittura e la comprensione di algoritmi che seguono una struttura ripetitiva o gerarchica (come l'attraversamento di alberi o grafi). - Permette di esprimere alcuni algoritmi in modo naturale e compatto. - **Svantaggi**: - Può essere meno efficiente rispetto all'uso di cicli iterativi, poiché ogni chiamata ricorsiva comporta un sovraccarico di memoria. - Se il caso base non è ben definito o se la ricorsione non si avvicina correttamente al caso base, può causare un **loop infinito** e un **stack overflow**. ### Esempi di Funzioni Ricorsive - **Serie di Fibonacci**: Calcolare l'n-esimo numero della serie di Fibonacci: ```cpp int fibonacci(int n) { if (n == 0) return 0; // Caso base if (n == 1) return 1; // Caso base return fibonacci(n - 1) + fibonacci(n - 2); // Passo ricorsivo } ``` - **Ricerca in un Array**: Ricerca binaria su un array ordinato (divide et impera): ```cpp int ricercaBinaria(int arr[], int sinistra, int destra, int target) { if (sinistra > destra) return -1; // Caso base: elemento non trovato int medio = (sinistra + destra) / 2; if (arr[medio] == target) return medio; if (arr[medio] > target) return ricercaBinaria(arr, sinistra, medio - 1, target); // Ricerca a sinistra else return ricercaBinaria(arr, medio + 1, destra, target); // Ricerca a destra } ``` ### Considerazioni Finali sulla Ricorsione Quando si utilizza la ricorsione, è importante bilanciare leggibilità e efficienza: - La ricorsione è adatta quando il problema può essere suddiviso facilmente in sottoproblemi più piccoli. - In casi in cui la ricorsione diventa troppo profonda (es. calcolo del Fibonacci senza ottimizzazioni), potrebbe essere più efficiente un approccio iterativo o una **memoizzazione** (salvataggio dei risultati intermedi).