Sommario
< Home
Stampa

Funzioni

Cenni matematici

Da un punto di vista matematico la definizione più semplice di funzione è una relazione tra due insiemi A (detto dominio) e B (detto codominio), che associa ad ogni elemento di A un elemento di B.

In termini formali si esprime nel seguente modo:

f:A->B

Dove per ogni x ∈ A si ha f(x) = B

Esempi:

  • F(x) = 2*x: dominio numeri interi e codominio numeri interi
  • F(x) = x / 2: dominio numeri interi e codominio numeri razionali

Da questa definizione più semplice discende la definizione più generale: dato un gruppo arbitrario (e finito) di insiemi A, B, C… K una funzione è definita come una relazione tra questi ed un insieme Z, che associa un elemento di A, un elemento di B, un elemento di C, e via andare ad un elemento di Z. Si ha ovverof:X,Y,Z…K → Zf:X,Y,ZK → Z

dove per a  A, b  B, c  C … k  K  si ha quindi che f(a, b, c, … k) = Z

a,b,c,…k sono una tupla di parametri (detti anche argomenti), e f(a,b,c,…k) è il risultato della funzione, cioè il suo valore di ritorno.

Le funzioni in informatica

La definizione matematica di funzione viene utilizzata in informatica per definire dei sottoprogrammi del programma principale. Questi sottoprogrammi svolgono quindi funzioni del programma e sono riutilizzabili in punti diversi del programma principale. L’importanza delle funzioni è quindi quella di rendere possibile la modularizzazione dei programmi informatici: è possibile suddividere un programma in sottoprogrammi, e riutilizzare gli stessi sottoprogrammi in programmi differenti.

Questo rende possibile quindi:

  • Scrivere meno codice, evitando ripetizioni;
  • Riusare codice già sviluppato;
  • Riusare codice di terze parti;
  • Sviluppare progetti complessi mediante la strategia del “divide et impera” che consiste nel suddividere un grosso problema in sottoproblemi, da risolvere separatamente;
  • Suddividere il lavoro di un singolo progetto tra più programmatori.

Le funzioni sono quindi alla base della cosiddetta programmazione modulare, ovvero una metodologia di programmazione che consiste nel realizzare programmi composti da moduli, ognuno dei quali specializzato in specifiche attività.

Struttura di una funzione

In linguaggio C/C++ (e in modo simile nella maggior parte dei linguaggi di programmazione) una funzione può essere dichiarata nel seguente modo:

returnType functionName ([parameter1Type parameter1][, parameter2Type parameter2]…) {
   … (code block)
  [ return expression; ]
}

Le parentersi quadre mostrano quali elementi sono facoltativi.

Vediamo singolarmente i singoli elementi:

returnType: tipo di ritorno, esso può essere qualsiasi tipo valido C++

functionName: nome della funzione

parameterType: tipo di dato passato come parametro

parameter: tipo di dato passato come parametro

return expression: istruzione che restituisce una espressione che deve essere del tipo di ritorno

Come si vede sopra, tutti i parametri sono facoltativi. L’argomento dei parametri sarà approfondito nella sezione relativa ai parametri.

La prima riga di intestazione della funzione (che contiene il tipo di ritorno, il nome, ed i parametri) viene chiamata firma.

Vediamo un esempio:

int somma(int x, int y) {
  return x+y;
}

Questa funzione prende due parametri x ed y di tipo intero e restituisce la loro somma.

All’interno di un blocco di funzione è possibile scrivere codice, compresa la creazione di variabile e l’utilizzo di costrutti.

Se in una funzione matematica (come sopra descritte) si trasforma un insieme di parametri in un risultato, le funzioni in ambito informatico estendono questo concetto. Esse infatti possono ricevere zero, uno o più parametri.

Possono anche non restituire alcun valore: in questo caso bisogna impostare come tipo di ritorno la parola chiave “void”. Esse inoltre non prevedono una istruzione return.

Esempio:

void printResult(char character) {
  cout << character;
}

Chiamata di funzione e sua esecuzione

Una funzione viene eseguita quando viene invocata dal main() oppure da altra funzione. La funzione da cui si chiama si chiama chiamante, la funzione chiamata si chiama funzione chiamata.

Esempio:

#include <iostream>
using namespace std;

int somma(int x, int y) {
  int sum = x+y;
  return sum;
}

int main() {
  int a;
  cout << “Inserisci il primo addendo: “;
  cin >> a;
  int b;
  cout << “Inserisci il secondo addendo: ”;
  cin >> b;
  cout << “La somma è: “;
  cout << somma(a, b);
  return 0;
}

Nell’esecuzione di questo codice, la funzione chiamante cede il controllo alla funzione somma, che utilizza i valori ricevuti per eseguire la sua elaborazione (una somma) e restituisce il valore che viene poi elaborato nel main (viene stampato a schermo).

I parametri con cui è definita la funzione

int somma(int x, int y)

sono chiamati parametri formali.

Quando viene chiamata dall’istruzione:

somma(a, b)

questi parametri formali assumono poi il valore effettivo di a e b.

Schema della memoria

Abbiamo già visto (in questa lezione) che il processo in esecuzione ha una memoria dedicata composta da uno spazio per il codice, uno per lo heap ed uno per lo stack:

notion image

L’argomento è approfondito qui.

L’utilizzo dello stack garantisce che l’esecuzione di una funzione abbia uno spazio di memoria dedicato. Quando la funzione viene eseguita il processore inserisce nello stack (che ha una struttura dati a pila) i parametri della funzione stessa, ed il punto del programma a cui tornare a funzione conclusa. Inoltre la funzione memorizza nello stack tutte le variabili locali che definisce. Al termine dell’esecuzione il processore rimuove dallo stack tutti questi elementi e lo riporta allo stato che aveva prima dell’esecuzione. Inoltre restituisce il valore di ritorno (quando presente) all’istruzione chiamante. Nell’esempio sopra, quindi, la funzione somma svolge le sue operazioni, e poi quando termina tutti i suoi dati vengono rimossi dallo stack. Questo meccanismo garantisce che lo spazio “locale” usato dalla funzione esista solo durante l’esecuzione della funzione, e che venga poi eliminato una volta che la funzione termina la sua esecuzione. La funzione è quindi una specie di bolla che viene creata e distrutta.

E’ importante quindi capire che i parametri passati alla funzione vengono copiati nello stack. Ed essendo delle copie, possono essere modificati senza che il valore degli originali sia modificato.

Se poi una funzione chiama un’altra funzione, viene creata una nuova area nello stack che si aggiunge alla precedente area dello stack, questo perché lo stack è una pila. Quindi anche la seconda funzione memorizzerà una copia dei parametri, più le proprie variabili locali, fino al termine della sua esecuzione (poi saranno cancellati).

Il codice che viene eseguito nella funzione vede solo i dati relativi alla funzione attualmente in esecuzione e non la funzione chiamante, e che una volta che viene conclusa l’esecuzione, torna visibile l’area sottostante.

Passaggio di parametri alle funzioni

Per valore

Il passaggio di parametri che abbiamo appena visto viene definito per “valore”: i parametri della funzione vengono copiate nello stack, e la funzione lavora solo con questa copia.

Nella chiamata per valore il chiamante può passare alla funzione parametri sia sotto forma di un simbolo (variabile o costante) che sotto forma di espressione. E’ quindi possibile eseguire:

somma(3, 4);

somma(a, b);

somma(a, 3);

somma(0, a);

La funzione chiamata copia i valori ricevuti nelle variabili definite negli argomenti, e può modificare il valore queste variabili. Siccome è un copia questa modifica non ha effetto sull variabile originaria.

Per riferimento

In molte situazioni può essere invece utile poter modificare i valori originali: le funzioni possono ritornare un solo valore ma quando ne vogliamo modificare più di uno, diventa molto scomodo.

Tuttavia se come parametro passiamo un puntatore ad una variabile, quindi il suo indirizzo di memoria, quello che viene effettivamente copiato è l’indirizzo, non il valore. Nella funzione possiamo quindi modificare il valore “puntato”, che andrà a modificare il valore della variabile originale. Questo perchè non abbiamo copiato la variabile ma solo il suo indirizzo di memoria.

Questo tipo di chiamata viene detta per riferimento.

In questo esempio vedremo una funzione senza valore di ritorno, che va a memorizzare direttamente il risultato con un puntatore di memoria.

void sommaConPuntatore(int* sum, int x, int y) {
   *sum = x + y;
}

int main() {
  int somma;
  sommaConPuntatore(&somma, 3, 4);
  cout << somma;
}

Come si può notare, la firma della funzione prevede che il primo parametro sia un puntatore ad intero (cioè l’indirizzo di memoria che contiene una variabile intera). Quando viene eseguita la funzione verrà quindi passato il riferimento alla variabile somma (cioè appunto il suo indirizzo di memoria).

Quando viene eseguita la funzione, quindi, questa accederà al valore originale della variabile somma, e questo rimarrà anche una volta che la funzione terminerà.

Vediamo un altro esempio, molto più significativo. Poniamo di voler scambiare il valore di due variabili, e creiamo una funzione che svolge per noi questa cosa.

#include <iostream>
using namespace std;

void swap(int *x, int *y) {
  int temp = *x;
  *x = *y;
  *y = temp;
}

int main() {
  int a;
  cout << “Inserisci il primo numero: “;
  cin >> a;
  int b;
  cout << “Inserisci il secondo numero: ”;
  cin >> b;
  swap(&a, &b);
  cout << “Adesso il primo è: “ << b << endl;
  cout << “Adesso il secondo è: “ << a << endl;
  return 0;
}

E’ importante osservare di nuovo che:

  • Nella firma della funzione swap, si usano degli argomenti puntatore (contraddistinti dal carattere *). Il puntatore  è il valore dell’indirizzo di memoria dove risiede la variabile.
  • Per accedere alla variabile puntata bisogna quindi referenziarla usando come prefisso il carattere *.

L’utilizzo di parametri con riferimento permette di aggirare l’ostacolo avere una funzione che restituisce un unico valore, in quanto è possibile modificare uno o più parametri della funzione.

Array

Le funzioni che prevedono degli array come parametri usano sempre la modalità per riferimento. Questo perché gli array non sono un tipo base, ma si riferiscono ad un’area di memoria che può contenere molti valori. E’ possibile chiamare una funzione con array con 3 modalità alternative ma equivalenti:

  • void myFunction(int *vector, …)

In questa modalità – la più generica – viene dato un generico puntatore a variabile. Sta al programmatore capire che non è un puntatore ad un singolo valore ma ad un intero array, e quale la dimensione dell’array.

Questo è dovuto al fatto che una variabile di tipo array è, di fatto, sempre un puntatore al primo elemento dell’array stesso.

E’ quindi fortemente raccomandato passare almeno un secondo parametro con la dimensione dell’array. Questo perché la funzione non ha modo di sapere la dimensione dell’array stesso.

  • void myFunction(int vector[], …)

In questa modalità viene indicato che il parametro è un array, ma è solo un formalismo, la funzione ignora la dimensione dell’array.

  • void myFunction(int vector[10], …)

In questa modalità viene indicato sia che il parametro è un array, sia viene data la dimensione. Questo significa che chi scrive la funzione si aspetta un array di quella dimensione: in questo caso è più difficile sbagliare, ma ovviamente funziona quando si sa già che gli array hanno una dimensione fissa.

Si ricorda che in C++ non esiste nessun meccanismo che impedisca di superare i limiti di un array. E’ compito del programmatore evitare questo tipo di errori.

Non è invece possibile, almeno con le strutture finora viste, creare una funzione che restituisca un array. Infatti quando si crea un array dentro ad una funzione, questo viene distrutto dallo stack quando si esce dalla funzione. Questo limite verrà superato con l’uso del modificatore static, che si vedrà successivamente.

Funzioni deterministiche e non deterministiche

L’esempio appena visto è un esempio di come il concetto di funzione in C++ è più generale rispetto a quello definito nelle funzioni matematiche.

Infatti, se una funzione matematica è una relazione che lega una determinata tupla di parametri ad un risultato, le funzioni in ambito informatico – ed in particolare in C++ – estendono questo concetto. Esse infatti possono:

  • Ricevere zero, uno o più parametri;
  • Restituire un risultato o anche nessun risultato;
  • Ricevere uno o più parametri e modificarli all’interno della funzione.

Le funzioni che legano una determinata tupla di parametri ad un risultato sono definite come come deterministiche, in quanto a partire da una determinata tupla di parametri forniscono sempre lo stesso risultato.

Se non soddisfano questo requisito sono definite non deterministiche.

Esempi di funzioni non deterministiche:

  • una funzione che non riceve parametri e da un risultato;
  • una funzione che anche con gli stessi parametri restituisce risultati differenti in chiamate differenti;
  • una funzione che non restituisce nulla.

Funzione main()

La funzione main è una funzione speciale, in quanto è il punto di ingresso dell’applicazione.

Essa riceve come parametro gli argomenti di avvio dell’applicazione (che corrispondono agli argomenti passati all’applicazione via linea di comando) e restituisce un numero intero, che corrisponde ad un codice, che può essere 0 se l’applicazione termina correttamente, altrimenti un codice diverso da 0 per indicare l’errore.

Il programma compilato quando viene lanciato dal sistema operativo esegue la funzione main, che (eventualmente) a sua volta esegue delle funzioni, che a loro volta possono eseguire altre funzioni.

Argomenti di default

E’ possibile definire degli argomenti di “default” nella definizione di una funzione.

Ad esempio si prenda in considerazione la seguente funzione:

void printChar(int count, char = “*”) {
   for (int i=0; i<=count; i++) {
   cout << char;
}
cout << endl;
}

Come si vede il secondo argomento è valorizzato.

La funzione si può chiamare in due modi:

printChar(5, “#”); -> stampa 5 volte il carattere #
printChar(5); -> stampa 5 volte il carattere di default (*)