Lezione XIII                   frames  noframes

 

Le funzioni

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.

Struttura di una funzione

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 */

}

Esempio

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

 

Esempio

/* 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;}

Funzioni void

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.

Regole di visibilità delle funzioni

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.

 

 

 

 

 

Argomenti delle funzioni e modalita’ di passaggio degli argomenti

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.

 

esercizio

 

 

Sintassi

 

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.

 

Ricorsione

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.

 

Esempio

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 .

 

esercizio

Test 13