Informatica 2 Liceo Scientifico Scienze Applicate/Funzioni Void e non Void: differenze tra le versioni

Contenuto cancellato Contenuto aggiunto
Nuova pagina: == Funzioni == Come e' stato detto precedentemente le funzioni vengono usate per risolvere problemi parametrici, il fatto di concentrare le istruzioni per risolvere un particolare p...
(Nessuna differenza)

Versione delle 18:46, 4 mar 2015

Funzioni

Come e' stato detto precedentemente le funzioni vengono usate per risolvere problemi parametrici, il fatto di concentrare le istruzioni per risolvere un particolare problema in una sezione limitata del codice (quella fra le { } ) permette di trovare eventuali errori piu' semplicemente , il fatto di scrivere il codice della funzione solo una volta e di poterlo richiamare successivamente facendo corrispondere i parametri attuali(quelli della chiamata) con quelli della dichaiarazione (formali) consente una maggior compattezza del codice e in presenza di eventuali modifica della funzione ( ad esempio se l'iva del calcolo passa dal 20% al 21%) di dover mettere mano solo all'interno del codice della funzione e di aver automaticamente aggiornato il funzionamento in tutto il resto del programma ( nei diversi punti di chiamata). Inoltre l'uso delle funzioni permette l'utilizzo di conoscenze di altri programmatori semplicemente tramite una chiamata alla funzione. Ora vediamo di evedenziare altre caratteristiche.

Funzioni Void e Funzioni non void

Le funzioni del C si dividono in funzioni void e non void (quelle dove possiamo restituire un valore tramite l'istruzione return). Ora noi le abbiamo distinte dicendo se si restituisce un solo singolo valore usiamo quelle void altrimenti usiamo quelle void, in realta' piu' che sul numero di parametri restituiti dobbiamo porre la nostra attenzione sulla modalita' di restituzione del risultato, quando abbiamo una funzione non void, il valore restituito tramite return permette di inserire il risultato in una espressione di calcolo, mentre le funzioni void non possono farlo e si limitano al massimo a restituire uno o piu' valori (usando dei puntatori) in specifiche celle di memoria il cui indirizzo e' passato in fase di chiamata della funzione usando i parametri attuali. Quindi se vogliamo scrivere una funzione che puo' essere inserita in una espressione di calcolo (cioe che si combina con il calcolo di una espressione matematica) usiamo le funzioni non void altrimenti usiamo le funzioni void. Pensiamo al calcolo di (-b-sqrt(delta) )/(2*a) sqrt e' una funzione non void che calcola sqrt(delta) e il risultato di questo calcolo viene inserito nella piu' ampia espressione (-b -sqrt(delta)).... al posto della chiamata sqrt(delta)

Pensiamo di scrivere una funzione che clacoli il doppio di un numero

Come e' stato detto precedentemente le funzioni vengono usate per risolvere problemi parametrici, il fatto di concentrare le istruzioni per risolvere un particolare problema in una sezione limitata del codice (quella fra le { } ) permette di trovare eventuali errori piu' semplicemente , il fatto di scrivere il codice della funzione solo una volta e di poterlo richiamare successivamente facendo corrispondere i parametri attuali(quelli della chiamata) con quelli della dichaiarazione (formali) consente una maggior compattezza del codice e in presenza di eventuali modifica della funzione ( ad esempio se l'iva del calcolo passa dal 20% al 21%) di dover mettere mano solo all'interno del codice della funzione e di aver automaticamente aggiornato il funzionamento in tutto il resto del programma ( nei diversi punti di chiamata). Inoltre l'uso delle funzioni permette l'utilizzo di conoscenze di altri programmatori semplicemente tramite una chiamata alla funzione. Ora vediamo di evedenziare altre caratteristiche. Le funzioni del C si dividono in funzioni void e non void (quelle dove possiamo restituire un valore tramite l'istruzione return. Ora noi le abbiamo distinte dicendo se si restituisce un solo singolo valore usiamo quelle void altrimenti usiamo quelle void, in realta' piu' che sul numero di parametri restituiti dobbiamo porre la nostra attenzione sulla modalita' di restituzione del risultato, quando abbiamo una funzione non void, il valore restituito tramite return permette di inserire il risultato in una espressione di calcolo, mentre le funzioni void non possono farlo e si limitano al massimo a restituire uno o piu' valori (usando dei puntatori) in specifiche celle di memoria il cui indirizzo e' passato in fase di chiamata della funzione usando i parametri attuali. Quindi se vogliamo scrivere una funzione che puo' essere inserita in una espressione di calcolo (cioe che si combina con il calcolo di una espressione matematica) usiamo le funzioni non void altrimenti usiamo le funzioni void. Pensiamo al calcolo di (-b-sqrt(delta) )/(2*a) sqrt e' una funzione non void che calcola sqrt(delta) e il risultato di questo calcolo viene inserito nella piu' ampia espressione (-b -sqrt(delta)).... al posto della chiamata sqrt(delta)

Pensiamo di scrivere una funzione che clacoli il doppio di un numero


float doppio(float n) { float z;

z=2*n;
return z;

}

che poteva essere anche scritta come

float doppio(float n) {

return 2*n;

}

perche' quello che viene restituito e' il valore dell'espressione (una volta calcolato) dopo l'istruzione return, ricordate inoltre che la funzione termina dopo aver restituito il valore e quindi eventuali istruzioni dopo il return non vengono eseguite, la funziona termina anche se incontra la } di chiusura del codice della funzione.


Ora una volta scritta la funzione doppio possiamo scrivere delle espressioni di calcolo del tipo:

z=3*4*sqrt(56)-doppio(5);

oppure

z=3+doppio(sqrt(45));

in questo caso il risultato della radice sqrt (che e' una funzione non void) viene usato compe parametro attuale della funzione doppio.

I parametri attuali possono essere specifiati attraverso delle costanti , delle singole variabili o delle espressioni di calcolo ( nella realta' le costanti e le singole variabili sono le forme piu' semplici di una espressione di calcolo).Allora possiamo anche scrivere:

z= 1+doppio( doppio(3));

oppure

z= doppio( 10 + doppio ( sqrt(4) )) +22;

ora z=50 cioe' si comportano come le espressioni matematiche che utilizziamo di solito.

le funzioni void non possono invece essere inserite in espressioni di calcolo, cioe' se


void doppio ( float *n) { *n= *n*2; }

non posso scrivere

float a=4;
z=doppio( &a)+7;

ma invece

float a=4;
doppio( &a);  
z=a+7; 

Funzioni Ricorsive

Le funzioni (void e non void) possono essere richiamate non solo da altre funzioni ma anche all'interno della funzione stessa, in questo caso si parla di funzioni ricorsive ( che cioe' richiamano se stesse)

Un tipico esempio e' il calcolo del fattoriale, in matematica il fattoriale del numero n si indica con il simbolo n! e per calcolarlo si seguono le seguenti regole

NB con n si intende un numero naturale 0,1,2,3,4,....


se n=0  allora  n!=1   cioe' 0!=1
se n>0  allora  n!= n* (n-1)!

quindi 3!= 3* 2! = 3* 2 *1!= 3*2*1*0!= 3*2*1*1 = 6

si noti la particolare definizione del fattoriale il fattoriale del numero n e' uguale a n moltiplicato per il fattoriale di n-1 in cui non spiego esattamente come calcolare il fattoriale, ma dico solo che per calcolare il fattoriale di n devo saper fare il fattoriale di n-1 , quindi spiego il fattoraile in funzione di se stesso (ecco la ricorsivita'); e poi per poter fare effettivamente il calcolo devo completare la spiegazione con una condizione di terminazione se n e' uguale a zero allora il fattoriale vale 1

Tutti i problemi ricorsivi (come pure le soluzioni dei problemi ricorsivi) hanno 2 elementi

  • una definizione/espressione ricorsiva (in cui spiego una cosa in funzione di se stessa)
  • e una condizione di terminazione

se vogliamo scrivere una funzione ricorsiva per il calcolo dell' n! scriviamo


long fattoriale (long n)
{ if(n==0)
    return 1;
  else
    return  n*fattoriale(n-1);
}  

se richiamo la funzione con

z=fattoriale(3); 

succede che si attiva il codice della funzione fattoriale(3) che si ferma nel calcolo di return 3*fattoriale(2) si ferma perche' deve ottenere il risultato della chiamata alla funzione fattoriale(2)

a questo punto si attiva una copia del codice della funzione fattoriale con valore di n=2 questa funzione fattoriale(2) si ferma pure lei nel calcolo di

return 2*fattoriale(1)   che richiede l'esecuzione della funzione fattoriale(1)

in memoria si forma una terza copia del codice della funzione fattoriale con un valore n=1 (attenzione ogni funzione fattoriale ha una propria variabile locale di nome n con valori diversi) che si ferma nel calcolo return 1*fattoriale(0) perche' in attesa del risultato della chiamata alla funzione fattoriale(0)


ora in memoria si crea una quarta copia della funzione rivolta al calcolo del fattoriale(0), in questo caso si attiva la condizione di terminaziome e return restituisce il valore 1 al precedente fattoriale (a questo punto la quarta copia del codice essendo la funzione terminata viene rimossa dalla memoria

ora la terza copia della funzione che ha ricevuto il risultato del fattoriale di zero puo' terminare il calcolo return 1*fattoriale(0) che restituisce il valore 1 (la terza copia del fattoriale termina e viene rimossa dalla memoria ora la seconda copia della funzione che ha ricevuto il risultato del fattoriale di uno puo' terminare il calcolo return 2*fattoriale(1) che restituisce 2 (la seconda copia del fattoriale termina e viene rimossa dalla memoria) ora la prima copia della funzione che ha ricevuto il risultato del fattoriale di due puo' terminare il calcolo return 3*fattoriale(2) che restituisce 6 (la prima copia del fattoriale termina e viene rimossa dalla memoria) ora z=fattoriale(3) riceve il valore del fattoriale(3) che vale 6 e lo assegna alla variabile z


Quindi nell'uso delle funzioni ricorsive c'e' una fase di espansione in cui si creano piu' copie della funzione ricorsiva e una fase di contrazione del numero di copie della funzione ricorsiva che parte quando si raggiunge la condizione di terminazione e termina quando si riesce a calcolare l'espressione iniziale.

Alcuni problemi di natura ricorsiva sono risolvibili semplicemente usando delle funzioni ricorsive, problemi di natura ricorsiva possono essere risolti anche non ricorsivamente, ma di solito con struttura risolutiva piu' complicata anche se piu, veloce (caso quicksort).


Passaggio dei parametri alle funzioni

Quando si utilizza una funzione possono essere passati anche degli array, il passaggio di un array (vettore,matrice etc) avviene sempre per indirizzo, la copia dei valori di un array (dal parametro attuale a quello formale) viene infatti considerata una operazione troppo onerosa, questo comporta che quando passiamo l'indirizzo di un vettore o di una matrice permettiamo alla funzione chiamata tramite i puntatori di cambiare i valori nelle celle di memoria della matrice passata. Ricordatevi inoltre che il passaggio dei parametri rallenta l'esecuzione del codice, per velocizzare al massimo una funzione possiamo usare le variabili globali.


I puntatori essendo fonte di errore tendono ad essere nei linguaggi di programmazione moderni sostituiti/nascosti da una sintassi semplificata che permette pero' di ottenere lo stesso risultato.

per passare un vettore di n elementi allora possiamo

dimensione vettore       parametro attuale   parametro formale        uso parametro formale nella funzione
costante (esempio=10)      vett                 int v[10]                    v[2]=7;
variabile  k               vett,k               int v[], int n               v[2]=7;    
variabile  k               vett,k               int *p , int n               *(p+2)=7;  
 

per passare una matrice di nr*nc elementi allora possiamo

dimensione matrice              parametro attuale   parametro formale        uso parametro formale nella funzione
riga e   colonna costanti 4*6      mat                int m[4][6]                      m[2][3]=7;
riga var colonna costante 6        mat,nr             int m[][6], int nr               m[2][3]=7;    
riga e col variabili               mat,nr,nc          int *p , int nr, int nc          *(p+2*nc+3)=7;  
                                                                                 o per elemento i,j   *(p+i*nc+j)=7;  

una variabile singola puo' essere passata per valore ,per indirizzo o per reference

                parametro attuale     parametro formale        uso parametro formale nella funzione
per valore        5/ a / 3*a                int z                  k=z+4;
per indirizzo       &a                      int *p                 *p=6;
per reference       a                       int &k                 k=7

il passaggio per reference e' un passaggio per indirizzo con sintassi semplificata , scrivendo il nome della variabile passo il suo indirizzo, il parametro attuale davanti al nome ha il simbolo & per ricordare che il passaggio e' per reference, la variabile formale (che nasconde internamente il puntatore) viene usata normalmente e tutto quello che le succede si riflette sulla variabile attuale