Traccia della lezione Lez13: Lucido 2: La distinzione tradizionale tra funzione e procedura è che le funzioni restituiscono un valore, le procedure no (quindi queste ultime sono di tipo void). Lucido 3: Il lucido introduce un codice con due funzioni ("quadrato" e "cubo"). Tutte i riferimenti a "quadrato" sono in rosso, quelli a "cubo" sono in blu. Ogni funzione ha: - un prototipo all'inizio del file - una definizione dopo il main - una o più chiamate nel main o nelle altre funzioni ("cubo" chiama "quadrato") Entrambe le funzioni hanno dati interi (fra parentesi) e un risultato intero. Lucido 6: L'intestazione non ha il ; come il prototipo perché non è un'istruzione: serve solo a introdurre il corpo della funzione. Una funzione può avere una o più istruzioni return, nel senso che il risultato potrebbe essere calcolato e restituito da punti diversi del corpo (ad esempio nel caso di un costrutto if o switch). E' preferibile avere un solo return, in omaggio al principio della programmazione strutturata secondo il quale ogni blocco deve avere uno e un solo ingresso, una e una sola uscita. Usando molti return, si aumenta la probabilità di errore. Ovviamente, si può essere flessibili se la funzione è molto semplice. L'istruzione return è indicata fra parentesi quadre per indicare che è strettamente necessaria nelle funzioni, ma in genere non si scrive nelle procedure. Anche se di solito le procedure in generale non terminano con return, non è vietato usare l'istruzione return, seguita da ; senza espressioni da valutare, per uscire dalla procedura in punti diversi dalla fine (ad esempio nei vari rami di un "if" o di uno "switch"). Ovviamente, questo è però contrario alla programmazione strutturata. Se in una funzione il cui risultato è di tipo diverso da void si scrive un return privo di espressione da valutare, il risultato della funzione è indeterminato, perché il processore riserva alcune celle in memoria per il risultato, e restituisce il contenuto di tali celle. Tale contenuto non è noto a priori e non è sempre lo stesso. Lucido 7: Mentre i parametri formali sono sempre gli stessi (essendo puri nomi), quelli attuali cambiano ad ogni chiamata (essendo i valori correnti dei dati). Lucido 11: Qui è bene seguire l'esecuzione del programma del lucido 3, disegnando lo stack e la sequenza di allocazioni, operazioni e deallocazioni. Nel disegno, lo heap sta da tutt'altra parte della memoria. Contiene le aree di memoria allocate con le funzioni malloc, calloc e realloc, e liberate con la funzione free oppure automaticamente alla fine del programma. Lucido 19: Il parametro attuale 3.5 viene copiato nella cella del parametro formale y. Poiché quest'ultimo è intero, le regole implicite di conversione trasformando 3.5 in intero, troncandolo a 3, senza dare messaggi. Quindi il risultato è 3*3 = 9. Lucido 20: Se si modifica il tipo di y da int a double, 3.5 viene copiato correttamente e moltiplicato, ma nel restituire il risultato l'istruzione return converte 3.5*3.5, che è 12.25, in un intero (la funzione "quadrato" è di tipo int). Quindi il risultato è 12. Per ottenere 12.25 occorre dichiarare double quadrato (double y). Lucido 21: Si noti che la località consente di usare lo stesso nome per parametri o variabili diverse contenute in funzioni diverse. Per esempio il parametro y di "quadrato" indica celle diverse da quelle del parametro y di "cubo". Lucido 23: Disegnando quel che avviene sullo stack durante l'esecuzione del codice, si vede che la y modificata dall'istruzione y = y * y; è quella interna allo spazio locale della funzione "quadrato". La variabile x del main è un'altra cella, e quindi non viene influenzata dall'operazione. Ovviamente, sarebbe così anche se le due variabili si chiamassero entrambe x, dato che indicano comunque celle diverse. Lucido 24: Anche in questo caso, conviene disegnare quel che avviene sullo stack eseguendo il codice, per rendersi conto che i parametri formali a e b sono celle diverse dai parametri attuali a e b: le prime celle stanno nello spazio allocato dalla funzione scambia, le ultime nello spazio del main. Quindi, le istruzioni della funzione modificano le prime, ma non le ultime, e al termine della funzione a e b mantengono il loro valore iniziale. Lucido 25: Il codice SCAMBIA.C illustra la differenza tra il passaggio per valore e quello per indirizzo, seguendo l'esempio riportato sui lucidi. Disegnando quel che avviene sullo stack durante l'esecuzione, ci si rende conto che i parametri formali pa e pb, che stanno nello spazio della funzione scambia, contengono gli indirizzi delle celle a e b del main. Quindi: - sono di tipo diverso (int * anziché int) - bisogna passare loro non a e b, ma &a e &b (cioè gli indirizzi delle variabili) - si usano per accedere al valore puntato, attravero l'operatore *pa e *pb Il passaggio dei parametri in C è sempre per valore (non esiste il passaggio per indirizzo). Quello qui illustrato è il trucco standard per simulare il passaggio per indirizzo. Molti programmatori chiamano i parametri formali come quelli attuali anche se non sono copie, ma indirizzi. Questa pratica può portare a far confusione fra i tipi dei due tipi di parametri. Nell'esempio, si è aggiunta una p all'inizio del nome dei parametri (pa anziché a e pb anziché b) per sottolineare che si tratta di puntatori, e non di copie o addirittura delle stesse variabili. Lucido 27: Il lucido illustra l'uso del passaggio per indirizzo allo scopo di produrre molti risultati, anziché uno solo. Tecnicamente, la funzione "decompone" non ha risultati (infatti è di tipo void). Tuttavia, i suoi effetti collaterali (modificare l e f) simulano la produzione di due risultati (parte intera e frazionaria di un numero). Lucido 28: In realtà si usa anche la scrittura int funzione (int V[]), specialmente se si vuole sottolineare che il vettore V è statico. Ma all'interno della funzione il parametro V è comunque un puntatore, non un vettore statico. Si noti che per le stringhe (vettori di char) è ovvio che non si passi la lunghezza, dato che il terminatore la determina automaticamente. Per altri tipi di vettori, invece, servirà quasi sempre un parametro aggiuntivo che specifichi la lunghezza. L'unica eccezione è quando questa lunghezza è una costante simbolica definita con una direttiva #define, e quindi accessibile a tutto il file. Ma per i vettori dinamici la lunghezza è una variabile, e anche per quelli statici usati solo in parte, la lunghezza effettiva è in genere una variabile. In questi casi, occorre un parametro aggiuntivo. Completezza vuole si aggiunga che, passando matrici statiche, le dimensioni successive alla prima vanno invece specificate. Ma su questo dettaglio rimandiamo a un manuale per non esagerare. Il codice MODIFYVECT.C illustra il tema delle modifiche ai vettori oppure ai loro elementi. Per prima cosa, si chiede all'utente di specificare un numero n e si costruisce un vettore che contiene i primi numeri interi da 1 a n. Quindi: - la funzione CambiaSegno(V,n) cambia il segno dei primi n elementi del vettore, mostrando che essi vengono modificati dalla funzione perché viene passato l'indirizzo della prima cella del vettore. Quindi il vettore è passato per copia, mentre è come se gli elementi venissero passati per indirizzo. - le istruzioni V += n/2 e V -= n/2 mostrano che si può scalare in avanti e all'indietro la prima cella del vettore, dato che esso è dinamico - la funzione ScalaVettore(V,n/2) cerca di ripetere l'operazione V += n/2, ma fallisce, perché l'operazione viene eseguita su una copia del puntatore V. Infatti, aggiungendo una stampa dentro la funzione, il vettore risulterebbe scalato correttamente. - la funzione ScalaVettorePerIndirizzo(&V,n/2) esegue l'operazione V += n/2 agendo su V attraverso il suo indirizzo &V. Infatti nella funzione si lavora con pV, che è un puntatore a vettore, cioè doppio puntatore a intero. Si è usata l'istruzione typedef int* vint; che introduce il tipo ausiliario "vint", e poi la definizione ScalaVettorePerIndirizzo(vint *pV, int n) in modo da chiarire meglio che si sta lavorando sull'indirizzo di un vettore per poterlo modificare. Altrimenti si sarebbe dovuto usare ScalaVettorePerIndirizzo(int **pV, int n) Lo stesso trucco si deve usare se si vuole allocare o deallocare un vettore all'interno di una funzione. Si provi infine a non riportare l'indirizzo di V al valore iniziale: la deallocazione (free(V)) dà errore in esecuzione, perché si sta passando un indirizzo diverso da quello usato in allocazione (questo può non succedere: è più probabile che succeda con vettori lunghi). Lucido 29: Si considerino le strutture typedef struct { int i; int Vstat[10]; int *Vdyn; } dati; dati S1; e si supponga che nel main tutti i campi di S1 ricevano dei valori e il campo Vdyn venga allocato. Se si passa la struttura a una funzione, i singoli campi vengono copiati uno per uno, ma - il campo i viene copiato - il vettore statico Vstat viene copiato elemento per elemento, - del vettore dinamico Vdyn viene copiato solo il puntatore. Quindi, modifiche a S1.i e a qualsiasi elemento di S1.Vstat rimangono confinate alla funzione, mentre modifiche agli elementi di S1.Vdyn rimangono valide anche nel main. Analogamente, se si definisce anche dati S2 e si assegna S2 = S1, i campi S1.i e S2.i sono copie, i campi S1.Vstat e S2.stat sono copie elemento per elemento, mentre i campi S1.Vdyn e S2.Vdyn sono puntatori alla stessa area dello heap, dunque alias: - S1.Vstat[3] = 10 non modifica S2.Vstat[3] - S1.Vdyn[3] = 10 modifica S2.Vdyn[3] Esercizio finale: si completi il codice SORT.C, che: - chiede all'utente di indicare un numero intero n, seguito da n numeri interi - ordina per valori crescenti il vettore stesso - stampa il vettore ordinato Si deve procedere in modo top-down, cioe' - prima si inserisce nel main le chiamate a tre funzioni che svolgano le operazioni sopra elencate, prima del main i loro prototipi e dopo il main le loro definizioni (vuote) - poi si completa la definizione della funzione che legge il vettore (si può sfruttare il codice DYNVECT.C della lezione 12, ma inglobandolo in una funzione) - poi si completa la definizione della funzione che stampa il vettore Perché prima la stampa e poi l'ordinamento? Perché così sappiamo se la lettura è andata a buon fine prima di aggiungere troppe linee di codice. - infine si realizza l'ordinamento; procederemo con l'algoritmo detto di "selection sort": finché il vettore corrente contiene più di un elemento, - cerchiamo la posizione dell'elemento massimo (con una funzione) - scambiamo l'elemento massimo con l'ultimo (per questo si può sfruttare il codice SCAMBIA.C) - accorciamo il vettore (banalmente, riducendo di 1 una variabile che ne indica la lunghezza)