La presentazione è in caricamento. Aspetta per favore

La presentazione è in caricamento. Aspetta per favore

Le strutture dati dinamiche La gestione statica della memoria -o a pila quando si usa la ricorsione- è molto efficiente ma non è priva di inconvenienti:

Presentazioni simili


Presentazione sul tema: "Le strutture dati dinamiche La gestione statica della memoria -o a pila quando si usa la ricorsione- è molto efficiente ma non è priva di inconvenienti:"— 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 lesecuzione 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 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 lindirizzo della variabile creata (più precisamente, il valore restituito è lindirizzo del primo byte riservato alla variabile). Se P è una variabile di tipo puntatore a TipoDato, listruzione P = malloc(sizeof(TipoDato)); assegna lindirizzo restituito dalla funzione malloc a P. Di conseguenza, dopo lesecuzione 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 lidentificatore 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 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 lindirizzo restituito da una funzione di allocazione dinamica di memoria ( malloc, nel nostro caso). Luso delle funzioni malloc e free richiede linclusione del file header tramite listruzione #include 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; Record di attivazione di Proc StackHeap 5 3 Punt1 Punt2...

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: P = Q; free(Q); Esempio: P puntatore a int e la cella potrebbe ricevere un valore di tipo char. Un riferimento a *P comporterebbe laccesso allindirizzo fisico puntato da P e linterpretazione 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 dellistruzione free. Viene lasciato alla macchina astratta del linguaggio lonere di effettuare garbage collection Morale: puntatori tecnica di programmazione di basso livello. Se ne raccomanda luso esclusivamente finalizzato alla costruzione di pseudotipi astratti dinamici come illustrato in seguito

5 La costruzione e la gestione della struttura dinamica (pseudotipo astratto) lista mediante puntatori Lidea base: Invece di dichiarare il tipo lista … si dichiarano i suoi elementi: struct EL { TipoElementoInfo; struct EL*Prox; }; typedef struct ELElemLista; 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. Lista e1e2en Ultimo elemento

6 A questo punto si può procedere come al solito per definire variabili di tipo lista: ListaDiElemLista1, 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 Inizializzazione assegna il valore NULL alla variabile testa della lista: loperazione: Inizializza (Lista) produce perciò leffetto indicato in figura: Lista Se però vogliamo eseguire loperazione 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 voidInizializza (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 lesecuzione seguente: Lista1 Lista Al termine dellesecuzione il parametro formale Lista viene eliminato e rimane leffetto voluto (inizializzazione mediante il valore NULL sul parametro attuale Lista1)

9 NB: lo stesso effetto si sarebbe potuto ottenere dichiarando lheader della procedura come segue: voidInizializza(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 luso di parametri -soprattutto se da passare per indirizzo- e sostituirlo con labuso delle variabili globali #include ElemLista*Lista1; voidInizializza(void) { Lista1 = NULL; } Si raccomanda di evitare questa prassi.

10 Controllo di lista vuota booleanListaVuota(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; else return Ricerca(Lista–>Prox, ElemCercato); }

13 Estrazione della testa o della coda da una lista (senza codice). TipoElementoTestaLista(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 */ ListaDiElemCodaLista(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 */ Lista e1e2en Ultimo elemento CodaLista

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

15 Infine si collega il nuovo elemento al precedente primo elemento della lista e la testa della lista viene fatta puntare al nuovo elemento: Lista e1e2en Punt Elem 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 voidInsercisciInTesta(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; } Lista1 e1e2en Punt Lista Lista1 e1e2en Lista Lista1 e1e2en Punt Lista Elem Punt Elem

17 voidInserisciInCoda(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 lesecuzione delle varie chiamate ricorsive della procedura applicata a un parametro attuale Lista1 di n elementi e1e2en-1en Lista1 Lista*1Lista*2Lista*3Lista*nLista*n+1 e1e2en-1en Lista1 Lista*1Lista*2Lista*3 Lista*n+1 Punt Elem e1e2en-1en Lista1 Lista*1Lista*2Lista*3 Lista*n+1 Punt Elem

19 voidInserisciInOrdine(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 Lista1 e1eken Punt Lista Lista1 e1eken Lista Elem Punt Elem ek+1 PuntPrecedente PuntCorrente ek+1 PuntPrecedente PuntCorrente

21 Cancellazione di un elemento da una lista voidCancella(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 allinterno 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- dellefficienza della struttura dinamica lista rispetto allarray: –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 allesecuzione 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 ---> lingegneria 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 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. –Linterfaccia di un modulo, detta anche definizione di un modulo, è linsieme 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. –Limplementazione di un modulo, detta anche corpo di un modulo, è linsieme 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 allinterfaccia 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 allutilizzatore 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 nellinterfaccia tutte le informazioni di rilievo, evitando che lutente del modulo sia costretto a esaminarne limplementazione. 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 nellambito della programmazione in piccolo: luso 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 dellhardware destinato allesecuzione 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 unenormità 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 dellesistente 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: –linterfaccia, –limplementazione.

29 Interfaccia del modulo Linterfaccia di un modulo è composta dai seguenti elementi: –Lidentificatore del modulo. Facciamo precedere lidentificatore dalla pseudo parola chiave module interface. –La clausola import, che lista tutte le entità importate da altri moduli e il rispettivo modulo sorgente: importA, B,... fromM1; importX, Y... fromM2; importAlpha, Beta,...fromM3; importZed, W,...fromM4; La clausola export, che lista tutte le entità esportate dal modulo. Ad esempio, se linterfaccia 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 è lunico infatti che importa elementi ma non ne esporta. Importanza dei commenti nellinterfaccia di un modulo. Il commento a un prototipo di una funzione, per esempio, può indicare, nel modo più preciso possibile, leffetto della funzione. Nellinterfaccia 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 linsieme dei numeri complessi, ben noto in matematica. Si noti il fatto che questo tipo è opaco: se ne riscontrerà limportanza tra poco.*/ typedef enum {RPIP, MODARG} Representation; /*indica il modo di rappresentare allesterno un numero compless.: RPIP: parte reale, parte immaginaria; MODARG: modulo, argomento. Ciò permetterà allutente 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 lutente 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 limpatto dellastrazione così ottenuta. Listruzione: –if (x.RealPar > 0)... è vietata: sarebbe accettabile per unimplementazione ma non per laltra. Se si vuole accedere alla parte reale di un numero complesso occorre definire unopportuna operazione nellinterfaccia.

37 Dal tipo di dato astratto al dato astratto [module interface] NameTableManagement [import printf from stdio import strcmp from string] { #define MaxLen 20 #define MaxElem 1000 typedef char Name[MaxLen]; void Insert(Name NewElem); /*inserisce il parametro nella prima posizione libera di NameTable, che è lunica 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 lelemento 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 lultimo valore della tabella e lo produce come valore risultato delloperazione.*/ 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 20 #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 lelemento 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(Lelemento 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. Linterfaccia può essere contenuta in un file avente nome uguale al nome del modulo ed estensione.h mentre limplementazione 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 #define MaxLen 20 #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 typedef Name ContentType[MaxElem]; typedef struct { int NumElem; ContentType Contents; } TableType; static TableType NameTable; /* Variabili dichiarate come static allinterno 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 lelemento 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(Lelemento da inserire è già in tabella) else { strcpy(NameTable.Contents[NameTable.NumElem], NewElem); NameTable.NumElem = NameTable.NumElem + 1; }...

45 ****************************************************************** Parti rilevanti del file gtmain.c ****************************************************************** #include 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 è unastrazione 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 è uneccezione 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 dellinsieme 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 lapertura del flusso tramite una funzione di libreria ( fopen ). Lapertura del flusso di comunicazione provoca lassegnamento 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: FILETabellaFileAperti[MaxNumFileGestibili]; è gestita dal SO (file system) e costituisce il ponte tra il programma e la macchina astratta gestita dal SO. nomefile: modouso: poscorr: …. nomefile: modouso: poscorr: …. nomefile: modouso: poscorr: …. nomefile: FileTre modouso: poscorr: …. nomefile: FileUno modouso: poscorr: …. nomefile: FileDue modouso: poscorr: …. Variabili puntatore stdin Tabella dei file aperti File StandardInput stdout stderr f1 f2 f3 StandardInput StandardError FileUno FileDue FileTre

49 Tre flussi standard vengono automaticamente aperti quando inizia lesecuzione 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 lindirizzo 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 allinizio del file), w (scrittura in modalità testo, posizionamento allinizio 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). Intfclose(FILE*fp) chiude il file cui fa riferimento il puntatore fp ; la chiusura comporta il rilascio del descrittore di tipo FILE. Se loperazione di chiusura viene eseguita correttamente restituisce valore uguale a 0, altrimenti viene restituito il valore particolare EOF ( EOF è una costante definita in stdio.h ). Intremove ( nomefile ) cancella il file identificato da nomefile. Restituisce 0 se loperazione è stata eseguita correttamente, un valore diverso da zero in caso contrario. Se si cerca di cancellare un file aperto il comportamento della funzione dipende dallimplementazione. Intrename(vecchionome,nuovonome) modifica il nome di un file da vecchionome a nuovonome. Restituisce 0 se...

50 Operazioni di gestione degli errori intferror(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. Intfeof(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. voidclearerr(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 laccesso linea per linea nel caso di flussi di testo e laccesso 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 dallutente 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 /* Contiene la definizione di EOF, del tipo FILE e le testate delle funzioni che operano su file */ #include /* Contiene la definizione di NULL */ main() { FILE*fp; charc; 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 loperazione è stata correttamente ultimata e valore 0 in caso contrario.

54 #include #defineOK1 #defineERROR 0 #defineMAXLINE100 int copiaselettiva(char refstr[]) { charline[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); return ERROR; } 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(fin); 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 typedefstruct{charnome[20]; charcognome[20]; charindirizzo[50]; } Persona; typedefcharCodFisc[16]; typedefstruct{charnome[20]; charcognome[20]; charindirizzo[50]; CodFiscCodiceFiscale; } NuovaPersona; /* I file Persone, CodiciFiscali e Nuove Persone si suppongono aperti dal main. pp, cf e np fanno riferimento ai tre file in questione */ voidAggiornaPersone (FILE *pp, FILE *cf, FILE *np) { PersonaPersonaCorrente; CodFiscCodFiscCorrente; NuovaPersonaNuovaPersonaCorrente; 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 lindicatore 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 ; questultimo può assumere tre diversi valori indicati in stdio.h: SEEK_SET indica uno scostamento rispetto allinizio 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. longftell(FILE*fp) restituisce il valore corrente dellindicatore di posizione del file specificato. Per file binari la posizione è il numero di byte rispetto allinizio del file, mentre per file testuali è un valore dipendente dallimplementazione. 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; inttempi, tempf; unsigned intdim;

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 linterpretazione 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 La gestione statica della memoria -o a pila quando si usa la ricorsione- è molto efficiente ma non è priva di inconvenienti:"

Presentazioni simili


Annunci Google