1 / 58

Le strutture dati dinamiche

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

gyula
Download Presentation

Le strutture dati dinamiche

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  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. 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 perP, 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. 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: • P = Q; • free(Q); • Esempio: P puntatore a inte 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. Ultimo elemento Lista e1 e2 en • La costruzione e la gestione della struttura dinamica (pseudotipo astratto) lista mediante puntatori • L’idea base: • Invece di dichiarare il tipo lista … • si dichiarano i suoi elementi: • struct EL  { • TipoElemento Info; • 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 typedefper rinominare il tipostruct EL come ElemLista; • la terza dichiarazione definisce il tipo ListaDiElemcome 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. 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; elsereturn 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). • 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 intesta 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; • } Punt Lista e1 e2 en Lista1 Punt Lista Elem e1 e2 en Lista1 Punt Lista 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 en ek+1 Lista1 PuntPrecedente PuntCorrente Punt Elem Lista e1 ek en ek+1 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. 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 MusaM’. • 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*/ • voidWriteCompl(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 20 • #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 20 • #define MaxElem 1000 • typedef char Name[MaxLen]; • typedef Name ContentType[MaxElem]; • typedefstruct { 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 (#includestdio.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 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 <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”) • else • { 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 unflusso 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. 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.

More Related