Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial
  • Lezione 13 di 93
  • livello avanzato
Indice lezioni

Introduzione alle Funzioni

Cosa sono le funzioni in C++, come creale e descriverle tramite prototipi, definire parametri per passare informazioni e ritornare i valori elaborati.
Cosa sono le funzioni in C++, come creale e descriverle tramite prototipi, definire parametri per passare informazioni e ritornare i valori elaborati.
Link copiato negli appunti

In questa lezione introdurremo il concetto di funzione, per esplorare alcuni aspetti del linguaggio C++ ispirati dai paradigmi della programmazione procedurale e funzionale.

Quando è necessario ripetere un certo numero di istruzioni è possibile incapsularle in un blocco di codice a cui viene associato un nome. In questo modo, è possibile invocare lo stesso blocco di codice in più parti del programma con un'unica istruzione, anzichè ripetere il suo contenuto.

Il beneficio che ne deriva va oltre il fatto di rendere più snello il nostro codice. La definizione di funzioni costituisce infatti una prima realizzazione del concetto di incapsulamento, uno dei capi saldi della programmazione moderna, soprattutto di quella orientata agli oggetti.

Una funzione, infatti, può essere vista come una scatola nera, cioè un oggetto di cui non necessariamente conosciamo il funzionamento interno, che produce un risultato a partire da un insieme di argomenti in input.

Per modificare (o correggere) il comportamento di una funzione che viene invocata in molteplici parti del programma, ci basta agire sul blocco di codice di cui è composta, lasciando intatto il resto. Si può cioè produrre un impatto notevole su tutto il codice modificandone solo una piccola parte. Da questo deriva la potenza semantica di questo costrutto e la sua importanza nella programmazione a tutti i livelli.

Firma o prototipo di una funzione

Il nome di una funzione e la relazione in termini di di ingresso/uscita sono definiti nella sua dichiarazione:

<tipo> <nome_funzione> (<tipo> <nome_arg1>, <tipo> <nome_arg2&gt, ... <tipo> <nome_argN&gt);

Il primo elemento della dichiarazione è il tipo del valore restituito. Esso è seguito dal nome della funzione e da una lista di parametri tra parentesi tonde, separati da virgole. Per ognuno di essi si indica il tipo ed il nome. La dichiarazione è terminata dal simbolo ";". Il nome scelto per la funzione dovrebbe dare un'idea del tipo di operazione che essa espleta.

Quando si ha intenzione di usare una funzione in molteplici punti del codice, è opportuno definire uno header file che ne contenga la sola dichiarazione. In questo nodo, usando la direttiva #include, si potrà invocare la funzione in qualsiasi listato.

La definizione di una funzione corrisponde all'elenco di istruzioni che vengono eseguite ogni volta che essa viene invocata. Il corpo della funzione è racchiuso tra i simboli "{}". L'esempio seguente mostra la dichiarazione e la definizione di una funzione d'esempio che calcola il valore percentuale di un valore intero.

// number_utils.h
float percentuale(int totale, float quota);
-------------------------------------------------------------------------
// number_utils.cpp
#include "number_utils.h"
float percentuale(int totale, float quota)
{
return (totale / 100.0f) * quota;
}
-------------------------------------------------------------------------
// main.cpp
#include <iostream>
#include "number_utils.h"
int main()
{
int a = 500;
float b = 20.0f;
std::cout << percentuale(a, b) << std::endl; // 20% di 500
return 0;
}

La parola chiave return seguita da un'espressione costituisce l'ultima istruzione eseguita dentro il corpo della funzione, che consiste nel restituire al contesto del chiamante il valore di ritorno (in questo caso il valore percentuale del primo argomento).

Passaggio di parametri per valore o per riferimento

Il concetto matematico di funzione definisce un'entità astratta che associa valori di ingresso a valori d'uscita. Ma da un punto di vista teorico, una funzione definisce soltanto una relazione tra queste istanze, e non ne altera il valore.

Nell'ambito della programmazione, il costrutto di funzione rispecchia questa dinamica di base, nel senso che non altera i parametri che le vengono dati. L'esempio seguente ne è una prova:

#include <iostream>
void swap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int x = 1;
int y = 2;
swap(x, y);
std::cout << "x: " << x;
std::cout << "y: " << y;
return 0;
}

La funzione swap, che dovrebbe scambiare i valori dei suoi parametri, in realtà non produce alcun effetto apprezzabile all'esterno.

Il motivo risiede nel fatto che le istruzioni contenute all'interno di una funzione operano su una copia dei parametri in ingresso. All'interno della funzione swap, quindi, a e b sono copie rispettivamente di x e y, che sopravvivono solo fino al termine della funzione. Per questo motivo, cambiarne il valore tramite un'operazione di assegnamento, come avviene nella nostra funzione, non produce alcun effetto nel contesto del chiamante dove risiedono x e y.

Questo meccanismo di base è noto come passaggio per valore dei parametri.

Copiare un parametro in ingresso ad una funzione, sebbene sia penalizzante da un punto di vista delle risorse (uso di memoria, cicli di processore, eccetera), è utile perchè protegge i parametri di ingresso da modifiche accidentali, dette effetti collaterali, che potrebbero invalidare l'esecuzione di un algoritmo formalmente corretto. Questo meccanismo di sicurezza è uno dei principi alla base del paradigma di programmazione funzionale, dove si assume che una funzione non possa mutare lo stato del programma se non mediante il valore da essa restituito.

Tuttavia, un programma non si valuta solo in base alla correttezza formale, ma anche in base alle sue prestazioni. Per questo motivo in C++ è possibile definire funzioni che accettano riferimenti alla memoria come parametri, pittosto che variabili ordinarie. Questa seconda modalità viene detta passaggio per riferimento e prevede l'uso di parametri di tipo puntatore a variabile o variabili reference, come mostra l'esempio seguente.

#include <iostream>
void swapPointer(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void swapReference(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int x = 1;
int y = 2;
swapPointer(&x, &y);
std::cout << "x: " << x;
std::cout << "y: " << y;
swapReference(x, y);
std::cout << "x: " << x;
std::cout << "y: " << y;
return 0;
}

Passare indirizzi di memoria come argomenti di una funzione, significa aggirarne il meccanismo di base di trasparenza referenziale di cui sopra. In questo caso, infatti, i parametri copiati sono gli indirizzi, ma non le locazioni di memoria cui essi si riferiscono. Ciò rende possibile modificare tali locazioni mediante un'operazione di dereferenziazione.

Una conseguenza di ciò è che si possono anche usare i parametri di ingresso per restituire più di un valore in uscita.

Si noti infine come l'uso di variabili reference mascheri l'operazione di dereferenziazione nel contesto del chiamante, rispetto alla variante che fa uso di puntatori. L'invocazione della funzione swapReference, infatti, non ha nulla di diverso dalla funzione swap definita in precedenza che opera su variabili ordinarie.

Il meccanismo di copia del valore di ritorno

L'istruzione return <espressione>; ha due effetti: la terminazione dell'esecuzione della funzione e la restituzione al contesto del chiamante di una copia del valore di ritorno.

Se il tipo di ritorno di una fuzione è void, la funzione non restituisce alcun valore, e l'istruzione return; ha il solo effetto di restituire il controllo di flusso all'istruzione successiva all'invocazione della funzione.

Il meccanismo di copia del valore di ritorno ha una motivazione meramente implementativa: il valore di ritorno di una funzione è tipicamente allocato a stack. L'uscita dal blocco di istruzioni della funzione determina la distruzione dello stack frame ad essa associato, e pertanto per far sopravvivere il valore di ritorno nel contesto esterno esso viene copiato in un valore temporaneo, un rvalue, che può così essere assegnato ad una variabile definita nel contesto esterno.

Lo standard C++ permette ai compilatori di implementare alcune tecniche per ottimizzare le prestazioni eliminando copie non necessarie del valore di ritorno, nel passaggio tra un contesto e l'altro. Nelle prossime lezioni introdurremo altri concetti fondamentali che ci consentiranno di dare in seguito una trattazione approfondita di questo argomento.

Ti consigliamo anche