La presentazione è in caricamento. Aspetta per favore

La presentazione è in caricamento. Aspetta per favore

Le strutture dati dinamiche

Presentazioni simili


Presentazione sul tema: "Le strutture dati dinamiche"— Transcript della presentazione:

1 Le strutture dati dinamiche
La gestione statica della memoria -o a pila quando si usa la ricorsione- è molto efficiente ma non è priva di inconvenienti: Soprattutto è rigida rispetto a informazioni la cui dimensione non è nota a priori o è destinata a variare durante l’esecuzione Caso tipico: tabella di elementi vari: può essere vuota, vi si possono inserire o cancellare elementi, ecc. Per gestirla mediante array occorre prevedere una dimensione massima, con rischio di spreco overflow Per questo motivo alcuni linguaggi permettono tipi di dato dinamici: Una tabella -o lista- può essere vuota, oppure può essere il risultato di aggiungere un elemento ad una lista esistente si noti la struttura ricorsiva della definizione del tipo di dato Un valore di un tipo di dato definito in questa maniera occupa una quantità di memoria non nota a compile time Per questo motivo i linguaggi tradizionali non li permettono Permettono però una -parziale- gestione dinamica della memoria ottenendo strutture dati -rigorosamente parlando, non tipi di dati- dinamiche Il risultato è ottenuto sfruttando i puntatori, ma non è privo di rischi: se ne raccomanda un uso molto disciplinato!!

2 Le operazioni di allocazione e cancellazione di memoria
Per arrivare al risultato finale -la “simulazione” di tipi astratti dinamici- il cammino è un po’ lunghetto Le operazioni di allocazione e cancellazione di memoria La chiamata di funzione malloc(sizeof(TipoDato)); crea in memoria una variabile di tipo TipoDato, e restituisce come risultato l’indirizzo della variabile creata (più precisamente, il valore restituito è l’indirizzo del primo byte riservato alla variabile). Se P è una variabile di tipo puntatore a TipoDato, l’istruzione P = malloc(sizeof(TipoDato)); assegna l’indirizzo restituito dalla funzione malloc a P. Di conseguenza, dopo l’esecuzione di questa istruzione P “punta” alla nuova variabile perdendo il suo valore originario. NB1: la memoria viene allocata per un dato di tipo TipoDato, non per P, che invece è una variabile già esistente. NB2: In C una variabile creata dinamicamente è necessariamente “anonima”: a essa si può fare riferimento solo tramite puntatore; una variabile dichiarata mediante un proprio identificatore può invece essere indicata tramite l’identificatore stesso ma può anche essere puntata. Un puntatore si comporta come una normale variabile identificata (esso può essere indicato mediante identificatore o puntato da un altro puntatore).

3 ... 5 3 ... Stack Heap Punt1 Punt2 Record di attivazione di Proc
Simmetricamente, free(P) rilascia lo spazio di memoria puntato da P; ciò significa che la corrispondente memoria fisica è resa nuovamente disponibile per qualsiasi altro uso. NB: free deve ricevere un puntatore al quale era stato assegnato come valore l’indirizzo restituito da una funzione di allocazione dinamica di memoria (malloc, nel nostro caso). L’uso delle funzioni malloc e free richiede l’inclusione del file header <stdlib.h> tramite l’istruzione #include <stdlib.h> Siccome però malloc e free possono essere chiamate in qualsiasi momento (non si segue più la disciplina LIFO tipica della stack), la gestione della memoria si complica: int *Punt1; int **Punt2; ... 5 Punt1 Punt2 3 ... Record di attivazione di Proc Stack Heap

4 Rischi della gestione dinamica della memoria
produzione di garbage (“spazzatura”): la memoria allocata dinamicamente risulta logicamente inaccessibile perché non esiste più alcun riferimento a essa: P = malloc(sizeof(TipoDato)); P = Q; “riferimenti fluttuanti” (dangling references). (simmetrico rispetto al precedente): riferimenti fasulli a zone di memoria logicamente inesistenti: free(Q); Esempio: P puntatore a int e la cella potrebbe ricevere un valore di tipo char. Un riferimento a *P comporterebbe l’accesso all’indirizzo fisico puntato da P e l’interpretazione del suo contenuto come un valore intero con risultati imprevedibili ma non facilmente individuabili come errati. garbage e dangling references chiaramente simmetrici, ma la seconda è più pericolosa. In alcuni linguaggi si accetta la prima per evitare il rischio della seconda: eliminazione dell’istruzione free. Viene lasciato alla macchina astratta del linguaggio l’onere di effettuare garbage collection Morale: puntatori tecnica di programmazione di basso livello. Se ne raccomanda l’uso esclusivamente finalizzato alla costruzione di “pseudotipi astratti dinamici” come illustrato in seguito

5 Invece di dichiarare il tipo lista … si dichiarano i suoi elementi:
La costruzione e la gestione della struttura dinamica (pseudotipo astratto) lista mediante puntatori L’idea base: Ultimo elemento Lista e1 e2 en Invece di dichiarare il tipo lista … si dichiarano i suoi elementi: struct EL  { TipoElemento Info; struct EL *Prox; }; typedef struct EL ElemLista; typedef ElemLista *ListaDiElem; Sintassi un po’ nuova: la prima dichiarazione del tipo strutturato struct EL definisce un primo campo, Info, del tipo TipoElemento e permette di dichiarare il campo Prox come puntatore al tipo strutturato che si sta definendo; la seconda dichiarazione utilizza typedef per rinominare il tipo struct EL come ElemLista; la terza dichiarazione definisce il tipo ListaDiElem come puntatore al tipo ElemLista.

6 A questo punto si può procedere come al solito per definire variabili “di tipo lista”:
ListaDiElem Lista1, Lista2, Lista3; dichiarazioni abbreviate: ElemLista *Lista1; se non interessa mettere in evidenza il tipo della lista. struct EL *Lista1 può sostituire entrambe le typedef se non è necessario nominare esplicitamente né il tipo della lista né il tipo dei suoi elementi. Ricordiamo che: un tipo di dato è individuato da un insieme di valori e da un insieme di operazioni. Vediamo dunque alcune fondamentali operazioni per la gestione di liste così realizzate.

7 Lista Inizializzazione
assegna il valore NULL alla variabile “testa della lista”: l’operazione: Inizializza (Lista) produce perciò l’effetto indicato in figura: Lista Se però vogliamo eseguire l’operazione in maniera parametrica su una lista generica occorre che il parametro sia passato “per indirizzo”. Avremo perciò a che fare con un doppio puntatore: il puntatore che realizza il parametro formale puntando alla lista che costituisce il parametro attuale il puntatore che realizza la testa della lista

8 Applicando perciò la tipica tecnica di realizzazione del passaggio parametri per indirizzo in C otteniamo il codice seguente #include <stdlib.h> void Inizializza (ListaDiElem *Lista) /* Lista è la variabile locale che punta alla "testa di lista". La funzione assegna alla “testa di lista" il valore NULL corrispondente al valore di lista vuota */ { *Lista = NULL; } La chiamata: Inizializza(&Lista1); produce l’esecuzione seguente: Lista Lista1 Al termine dell’esecuzione il parametro formale Lista viene eliminato e rimane l’effetto voluto (inizializzazione mediante il valore NULL sul parametro attuale Lista1)

9 NB: lo stesso effetto si sarebbe potuto ottenere dichiarando l’header della procedura come segue:
void Inizializza(ElemLista **Lista) La complicazione del passaggio per indirizzo in C -tramite puntatori- ha indotto una cattiva prassi nel “gergo della programmazione C” (anche nei libri!): evitare l’uso di parametri -soprattutto se da passare per indirizzo- e sostituirlo con l’abuso delle variabili globali #include <stdlib.h> ElemLista *Lista1; void Inizializza(void) { Lista1 = NULL; } Si raccomanda di evitare questa prassi.

10 Controllo di lista vuota
boolean ListaVuota(ListaDiElem Lista) /* Produce il valore true se la lista passata come parametro è vuota, false in caso contrario, a Lista viene passato il valore contenuto nella variabile testa di lista. Lista punta pertanto al primo elemento della lista considerata */ { if (Lista == NULL) return true; else return false; } chiamata: ListaVuota(Lista1)

11 Ricerca di un elemento in una lista
boolean Ricerca (ListaDiElem Lista, TipoElemento ElemCercato) { ElemLista *Cursore; if (Lista != NULL) Cursore = Lista; /* La lista non è vuota */ while (Cursore != NULL) if (Cursore–>Info == ElemCercato) return true; Cursore = Cursore–>Prox; /* In questa maniera Cursore viene fatto puntare all'elemento successivo della lista */ } return false;

12 Versione ricorsiva della ricerca di un elemento in una lista
boolean Ricerca (ListaDiElem Lista, TipoElemento ElemCercato) { if (Lista == NULL) return false; else if (Lista–>Info == ElemCercato) return true; return Ricerca(Lista–>Prox, ElemCercato); }

13 Estrazione della testa o della coda da una lista
(senza codice). TipoElemento TestaLista(ListaDiElem Lista) /* È applicabile solo a liste non vuote. Se la lista è vuota segnala l'errore in modo opportuno; in caso contrario produce come risultato il valore del campo Info del primo elemento della lista */ ListaDiElem CodaLista(ListaDiElem Lista) /*Produce come risultato un puntatore alla sottolista ottenuta da Lista cancellandone il primo elemento. Essa non deve modificare il parametro originario. Anche questa assume l'ipotesi che il parametro passatole sia una lista non vuota */ CodaLista Ultimo elemento Lista e1 e2 en

14 Inserimento di un nuovo elemento in testa alla lista:
Si fa uso di un puntatore locale: Punt; Punt Lista e1 e2 en Si crea un nuovo elemento puntato da Punt e vi si inserisce il valore desiderato Punt = malloc(sizeof(ElemLista)); Punt–>Info = Elem; Punt Elem Lista e1 e2 en

15 Infine si collega il nuovo elemento al precedente primo elemento della lista e la testa della lista viene fatta puntare al nuovo elemento: Punt Elem Lista e1 e2 en Come in precedenza dobbiamo però costruire un codice parametrico rispetto alla lista in cui inserire il nuovo elemento attraverso il passaggio parametri per indirizzo:

16 void InsercisciInTesta(ListaDiElem *Lista, TipoElemento Elem)
{ ElemLista *Punt; /* Allocazione dello spazio necessario per la memorizzazione del nuovo elemento e inizializzazione del puntatore */ Punt = malloc(sizeof(ElemLista)); Punt–>Info = Elem; Punt–>Prox = *Lista; *Lista = Punt; } Lista Punt e1 e2 en Lista1 Lista Punt Elem e1 e2 en Lista1 Lista Punt Elem e1 e2 en Lista1

17 void InserisciInCoda(ListaDiElem *Lista, TipoElemento Elem);
{ ElemLista *Punt; if (ListaVuota(*Lista)) Punt = malloc(sizeof(ElemLista)); Punt–>Prox = NULL; Punt–>Info = Elem; *Lista = Punt; } else InserisciIncoda(&((*Lista)–>Prox), Elem);

18 Esaminiamo con attenzione l’esecuzione delle varie chiamate ricorsive della procedura applicata a un parametro attuale Lista1 di n elementi Lista*1 Lista*2 Lista*3 Lista*n Lista*n+1 e1 e2 en-1 en Lista1 Lista*1 Lista*2 Lista*3 Punt Lista*n+1 Elem e1 e2 en-1 en Lista1 Lista*1 Lista*2 Lista*3 Punt Lista*n+1 Elem e1 e2 en-1 en Lista1

19 void InserisciInOrdine(ListaDiElem *Lista, TipoElemento Elem)
{ ElemLista *Punt, *PuntCorrente, *PuntPrecedente; PuntPrecedente = NULL; PuntCorrente = *Lista; while (PuntCorrente != NULL && Elem > PuntCorrente–>Info) PuntPrecedente = PuntCorrente; PuntCorrente = PuntCorrente->Prox; } Punt = malloc(sizeof(ElemLista)); Punt–>Info = Elem; Punt–>Prox = PuntCorrente; if (PuntPrecedente) != NULL PuntPrecedente–>Prox = Punt; /* Inserisci internamente alla lista */ else *Lista = Punt; /* Inserisci in testa alla lista */

20 Punt Lista Elem e1 ek ek+1 en Lista1 PuntPrecedente PuntCorrente Punt Elem Lista e1 ek ek+1 en Lista1 PuntPrecedente PuntCorrente

21 Cancellazione di un elemento da una lista
void Cancella(ListaDiElem *Lista, TipoElemento Elem) /* Cancella dalla lista passata "per indirizzo" l'elemento Elem, se esiste, assumendo che nella lista non vi siano ripetizioni */ { ElemLista *PuntTemp; if (ListaVuota (*Lista) == false) if ((*Lista)–>Info == Elem) PuntTemp = *Lista; *Lista = CodaLista(*Lista); free(PuntTemp); } else Cancella(&((*Lista)–>Prox), Elem);

22 Riassumendo Strutture dati dinamiche = “pseudotipi di dato astratti” dinamici Realizzate mediante puntatori Puntatori: struttura di basso livello ---> a rischio (Puntatori: meccanismo tipico e “storico” del C: ponte tra assembler e linguaggi di alto livello) Raccomandazione: usare i puntatori solo all’interno delle operazioni astratte associate alle relative strutture Liste: primo e fondamentale -ma non unico!!- esempio di struttura dinamica Altri più complessi e potenti verranno visti in corsi successivi Una prima valutazione -a spanne- dell’efficienza della struttura dinamica lista rispetto all’array: Si evita lo spreco di memoria/rischio di overflow (obiettivo iniziale) A prezzo di un -lieve- aumento dovuto ai puntatori Da un punto di vista del tempo necessario all’esecuzione degli algoritmi: pro e contro (inserire in testa meglio, inserire in coda peggio, … però la ricerca in una lista ordinata non si può fare con algoritmi del tipo della ricerca in un vocabolario … il seguito alle prossime puntate (corsi).

23 Introduzione alla programmazione modulare
Ormai costruire un “sistema informatico” è impresa ben più complessa -meglio, diversa- che “inventare” un algoritmo e codificarlo. Il problema della progettazione -e gestione- del SW va ben oltre gli scopi di un corso di base ---> l’ingegneria del SW E’ però concetto di base il principio della modularizzazione: Ogni volta che un manufatto si rivela di dimensioni e complessità difficili da dominare la cosa migliore per affrontarne la costruzione è modularizzarlo: scomporlo in sottoproblemi, affrontare questi separatamente, indi ricomporre le soluzioni parziali in una soluzione globale Meccanismi di supporto alla modularizzazione sono già entrati in gioco: la tipizzazione i sottoprogrammi Essi non sono però totalmente adeguati alle esigenze sempre maggiori di costruzione di sistemi sempre più complessi: principalmente essi hanno senso solo nel contesto del programma cui appartengono -anche se con sfumature diverse da linguaggio a linguaggio un sistema informatico invece è cosa ben più ampia rispetto al concetto di programma Occorre dunque almeno gettare le basi della programmazione modulare -detta anche programmazione in grande, in contrapposizione alla programmazione in piccolo.

24 Ogni modulo è costituito da una interfaccia e da un corpo.
Un sistema software è costituito da un insieme di moduli e da relazioni intercorrenti tra i vari moduli. Ogni modulo è costituito da una interfaccia e da un corpo. L’interfaccia di un modulo, detta anche definizione di un modulo, è l’insieme di tutti e soli i suoi elementi che devono essere conosciuti da chi usa il modulo per farne un uso appropriato.Tali elementi vengono anche chiamati risorse esportate dal modulo. L’implementazione di un modulo, detta anche corpo di un modulo, è l’insieme dei meccanismi che permettono di realizzare le funzionalità, ossia i compiti, che il modulo stesso deve garantire al resto del sistema. Due importanti relazioni tra moduli: La relazione di importazione / esportazione. Diciamo che un modulo M importa una risorsa dal modulo M’ quando esso la usa. Un modulo M può importare da un altro modulo M’ solo risorse appartenenti all’interfaccia di M’. Quando non vengono precisate le risorse importate da parte di M da M’, si dice semplicemente che M usa M’. La relazione è_composto_da. Si dice che un modulo M è_composto_da un insieme di moduli {M1, M2, ..., Mk} se tale insieme permette di realizzare tutte le funzionalità di M. Di conseguenza si dice anche M1, M2, ..., Mk sono componenti di M.

25 Un esempio di architettura modulare

26 Criteri di buona modularizzazione
Principio di information hiding (occultamento delle informazioni) In generale, meno informazioni sono rese note all’utilizzatore di un modulo, meno condizionamenti vengono posti al suo implementatore e maggiore sicurezza si ha nel suo uso. Ovviamente, al contrario, non bisogna dimenticare di definire nell’interfaccia tutte le informazioni di rilievo, evitando che l’utente del modulo sia costretto a esaminarne l’implementazione. Principio di low coupling and high cohesion (basso accoppiamento e alta coesione) E’ bene che variabili, procedure e altri elementi spesso utilizzati congiuntamente siano raggruppati nello stesso modulo dando ad ogni modulo un alto livello di coesione interna, mentre altri elementi che raramente interagiscono tra loro possono essere allocati nelle interfacce di moduli diversi, ottenendo così moduli con un basso livello di accoppiamento. Principio di design-for-change (progetto in funzione del cambiamento) Un esempio molto semplice ed efficace di design-for-change è già stato visto nell’ambito della programmazione in piccolo: l’uso delle costanti. La dichiarazione della costante PiGreco, ad esempio, cosituisce la cornice che racchiude il possibile cambiamento del suo valore dovuto a nuove assunzioni. Un altro tipico esempio di cambiamento prevedibile è quello dell’hardware destinato all’esecuzione di un certo software. E’ perciò utile,costruire un modulo DriverDiPeriferica.

27 Progettazione top-down e bottom-up
La modularizzazione si accoppia molto bene con il metodo dei raffinamenti successivi (già visto fin dagli inizi) Questo metodo può “sposarsi” con criteri di buona modularizzazione in varie maniere (i principi generali hanno un’enormità di sfumature nella loro realizzazione -per fortuna e purtroppo …) Senza entrare in tecniche specifiche: Progettazione top-down (centrata sul problema): Si parte da una visione -modulo- globale e lo si raffina -scompone- fino ad ottenere moduli elementari. Progettazione bottom-up (centrata sul riuso dell’esistente e quindi più in auge in tempi moderni): si aggregano moduli -esistenti o nuovi- fino ad ottenere il sistema voluto

28 Definizione e implementazione di moduli
(per il momento lasciamo perdere il C … o almeno lasciamolo un po’ in disparte) Un programma consiste in un gruppo di moduli: un modulo principale detto modulo-programma (o modulo master) e alcuni moduli detti moduli-asserviti (o moduli slave). Il modulo-programma usa altri moduli, che, loro volta, possono usarne altri ancora. Per motivi che qui non intendiamo approfondire, sono vietate le “circolarità” nella relazione usa. Ogni modulo, che non sia un modulo-programma, è costituito da, e deve chiaramente separare, le sue due parti: l’interfaccia, l’implementazione.

29 Interfaccia del modulo
L’interfaccia di un modulo è composta dai seguenti elementi: L’identificatore del modulo. Facciamo precedere l’identificatore dalla ‘pseudo parola chiave’ module interface. La clausola import, che lista tutte le entità importate da altri moduli e il rispettivo modulo sorgente: import A, B, from M1; import X, Y from M2; import Alpha, Beta, ... from M3; import Zed, W,... from M4; La clausola export , che lista tutte le entità esportate dal modulo. Ad esempio, se l’interfaccia di un modulo MD dichiara un tipo T e un altro modulo M1 importa T da MD, allora M1 può dichiarare variabili di tipo T come se T fosse stato dichiarato in M1 stesso. Il modulo principale è l’unico infatti che importa elementi ma non ne esporta. Importanza dei commenti nell’interfaccia di un modulo. Il commento a un prototipo di una funzione, per esempio, può indicare, nel modo più preciso possibile, l’effetto della funzione. Nell’interfaccia di un modulo la dichiarazione di un tipo può non precisare la struttura del tipo stesso. Il tipo così definito si dice opaco e la sua struttura risulta nascosta: Typedef [hidden] Type1;

30 Un primo esempio: il tipo astratto numeri complessi
[module interface] ComplexNumbers [import scanf, printf from stdio] /*segue la lista degli elementi esportati*/ { typedef [hidden] Complex; /*E’ l’insieme dei numeri complessi, ben noto in matematica. Si noti il fatto che questo tipo è opaco: se ne riscontrerà l’importanza tra poco.*/ typedef enum {RPIP, MODARG} Representation; /*indica il modo di rappresentare all’esterno un numero compless.: RPIP: “parte reale, parte immaginaria”; MODARG: “modulo, argomento”. Ciò permetterà all’utente di scegliere la scrittura di un dato di tipo complesso nella forma preferita senza per questo condizionare la rappresentazione interna dei dati,.*/ Complex SumCompl(Complex Add1, Complex Add2); /*esegue la somma tra i due parametri complessi Add1 e Add2. Non produce side-effect.*/ Complex MultCompl(Complex Mult1, Complex Mult2); /*esegue il prodotto tra i due parametri complessi Mult1 e Mult2. Non produce side-effect.*/ ... /*altre operazioni aritmetiche eseguibili su numeri complessi.*/

31 void WriteCompl(Complex Par1, Representation Rep);
/*stampa sul file stdout il valore complesso passatole come primo parametro. Il formato di stampa varia al variare del valore del secondo parametro.*/ void ReadCompl(Complex *Par, Representation Rep); /*legge dal file stdin un valore complesso, memorizzandolo nella variabile indicata come primo parametro – passato necessariamente per indirizzo. La procedura interagisce con l’utente chiedendogli di immettere i dati in forma diversa a seconda del valore del secondo parametro.*/

32 Prima implementazione (parte reale e parte immaginaria)
[module implementation] ComplexNumbers [import scanf, printf from stdio import sin, cos, asin, acos, pow, sqrt from math] { typedef struct { float RealPart; float ImaginaryPart; } Complex; Complex SumCompl(Complex Add1, Complex Add2) Complex Result; Result.RealPart = Add1.RealPart + Add2.RealPart; Result.ImaginaryPart = Add1.ImaginaryPart + Add2.ImaginaryPart; return Result; }

33 Complex MultCompl(Complex Mult1, Complex Mult2)
{ Complex Result; Result.RealPart = Mult1.RealPart * Mult2.RealPart - Mult1.ImaginaryPart * Mult2.ImaginaryPart; Result.ImaginaryPart = Mult1.ImaginaryPart * Mult2.RealPart + Mult2.ImaginaryPart * Mult1.RealPart; return Result; }

34 ... /*implementazione delle altre operazioni aritmetiche sui numeri complessi*/ void WriteCompl(Complex Par, Representation Rep) { float Mod, Arg; if (Rep == RPIP) printf(“Parte Reale: %f, Parte Immaginaria: %f\n”, Par.RealPart, Par.ImaginaryPart); else Mod = sqrt(pow(Par.RealPart, 2) + pow(Par.ImaginaryPart, 2)); Arg = acos(Par.ImaginaryPart/Mod); printf(“Modulo: %f, Argomento: %f\n”, Mod, Arg); } void ReadCompl(Complex *Par, Representation Rep) ….

35 Seconda implementazione (modulo e argomento)
[module implementation] ComplexNumbers [import scanf, printf from stdio import sin, cos, asin, acos, sqrt from math] { typedef struct { float Modulus; float Argument; } Complex; Complex SumCompl(Complex Add1, Complex Add2) Complex Result; float RealPar1, RealPar2, ImPar1, ImPar2, RealParRes, ImParRes; RealPar1 = Add1.Modulus * cos(Add1.Argument); RealPar2 = Add2.Modulus * cos(Add2.Argument); ImPar1 = Add1.Modulus * sin(Add1.Argument); ImPar2 = Add2.Modulus * sin(Add2.Argument); RealParRes = RealPar1 + RealPar2; ImParRes = ImPar1 + ImPar2; Result.Modulus = sqrt(RealParRes * RealParRes + ImParRes * ImParRes); Result.Argument = acos(RealParRes/Result.Modulus); return Result; }

36 Complex MultCompl(Complex Mult1, Complex Mult2)
{ Complex Result; Result.Modulus = Mult1.Modulus * Mult2.Modulus; Result.Argument = Mult1.Argument + Mult2.Argument; return Result; } ... /*implementazione delle altre operazioni aritmetiche sui numeri complessi*/ void WriteCompl(Complex Par, Representation Rep) void ReadCompl(Complex *Par, Representation Rep) Esaminiamo l’impatto dell’astrazione così ottenuta. L’istruzione: if (x.RealPar > 0) ... è vietata: sarebbe accettabile per un’implementazione ma non per l’altra. Se si vuole accedere alla parte reale di un numero complesso occorre definire un’opportuna operazione nell’interfaccia.

37 Dal tipo di dato astratto al dato astratto
[module interface] NameTableManagement [import printf from stdio import strcmp from string] { #define MaxLen #define MaxElem 1000 typedef char Name[MaxLen]; void Insert(Name NewElem); /*inserisce il parametro nella prima posizione libera di NameTable, che è l’unica variabile globale su cui vengono eseguite le varie operazioni e che viene esportata. Gli elementi da inserire sono invece passati alla funzione da altri moduli, dai quali essa è chiamata. Se la tabella è piena o se l’elemento da inserire è già presente in tabella, stampa un opportuno messaggio sul file stdout.*/ boolean Exist(Nam Elem); /*la funzione accede alla variabile globale NameTable e ritorna il valore true se il parametro passato esiste nella tabella, false in caso contrario.*/ Name DeleteReturnLast(void); /*elimina l’ultimo valore della tabella e lo produce come valore risultato dell’operazione.*/ void Print(void); /*stampa il contenuto della tabella, un nome per ogni riga.*/ }

38 [module implementation] NameTableManagement
[import printf from stdio import strcmp from string] { #define MaxLen #define MaxElem 1000 typedef char Name[MaxLen]; typedef Name ContentType[MaxElem]; typedef struct { int NumElem = 0; ContentType Contents; } TableType; TableType NameTable;

39 void Insert(Name NewElem)
{ int Count; boolean Found; if (NameTable.NumElem == MaxElem) printf(“La tabella è già piena”); else /*si verifica se l’elemento da inserire esiste già*/ Found = false; for (Count = 0; Count < NumElem; Count++) if (strcmp(NameTable.Contents[Count], NewElem) == 0) Found = true; if (Found == true) printf(“L’elemento da inserire è già in tabella”); else strcpy(NameTable.Contents[NameTable.NumElem], NewElem); NameTable.NumElem = NameTable.NumElem + 1; } boolean Exist(Name Elem) ... Name DeleteReturnLast (void) void Print (void)

40 Dallo pseudo C al C Il C -un po’ vecchiotto- non ha costrutti espliciti per la scrittura di interfaccia e implementazione di moduli. Contano però più i concetti che le peculiarità di un linguaggio: Con un po’ di metodo si può ottenere una buona modularizzazione anche in C adattando e “approssimando” i meccanismi ideali a quelli offerti dal C Un programma C è articolabile e distribuibile su più file. E’ possibile quindi creare programmi C composti da un modulo-programma, contenuto in un file, e da più moduli asserviti contenuti ciascuno in uno o più file separati. I moduli asserviti possono poi essere ulteriormente suddivisi in interfaccia e implementazione. L’interfaccia può essere contenuta in un file avente nome uguale al nome del modulo ed estensione .h mentre l’implementazione potrà essere contenuta in un file avente nome uguale al nome del modulo ed estensione .c. La direttiva #include viene utilizzata per indicare la clausola di importazione anche se il suo effetto non è esattamente lo stesso: non consente di precisare quali elementi sono importati Constatiamo quindi che in realtà abbiamo scritto programmi modulari in C fin dal primo programma eseguibile (#include stdio.h) modulo stack contiene la dichiarazione del tipo pila e le operazioni su di esso definite. Il file stack.h contiene la dichiarazione del tipo e i prototipi delle funzioni. ...

41 La realizzazione della gestione tabella nomi in C

42 **************************************************
Parti rilevanti del file gtab.h ****************************************************************** #include <stdio.h> #include <string.h> #define MaxLen #define MaxElem 1000 typedef char Name[MaxLen]; void Insert(Name NewElem); boolean Exist(Name Elem); void Print(void); ...

43 ******************************************************************
Parti rilevanti del file gtab.c #include <gtab.h> typedef Name ContentType[MaxElem]; typedef struct { int NumElem; ContentType Contents; } TableType; static TableType NameTable; /*Variabili dichiarate come static all’interno di un file possono essere manipolate quindi solo da funzioni dichiarate in quel file*/

44 void Insert(Name NewElem)
{ int Count; boolean Found; if (NameTable.NumElem == MaxElem) printf(“La tabella è già piena”); else /*si verifica se l’elemento da inserire esiste già*/ Found = false; for (Count = 0; Count < NameTable.NumElem; Count++) if (strcmp(NameTable.Contents[Count], NewElem) == 0) Found = true; if (Found == true) printf(“L’elemento da inserire è già in tabella”) strcpy(NameTable.Contents[NameTable.NumElem], NewElem); NameTable.NumElem = NameTable.NumElem + 1; } ...

45 ******************************************************************
Parti rilevanti del file gtmain.c #include <gtab.h> Name NewName; main() { ... Insert(NewName); printf(“Il contenuto della tabella è il seguente:”); Print(); }

46 La gestione dei file in C
Ricapitoliamo alcune caratteristiche fondamentali dei file il file è un’astrazione molto ampia nella descrizione di un sistema informatico: “nastro” di I/O supporto di comunicazione macchina/ambiente di ogni tipo (sensori, attuatori, ….) zona di memoria di massa è un supporto di memoria -in senso lato- ma profondamente diverso dalla memoria centrale -non solo da un punto di vista tecnologico è uno snodo fondamentale di flussi di informazione anche tra applicazioni diverse ciò implica che un file sia “visto” necessariamente da diversi elementi della macchina (astratta): i programmi il file system (sistema operativo) difficile applicare ai file i concetti di tipo di dato che sono in genere specifici dei singoli linguaggi nel conflitto tra linguaggi e sistema operativo sulle competenze sui file finisce di solito con prevalere il SO ---> la tendenza moderna è di non definire la struttura dei file nei linguaggi di programmazione, lasciandola ad apposite librerie -spesso system dependent. Il C, in un certo senso è un’eccezione in positivo, grazie alla standard library. I file sono strutture sostanzialmente sequenziali, anche se, quando è possibile, permettono un accesso diretto ai vari record.

47 Flussi, file e programmi C
Un programma C che desidera utilizzare un file per operazioni di memorizzazione permanente o di ingresso/uscita deve aprire un flusso di comunicazione indicando al sistema operativo la sua intenzione di aprire un file esistente o la necessità di creare e aprire un nuovo file. Al termine dell’insieme di operazioni che coinvolgono quel file il flusso di comunicazione viene chiuso chiudendo il file utilizzato. NB: operazioni di input/output sono sia le operazioni che coinvolgono un dispositivo di ingresso/uscita sia le operazioni di memorizzazione permanente. Per aprire un flusso un programma C deve dichiarare una variabile di tipo puntatore e chiedere l’apertura del flusso tramite una funzione di libreria (fopen). L’apertura del flusso di comunicazione provoca l’assegnamento della variabile puntatore (che serve al programma per far riferimento al file corrispondente). La chiusura del flusso (tramite fclose) impedisce ulteriori riferimenti al file. Un flusso di comunicazione può essere binario (sequenza di byte) o di tipo testo (sequenza di caratteri ) La variabile puntatore locale al programma creata dalla fopen punta a un oggetto di tipo FILE capace di registrare tutte le informazioni necessarie a controllare un flusso. Esso contienediversi campi: modalità di utilizzo del file (lettura, scrittura o lettura e scrittura); posizione corrente sul file (punta al prossimo byte da leggere o scrivere sul file); indicatore di errore; indicatore di end-of-file (eof).

48 Ogni variabile che punta a un file deve essere definita come segue:
FILE *fp; Una “tabella file aperti”: FILE TabellaFileAperti[MaxNumFileGestibili]; è gestita dal SO (file system) e costituisce il “ponte” tra il programma e la macchina astratta gestita dal SO. File StandardInput Variabili puntatore stdin Tabella dei file aperti nomefile: modouso: poscorr: …. StandardInput stdout nomefile: modouso: poscorr: …. StandardError stderr nomefile: modouso: poscorr: …. FileUno f1 nomefile: FileTre modouso: poscorr: …. FileDue f2 nomefile: FileUno modouso: poscorr: …. FileTre f3 nomefile: FileDue modouso: poscorr: ….

49 Operazioni di gestione dei file (dalla standard library)
Tre flussi standard vengono automaticamente aperti quando inizia l’esecuzione di un programma: stdin, stdout e stderr. Normalmente questi tre flussi “rappresentano” il video del terminale (stdout e stderr) o la tastiera del terminale (stdin). printf e scanf utilizzano questi flussi standard. Queste operazioni sono una parte delle Operazioni di gestione dei file (dalla standard library) FILE *fopen(nomefile, modalità) apre un file, eventualmente creandolo, e vi associa un flusso; restituisce l’indirizzo della struttura di tipo FILE che descrive il file aperto; richiede in ingresso il nome del file da aprire e la modalità di apertura : “r” (lettura in modalità testo, posizionamento all’inizio del file), “w” (scrittura in modalità testo, posizionamento all’inizio del file), “a” (scrittura in modalità testo a partire dalla fine del file), “rb”, “wb” e “ab” (lettura, scrittura e scrittura a fine file con modalità binaria), “r+”, “w+”, “a+”, “rb+”, “wb+”, “ab+” (lettura e scrittura su file con modalità di testo o modalità binaria). Int fclose(FILE *fp) chiude il file cui fa riferimento il puntatore fp; la chiusura comporta il rilascio del descrittore di tipo FILE. Se l’operazione di chiusura viene eseguita correttamente restituisce valore uguale a 0, altrimenti viene restituito il valore particolare EOF (EOF è una costante definita in stdio.h). Int remove(nomefile) cancella il file identificato da nomefile. Restituisce 0 se l’operazione è stata eseguita correttamente, un valore diverso da zero in caso contrario. Se si cerca di cancellare un file aperto il comportamento della funzione dipende dall’implementazione. Int rename(vecchionome, nuovonome) modifica il nome di un file da vecchionome a nuovonome. Restituisce 0 se...

50 Operazioni di gestione degli errori
int ferror(FILE *fp) controlla se è stato commesso un errore nella precedente operazione di lettura o scrittura. Restituisce 0 se nessun errore è stato commesso, un valore diverso da 0 in caso contrario. Int feof(FILE *fp) controlla se è stata raggiunta la fine del file nella precedente operazione di lettura o scrittura. Restituisce 0 se la condizione di fine file non è stata raggiunta, un valore diverso da 0 in caso contrario. void clearerr(FILE *fp) riporta al valore di default i campi eof ed error della struttura che descrive lo stato del file cui fa riferimento il puntatore fp.

51 Operazioni di lettura e scrittura
Le operazioni di lettura e scrittura su file possono essere effettuate in quattro modi diversi: precisando il formato dei dati in ingresso e in uscita, accedendo ai dati carattere per carattere, linea per linea blocco per blocco. Generalmente si adotta l’accesso linea per linea nel caso di flussi di testo e l’accesso carattere per carattere o blocco per blocco in presenza di flussi binari. Lettura e scrittura formattata Le funzioni fprintf e fscanf consentono operazioni formattate analoghe a quelle di scanf e printf ma coinvolgono il file precisato dall’utente tramite il puntatore fp. Restituiscono il numero degli elementi effettivamente letti o stampati o restituiscono un numero negativo in caso di errore: int fprintf(FILE *fp, stringa di controllo, elementi) int fscanf(FILE *fp, stringa di controllo, indirizzo elementi) Lettura e scrittura di caratteri Sei funzioni della standard library consentono la lettura e la scrittura di caratteri su file. getchar legge da Standard Input il prossimo carattere restituendolo come intero. putchar scrive come prossimo carattere sul file di Standard Output il carattere che riceve come parametro restituendo il carattere scritto. getc e fgetc leggono il prossimo carattere del file specificato tra i parametri di ingresso restituendolo come intero. putc e fputc scrivono come prossimo carattere del file il carattere specificato tra i parametri di ingresso restituendolo come intero. Tutte le funzioni restituiscono EOF in caso di errore: per verificare se si tratta di un caso di fine file o di altro errore bisogna utilizzare feof o ferror. Le funzioni getc e putc sono normalmente implementate in modo da risultare più veloci in esecuzione ma presentano il rischio di effetti collaterali.

52 Il seguente programma legge e mostra sul video il contenuto del file di tipo testo filechar:
#include <stdio.h> /* Contiene la definizione di EOF, del tipo FILE e le testate delle funzioni che operano su file */ #include <stddef.h> /* Contiene la definizione di NULL */ main() { FILE *fp; char c; if ((fp = fopen("filechar", "r")) != NULL) /* Il file viene aperto in lettura con modalità testo */ while ((c = fgetc(fp)) != EOF) /* Viene letto e stampato un carattere per volta sino a fine file */ putchar(c); fclose(fp); } else printf("Il file non può essere aperto\n");

53 Lettura e scrittura di stringhe (accesso per linee)
Quattro funzioni della standard library consentono la lettura e la scrittura di stringhe di caratteri su file: gets e puts rispettivamente leggono da Standard Input e scrivono su Standard Output, fgets e fputs rispettivamente leggono o scrivono linee (stringhe di caratteri terminate da un newline) dal o sul file specificato come parametro di ingresso. Per non farla troppo lunga … la seguente funzione riceve come parametro di ingresso una stringa di riferimento, legge per linee il contenuto del file di testo filein e scrive, nel file di testo fileout, solo le linee che contengono la stringa di riferimento. La funzione restituisce valore pari a 1 se l’operazione è stata correttamente ultimata e valore 0 in caso contrario.

54 #include <stdio.h>
#include <stddef.h> #include <string.h> #define OK 1 #define ERROR   0 #define MAXLINE 100 int copiaselettiva(char refstr[]) { char line[MAXLINE]; FILE *fin, *fout; If ((fin = fopen("filein", "r")) == NULL) /* filein viene aperto in lettura con modalità testo */ return ERROR; if ((fout = fopen("fileout", "w")) == NULL) /* fileout viene aperto in scrittura con modalità testo */ fclose(fin); } while (fgets(line,MAXLINE,fin) != NULL) /* fgets legge da filein al più MAXLINE–1 caratteri e assegna al vettore line i caratteri letti, incluso l'eventuale carattere di newline, e termina la stringa con il carattere \0 */ if (strstr (line,refstr) != NULL) /* La funzione strstr restituisce la posizione della prima occorrenza della stringa puntata da refstr nella stringa puntata da line; se la seconda stringa non è contenuta nella prima viene restituito il valore NULL */ fputs(line,fout); fclose(fout); return OK;

55 -------------------------------------------
Lettura e scrittura di strutture (accesso per blocchi) int fread(void *ptr, dimelemento, numelementi, FILE *fp); legge un blocco di dati binari o testuali dal file cui fa riferimento fp e li memorizza nel vettore identificato da ptr. La funzione termina correttamente se legge il numero di byte richiesti (dimelemento*numelementi); termina anche se incontra la fine del file o se si verifica un errore di lettura. La funzione restituisce il numero di elementi effettivamente letti: se tale numero è inferiore rispetto al numero richiesto è necessario usare feof o ferror per capire i motivi del (mal)funzionamento ottenuto. int fwrite(void *ptr, dimelemento, numelementi, FILE *fp); scrive un blocco … La funzione termina correttamente se … Un file Persone è costituito da record di tipo Persona. Ogni Persona contiene i campi nome, cognome, indirizzo. Si vuole modificare il file aggiungendo a ogni persona il campo CodiceFiscale. Un file CodiciFiscali contiene i codici fiscali delle persone contenute in persone, nello stesso ordine. Si vuole costruire un file NuovePersone I tre file sono binari. Questa operazione è svolta, in maniera parametrica rispetto ai file utilizzati, dalla seguente funzione, cui sono premesse le necessarie dichiarazioni di tipo.

56 typedef struct { char nome[20];
char cognome[20]; char indirizzo[50]; } Persona; typedef char CodFisc[16]; CodFisc CodiceFiscale; } NuovaPersona; /* I file Persone, CodiciFiscali e Nuove Persone si suppongono aperti dal main. pp, cf e np fanno riferimento ai tre file in questione */ void AggiornaPersone (FILE *pp, FILE *cf, FILE *np) { Persona PersonaCorrente; CodFisc CodFiscCorrente; NuovaPersona NuovaPersonaCorrente; rewind(pp); /* Rende possibile le seguenti operazioni di lettura e scrittura sul file identificato da pp, iniziando dal primo byte del file.*/ rewind(cf); rewind(np); while (fread(&PersonaCorrente,sizeof(Persona),1,pp) != 0) /* Finché non si è raggiunta la fine del file */ fread(CodFiscCorrente,sizeof(CodFisc),1,cf); strcpy(NuovaPersonaCorrente.nome, PersonaCorrente.nome); strcpy(NuovaPersonaCorrente.cognome,PersonaCorrente.cognome); strcpy(NuovaPersonaCorrente.indirizzo, PersonaCorrente.indirizzo); strcpy(NuovaPersonaCorrente.CodiceFiscale, CodFiscCorrente); fwrite(&NuovaPersonaCorrente,sizeof(NuovaPersona),1,np); }

57 Accesso diretto int fseek(FILE *fp, long offset, int refpoint) sposta l’indicatore di posizione per effettuare accessi diretti al file a cui fa riferimento fp. Lo scostamento offset (può assumere valori positivi o negativi ed è espresso in byte) si riferisce alla posizione fissa indicata da refpoint; quest’ultimo può assumere tre diversi valori indicati in stdio.h: SEEK_SET indica uno scostamento rispetto all’inizio del file, SEEK_CUR indica uno scostamento rispetto alla posizione corrente, SEEK_END indica uno scostamento rispetto alla fine del file. La funzione fseek restituisce zero se la richiesta è corretta, un valore diverso da zero altrimenti. long ftell(FILE *fp) restituisce il valore corrente dell’indicatore di posizione del file specificato. Per file binari la posizione è il numero di byte rispetto all’inizio del file, mentre per file testuali è un valore dipendente dall’implementazione. rewind(f) equivale a fseek (f, 0, SEEK_SET); A differenza di fseek, rewind non restituisce alcun valore. Inversione del contenuto di un file numint di interi: ... main() { FILE *f; long int inizio, fine; int tempi, tempf; unsigned int dim;

58 if ((f = fopen("numint", "rb+")) == NULL)
{ puts("Non è stato possibile aprire il file numint"); /* più efficiente della printf per la stampa di un messaggio dato che non richiede la scansione e l’interpretazione della stringa di controllo */ exit(1); /* La funzione exit provoca una conclusione non anomala del programma e la restituzione del controllo al sistema operativo */ } inizio = 0; dim = sizeof(int); fseek(f, –dim, SEEK_END); /* SEEK_END è una costante definita nel file stdio.h. Ha valore 2 */ fine = ftell(f); while (inizio < fine) fseek(f, inizio, SEEK_SET); /* SEEK_SET è una costante definita nel file stdio.h. Ha valore 0 */ fread(&tempi, dim, 1, f); fseek (f, fine, SEEK_SET); fread (&tempf, dim, 1, f); /* È necessario riposizionarsi dato che la precedente istruzione fread ha spostato la posizione corrente sul successivo elemento */ fseek (f, fine, SEEK_SET); fwrite (&tempi, dim, 1, f); fseek (f, inizio, SEEK_SET); fwrite (&tempf, dim, 1, f); inizio = inizio + dim; fine = fine – dim;


Scaricare ppt "Le strutture dati dinamiche"

Presentazioni simili


Annunci Google