Si consideri il seguente calcolo:
#include <cstdlib>
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
int Potenza = 1;
int Base = 3;
int Esponente = 4;
for (int i = 0; i < Esponente; i++) {
Potenza *= Base;
}
cout << Base << "^" << Esponente << " = " << Potenza << endl;
system("PAUSE");
return EXIT_SUCCESS;
}
Con la tecnica Copia-Incolla le righe del programma possono essere riprodotte in un altro punto del programma dove è richiesto lo stesso calcolo o, con la sola modifica dei valori numerici, se si deve calcolare una potenza diversa.
Questo semplice procedimento non è conveniente perchè la ripetizione di tutte le istruzioni fa crescere inutilmente la dimensione del programma. Le funzioni esistono per poter richiamare un procedimento di calcolo in punti diversi del programma, specificando ogni volta i valori da usare per il nuovo calcolo.
Definizione
Una funzione è un programma che riceve un certo numero di parametri di ingresso e
restituisce un risultato (o nessun risultato).
Nel corso dell'esecuzione di un programma, una funzione viene chiamata, specificando il suo nome a secondo membro di un'espressione. Il programma chiamante deposita i parametri in un'area convenzionale della memoria e poi cede il controllo alla funzione. Dopo aver svolto le operazioni richieste, la funzione scrive il risultato in un'area della memoria e restituisce il controllo al programma chiamante.
La funzione chiamata opera su una copia dei parametri ricevuti, nel senso che non modifica i valori del programma chiamante
L'area di scambio dei parametri è lo stack, una struttura dinamica necessaria al sistema di calcolo per mantenere gli indirizzi di ritorno ogni volta che viene ceduto il controllo ad una funzione. Di conseguenza, quando una funzione termina, i parametri ricevuti vengono distrutti.
Si può osservare che chiamando una funzione si evita di ripetere le istruzioni nei punti in cui si richiede un calcolo, ma si introduce un ritardo dovuto al tempo richiesto per acquisire i parametri dallo stack. Quindi per una funzione breve, che richiede molti parametri, conviene ripetere le istruzioni anzichè scrivere la funzione.
Nel programma in cui si deve richiamare la funzione si usano delle espressioni come nel seguente esempio:
#include <cstdlib>
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
int treA4 = Potenza(3, 4);
cout << "3^4 = " << treA4 << endl;
int quattroA5 = Potenza(4, 5);
cout "4^5 = " << quattroA5 << endl;
int treA5 = Potenza(3, 5);
cout "3^5 = " << treA5 << endl;
system("PAUSE");
return EXIT_SUCCESS;
}
La funzione chiamante invoca la funzione Potenza usando dei parametri effettivi, cioè i valori da passare. La funzione chiamata assegna questi valori ai parametri formali.
Ad esempio nell'espressione: int treA4 = Potenza(3, 4); il valore 3 viene assegnato al parametro formale base e il valore 4 viene assegnato al parametro formale esponente. Quando la funzione termina, il risultato viene memorizzato nella variabile scritta nel primo membro dell'espressione e tutte le variabili usate dalla funzione vengono distrutte.
Il valore restituito dalla funzione deve essere del tipo specificato nell'intestazione della dichiarazione. Se la funzione non restituisce nessun valore, deve avere il tipo void. Una funzione non può restituire più di un valore.
Nell'utilizzo di una funzione ci sono almeno due vantaggi: L'espressione è più facilmente comprensibile, se la funzione possiede un nome appropriato, inoltre una eventuale modifica dell'algoritmo deve essere fatta in un solo punto del programma.
L'istruzione return è un'istruzione di salto incondizionato, cioè la funzione restituisce il controllo alla funzione chiamante, la quale prosegue l'esecuzione a partire da quella successiva alla chiamata. L'istruzione return non deve essere necessariamente l'ultima istruzione della funzione, essa può trovarsi in qualsiasi punto della funzione, allorchè si verifica la condizione per terminare la funzione.
Si distinguono due modalità di passaggio dei parametri: per valore e per riferimento. Normalmente La funzione chiamata riceve una valore dalla funzione chiamante e lo memorizza in una variabile locale. In questo modo tutte le modifiche che avvengono sulla variabile locale non influenzano la variabile usata dalla funzione chiamante.
Il passaggio dei parametri per riferimento consiste nel fornire, alla funzione chiamata, l'indirizzo della variabile che contiene il valore da passare alla funzione. In questo modo la funzione chiamata, tramite questo indirizzo ricevuto, opera sulla stessa locazione di memoria che usa la funzione chiamante. Quindi la funzione chiamata modifica anche il valore visto dalla funzione chiamante. Il passaggio dei parametri per riferimento consente di superare la limitazione che una funzione restituisce un solo valore, infatti, se la funzione chiamata opera sulla stessa variabile della chiamante risulta inutile che la chiamante acquisisca il valore di ritorno.
Nel prototipo di una funzione, in C++, si possono assegnare valori di default ai parametri. Se, poi, in una chiamata di funzione si omette il parametro, il compilatore usa i valori di default per quel parametro. Se invece si forniscono dei valori il compilatore usa questi valori al posto di quelli di default. Il seguente prototipo di funzione illustra questo caso:
void funzione( int i = 5, double d = 1.23 );
In questa dichiarazione i numeri 5 e 1.23 sono i valori di default per i parametri. Ci sono diversi modi per chiamare questa funzione:
funzione( 12, 3.45) // vengono cambiati entrambi i valori di default
funzione( 3); // equivalente a funzione( 3, 1.23 )
funzione(); // equivalente a funzione( 5, 1.23 )
Se si omette il primo parametro si devono omettere tutti i successivi. Se si omette il secondo parametro si deve assegnare un valore al primo. Questa regola vale in generale, qualsiasi sia il numero dei parametri.
Non è consentito omettere un parametro senza omettere tutti quelli che lo seguono nella lista della dichiarazione dei parametri del prototipo. Ad esempio la seguente chiamata di funzione è sbagliata:
// Errore: non si può omettere solo il primo parametro:
funzione( , 3.5 );
L'esempio che segue dichiara il prototipo della funzione mostra e specifica dei valori di default per i parametri:
void mostra(int = 1, float = 2, long = 4);
int main() {
mostra(); // tutti i parametri di default
mostra(5); // fornito solo il primo parametro
mostra(6, 7.8); // fornito il primo e il secondo parametro
mostra(9, 10.11, 12L); // forniti tutti i parametri
system("PAUSE");
return EXIT_SUCCESS;
}
void mostra(int primo, float secondo, long terzo) {
cout << endl << "primo: " << primo;
cout << ", secondo: " << secondo;
cout << ", terzo: " << terzo;
}
Quando di esegue il programma si osservano i seguenti risultati:
primo = 1, secondo = 2.3, terzo = 4
primo = 5, secondo = 2.3, terzo = 4
primo = 6, secondo = 7.8, terzo = 4
primo = 9, secondo = 10.11, terzo = 12
Il vantaggio di usare i parametri di default consiste nella possibilità di evitare di specificare i valori quando la funzione viene chiamata in più punti del programma con gli stessi valori.
In C è obbligatorio dichiarare le variabili all'inizio della funzione. In C++ le variabili possono essere dichiarate in qualsiasi punto del programma, purchè vengano fatto prima di usarle.
In questo modo la dichiarazione delle variabili viene a trovarsi vicino alle istruzioni che le usano. La possibilità di dichiarare una variabile ovunque, all'interno di un blocco, consente di avere espressioni simili alla seguente:
for( int conta = 0; conta < MAX; conta++ )
Espressioni come le seguenti, invece, non sono consentite:
if( int i == 0) // Errore
:
while( int j == 0 ) // Errore
:
Queste espressioni sono prive di significato perchè interrogano il valore di una variabile nel momento in cui
essa viene dichiarata.
È opportuno prestare attenzione alla regione di visibilità delle variabili, perchè all'esterno
del blocco in cui esse vengono dichiarate, non sono più disponibili. Ad esempio:
for (int i=0; i<10; i++) {
:
}
La variabile i non è più accessibile all'esterno del ciclo for
Una funzione, in C++, può avere il qualificatore inline. Quando una funzione è dichiarata inline il compilatore sostituisce il corpo della funzione ogni volta che incontra la chiamata a quella funzione. Ad esempio se una funzione inline viene chiamata da 20 posti diversi del programma, il compilatore inserisce 20 copie del corpo della funzione in quei punti.
Il file eseguibile diventa più grande, ma in compenso si evita il tempo richiesto per passare i parametri tramite lo stack.
Per tale ragione le funzioni dovrebbero essere dichiarate inline solo se sono molto piccole, o se vengono chiamate poche volte.
Le funzioni inline sono simili alle macro dichiarate con la direttiva #define. Le funzioni inline
sono riconosciute dal compilatore, mentre le macro rappresentano una semplice sostituzione, all'interno del programma,
del nome della macro con il testo che essa specifica.
ad esempio, la macro:
#define MAX(A, B) ((A) > (B) ? (A) : (B))
significa che, durante la compilazione, quando si incontra il testo MAX(A, B) lo si deve sostituire con il testo scritto nella seconda parte: ((A) > (B) ? (A) : (B)).
Una importante differenza risiede nel fatto che, in una funzione inline, il compilatore riesce a controllare la corrispondenza tra il tipo dei parametri formali e il tipo dei parametri effettivi.
L'esempio seguente illustra un potenziale problema nell'uso delle macro, l'effetto collaterale:
#define MAX(A, B) ((A > (B) ? (A( : (B))
inline int max(int a, int b) {
if (a> b) return a;
return b;
}
int main() {
int i, x, y;
x = 23;
y = 45;
i = MAX(x++, y++); // effetto collaterale:
// il valore più grande viene incrementato 2 volte
cout >> "X: " >> x >> " y: " >> y >> endl;
x = 23;
y = 45;
i = max(x++, y++); // risultati corretti
cout >> "x: " >> x >> " y: " >> y >> endl;
system("PAUSE");
return 0;
}
Questo esempio produce i seguenti risultati:
x=24 y=47
x=24 y=46
La macro ha prodotto l'effetto collaterale, perchè la sostituzione della stringa
((A) > (B) ? (A) : (B))
al posto di
MAX(x++, y++)
produce la seguente istruzione:
((x++) > (y++) ? (x++) : (y++))
nella quale si vede che le due variabili vengono incrementate dopo il confronto e quella che viene restituita, viene incrementata una seconda volta.
Se si richiede che una funzione come max accetti parametri di tipo qualsiasi si deve ricorrere alla tecnica delle funzioni ridefinite (mostrata più avanti). Le funzioni inline devono essere sempre dichiarate prima di chiamarle.
Se una funzione inline deve essere chiamata da più file conviene inserire la dichiarazione in un file header e
includere tale file nei sorgenti che riferiscono la funzione.
È ovvio che una modifica alla funzione inline richiede la compilazione di tutti i file in cui questa
è chiamata. (infatti, si ricorda, il compilatore sostituisce il corpo della funzione ovunque questa sia riferita).
Dichiarare che una funzione è inline rappresenta un suggerimento per il compilatore, perchè
se la funzione è grande, il compilatore preferirà non fare la sostituzione, ma lasciare la
chiamata di funzione.
Una enumerazione è un tipo di dati, i cui possibili valori (costanti) vengono elencati dal programmatore.
Per dichiarare una enumerazione si usa il tipo enum. Il seguente esempio mostra come usare variabili di tipo enumerato:
#include <iostream.h>
enum colore { rosso, arancio, giallo, verde, blu, viola };
void main() {
colore coloreAuto;
coloreAuto = blue;
}
Notare che la dichiarazione di coloreAuto usa solo l'identificatore colore; il tipo enum non è necessario. Dopo aver dichiarato che colore è una enumerazione, colore diventa un nuovo tipo. (Anche una classe, dopo che viene definita diventa un nuovo tipo, da cui si possono creare variabili)
Ogni elemento di una enumerazione è associato ad un valore intero, che, per default, è una successione
di valori consecutivi.
Il primo elemento è associato al numero 0, tranne se si specifica un valore diverso.
È possibile specificare valori diversi per gli elementi che seguono, oppure si possono anche ripetere i valori.
Per esempio:
enum colore {rosso, arancio, giallo, verde, blu, viola);
// Valori: 0, 1, 2, 3, 4, 5
enum giorno { domenica = 1, lunedì, martedì, mercoledì = 24, giovedì, venerdì, sabato };
// Valori: 1, 2, 3, 24, 25, 26, 27
enum direzione { nord = 1, sud, est = 1, ovest };
// Valori: 1, 2, 1, 2
Una enumerazione può essere convertita in un intero, ma non è consentito il contrario. Per esempio:
// ENUM.CPP
enum colore {rosso, arancio, giallo, verde, blu, viola);
void main() {
colore coloreAuto, coloreMoto;
int i;
coloreAuto = blue;
i = coloreAuto; // Consentito i = 4
// coloreMoto = 5; // Errore: impossibile convertire da int a colore
coloreMoto = (colore) 4; // ammesso
}
La conversione esplicita da intero a enumerazione non è un'operazione corretta, perchè se l'intero si trova all'esterno dell'intervallo numerico dei valori dell'enumerazione, o se l'enumerazione contiene valori duplicati, la conversione resta indefinita.
Ridefinire una funzione rappresenta un modo per migliorare la leggibilità di un programma.
Si supponga di scrivere una funzione che calcoli la radice quadrata di numeri interi, un'altra funzione che calcoli la radice quadrata di numeri reali e un'altra ancora per i numeri reali in doppia precisione. Si dovrebbero usare 3 nomi diversi per ciascuna funzione, nonostante abbiano lo stesso significato. Il linguaggio C++ consente di usare lo stesso nome "radiceQuadrata" per tutte le tre funzioni. Questa caratteristica, di attribuire più di un significato alla stessa funzione è denominata Ridefinizione di funzione.
Il compilatore distingue più versioni di una stessa funzione confrontando l'elenco dei parametri. Il seguente esempio ridefinisce la funzione mostraOra per accettare o una struttura tm o una struttura time:
#include <time.h>
void mostraOra(const struct tm *adesso) {
cout << "1 - data e ora attuale: " << asctime(adesso) << endl;
}
void mostraOra(const time_t *adesso) {
cout << "2 - data e ora attuale: " << ctime(adesso);
}
int main() {
time_t adesso = time(NULL);
struct tm *oraLocale = localtime(&adesso);
mostraOra(oraLocale);
mostraOra(&adesso);
system("PAUSE");
return 0;
}
L'esempio acquisisce, dapprima, la data e l'ora di sistema chiamando le funzioni time e localtime. Poi chiama in successione le due versioni della funzione ridefinita mostraOra. Il compialtore osserva il tipo del parametro per decidere a quale versione si riferisce ciascuna chiamata.
Le funzioni ridefinite possono differire anche nel tipo del valore restituito. Quindi si potrebbe avere una funzione MAX che confronta due interi e restituisce un intero, oppure una funzione MAX che confronta due double e restituisce un double, ecc. Le funzioni ridefinite devono differire nel tipo e nella quantità dei parametri passati, non possono differire solo per il tipo del valore restituito. Esempio:
int cerca( char *elemento );
char *cerca( char *nome); //Errore: i parametri sono dello stesso tipo
Il compilatore distingue due funzioni che hanno lo stesso nome, osservando il tipo dei parametri formali. È consentito ridefinire un nome pre descrivere funzioni che hanno un numero diverso di parametri, ma che svolgono le stesse operazioni. Per esempio la funzione strcpy copia una stringa in un'altra. La funzione strncpy copia una stringa in un'altra ma si ferma quando la stringa sorgente termina oppure dopo aver copiato uno specificato numero n di caratteri.
Il seguente esempio sostituisce strcpy e strncpy con una sola funzione avente il nome copiaStringa:
#include <string.h>
inline void copiaStringa(char *dest, const char *srg) {
strcpy(dest, srg);
}
inline void copiaStringa(char *dest, const char *srg, int lung) {
strncpy(dest, srg);
}
static char stringa1[20], stringa2[20];
int main() {
copiaStringa(stringa1, "Quella");
copiaStringa(stringa2, "Questa e' una stringa", 4);
cout << stringa2 << " e " << stringa1;
system("PAUSE");
return 0;
}
In questo programma ci sono due funzioni denominate copiaStringa che differiscono per il numero dei parametri. La prima funzione riceve due puntatori a char, la seconda due puntatori a char e un intero.
Si esamini cosa succede se alla seconda funzione si assegna un valore di default al terzo parametro:
copiaStringa( char *dest, const char *srg, int lung = 10 );
In questo caso la seguente chiamata di funzione è ambigua:
copiaStringa( stringa1, "Quella" ); // Errore
Questa chiamata di funzione potrebbe riferirsi a entrambe le funzioni: quella con due parametri e quella che, oltre ai due parametri ne ha anche uno di default. Il compilatore non ha modo di distinguere a quale funzione si riferisce la chiamata.