Sottoprogrammi e funzioni
Come ogni moderno linguaggio, sia il C che il C++ consentono
di dichiarare sottoprogrammi che possono essere invocati nel corso
dell'esecuzione di una sequenza di istruzioni a partire da una sequenza
principale (il corpo del programma). Nel caso del C e del C++ questi
sottoprogrammi sono chiamati funzioni e sono simili alle funzioni
del Pascal. Anche il corpo del programma e` modellato tramite una funzione
il cui nome deve essere sempre main
(vedi esempio).
Funzioni
Una funzione C/C++, analogamente ad una
funzione Pascal, e` caratterizzata da un nome che la distingue univocamente
nel suo scope (le regole di visibilita` di una funzione sono analoghe a
quelle viste per le variabili), da un
insieme (eventualmente vuoto) di argomenti (parametri della funzione)
separati da virgole, e eventualmente il tipo del valore ritornato:
|
// ecco una funzione che riceve due interi
// e restituisce un altro intero
int Sum(int a, int b);
|
Gli argomenti presi da una funzione sono quelli racchiusi tra le parentesi
tonde, si noti che il tipo dell'argomento deve essere specificato
singolarmente per ogni parametro anche quando piu` argomenti hanno lo
stesso tipo; la seguente dichiarazione e` pertanto errata:
|
int Sum2(int a, b); // Errore!
|
Il tipo del valore restituito dalla funzione deve essere specificato prima
del nome della funzione e se omesso si sottointende int; se una
funzione non ritorna alcun valore va dichiarata void, come mostra
quest'altro esempio:
|
// ecco una funzione che non ritorna alcun valore
void Foo(char a, float b);
|
Non e` necessario che una funzione abbia dei parametri, in questo caso
basta non specificarne oppure indicarlo esplicitamente:
|
// funzione che non riceve parametri
// e restituisce un int (default)
Funny();
// oppure
Funny2(void);
|
Il primo esempio vale solo per il C++, in C non specificare alcun argomento
equivale a dire "Qualsiasi numero e tipo di argomenti"; il
secondo metodo invece e` valido in entrambi i linguaggi, in questo caso
void assume il significato "Nessun argomento".
Anche in C++ e` possibile avere funzioni con numero e tipo di argomenti non
specificato:
|
void Esempio1(...);
void Esempio2(int Args, ...);
|
Il primo esempio mostra come dichiarare una funzione che prende un numero
imprecisato (eventualmente 0) di parametri; il secondo esempio invece
mostra come dichiarare funzioni che prendono almeno qualche parametro, in
questo caso bisogna prima specificare tutti i parametri necessari e poi
mettere ... per indicare eventuali altri parametri.
Quelle che abbiamo visto finora comunque non sono definizioni di funzioni,
ma solo dichiarazioni, o per utilizzare un termine proprio del C++,
prototipi di funzioni.
I prototipi di funzione sono stati introdotti nel C++ per
informare il compilatore dell'esistenza di una certa funzione e consentire
un maggior controllo al fine di identificare errori di tipo (e non solo) e
sono utilizzati soprattutto all'interno dei
file header per la suddivisione di
grossi programmi in piu` file e la realizzazione di librerie di funzioni;
infine nei prototipi non e` necessario indicare il nome degli argomenti
della funzione:
|
// la funzione Sum vista sopra poteva
// essere dichiarata anche cosi`:
int Sum(int, int);
|
Per implementare (definire) una funzione occorre ripetere il prototipo,
specificando il nome degli argomenti (necessario per poter riferire ad
essi, ma non obbligatorio se l'argomento non viene utilizzato), seguito da
una sequenza di istruzioni racchiusa tra parentesi
graffe:
|
int Sum(int x, int y) {
return x+y;
}
|
La funzione Sum e` costituita da una sola istruzione che
calcola la somma degli argomenti e restituisce tramite la keyword
return il risultato di tale operazione. Inoltre, benche` non
evidente dall'esempio, la keyword return provoca l'immediata
terminazione della funzione; ecco un esempio non del tutto corretto, che
pero` mostra il comportamento di return:
|
// calcola il quoziente di due numeri
int Div(int a, int b) {
if (b==0) return "errore";
return a/b;
}
|
Se il divisore e` 0, la prima istruzione return restituisce
(erroneamente) una stringa (anzicche` un intero) e provoca la terminazione
della funzione, le successive istruzioni della funzione quindi non
verrebbero eseguite. Concludiamo questo paragrafo con alcune
considerazioni:
- La definizione di una funzione non deve essere seguita da ;
(punto e virgola), cio` tra l'altro consente di distinguere facilmente
tra prototipo (dichiarazione) e definizione di funzione poiche`
un prototipo e` terminato da ; (punto e virgola),
mentre in una definizione la lista di argomenti e` seguita da
{ (parentesi graffa aperta);
- Ogni funzione dichiarata non void deve restituire un valore, ne segue
che da qualche parte nel corpo della funzione deve esserci una
istruzione return con un qualche argomento (il valore
restituito), in caso contrario viene segnalato un errore; analogamente
l'uso di return in una funzione void costituisce un
errore, salvo il caso in cui la keyword sia utilizzata senza
argomenti (provocando cosi` solo la terminazione della funzione);
- La definizione di una funzione e` anche una dichiarazione per quella
funzione e all'interno del file che definisce la funzione non e`
obbligatorio indicarne il prototipo, vedremo meglio l'importanza dei
prototipi piu` avanti;
- Non e` possibile dichiarare una funzione all'interno del corpo di
un'altra funzione.
Ecco ancora qualche esempio relativo alla seconda nota:
|
int Sum(int a, int b) {
a + b;
} // ERRORE! Nessun valore restituito.
int Sum(int a, int b) {
return;
} // ERRORE! Nessun valore restituito.
int Sum(int a, int b) {
return a + b;
} // OK!
void Sleep(int a) {
for(int i=0; i < a; ++i) {};
} // OK!
void Sleep(int a) {
for(int i=0; i < a; ++i) {};
return;
} // OK!
|
La chiamata di una funzione puo` essere eseguita solo nell'ambito dello
scope in cui appare la sua dichiarazione (come gia` detto le regole di
scoping per le dichiarazioni di funzioni sono identiche a quelle per le
variabili) specificando il valore assunto da ciascun parametro formale:
|
void Sleep(int Delay); // definita da qualche parte
int Sum(int a, int b); // definita da qualche parte
void main(void) {
int X = 5;
int Y = 7;
int Result = 0;
/* ... */
Sleep(X);
Result = Sum(X, Y);
Sum(X, 8); // Ok!
Result = Sleep(1000); // Errore!
return 0;
}
|
La prima e l'ultima chiamata di funzione mostrano come le funzioni void
(nel nostro caso Sleep) siano identiche alle procedure
Pascal, in particolare l'ultima chiamata a Sleep e` un errore
poiche` Sleep non restituisce alcun valore.
La seconda chiamata di funzione (la prima di Sum) mostra come
recuperare il valore restituito dalla funzione (esattamente come in
Pascal). La chiamata successiva invece potrebbe sembrare un errore, in
realta` si tratta di una chiamata lecita, semplicemente il valore tornato
da Sum viene scartato; l'unico motivo per scartare il
risultato dell'invocazione di una funzione e` quello di sfruttare eventuali
effetti laterali di tale chiamata.
Passaggio di parametri e argomenti di default
I parametri di una funzione si comportano
all'interno del corpo della funzione come delle variabili locali e possono
quindi essere usati anche a sinistra di un assegnamento (per quanto
riguarda le variabili locali ad una funzione, si rimanda al
capitolo III, paragrafo 3):
|
void Assign(int a, int b) {
a = b; // Tutto OK, operazione lecita!
}
|
tuttavia qualsiasi modifica ai parametri formali (quelli cioe` che
compaiono nella definizione, nel nostro caso a e
b) non si riflette (per quanto visto fin'ora) automaticamente
sui parametri attuali (quelli effettivamente usati in una chiamata della
funzione):
|
#include < iostream >
using namespace std;
void Assign(int a, int b) {
cout << "Inizio Assign, parametro a = " << a << endl;
a = b;
cout << "Fine Assign, parametro a = " << a << endl;
}
int main(int, char* []) {
int X = 5;
int Y = 10;
cout << "X = " << X << endl;
cout << "Y = " << Y << endl;
// Chiamata della funzione Assign
// con parametri attuali X e Y
Assign(X, Y);
cout << "X = " << X << endl;
cout << "Y = " << Y << endl;
return 0;
}
|
L'esempio appena visto e` perfettamente funzionante e se eseguito
mostrerebbe come la funzione Assign, pur eseguendo una
modifica ai suoi parametri formali, non modifichi i parametri attuali.
Questo comportamento e` perfettamente corretto in quanto i parametri
attuali vengono passati per valore: ad ogni chiamata della funzione viene
cioe` creata una copia di ogni parametro localmente alla funzione
stessa; tali copie vengono distrutte quando la chiamata della funzione
termina ed il loro contenuto non viene copiato nelle eventuali variabili
usate come parametri attuali.
In alcuni casi tuttavia puo` essere necessario fare in modo che la funzione
possa modificare i suoi parametri attuali, in questo caso e` necessario
passare non una copia, ma un riferimento o un puntatore e
agire su questo per modificare una variabile non locale alla funzione.
Per adesso non considereremo queste due possibilita`, ma rimanderemo la
cosa al capitolo successivo non appena
avremo parlato di puntatori e reference.
A volte siamo interessati a funzioni il cui comportamento e` pienamente
definito anche quando in una chiamata non tutti i parametri sono
specificati, vogliamo cioe` essere in grado di avere degli argomenti che
assumano un valore di default se per essi non viene specificato alcun
valore all'atto della chiamata. Ecco come fare:
|
int Sum (int a = 0, int b = 0) {
return a+b;
}
|
Quella che abbiamo appena visto e` la definizione della funzione
Sum ai cui argomenti sono stati associati dei valori di
default (in questo caso 0 per entrambi gli argomenti), ora se la funzione
Sum viene chiamata senza specificare il valore di
a e/o b il compilatore genera una chiamata a
Sum sostituendo il valore di default (0) al parametro
non specificato. Una funzione puo` avere piu` argomenti di default,
ma le regole del C++ impongono che tali argomenti siano specificati alla
fine della lista dei parametri formali nella dichiarazione della funzione:
|
void Foo(int a, char b = 'a') {
/* ... */
} // Ok!
void Foo2(int a, int c = 4, float f) {
/* ... */
} // Errore!
void Foo3(int a, float f, int c = 4) {
/* ... */
} // Ok!
|
La dichiarazione di Foo2 e` errata poiche` quando viene
specificato un argomento con valore di default, tutti gli argomenti
seguenti (in questo caso f) devono possedere un valore di
default; l'ultima definizione mostra come si sarebbe dovuto definire
Foo2 per non ottenere errori.
La risoluzione di una chiamata di una funzione con argomenti di default
naturalmente differisce da quella di una funzione senza argomenti di
default in quanto sono necessari un numero di controlli maggiori;
sostanzialmente se nella chiamata per ogni parametro formale viene
specificato un parametro attuale, allora il valore di ogni parametro
attuale viene copiato nel corrispondente parametro formale sovrascrivendo
eventuali valori di default; se invece qualche parametro non viene
specificato, quelli forniti specificano il valore dei parametri formali
secondo la loro posizione e per i rimanenti parametri formali viene
utilizzato il valore di default specificato (se nessun valore di default e`
stato specificato, viene generato un errore):
|
// riferendo alle precedenti definizioni:
Foo(1, 'b'); // chiama Foo con argomenti 1 e 'b'
Foo(0); // chiama Foo con argomenti 0 e 'a'
Foo('c'); // ?????
Foo3(0); // Errore, mancano parametri!
Foo3(1, 0.0); // chiama Foo3(1, 0.0, 4)
Foo3(1, 1.4, 5); // chiama Foo3(1, 1.4, 5)
|
Degli esempi appena fatti, il quarto, Foo3(0), e` un errore poiche`
non viene specificato il valore per il secondo argomento della funzione (che non
possiede un valore di default); e` invece interessante il terzo
(Foo('c');): apparentemente potrebbe sembrare un errore, in realta`
quello che il compilatore fa e` convertire il parametro attuale 'c'
di tipo char in uno di tipo int e chiamare la funzione sostituendo al
primo parametro il risultato della conversione di 'c' al tipo int. La
conversione di tipo sara` oggetto di una apposita appendice.
Pagina precedente - Pagina successiva
C++, una panoramica sul linguaggio - seconda edizione © Copyright 1996-1999, Paolo Marotta
|