Questo sito utilizza cookie per raccogliere dati statistici.
Privacy Policy
# 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).