Il
concetto di funzione e’ reso necessario poiche’ i problemi, che bisogna
risolvere nella pratica, attraverso la automatizzazione mediante un programma ,
sono molto piu’ complessi di quelli visti sin’ora. Quindi si usa il metodo dividi
et impera (ossia un problema complesso viene trasformato in un insieme di
problemi piu’ semplici); cosi’ allo stesso modo il programma complessivo atto a
risolvere tale problema , viene diviso in moduli piu’ piccoli di piu’ facile
programmazione. Questi moduli (sottoprogrammi)
prendono il nome di funzioni. Con questo metodo, il programma risulta
sicuramente piu’ semplice da programmare e di piu’ facile correzione e
comprensione.
I
programmi in C tipicamente sono scritti
combinando le nuove funzioni scritte dal programmatore con le funzioni
disponibili nella libreria standard del C.
Sembra ovvio che se una funzione esiste nella libreria standard, il programmatore utilizzera’ tale funzione senza crearne una nuova.
Nel
linguaggio C la struttura generale di
una funzione e’ la seguente:
<tipo_risultato> <
nome_funzione>(<lista_degli_argomenti>)
{
< corpo della funzione>
}
il <corpo della funzione > sara’ costituito da :
< dichiarazioni_di_variabili > da istruzioni ed
eventualmente da return(<valore_restituito>);
· <tipo_risultato> (e’ il tipo del valore restituito “calcolato” dalla funzione) indica a quale tipo di dati appartiene il valore che la funzione ritorna, attraverso l’istruzione return posta alla fine del corpo della funzione. Se non viene specificato nessun tipo per default viene restituito un tipo intero.
· <nome_funzione> (e’ un identificatore usato per la chiamata della
funzione)
· <lista_degli_argomenti> (elenco
delle dichiarazioni dei parametri formali “tipo e nome” )
e’
una lista di parametri; un elenco di nomi di variabili separati da virgole e
precedute dal tipo della variabile. Questi forniscono il mezzo per comunicare
le informazioni tra le funzioni ossia ricevono i nomi degli argomenti quando
vengono chiamate le funzioni.
Questi
parametri sono variabili locali alla funzione al pari delle variabili che
vengono dichiarate nel corpo della funzione. Con variabile locale si intende
che essa e’ nota soltanto nel campo di azione della funzione in cui e’ dichiarata.
La <lista_degli_argomenti> puo’ essere anche vuota, ossia si hanno soltanto le parentesi tonde con dentro nessun parametro.
· < dichiarazioni_di_variabili
> (variabili locali al blocco della funzione)
· <valore_restituito> (espressione la cui valutazione
fornisce il valore che viene restituito nel luogo della chiamata “deve essere
dello stesso tipo del <tipo_risultato”)
E’ importante che ogni funzione si limiti ad eseguire un singolo compito ben definito;
il
nome della funzione dovrebbe chiarire di che compito si tratti, per motivi di
leggibilita’ del programma, utile sia al programmatore che ad altre persone che
vogliano/debbano consultare il codice, per i piu’ disparati motivi.
Nel corpo della funzione possiamo dichiarare un insieme di variabili dello stesso tipo, con il nome del tipo e l’elenco delle variabili separate da una virgola; notate che questo non puo’ essere fatto per i parametri delle funzioni, ossia anche se si ha piu’ di una variabile dello stesso tipo: prima del nome della variabile va sempre indicato il tipo.
Esempio di funzione
double cubo ( double l)
{ return (l*l*l); /* il controllo torna
all’unita’ chiamante */
}
programma main che delega il compito di risolvere un certo sottoproblema ad una funzione (sottoprogramma) definita dal programmatore.
/*programma che calcola il quadrato dei
numeri interi da 1 fino a 10*/
#include<stdio.h>
int
quadrato(int y); /* prototipo della
funzione*/
int main( )
{
int x;
for (x=1; x<=10; x++)
printf (“%d ”,
quadrato(x)); /*chiamata della funzione
quadrato(x)*/
printf (“\n”);
return ( 0 );
}
/*definizione della funzione di nome
quadrato*/
int
quadrato( int n )
{
return ( n*n) ;
}
La
funzione (sottoprogramma) quadrato, non
fa altro che calcolare il quadrato del numero n e restituirlo alla funzione.
Nel
main, quando viene invocata “chiamata” la funzione quadrato(x) , al suo posto
compare il quadrato del numero x calcolato dalla funzione stessa.
La funzione quadrato ricevera’ una copia del valore di x nel parametro n. In seguito quadrato calcolera’ n*n ed il risultato viene restituito alla funzione; La printf , all’ interno del main, visualizzera' tale risultato nel punto in cui e’ invocata quadrato.
Nel
nostro programma avremo la ripetizione di questo processo per 10 volte in
quanto la funzione viene richiamata dentro la struttura di iterazione for.
Il
risultato del programma sara’ la stampa sul video seguente:
1 4 9 16 25 36 49 64 81 100
/* programma che legge un
carattere e ne visualizza il suo codice ASCII usando una funzione*/
#include<stdio.h>
int ascii ( char car ) { return (( int ) car); /*il controllo torna all’unita’ chiamante*/
}
int main ( ) { char ch; printf (“Inserite un carattere\n);
scanf (“%c”,&ch);
printf (“Il codice di %c e’ %d\n…fine”,ch, ascii(ch)); return 0;}
Se
dichiariamo una funzione nel modo seguente:
void
GAUSS ( )
{
int n,g;
scanf
(“\n%d”,&n);
g = n*(n+1)/2 ;
printf("la
somma dei primi %d numeri interi e = ",n, g);
}
con void abbiamo indicato che la funzione GAUSS non
restituisce alcun valore, ma quando viene invocata all’ interno di un programma
esegue un suo compito che e’ quello di leggere un numero n da input, calcolare
l’espressione n*(n+1)/2 ed assegnare il risultato di tale espressione alla
variabile g locale alla funzione e stamparne il valore.
Quindi la funzione GAUSS si limita a modificare , ogni volta che viene invocata, il valore della variabile g assegnando ad essa il valore della espressione n*(n+1)/2.
Nelle funzioni di tipo void si puo’ omettere l’istruzione return, visto che non devono restituire alcuni valore.
Se
manca return la funzione termina quando si raggiunge la ‘}’ del
suo body.
Nel linguaggio C ogni funzione e’ una entita’ privata (blocco di codice a se stante) a cui e’ possibile accedere da altre parti del codice chiamando (invocando ) il nome della funzione.
Una funzione non puo’ essere innestata in un’altra (tutte le funzioni sono dichiarate al medesimo livello) . Comunque una funzione puo’ essere chiamata da un’altra funzione, se quest’ultima e’ nota ad essa, ossia
- sia stata definita prima della funzione chiamante (il compilatore arriva alla funzione chiamante essendo gia’ passato su quella chiamata).
Oppure
- sia dichiarata nella funzione chiamante
Notare bene che nel primo caso la funzione chiamata puo’ trovarsi anche in un altro file, a condizione che il file sia incluso in modo opportuno nel file della funzione chiamante.
Nel secondo caso per dichiarare una funzione da chiamare in un’altra (funzione chiamante) si deve soltanto specificare nella funzione chiamante il cosiddetto prototipo della funzione da chiamare;
Il prototipo di una funzione si pone nella funzione chiamante tra le altre dichiarazioni ed e’ del tipo <tipo_funzione><nome_funzione>(<tipi_degli_argomenti>);
Notiamo
che nel prototipo si puo’ inserire la <lista_degli_argomenti> al posto
del solo <tipi_degli_argomenti>, non causa errori, ma il compilatore
ignora gli identificatori che seguono il tipo.
Le
variabili che si dichiarano dentro una funzione sono dette variabili locali
(appunto locali alla funzione). Una variabile locale ha un tempo di vita
brevissimo: comincia ad esistere quando viene invocata la funzione e termina di
esistere quando termina la funzione.
Quindi
una variabile locale non mantiene il suo valore tra una chiamata di funzione e
la successiva chiamata (eccezione a questa ultima regola e’ se dichiariamo una
variabile locale con static tipo variabile_locale ; in questo caso il compilatore tratta la
variabile locale come globale per quanto riguarda la memorizzazione e come
locale per quanto riguarda la visibilita’ “variabile visibile solo all’interno
della funzione”).Precisando meglio se una variabile locale ad una funzione la
dichiariamo con static,essa tra una chiamata e l’altra della funzione conserva
il valore che aveva nella penultima chiamata, anche se tale valore viene visto
soltanto dentro la funzione, in quanto variabile locale.
Nel
linguaggio C non e’ possibile definire una funzione all’interno di un’
altra funzione “funzioni innestate” in quanto le funzioni hanno tutte lo stesso
livello di visibilita’.
Tutte
le funzioni sono fra di loro indipendenti e si collocano al medesimo livello
gerarchico, ossia non vi sono funzioni piu’ importanti di altre (eccezione
fatta per la funzione main ( ) in quanto essa deve esistere obbligatoriamente
ed e’ sempre chiamata per prima).
Quando una funzione chiama un’altra funzione il controllo dell’esecuzione passa a quest’ultima che al termine del proprio codice od in corrispondenza della istruzione return lo restituisce alla funzione chiamante.
Osservazione : possiamo avere piu’ di una return nella funzione (per chiudere tutti i percorsi della sua esecuzione)
Notiamo che una funzione puo’ anche autochiamarsi; questa tecnica e’ nota con il nome di ricorsione.
Faremo
in seguito qualche esempio di funzione ricorsiva.
Consideriamo il seguente programma:
#include<stdio.h>
passaggio_parametri( int );
/*prototipo
function passaggio_parametri*/
main () /*chiamante*/
{
passaggio_parametri(pa); /*chiamata della funzione */
..….
…..
}
/*function*/
passaggio_parametri( int pf )
{
……
.……
}
La
variabile pf viene detta parametro
formale della funzione ed ha un
comportamento simile alle variabili locali della funzione e come tale rimane in
vita finche’ vive la funzione.
I
parametri formali hanno lo scopo di ricevere dei valori da comunicare
all’interno della funzione in cui sono definiti.
Quando
la funzione passaggio_parametri viene chiamata (ad esempio da una istruzione
dentro il main( ) ) le corrispondenti variabili con cui avviene la chiamata
sono detti parametri attuali . Nel nostro esempio abbiamo un solo parametro
attuale, detto pa.
In questo caso il main () e’ il programma chiamante e chiama la funzione usando i parametri attuali.
I parametri attuali si sostituiscono ai parametri formali della funzione nella chiamata ed e’ importante che il tipo dei parametri attuali corrisponda con il tipo dei parametri formali.
Il passaggio dei parametri attuali ai parametri formali puo’ avvenire in due modi, stabilendo due tecniche di legame dei parametri:
1) legame per valore ( passaggio per valore )
2) legame per riferimento ( passaggio per riferimento o indirizzo )
Il primo tipo di passaggio e’ molto semplice: il valore del parametro attuale viene ricopiato, nella variabile argomento della funzione che costituisce, il parametro formale.
Eventuali modifiche eseguite sul parametro formale non hanno efficacia sulla variabile usata all’ atto della chiamata.
Ossia nella chiamata per valore, sara’ preparata una copia dei valori dei parametri attuali e questa copia viene passata al parametro formale ed eventuali modifiche effettuale alla copia non interessano il valore originale della variabile definita nel chiamante (parametro attuale).
Esempio di passaggio degli argomenti per valore :
#include<stdio.h>
void change(int); /*prototipo*/
int main ()
{
int i=10;
char car;
/*variabile utilizzata per visualizzare l'output*/
printf("\nprima i vale %d\n", i);
printf("\nchiamata \"change(i)\"\n");
change(i);
printf("\n poi i vale %d\n",i);
printf("\ninserite un carattere qualunque per uscire\n");
scanf("%c",&car);
return (0);
}
void change(int j)
{
while(j)
{
printf("\nsto cambiando %d in ", j);
printf("%d...",--j);
}
}
Passaggio per indirizzo
Ne discuteremo nella lezione 14 dopo aver introdotto i puntatori.
Adesso possiamo riassumere la sintassi della definizione di una funzione:
<def_funzione> ::= <intestazione> <blocco>
<intestazione> ::= <tipo_risultato> <nome_funzione><param_formali>
<tipo_risultato> ::= <nome_tipo_risultato> | void
<param_formali> ::= | ( lista_param> ) | ( ) | ( void )
<lista_param> ::= < param > | < param > , <lista_param>
<param> ::= < nome_tipo_param> <nome_param>
Con <blocco> intendiamo una parte dichiarativa locale e una parte esecutiva detta corpo della funzione. Nella parte esecutiva di solito vi e’ una istruzione di return che ha la seguente sintassi:
<istruz_return > ::= return <espressione>
Con l’istruzione return si restituisce il controllo al chiamante.
<espressione> deve avere lo stesso tipo del risultato <nome_tipo_risultato> dichiarato nella intestazione della funzione.
Con <tipo_risultato> indichiamo il tipo del risultato della funzione che puo’ essere il tipo speciale void ossia la funzione non restituisce alcun risultato ma si limita a calcolare qualcosa ed il modo con cui questo calcolo puo’ essere utilizzato dal chiamante e’ quello che la funzione modifichi qualche variabile globale o non locale alla funzione.
La chiamata delle funzioni ha la seguente sintassi:
<chiamata_funzione> ::= <nome_funzione> ( <param_attuali> ) |
<nome_funzione> ( )
<param_attuali> ::= <espressione> | <espressione> , <param_attuali >
Al primo parametro formale viene assegnato il corrispondente primo parametro attuale al secondo parametro formale viene assegnato il corrispondente secondo parametro attuale e cosi di seguito.
Una
funzione matematica e’ definita ricorsivamente quando nella sua definizione
compare un riferimento a se stessa. In un programma ricorsivo serve un caso
base
(da raggiungere) ed un caso induttivo (con il quale ricondursi
man mano al caso base ). Se non ci si avvicina al caso base il programma non
termina.
Consideriamo il fattoriale di un intero non negativo n, scritto n! (leggere fattoriale di n) :
n! = n * (n-1) * (n-2) * .... * 3 * 2 * 1
fact (n) = 1 per n=0 1 fact(n) = Õ i per n>=1 i = n |
0! = 1 per definizione ed 1! =1;
Il fattoriale di un numero intero num >= 0, puo’ essere calcolato iterativamente od in modo ricorsivo.
Nella versione iterativa della funzione che calcola il fattoriale useremo la struttura for come segue: fact=1; for(cont= num; cont>=1; cont--) fact=fact*cont; ed avremo:
fattoriale(int num)
{ int fact, cont;
fact=1; for(cont= num; cont>=1; cont--) fact=fact*cont;
return(fact);}
la funzione fattoriale ci permette di ottenere una definizione ricorsiva, osservando che per essa valgono le seguenti proprieta’:
n! = n * (n-1)! Ossia : fact (n) = n * fact(n-1) e che 0! = 1 ossia fact(0)=1;
esempio
Programma che accetta una sequenza di input interi non negativi e produce i relativi fattoriali.
#include<stdio.h>
int main()
{
long fact(int);
int i = 1000;
while(i!=0)
{
printf("\ninserite un numero intero positivo (0 per terminare):");
scanf("%d",&i);
/*controllo dell'input*/
while(i<0){ printf("num negativo...reinserirlo\n");scanf("%d",&i);}
printf("\n\t fattoriale di %d = %ld\n", i,fact(i));
}
return(0);
}
long fact(int n){if(n = =1)
return(1); /* caso base*/
else
return (n*fact(n-1)); /*caso ricorsivo*/
}
successione delle chiamate per il calcolo del fattoriale di 3
valori restituiti ad ogni chiamata ricorsiva: valore finale=6
La funzione fact e’ stata dichiarata in modo da ricevere un parametro di tipo long e restituire un risultato dello stesso tipo;
long e’ una notazione abbreviata per long int.
Come esercitazione sulle funzioni ricorsive risolvere il seguente esercizio_a .