u o r d g i t o r g e i i t o r u o r d e e i e A. Chianese, V. Moscato, A. Picariello ALLA SCOPERTA DEI FONDAMENTI DELL’INFORMATICA l i l i Questo testo è uno strumento didattico utile per la comprensione dei principali metodi ed algoritmi di Analisi Numerica e delle problematiche connesse all’uso di un elaboratore nella risoluzione di un problema di matematica applicata. Gli argomenti trattati riguardano: l’analisi degli errori, la risoluzione di sistemi lineari e di equazioni non lineari, l’interpolazione e lo smoothing di dati, l’approssimazione di integrali e la risoluzione numerica di equazioni differenziali e il sistema interattivo MATLAB. Il testo è corredato di numerosi esempi ottenuti implementando gli algoritmi esposti in ambiente MATLAB. Alessandra D'Alessio è docente di Calcolo Numerico presso la Facoltà di Ingegneria dell’Università di Napoli. COD. U ANGELO CHIANESE VINCENZO MOSCATO ANTONIO PICARIELLO Alla scoperta dei fondamenti dell’informatica Un viaggio nel mondo dei BIT 35,00 u o r d g i t o r l i g e i i t o r u o r d e e i e l i MANUALI Angelo Chianese, Vincenzo Moscato, Antonio Picariello Alla scoperta dei fondamenti dellíinformatica Un viaggio nel mondo dei BIT Liguori Editore Questa opera è protetta dalla Legge sul diritto d’autore (Legge n. 633/1941: http://www.giustizia.it/cassazione/leggi/l633_41.html). Tutti i diritti, in particolare quelli relativi alla traduzione, alla citazione, alla riproduzione in qualsiasi forma, all’uso delle illustrazioni, delle tabelle e del materiale software a corredo, alla trasmissione radiofonica o televisiva, alla registrazione analogica o digitale, alla pubblicazione e diffusione attraverso la rete Internet sono riservati, anche nel caso di utilizzo parziale. La riproduzione di questa opera, anche se parziale o in copia digitale, è ammessa solo ed esclusivamente nei limiti stabiliti dalla Legge ed è soggetta all’autorizzazione scritta dell’Editore. La violazione delle norme comporta le sanzioni previste dalla legge. Il regolamento per l’uso dei contenuti e dei servizi presenti sul sito della Casa Editrice Liguori è disponibile al seguente indirizzo: http://www.liguori.it/politiche_contatti/default.asp?c=legal L’utilizzo in questa pubblicazione di denominazioni generiche, nomi commerciali e marchi registrati, anche se non specificamente identificati, non implica che tali denominazioni o marchi non siano protetti dalle relative leggi o regolamenti. Liguori Editore - I 80123 Napoli http://www.liguori.it/ © 2008 by Liguori Editore, S.r.l. Tutti i diritti sono riservati Prima edizione italiana Marzo 2008 Chianese, Angelo : Alla scoperta dei fondamenti dell’informatica. Un viaggio nel mondo dei BIT/ Angelo Chianese, Vincenzo Moscato, Antonio Picariello Napoli : Liguori, 2008 ISBN-13 978 - 88 - 207 - 4305 - 5 1. Programmazione strutturata 2. Linguaggio C, MATLAB I. Titolo Aggiornamenti: 16 15 14 13 12 11 10 09 08 10 9 8 7 6 5 4 3 2 1 0 Indice PREFAZIONE CAPITOLO PRIMO 1. L’INFORMAZIONE E LE SUE RAPPRESENTAZIONI ....1 1.1. L’informatica ed il mondo moderno.......................................................1 1.1.1. Una definizione di Informatica..............................................................2 1.1.2. La rappresentazione dell’informazione .................................................2 1.2. La rappresentazione digitale ...................................................................5 1.2.1. I numeri in binario.................................................................................6 1.2.2. La rappresentazione dei numeri relativi ..............................................15 1.2.3. La rappresentazione dei numeri reali...................................................20 1.3. Gli operatori booleani ............................................................................26 1.4. La convergenza digitale .........................................................................27 1.4.1. La codifica delle informazioni testuali ................................................31 1.4.2. La codifica delle immagini..................................................................35 1.4.3. Immagini in movimento o video .........................................................40 1.4.4. La codifica del suono ..........................................................................42 1.5. Dati e metadati .......................................................................................43 CAPITOLO SECONDO 2. IL MODELLO DI ESECUTORE......................................45 VI 2.1. Processi e processori ..............................................................................45 2.2. Modello di Von Neumann......................................................................46 2.2.1. Le memorie .........................................................................................48 2.2.2. La CPU................................................................................................51 2.2.3. I bus.....................................................................................................53 2.2.4. Il clock.................................................................................................55 2.3. Firmware, software e middleware ........................................................57 2.4. Evoluzione del modello di Von Neumann ............................................61 2.5. Il modello astratto di esecutore .............................................................63 2.6. I microprocessori....................................................................................67 2.7. Un modello di processore.......................................................................74 2.7.1. La programmazione in linguaggio assemblativo.................................79 CAPITOLO TERZO 3. ALGORITMI E PROGRAMMI.........................................97 3.1. Informatica come studio di algoritmi ...................................................97 3.1.1. La soluzione dei problemi ...................................................................97 3.1.2. La calcolabilità degli algoritmi............................................................99 3.1.3. La trattabilità degli algoritmi.............................................................103 3.2. La descrizione degli algoritmi .............................................................105 3.2.1. Sequenza statica e dinamica di algoritmi ..........................................107 3.3. I linguaggi di programmazione...........................................................110 3.4. I metalinguaggi.....................................................................................111 3.5. La programmazione strutturata .........................................................113 3.5.1. La progettazione dei programmi di piccole dimensioni ....................117 3.5.2. La documentazione dei programmi ...................................................121 CAPITOLO QUARTO 4. LA STRUTTURA DEI PROGRAMMI ...........................123 Indice VII 4.1. Le frasi di un linguaggio di programmazione....................................123 4.1.1. Le dichiarazioni.................................................................................123 4.1.2. Le frasi di commento.........................................................................124 4.1.3. L’istruzione di calcolo ed assegnazione ............................................124 4.1.4. I costrutti di controllo ........................................................................125 4.2. La potenza espressiva ..........................................................................130 4.3. La modularità.......................................................................................135 4.3.1. La parametrizzazione del codice .......................................................137 4.3.2. Le funzioni ........................................................................................140 4.3.3. La visibilità........................................................................................142 4.3.4. L'allocazione dinamica ......................................................................146 4.3.5. La ricorsione......................................................................................149 4.3.6. Gli effetti collaterali ..........................................................................149 4.3.7. Il riuso dei sottoprogrammi ...............................................................151 4.3.8. L'information hiding..........................................................................151 CAPITOLO QUINTO 5. I DATI ...........................................................................153 5.1. Informazione e dato .............................................................................153 5.2. La classificazione dei tipi .....................................................................156 5.3. I tipi atomici..........................................................................................157 5.3.1. Il tipo booleano..................................................................................157 5.3.2. Il tipo carattere ..................................................................................159 5.3.3. Il tipo intero.......................................................................................160 5.3.4. Il tipo reale ........................................................................................161 5.3.5. Il tipo per enumerazione....................................................................163 5.3.6. Il tipo subrange..................................................................................163 5.4. I tipi strutturati ....................................................................................164 5.4.1. Gli array.............................................................................................166 5.4.2. Il tipo stringa di caratteri ...................................................................169 5.4.3. Il record .............................................................................................171 5.5. I puntatori.............................................................................................172 5.6. I file........................................................................................................174 5.6.1. I file sequenziali ................................................................................174 5.6.2. I file di caratteri o textfile..................................................................177 5.6.3. La connessione e sconnessione dei file .............................................179 VIII 5.6.4. I file ad accesso diretto (RANDOM).................................................181 5.7. L’astrazione sui dati.............................................................................181 5.7.1. Il tipo pila ..........................................................................................182 5.7.2. Il tipo coda.........................................................................................182 5.7.3. Il tipo tabella......................................................................................183 CAPITOLO SESTO 6. 6.1. IL LINGUAGGIO C.......................................................185 Introduzione .........................................................................................185 6.2. Le caratteristiche generali del linguaggio C ......................................185 6.2.1. Il vocabolario del linguaggio.............................................................186 6.2.2. Separatori ed identificatori ................................................................186 6.2.3. I simboli speciali ...............................................................................187 6.2.4. Parole chiavi......................................................................................188 6.2.5. I delimitatori......................................................................................188 6.2.6. Le frasi di commento.........................................................................188 6.2.7. Le costanti .........................................................................................189 6.2.8. Le stringhe.........................................................................................191 6.3. Il programma e la gestione dei tipi in C .............................................191 6.3.1. L’intestazione di una funzione ..........................................................194 6.3.2. Il blocco di una funzione ...................................................................194 6.3.3. I tipi semplici.....................................................................................195 6.3.4. Dichiarazione di variabili ..................................................................196 6.3.5. Alias di tipi ........................................................................................198 6.3.6. Il tipo enumerativo ............................................................................198 6.3.7. I tipi derivati ......................................................................................198 6.3.8. Il tipo array ........................................................................................198 6.3.9. Il tipo struct .......................................................................................199 6.3.10. Il tipo unione.................................................................................200 6.3.11. I campi ..........................................................................................200 6.3.12. Stringhe di caratteri ......................................................................201 6.3.13. I Puntatori .....................................................................................201 6.4. Gli operatori del linguaggio.................................................................202 6.4.1. Operatori Aritmetici ..........................................................................202 6.4.2. Operatori Relazionali ........................................................................202 6.4.3. Operatori Logici ................................................................................202 6.4.4. Operatori di incremento e decremento ..............................................202 6.4.5. Operatori sui puntatori.......................................................................203 6.4.6. Operatori logici bit oriented ..............................................................204 Indice IX 6.5. La specifica dell’algoritmo in C ..........................................................204 6.5.1. Istruzioni di assegnazione..................................................................204 6.5.2. Richiamo di funzioni .........................................................................205 6.5.3. Costrutti selettivi ...............................................................................206 If-else ..........................................................................................................206 Switch-case .................................................................................................208 6.5.4. Costrutti Iterativi ...............................................................................209 Il ciclo while ...............................................................................................209 Il ciclo do-while ..........................................................................................210 Il ciclo for....................................................................................................211 6.6. Le librerie di funzioni ..........................................................................212 6.6.1. La gestione dell’I/O...........................................................................213 Apertura file................................................................................................213 Lettura da file..............................................................................................214 Scrittura su file............................................................................................215 La gestione delle stringhe ...........................................................................216 Funzioni per il calcolo matematico .............................................................217 6.7. Gli algoritmi di base in C.....................................................................218 6.7.1. Lo scambio di valore .........................................................................218 6.7.2. Inserimento in un vettore...................................................................220 6.7.3. Eliminazione in un vettore.................................................................222 6.7.4. Eliminazione di una colonna da una matrice.....................................224 6.7.5. Eliminazione di una riga da una matrice ...........................................226 6.7.6. Ricerca sequenziale ...........................................................................228 6.7.7. Ricerca binaria...................................................................................230 6.7.8. La ricerca del valore massimo in un vettore......................................233 6.7.9. La posizione del valore minimo in un vettore ...................................235 6.7.10. Ordinamento di un vettore col metodo della selezione.................237 6.8. Esempi di programmi completi in C...................................................239 6.8.1. Gestione di un array ..........................................................................240 6.8.2. Gestione di un archivio......................................................................244 CAPITOLO SETTIMO 7. IL LINGUAGGIO DELL’AMBIENTE MATLAB ............249 7.1. Caratteristiche del linguaggio .............................................................249 7.1.1. Il vocabolario del linguaggio.............................................................250 7.1.2. I separatori.........................................................................................251 7.1.3. Gli identificatori e le parole chiavi ....................................................251 7.1.4. I simboli speciali ...............................................................................252 Operatori Aritmetici....................................................................................252 X Operatori Relazionali ..................................................................................253 Operatori Logici..........................................................................................253 I delimitatori ...............................................................................................253 7.1.5. Le frasi di commento.........................................................................253 7.1.6. Le costanti numeriche .......................................................................254 7.1.7. Le costanti stringhe di caratteri .........................................................254 7.2. La struttura del programma ...............................................................255 7.2.1. La dichiarazione e gestione dei tipi ...................................................255 Tipi Semplici: il tipo intero.........................................................................255 Tipi Semplici: il tipo reale ..........................................................................256 Tipi Semplici: il tipo char ...........................................................................256 Tipi Semplici: il tipo booleano....................................................................256 Tipi Strutturati: il tipo array ........................................................................256 Tipi Strutturati: il tipo record ......................................................................258 Tipi Strutturati: il tipo intervallo.................................................................259 Tipi Strutturati: il tipo file...........................................................................260 7.2.2. La dichiarazione di funzione .............................................................260 7.2.3. La specifica dell’algoritmo................................................................261 7.2.4. Assegnazione.....................................................................................261 7.2.5. Richiamo di funzione ........................................................................262 Funzioni predefinite ....................................................................................263 Funzioni per l’I/O .......................................................................................263 Lettura da file di INPUT .............................................................................264 Scrittura su file di OUTPUT .......................................................................264 7.2.6. Gli enunciati di selezione ..................................................................265 Il costrutto IF-ELSE....................................................................................265 Il costrutto switch-case ...............................................................................266 7.2.7. Le strutture iterativa ..........................................................................268 Il while ........................................................................................................268 Il for ............................................................................................................269 7.3. Gli algoritmi di base in MATLAB ......................................................270 7.3.1. Lo scambio di valore .........................................................................270 7.3.2. L’inserimento in un vettore ..............................................................272 7.3.3. L’eliminazione in un vettore .............................................................273 7.3.4. L’eliminazione di una colonna da una matrice..................................275 7.3.5. L’eliminazione di una riga da una matrice ........................................276 7.3.6. La ricerca sequenziale .......................................................................277 7.3.7. La ricerca binaria...............................................................................280 7.3.8. Il valore massimo in un vettore .........................................................283 7.3.9. La posizione del valore minimo di un vettore ...................................285 7.3.10. Minimo e massimo in una matrice................................................286 7.3.11. Ordinamento di un vettore col metodo della selezione.................288 7.3.12. Ordinamento di un vettore col metodo del gorgogliamento .........290 7.4. Esercizi completi...................................................................................293 Indice XI CAPITOLO OTTAVO 8. 8.1. LA TRADUZIONE DEI PROGRAMMI..........................299 Introduzione .........................................................................................299 8.2. Il processo di traduzione......................................................................300 8.2.1. La compilazione ................................................................................301 8.2.2. Il collegamento ..................................................................................305 8.2.3. Il caricamento ....................................................................................307 8.2.4. Gli interpreti ......................................................................................308 8.3. La verifica della correttezza dei programmi......................................309 8.4. Gli ambienti integrati...........................................................................313 8.4.1. L’ambiente DEV-C++.......................................................................313 8.4.2. L’ambiente MATLAB.......................................................................319 CAPITOLO NONO 9. 9.1. LA PROGRAMMAZIONE ORIENTATA AGLI OGGETTI 331 I limiti del paradigma procedurale.....................................................331 9.2. Introduzione al paradigma object oriented .......................................332 9.2.1. La nascita della programmazione ad oggetti .....................................335 9.3. I fondamenti della programmazione ad oggetti.................................336 9.3.1. Oggetti e classi ..................................................................................336 9.3.2. Oggetti software e classi come implementazioni di tipi di dato astratto 339 9.3.3. Il meccanismo dell’ereditarietà .........................................................344 9.3.4. Polimorfismo e binding dinamico .....................................................346 9.3.5. Note di progetto.................................................................................348 CAPITOLO DECIMO 10. INTRODUZIONE AI SISTEMI OPERATIVI...............349 XII 10.1. Introduzione .........................................................................................349 10.2. Caratteristiche di un Sistema Operativo............................................352 10.2.1. L’evoluzione storica dei Sistemi Operativi...................................354 10.2.2. Alcuni esempi di Sistemi Operativi ..............................................356 DOS ............................................................................................................357 UNIX e LINUX ..........................................................................................357 OS/2 ............................................................................................................357 WINDOWS.................................................................................................357 Mac OS .......................................................................................................358 10.3. L’architettura dei Sistemi Operativi ..................................................358 10.3.1. La gestione dei processi................................................................360 10.3.2. La gestione della memoria............................................................363 10.3.3. Il file system .................................................................................364 10.3.4. L’interprete dei comandi: la shell .................................................365 CAPITOLO UNDICESIMO 11. LE RETI DI COMUNICAZIONE ................................367 11.1. I sistemi di comunicazione...................................................................367 11.1.1. Codici e codifica...........................................................................368 11.1.2. Il problema degli errori.................................................................369 11.1.3. La trasmissione dell’informazione................................................370 11.1.4. I mezzi trasmissivi ........................................................................371 11.2. Le reti di calcolatori .............................................................................373 11.2.1. Tipologie di reti di calcolatori ......................................................375 11.2.2. Cenni all’Internetworking.............................................................382 11.2.3. Aspetti software delle reti di calcolatori .......................................382 11.2.4. Il modello “Internet”.....................................................................386 11.2.5. La struttura di una rete TCP/IP .....................................................387 11.2.6. Le applicazioni di una rete TCP/IP...............................................390 CAPITOLO DODICESIMO 12. IL MONDO DI INTERNET.........................................393 12.1. Introduzione .........................................................................................393 12.1.1. Una ragnatela di connessioni ........................................................395 12.1.2. Internet ed il modello di comunicazione.......................................397 Indice XIII 12.1.3. La storia di Internet.......................................................................398 12.2. Il World Wide Web..............................................................................401 12.2.1. Nuovi standard per il Web............................................................404 12.2.2. I browser.......................................................................................405 12.2.3. La ricerca dell’informazione ed i motori di ricerca ......................405 Indici di rete ................................................................................................406 Motori di ricerca .........................................................................................406 12.2.4. I Portali .........................................................................................408 12.3. Internet ed il mondo del business........................................................409 12.3.1. L’e-commerce...............................................................................410 12.3.2. L’e-banking ..................................................................................410 12.3.3. L’e-trading....................................................................................411 12.3.4. L’e-procurement ...........................................................................411 12.3.5. Internet e la Pubblica Amministrazione: l’e-governement ...........412 12.3.6. Internet ed il mondo della formazione: l’e-learning .....................413 CAPITOLO TREDICESIMO 13. BASI DI DATI E SISTEMI INFORMATIVI.................417 13.1. Sistemi Informativi e Sistemi Informatici ..........................................417 13.1.1. Sistemi Informativi aziendali: l’evoluzione storica ......................419 13.2. Analisi e progettazione di un Sistemi Informativo ............................420 13.2.1. Le basi di dati ...............................................................................422 13.2.2. I Data Base Management Systems................................................422 13.2.3. L’evoluzione dei DBMS...............................................................425 13.2.4. Le funzionalità di un DBMS.........................................................426 13.2.5. Ricerche in un database: introduzione al linguaggio SQL............427 APPENDICE A 14. VADEMECUM DEI PRINCIPALI TERMINI USATI IN INFORMATICA....................................................................429 14.1. Fasce di Computer ...............................................................................429 14.2. Componenti Interni di un Computer..................................................430 14.3. I dispositivi di input e output ..............................................................433 XIV APPENDICE B 15. ESERCIZI DI PROGRAMMAZIONE.........................437 15.1. Premessa ...............................................................................................437 15.2. Esercizi sulle variabili non strutturate ...............................................438 15.3. Esercizi sui vettori ................................................................................439 15.4. Esercizi sulle matrici ............................................................................442 15.5. Esercizi sui record ................................................................................447 15.6. Esercizi sulle stringhe ..........................................................................449 15.7. Esercizi sui file ......................................................................................453 INDICE DEI TERMINI ................................. 455 GLI AUTORI ............................................... 461 a Ilaria, Miriam e Susanna a Marco e a Vinni e a chi non esiste, ed è bello pensare che c’è Prefazione Il titolo “Alla scoperta dei fondamenti dell’informatica” non è stato scelto con facilità. Per condividerlo gli autori hanno discusso a lungo e tra i tanti titoli sui quali hanno litigato, lo hanno preferito perché sottolinea i diversi obiettivi che si erano prefissi quando hanno iniziato la loro avventura di docenti in corsi di base dell’informatica. Negli ultimi anni il mondo dell’informatica ha vissuto frenetici cambiamenti indotti dalle innovazioni tecnologiche imposte dalle esigenze di una società che ha scoperto nella conoscenza, e nella sua gestione, una primaria occasione di competizione in un mercato sempre più globalizzato. Non è un caso che molte delle tecnologie che hanno caratterizzato momenti della nostra vita sono diventate rapidamente vecchie, inutilizzabili, o come si dice obsolete. Tanti possono essere gli esempi di oggetti tecnologici che hanno mostrato una vita brevissima se confrontata con la storia dell’uomo o con altri aspetti della società. Da archeologi appassionati della materia che proviamo ad insegnare abbiamo selezionato quei concetti di base che vivono senza rischiare di arrugginire con il tempo che passa. Non sappiamo se ci siamo riusciti, ma le nostre diverse esperienze di insegnamento ci hanno fatto convergere su un testo che crediamo possa rispondere alle aspettative. A dimostrazione di tale tesi nel libro sono presenti sia il linguaggio C che quello di MATLAB per ribadire uno dei fondamenti dell’informatica, la tesi di Church, che viene spesso dimenticata per dare spazio ad esigenze di mercato o a mode: saper progettare un programma è di gran lunga più appassionante del saper codificare un algoritmo in un linguaggio di programmazione. Il titolo vuole sottolineare anche il desiderio di insegnare senza annoiare, coinvolgendo il lettore in un viaggio alla scoperta di un una materia di cui molti parlano dilungandosi solitamente su temi secondari, specifici, o semplicemente tecnologici. Il libro non è un tradizionale testo di fondamenti di informatica, né uno di preparazione al conseguimento del patentino europeo. Ha la pretesa di volersi rivolgere a tutte le persone curiose di capire cosa si nasconde dietro un sipario fatto di tanta tecnologia, ma soprattutto di tanto lavoro di persone, gli informatici, capaci di trasformare idee in soluzioni, esigenze in applicazioni, realtà in immaginario della realtà. E cerca di farlo camminando su un filo sottile teso tra due esigenze difficili da coniugare: la precisione scientifica importante per insegnare e il raccontare con l’intenzione di appassionare. Abbiamo così scoperto che un viaggio nella storia dell’informatica poteva essere il collante giusto. E il libro si presenta al XVIII suo lettore proprio come un viaggio attraverso le tappe più significative di un cammino recente: dalla introduzione del codice binario, all’affermazione del modello architetturale di Von Neumann, alla introduzione dei linguaggi di programmazione, per terminare con le diffuse applicazioni tra cui i Sistemi Operativi ed Internet. Il libro si presta ad essere letto in modi diversi a seconda degli obiettivi didattici. Ad esempio per i corsi di Fondamenti di Informatici per l’Ingegneria Informatica si consigliano i capitoli I, II, III, IV, V, VI, VIII e X; mentre per i corsi di Elementi di Informatica per i non informatici si consigliano i capitoli XII, II (senza microprocessori), I, X, VIII, VII, XIII (per i corsi di ingegneria gestionale). Come autori abbiamo una grande consapevolezza sorretta dai cambiamenti intervenuti nella filiera di produzione del mondo dell’editoria. I libri non sono più blocchi granitici difficili da modificare. Oggi possono seguire non solo le esperienze degli autori, ma soprattutto dei lettori che con le loro indicazioni possono contribuire a miglioramenti e adeguamenti. Non è un caso che il materiale da cui siamo partiti, è stato per diversi anni sperimentato in corsi universitari. Anche il presente libro sarà sottoposto a continue revisioni per le quali ci auguriamo che forte sia il coinvolgimento costruttivo principalmente dei nostri studenti. Ai quali va sin da ora il nostro ringraziamento. A tal fine ci preme sottolineare che la trattazione degli argomenti in un libro per la didattica è frutto dell’esperienza che gli autori hanno maturato grazie al confronto con il mondo circostante. Come non ringraziare chi prima è stato nostro maestro e chi poi ha collaborato con noi; ma anche tutti i colleghi con i quali abbiamo discusso e con i quali continuiamo a condividere la passione per l’informatica. Il nostro primo pensiero va al professore Bruno Fadini che oggi non è più tra noi: ci fa piacere ricordarne la grande passione per la ricerca e la didattica che ci ha contagiato rendendo il nostro lavoro appassionante. Un particolare ringraziamento ci sembra giusto rivolgerlo a Nello Cimitile che per primo ci ha insegnato i principi della programmazione strutturata: il sogno di alcuni di noi è poter scrivere un libro con lui spiegando il mondo dell’informatica con il semplice concetto di tipo. Infine un ringraziamento tutto particolare va al professore Nino Mazzeo per il grande lavoro che sta attualmente facendo per affermare il ruolo positivo dell’informatica: ci aspettiamo da lui che le discussioni di questo ultimo periodo continuino con la stessa partecipazione e piacere reciproco. I collaboratori da ringraziare sono tanti per la loro presenza costante alle attività del gruppo di ricerca sui sistemi informativi multimediali, ma una citazione specifica va ad Antonio d’Acierno e Antonio Penta. Non abbiamo bisogno di ringraziare l’amico e maestro Lucio Sansone: è ormai parte di noi e con noi procede sul lungo cammino della didattica e della ricerca. Infine intendiamo dedicare anche il presente libro a tutti i docenti e ricercatori universitari che come noi vivono nell’Università credendo che la didattica e la ricerca sono importanti per lo sviluppo di una società più giusta. E gli autori hanno scritto il libro principalmente perché amano il proprio lavoro di docenti e ricercatori. va2 Capitolo primo L’informazione e le sue rappresentazioni 1.1. L’informatica ed il mondo moderno L’attuale società moderna è sempre più dipendente dall’informatica. In molti aspetti della vita quotidiana è possibile avvertire la “presenza” di elaboratori elettronici, il più delle volte nascosti all’interno di altre macchine. Le plurifunzionalità (agende, giochi, messaggi multimediali, etc…) offerte dai moderni telefoni cellulari, da sole, basterebbero a testimoniare la diffusione del suddetto fenomeno, favorita anche dall’avvento di Internet che ha abbattuto ogni tipo di barriera e distanza sociale, culturale e tecnologica, rendendo possibile a tutti l’accesso a informazioni e servizi distribuiti in diverse parti del globo terrestre. I moderni elaboratori, attraverso l’insieme delle applicazioni software (sistema operativo più programmi) in esso contenute, mettono a disposizione dell’utente finale una vasta gamma di funzionalità in grado di adattarsi e rispondere alle sue più disparate esigenze del mondo del lavoro, intrattenimento o altro. Grazie poi alla presenza delle interfacce grafiche, sempre più “user-friendly” (facili da usare per i suoi utilizzatori o utenti), di cui sono dotati i recenti sistemi operativi e le applicazioni moderne, l’utilizzo degli elaboratori stessi è diventato sempre più semplice, accattivante ed intuitivo, suscitando un notevole interesse soprattutto nelle generazioni più giovani. Già dalle scuole elementari i ragazzi iniziano ad utilizzare il computer per giocare ai videogiochi, navigare su Internet, ascoltare canzoni, scrivere documenti, vedere film, leggere la posta elettronica e così via. Molte delle attività del mondo contemporaneo hanno risentito della preponderante invasione degli elaboratori. Ad esempio sia nelle aziende che negli uffici pubblici tutto il flusso (workflow) dei documenti (produzione, trasferimento, archiviazione) avviene oramai mediante l’utilizzo di elaboratori in un formato “immateriale” detto digitale; nei più moderni studi medici e aziende ospedaliere, la gestione dei pazienti avviene attraverso “cartelle cliniche digitali”; nelle industrie la maggior parte dei processi logistici, amministrativi e di produzione sono automatizzati grazie all’uso di elaboratori; nel settore dei trasporti aereo e ferroviario risulta da anni possibile prenotare ed acquistare biglietti “on-line”; analogamente in settori paralleli stanno sempre più aumentando le aziende, i negozi e gli enti che, attraverso siti Internet, consentono l’acquisto di merci e servizi (e-commerce); nel settore televisivo e dello spettacolo, è grazie all’informatica, che si stanno diffondendo sempre nuovi e più sofisticati servizi interattivi; parte del settore dell’intrattenimento (sport, giochi, musica) è stato, poi, 2 Capitolo primo di fatto completamente rivoluzionato dagli elaboratori (videogiochi, software musicali, moviole “in campo”); infine anche le più moderne conquiste dell’umanità (viaggi spaziali, scoperte nel campo medico e scientifico, etc.) sono state in parte rese possibili grazie all’informatica. 1.1.1. Una definizione di Informatica La diffusa presenza degli elaboratori elettronici in tantissime e diversissime realtà sociali, ha determinato nella quotidianità la spontanea associazione tra il termine Informatica e l’utilizzo delle macchine informatiche. In realtà l’informatica è un campo di studio molto giovane. I primi elaboratori risalgono infatti solo al 1946, ed allora, essa non era considerata una disciplina separata dall’elettronica e dalla matematica. Nel corso degli anni, con l’incremento delle capacità di calcolo (o computazionale) degli elaboratori (anche detti per tali motivi “computer”), ci si rese conto che il compito di “programmare” queste macchine era estremamente difficile e richiedeva teorie e pratiche diverse da quelle dei campi esistenti. L’informatica, ovvero la scienza della risoluzione dei problemi con l’aiuto degli elaboratori, divenne quindi una disciplina autonoma. L’informatica è una disciplina complessa che abbraccia campi molto diversi tra loro, in cui l’elaboratore elettronico è solo lo strumento mediante il quale si possono attuare le tante applicazioni per esso pensate. Non a caso il termine informatica deriva dalle parole “informazione” e “automatica” per sottolinearne caratteristiche e finalità. Infatti l’informatica è soprattutto la scienza della gestione e della elaborazione della informazione, e ciò, spiega perché essa è penetrata in tutte le attività umane rappresentando nei paesi più evoluti l’infrastruttura fondamentale per lo sviluppo culturale ed economico della società. In tale ottica l’elaboratore assolve al compito di gestire e memorizzare le informazioni o dati, mentre l’esperto informatico progetta le applicazioni necessarie ad organizzare e quindi gestire le informazioni sia mediante l’uso di macchine (tra le quali lo stesso elaboratore), sia facendo ricorso a sole risorse umane, o infine a soluzioni miste in cui macchine ed uomini provvedono alla attuazione dei processi individuati. 1.1.2. La rappresentazione dell’informazione Affinché un’informazione venga correttamente memorizzata e gestita da un elaboratore risulta fondamentale “rappresentarla” in una forma che sia ad esso comprensibile. Risulta pertanto importante definire i concetti di informazione e rappresentazione. Spesso, si attribuisce al termine informazione un senso molto generico: esso deriva da informare, ossia dare forma e fa riferimento ad un concetto astratto, quanto mai vasto e differenziato, che può, in linea del tutto generale, coincidere con qualunque notizia o racconto. In termini molto semplici si può dire che l'informazione è qualcosa che viene comunicato in una qualsiasi forma scritta o orale. Nella Teoria dell'Informazione l'informazione viene associata al concetto di messaggio, anche se esso ha il solo compito di rappresentarla e trasportarla. In tal caso, risulta evidente che, affinché un messaggio possa essere interpretato, mittente L’informazione e le sue rappresentazioni 3 e destinatario debbano aver concordato un insieme di regole con le quali scrivere, e in seguito leggere, il messaggio stesso. Inoltre perché esista informazione, il messaggio deve contribuire ad eliminare incertezza: infatti, se una sorgente di messaggi inviasse ripetutamente un solo simbolo, la quantità di informazione ricevuta a destinazione sarebbe nulla. Infine i messaggi possono aumentare le conoscenze se l’informazione ricevuta si aggiunge a quelle preesistenti nel senso che, dopo aver ricevuto un messaggio, si sa di più rispetto alle situazioni precedenti. Come già anticipato, perché persone o macchine possano utilizzare un’informazione hanno bisogno che essa sia appropriatamente “rappresentata”. A tale proposito, la storia dell’uomo è ricca di esempi che testimoniano l’importanza della rappresentazione efficace delle informazioni: basti pensare che se non fosse esistita la scrittura non avremmo un resoconto oggettivo degli avvenimenti dell’uomo dalla sua nascita fino ad oggi. Del resto scrivere, leggere ed elaborare informazioni implica che chi lo fa abbia preliminarmente concordato un codice, ossia una serie di regole e convenzioni da seguire; e il mondo che ci circonda è ricco di esempi in campi anche molto diversi tra loro. In generale esistono due modalità di rappresentazione. Nella prima, le proprietà del fenomeno rappresentato sono omomorfe alla forma della rappresentazione. Essa viene detta analogica proprio perché la rappresentazione varia in analogia con la grandezza reale: si pensi ad un termometro tradizionale, nel quale la dilatazione del mercurio è messa in relazione con la variazione rilevata di temperatura. Nella rappresentazione analogica una grandezza è rappresentata in modo continuo e la gran parte delle grandezze fisiche della realtà sono di tipo continuo. La seconda modalità di rappresentazione è invece discreta, nel senso che si utilizza un insieme finito di rappresentazioni distinte che vengono messe in relazione con alcuni elementi dell’universo rappresentato. Una tale rappresentazione è un’approssimazione di quella analogica: infatti, se ad esempio si prova a descrivere il colore del mare, ci si accorge che non si hanno nel linguaggio naturale tanti termini quanti ne servirebbero per descrivere le infinite sfumature percepite dai nostri occhi. La scelta del livello di approssimazione dipende dall’uso della rappresentazione discreta in quanto in molte applicazioni reali non sempre hanno importanza tutti gli infiniti valori che l’informazione reale può assumere. È noto, ad esempio, a tutti che in una proiezione cinematografica l’occhio umano percepisce il movimento per effetto della sovrapposizione successiva di 24 fotogrammi in un secondo: non servirebbe a molto aggiungere altre istantanee. In tale caso si sfrutta un limite dell’utilizzatore della informazione rappresentata per estrarre da un insieme infinito di valori un suo opportuno sottoinsieme. Noti ora i concetti di informazione e rappresentazione, ritornando al caso degli elaboratori elettronici, in maniera più dettagliata è possibile affermare che un’informazione per essere correttamente elaborata deve essere codificata in una rappresentazione comprensibile all’elaboratore stesso, e, a tale proposito, risultano fondamentali i concetti di “codifica” e “codice” riportati di seguito. La codifica è l’insieme di convenzioni e di regole da adottare per trasformare un’informazione in una sua rappresentazione e viceversa. Si noti che la stessa informazione può essere codificata in modi diversi (rappresentazioni diverse) a 4 Capitolo primo seconda del contesto: le rappresentazioni araba o romana dei numeri ne sono un esempio (i simboli “1” e “I” costituiscono due codifiche diverse della stessa informazione numerica). Un codice è un sistema di simboli che permette la rappresentazione dell’informazione ed è definito dai seguenti elementi: - i simboli che sono gli elementi atomici della rappresentazione; - l’alfabeto che rappresenta l’insieme dei simboli possibili: con cardinalità (n) del codice si indica il numero di elementi dell’alfabeto; - le parole codice o stringhe che rappresentano sequenze possibili (ammissibili) di simboli: per lunghezza (l) delle stringhe si intende poi il numero di simboli dell’alfabeto da cui ciascuna parola codice risulta composta; - il linguaggio che definisce le regole per costruire parole codici che abbiano significato per l’utilizzatore del codice. La scrittura è l’esempio più noto di codifica nella quale trovare facilmente un esempio dei concetti indicati. Siano allora V = {v1, v2, ..., vm } l’insieme degli m valori diversi di una data informazione e A = {s1, s2, ..., sn } un alfabeto composto da n simboli distinti. Si considerino diverse lunghezze delle parole codice: - con l = 1 si hanno tante parole codice diverse (n1) quanti sono i simboli dell’alfabeto; - con l = 2 si hanno tante parole codice diverse quante sono le combinazioni con ripetizione degli n simboli nelle due posizioni, ossia n2; - con l = 3 si hanno n3 parole codice diverse. In generale il numero di parole codice differenti è uguale a nl. Ad esempio, fissato l’alfabeto A = {-,.} del codice Morse con n=2, si hanno le diverse rappresentazioni riportate in tabella 1. nl 2 4 8 16 l=1 . l=2 --. ... l=3 ----. -.-.. .-.-. ..… l=4 ------. --.--.. -.--.-. -..-… .--.--. .-..-.. ..-..-. ....… Tabella 1 – Esempi di codici Morse a lunghezza differente Se la codifica deve mettere in corrispondenza biunivoca i valori dell’informazione con le parole codice, ossia ad ogni vi deve corrispondere una ed una sola sequenza s1i s2j ...sni , allora la lunghezza l deve essere scelta in modo che: nl > m L’informazione e le sue rappresentazioni 5 Si noti che nel caso di nl > m non tutte le configurazioni possibili (parole codice) vengono utilizzate per la rappresentazione, come l’esempio nella tabella 2 mostra nei primi due casi. Informazione Giorni settimana Colori semaforo Risposta Suoi Valori lunedì, martedì, mercoledì, giovedì, venerdì, sabato, domenica rosso, giallo, verde si, no Sue rappresentazioni --- , --. , -.- , -.. , .-- , .-. , ..-- , -., .-,. Tabella 2 – Esempi di rappresentazione di informazioni diverse con codici a lunghezza differente Infine la codifica può essere a lunghezza fissa o variabile. Nel primo caso tutte le parole codice hanno sempre la stessa lunghezza fissata da particolari esigenze applicative. La scrittura è un caso, di contro, di codifica a lunghezza variabile come è possibile verificare in un qualsiasi vocabolario. I calcolatori adottano codifiche a lunghezza fissata e definita. 1.2. La rappresentazione digitale La scrittura alfabetica e le cifre della numerazione araba sono alcuni esempi tra le numerose forme di quella che è stata precedentemente definita rappresentazione discreta. Ai fini informatici assume particolare interesse la rappresentazione binaria digitale basata su un alfabeto costituito da due soli simboli distinti, che assumono convenzionalmente la forma di “0” e “1”. Tali due simboli rappresentano le unità minime di rappresentazione e memorizzazione digitale e vengono denominate bit da “binary digit”. Solitamente si indica con digitale la rappresentazione basata sui bit, anche se essa teoricamente sottintende una rappresentazione con qualsiasi tipo di cifre. Inoltre la diffusione dell’informatica nella società ha comportato un’estensione del significato del termine ed oggi, nella sua accezione più ampia, digitale assume il significato di informazione codificata in contrapposizione con analogico che invece descrive la realtà nelle sue infinite forme e varietà. La rappresentazione digitale, sebbene implica l’adozione di parole codici più lunghe per rappresentare una determinata quantità di informazioni, semplifica la memorizzazione delle informazioni e rende i sistemi digitali meno soggetti ai disturbi elettrici rispetto ai sistemi analogici. Non a caso la rappresentazione delle informazioni all’interno dell’elaboratore si basa sull’alfabeto binario {0,1} in quanto i supporti di memorizzazione delle informazioni, i registri di memoria, vengono realizzati con componenti elementari semplici detti flip-flop, che operano in due soli stati possibili. In generale esistono tanti fenomeni diversi che possono essere facilmente associati ad un bit: - la presenza o assenza di tensione elettrica in un circuito elettrico; 6 Capitolo primo - le polarità positiva e negativa di un magnete o di un supporto con caratteristiche magnetiche tipo nastri e dischi; la presenza o l’assenza di un buco su un supporto ottico come quello dei cd-rom; l’apertura o chiusura di una conduttura; la condizione di acceso o di spento di un interruttore. Figura 1 – Esempio di rappresentazione digitale di un segnale di tensione La rappresentazione digitale è anche più affidabile (probabilità di errore bassa), in quanto disturbi provenienti dall’ambiente o interferenze (rumore) indotte da altri componenti difficilmente possono far variare lo stato di un componente che memorizza i bit. Infatti adottando due soli stati si può scegliere una separazione massima tra le corrispondenti grandezze indicative dello zero e dell’uno, per cui il rumore pur sommandosi non produce significative alterazioni. La trasformazione delle grandezze analogiche nella loro rappresentazione digitale ha tra i tanti vantaggi quello della fedeltà della riproduzione e quello della trasmissione dell’informazione a diversi tipi di dispositivi elettronici. Alcune informazioni nascono già in formato digitale grazie a strumenti che operano direttamente con tale rappresentazione: tra essi il calcolatore elettronico con le sue tante applicazioni, ma anche le moderne macchine fotografiche digitali , i telefoni cellulari, i registratori di suoni e video. Per elaborare con un calcolatore delle grandezze reali di tipo continuo, bisogna utilizzare la loro rappresentazione digitale con una approssimazione che dipende dal processo di trasformazione in grandezze a valori discreti e dalla precisione della rappresentazione digitale dei numeri. 1.2.1. I numeri in binario Come già anticipato, l’aritmetica usata dai calcolatori è diversa da quella comunemente utilizzata dalle persone ed utilizza il sistema binario poiché più adatto ad essere “maneggiato” da circuiti elettronici. È facile osservare che il codice binario utilizza un alfabeto A = {0,1} con n=2. Le informazioni numeriche vengono quindi rappresentate mediante stringhe di bit di lunghezza l che producono 2l configurazioni (parole codice) diverse. Viceversa se si devono rappresentare K informazioni diverse occorrono log2K bit per associare ad esse codici diversi. La precisione con cui i numeri possono essere espressi è finita e predeterminata, poiché questi devono essere memorizzati con parole codice di lunghezza fissata. Per ragioni legate alla costruzione dei moderni calcolatori, è d'uso fare riferimento a stringhe con l uguale ad 8 che vengono dette byte. Sequenze di bit più lunghe di un byte sono invece denominate word, la loro lunghezza dipende dalle caratteristiche del sistema, ma è sempre un multiplo del L’informazione e le sue rappresentazioni 7 byte: 16, 32, 64 o 128 bit. Poiché i calcolatori trattano con molti bit/byte sono state introdotte le unità di misura binarie riportate in tabella 3. Sigla B KB MB GB TB Nome Byte KiloByte MegaByte GigaByte TeraByte Numero byte 1 210=1024 220=1.048.576 230=1.073.741.824 240=1.099.511.627.776 Numero bit 8 8.192 8.388.608 8.589.934.592 8.796.093.022.208 Tabella 3 – Unità di misura binarie Con otto bit si rappresentano solo 28 (256) valori diversi. Nel caso in cui un solo byte non fosse sufficiente per rappresentare i K valori dell’informazione, allora si individua il numero b di byte tale che: 2 (b*8) > K In altri termini, all’interno dei moderni calcolatori, la codifica è a lunghezza fissa ed adotta parole codice con una lunghezza che ha valori multipli di 8. Numero di byte b 1 2 3 4 Numero di bit (b*8) 8 16 24 32 2 (b*8) 28 216 224 232 Configurazioni 256 65.536 16.777.216 4.294.967.296 Tabella 4 – Valori di informazione rappresentabili al variare del numero di byte L’adozione di stringhe a lunghezza finita e definita implica che i numeri gestiti siano a precisione finita, ossia siano quelli rappresentati con un numero finito di cifre, o più semplicemente definiti all’interno di un prefissato intervallo di estremi [min,max] determinati. Nel mondo reale esistono esempi di sistemi di numerazione basati su numeri a precisione finita come i sistemi di misura dei gradi degli angoli ([0, 360]) e del tempo ([0, 60] per minuti e secondi e [0, 24] per le ore). In tali sistemi di numerazione si introduce il concetto di periodicità, per cui valori non compresi nell’intervallo di definizione vengono fatti ricadere in esso. In generale, nei sistemi di calcolo con numeri a precisione finita, le operazioni possono causare errori quando il risultato prodotto non appartiene all’insieme dei valori rappresentabili. Si dice condizione di underflow quella che si verifica quando il risultato dell’operazione è minore del più piccolo valore rappresentabile (min). Si chiama overflow la condizione opposta, ossia quella che si verifica quando il risultato dell’operazione è maggiore del più grande valore rappresentabile (max). Infine il risultato dell’operazione non appartiene all’insieme quando non è compreso nell’insieme dei valori rappresentabili, pur non essendo né troppo grande nè troppo piccolo. 8 Capitolo primo La tabella seguente mostra alcuni casi di overflow, di underflow e di non appartenenza all’insieme di definizione per una calcolatrice decimale dotata di sole tre cifre, con intervallo di definizione formato da numeri interi compresi nell’intervallo [-999,+999]. Operazione 200 +100 730 + 510 -500-720 2:3 Condizione risultato rappresentabile Overflow Underflow risultato non rappresentabile Tabella 5 – Esempi di operazioni che generano risultati rappresentabili e non Anche l’algebra dei numeri a precisione finita è diversa da quella convenzionale poiché alcune delle proprietà: - proprietà associativa: a + (b - c) = (a + b) – c - proprietà distributiva: a × (b - c) = a × b – a × c non sempre vengono rispettate in base all’ordine con cui le operazioni vengono eseguite, come i casi seguenti mostrano usando la stessa calcolatrice di tre cifre. a 100 b 900 C 600 a 200 b 90 C 88 a + (b - c) 100 + (900 - 600) a × (b - c) 200 × (90 – 88) condizione ok (a + b) – c (100 + 900) - 600 Condizione Overflow condizione Ok a×b–a×c 200 × 90 – 200 × 88 Condizione Overflow Tabella 6 – Applicazione delle proprietà associativa e distributiva per algebre a precisione finita L’algebra dei numeri a precisione finita deve essere gestita applicando i noti criteri di periodicità e tenendo in considerazione le condizioni di errore indicate. Per la periodicità i valori esterni all’intervallo di definizione vengono ricondotti ad esso prendendo il resto della divisione dei valori per il periodo. intervallo [0,360] [0,60] [0,60] periodo 360 60 60 valore 1200 61 55 divisione 1200 : 360 61 : 60 60 : 55 resto 120 1 55 Tabella 7 – Sistemi periodici Le operazioni in sistemi periodici possono essere effettuate utilizzando la “sveglia” della figura seguente che associa alle tacche i valori definiti nell’intervallo fissato. Per comodità di disegno si è ristretto l’intervallo di definizione a [-7, 8]. La sveglia può essere assimilata ad una calcolatrice che fa somme e sottrazioni: basta posizionare la lancetta sul valore del primo operando, spostarla di un numero di posizioni uguale al secondo operando in senso orario per L’informazione e le sue rappresentazioni 9 la somma e in senso antiorario per la sottrazione, quindi leggere come risultato la sua posizione finale. Si nota che, muovendo le lancette in senso orario, al valore più grande fa seguito quello più piccolo; viceversa quello più piccolo è preceduto da quello più grande. Se a 6 si somma 8 si ottiene come risultato -2 e non 14. Figura 2 – Calcolatrice per un sistema di numerazione finita Il sistema binario ha una importanza capitale in informatica in quanto consente di rappresentare numeri mediante la combinazione di due soli simboli, ovvero di codificare i numeri direttamente in bit, secondo la notazione interna dei circuiti numerici. Inoltre all’interno dei calcolatori viene adottata un’algebra dei numeri a precisione finita con un intervallo di definizione che dipende dal numero di byte associato alla rappresentazione. Poiché le considerazioni che seguono non dipendono da tale intervallo, si adotterà una rappresentazione dei numeri che adotta un solo byte, solo per semplicità degli esempi. La tabella seguente riporta alcuni numeri rappresentati in binario. 1 0 1 0 Peso 7 0 0 1 0 Peso 6 1 0 1 0 Peso 5 0 0 1 0 Peso 4 0 0 1 0 Peso 3 1 0 1 0 Peso 2 0 0 1 0 Peso 1 1 1 1 0 Peso 0 165 1 255 0 Tabella 8 – Numeri in binario Nel byte il bit più a destra è quello meno significativo (posizione o peso 0, detto anche LSB da Least Significant Bit) mentre quello più a sinistra è quello più significativo (posizione o peso 7, detto anche MSB da Most Significant Bit). Poiché un byte può rappresentare 28 valori diversi, si possono, ad esempio con 8 bit gestire i seguenti intervalli di numeri interi: - [0, 255] (in binario [00000000,11111111]) - [-127, 128] (in binario [11111111,01111111]) 10 Capitolo primo entrambi costituiti da 256 numeri. Un sistema di numerazione può essere visto come un insieme di simboli (cifre) e regole che assegnano ad ogni sequenza di cifre uno ed un solo valore numerico. I sistemi di numerazione vengono di solito classificati in sistemi posizionali e non posizionali. Nei primi (un esempio è il sistema decimale) ogni cifra della sequenza ha un’importanza variabile a seconda della relativa posizione (nel sistema decimale la prima cifra a destra indica l’unità, la seconda le centinaia, etc…), nei secondi (un esempio è dato dal sistema romano), di contro, ogni cifra esprime una quantità non dipendente dalla posizione (nel sistema romano il simbolo “L” esprime la quantità 50 indipendentemente dalla posizione). È possibile osservare che una data stringa di bit può essere interpretata come una qualsiasi sequenza di cifre in un sistema di numerazione posizionale che associa alle cifre c un diverso peso in base alla posizione i occupata nella stringa che compone il numero, dove il peso dipende dalla base b di numerazione: ci × bi + ci - 1 × bi - 1 + ci - 2 × bi - 2 + ………+ c2 × b2 + c1 × b1 + c0 × b0 + c-1 × b-1 + c-2 × b-2 + …… Nel caso dei numeri interi scompaiono le potenze negative della base e la formula diventa: ci × bi + ci - 1 × bi - 1 + ci - 2 × bi - 2 + ………+ c2 × b2 + c1 × b1 + c0 × b0 Un sistema di numerazione posizionale è quindi definito dalla base (o radice) utilizzata per la rappresentazione. In un sistema posizionale in base b servono b simboli per rappresentare i diversi valori delle cifre compresi tra 0 e (b-1). Base 10 2 8 16 Denominazione Decimale Binaria Ottale Esadecimale Valori delle cifre 0123456789 01 01234567 0123456789ABCDEF Tabella 9 – Sistemi posizionali Poiché basi diverse fanno uso delle stesse prime cifre, si rende necessario distinguere le rappresentazioni dei numeri con un pedice indicante la base dopo aver racchiuso la stringa tra parentesi. (101111)2 = (142)5 = (47)10 Inoltre, poiché nel sistema decimale la prima cifra a destra indica le unità, la seconda indica le decine, la terza le centinaia, la quarta le migliaia, e così di seguito secondo le potenze del dieci, solo in esso è possibile leggere i numeri come tremilacentouno, unmilioneetrenta. Negli altri sistemi di numerazione devono essere scandite le cifre, da quella di peso maggiore fino a quella di minor peso, con indicazione della base (ad esempio “unoquattrodue” in base cinque). In tutte le L’informazione e le sue rappresentazioni 11 basi gli zeri a sinistra possono essere omessi, così come quelli a destra se il numero è dotato di virgola. Nel passaggio da una base all’altra alcune proprietà dei numeri si perdono: ad esempio un risultato di una divisione può essere periodico nella base dieci ma non è detto che lo sia in un’altra base, così come la proprietà di un numero di essere divisibile per cinque ha senso solo se la base è maggiore di cinque. La introdotta interpretazione del numero secondo il sistema di numerazione posizionale pesato consente di convertire nella base 10 il valore rappresentato in una qualsiasi base b, calcolando la sommatoria dei prodotti delle cifre per i pesi: Valore l 1 ci b i (1) i 0 Ad esempio: (101111)2 = 1 × 25+ 0 × 24+ 1 × 23+ 1 × 22+ 1 × 21+ 1 × 20= 32 + 8 + 4 + 2 +1 = (142)5 = 1 × 52 + 4 × 51 + 2 × 50 = 25 +20 +2 = (47)10 = 4 × 101 + 7 × 100 L'impiego nella base 2 di un minor numero simboli rispetto al sistema decimale (2 contro 10) implica che lo stesso numero abbia una parola-codice più lunga in notazione binaria che non in quella decimale. Poiché per rappresentare le dieci cifre ci vogliono log210 bit ( 3,3 bit), solitamente la stringa di cifre in bit è approssimativamente tre volte più lunga di quella decimale come l’esempio seguente mostra: (1001101)2 = 1 × 26 + 0 × 25 + 0 × 24 + 1 × 23 + 1 × 22 + 0 × 21 + 1 × 20 = = 64 + 0 + 0 + 8 + 4 + 0 + 1 = = (77)10 In informatica, per evitare di dover trattare con stringhe di bit troppo lunghe, sono stati introdotti il sistema ottale ed esadecimale. La tabella seguente mostra la corrispondenza tra le cifre usate in tali rappresentazioni e i bit che le rappresentano. Le numerazioni ottale ed esadecimale sono interessanti perché la trasformazione di rappresentazioni di valori tra esse si e la base 2 (e viceversa) è immediata. Infatti la trasformazione di un valore da binario in ottale è molto semplice dato che una cifra del sistema ottale è rappresentabile esattamente con tre bit del sistema binario il cui valore è uguale proprio alla cifra rappresentata. La conversione avviene raggruppando le cifre binarie in gruppi di tre a partire dalla posizione di peso minore. La conversione opposta è ugualmente semplice: ogni cifra ottale viene esplosa esattamente nelle tre cifre binarie che la rappresentano. 12 Capitolo primo Ottale 0 1 2 3 4 5 6 7 Binario 000 001 010 011 100 101 110 111 Esadecimale 0 1 2 3 4 5 6 7 8 9 A B C D E F Binario 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 Tabella 10 – Rappresentazione cifre nei sistemi Ottale ed Esadecimale La rappresentazione esadecimale è ancora più compatta: il processo di conversione è equivalente a quello binario-ottale ma le cifre binarie devono essere raggruppate in gruppi di quattro. Figura 3 – Conversione tra Binario ed Ottale e Binario ed Esadecimale Dagli esempi precedenti si è visto che per convertire un valore da una qualsiasi base b di numerazione al corrispondente valore nel sistema decimale, si deve calcolare la somma delle potenze della base b assegnata. Il procedimento inverso, cioè quello da applicare per convertire un valore decimale in un altro sistema di numerazione, viene mostrato per la conversione in binario ma può essere applicato per analogia con qualsiasi altra base di numerazione. Per convertire un numero decimale in binario si procede separando la parte intera da quella decimale e applicando due procedimenti (algoritmi) diversi. Dato un valore d decimale, lo si può rappresentare come: d = ci × bi + ci-1 × bi-1 + ………+ c2 × b2 + c1 × b1 + c0 × b0 + c-1 × b-1 + c-2 × b-2 + …… L’informazione e le sue rappresentazioni 13 e scomporlo in parte intera e frazionaria. Nel caso di b=2: dpi = ci × 2i + ci-1 × 2i-1 +………+ c2 × 22 + c1 × 21 + c0 × 20 dpf = c-1 × b-1 + c-2 × b-2 + …… con: d = dpi + dpf se si divide la parte intera per 2: dpi /2 = ci × 2i-1 + ci-1 × 2i-2 +………+ c2 × 21 + c1 × 20 + c0 × 2-1 si ottiene: dpi1= ci × 2i-1 + ci-1 × 2i-2 +………+ c2 × 21 + c1 × 20 con c0 resto della divisione. Se ora si divide la parte intera ottenuta precedentemente dpi1 ancora per la base 2: dpi1 /2 = ci × 2i-2 + ci-1 × 2i-3 +………+ c2 × 20 + c1 × 2-1 si ottiene: dpi2 = ci × 2i-2 + ci-1 × 2i-3 +………+ c2 × 20 con c1 resto della divisione. Si comprende allora che il procedimento deve essere ripetuto fino a quando si ottiene un quoziente uguale a 0. L’insieme dei resti delle diverse divisioni comporranno la stringa in binario del numero d, in particolare: il resto della prima divisione corrisponde alla cifra binaria del bit meno significativo, quello dell’ultima il bit più significativo come l’esempio che segue mostra. ALGORITMO 1) dividere la parte intera del numero d per la base b 2) scrivere il resto della divisione 3) se il quoziente è maggiore di zero, usare tale risultato al posto del numero d di partenza e continuare dal punto 1) 4) se il quoziente è zero, scrivere tutte le cifre ottenute come resto in sequenza inversa Esempio 1 – Algoritmo per la conversione della parte intera di un numero in base b nel corrispondente decimale 14 Capitolo primo Si noti che l’algoritmo nell’esempio 1 consente di convertire un numero intero in base dieci in una qualunque base b. Nel caso di b =2 si ottiene la conversione in binario del numero assegnato. Per la conversione della parte frazionaria si procede al contrario moltiplicando per 2: dpf × 2 = c-1 × b0 + c-2 × b-1 + …… dpf1 = c-2 × b-1 + …… per spostare c-1 a sinistra della virgola che diventa parte intera. Anche in questo si continua a moltiplicare per 2 solo la parte frazionaria fino a quando non si verifica una delle seguenti condizioni: - la parte frazionaria dpfi-esima non si annulla; - la parte frazionaria dpfi-esima si ripete con periodicità; - ci si accontenta di una rappresentazione approssimata con un numero di bit inferiore a quello che andrebbero calcolati per raggiungere una delle condizioni precedenti. Si noti che solo la prima condizione garantisce una conversione senza approssimazione. ALGORITMO 1) moltiplicare la parte frazionaria del numero d per la base b 2) scrivere la parte intera del prodotto 3) se la nuova parte frazionaria del prodotto è diversa da zero o non si ripete periodicamente, oppure si non sono state determinate le cifre binarie prefissate, usare tale risultato al posto del numero d di partenza e continuare dal punto 1) 4) se la nuova parte frazionaria verifica una delle tre condizioni di terminazione, scrivere tutte le cifre ottenute come parte intera nell’ordine in cui sono state calcolate Esempio 2 – Algoritmo per la conversione della parte frazionaria di un valore in base b nel corrispondente decimale Si noti che l’algoritmo consente di convertire un numero frazionario in base dieci in una qualunque base b. Nel caso di b =2 si ottiene la conversione in binario del numero assegnato. L’informazione e le sue rappresentazioni 15 L’adozione della notazione posizionale pesata senza altra convenzione (posizione della virgola o indicazione di un eventuale segno) consente di interpretare una sequenza di bit come rappresentazione di un numero naturale. Su tali numeri si applicano gli algoritmi di somma e sottrazione, prodotto e divisione in modo del tutto analogo ai numeri in base 10, l'unica differenza risiede nella poca pratica che si ha con essi. Come nel decimale si definiscono la tavola dell’addizione e la tabellina del prodotto per le cifre binarie. Tabella 11 – Tabelle di somma e prodotto per le cifre binarie Di seguito sono riportati alcuni esempi di operazioni: si noti che nella somma si deve tener conto del riporto (che si propaga a sinistra così come nell’aritmetica decimale) mentre nella sottrazione, in presenza dell’operazione “0 – 1”, si deve attivare il prestito dalle cifre più a sinistra (come nell’aritmetica decimale). Esempio 3 – Esempi di somma, sottrazione e prodotto fra numeri binari La rappresentazione finora presentata, che fa uso dell’equazione (1) per la codifica da binario a decimale e dell’algoritmo nell’esempio 1 per la codifica da decimale a binario, viene anche detta “in binario puro”. Essa rende possibile rappresentare tutti i numeri interi positivi appartenenti all’intervallo [0, 2l – 1] con l ad indicare i bit a disposizione. 1.2.2. La rappresentazione dei numeri relativi Per i numeri relativi, ovvero tutti i numeri interi, positivi e negativi, incluso lo zero, si utilizzano invece altre tipologie di rappresentazioni. A tale proposito, nell’evoluzione dell’aritmetica binaria, sono state definite rappresentazioni diverse 16 Capitolo primo (segno e modulo, complemento a uno, complemento a due, eccesso 2l - 1) per cercare di realizzazione circuiti elettronici capaci di effettuare le operazioni aritmetiche all’interno di un calcolatore in modo ottimizzato e allo stesso tempo semplice. Poiché il segno assume due soli valori (“+” oppure “–“), allora lo si può codificare con un singolo bit utilizzando il bit più significativo per indicarlo: ad esempio, “0” per indicare un valore positivo ed “1” per indicarne uno negativo. Con l bit, l – 1 di essi vengono attribuiti alla rappresentazione del valore assoluto del numero, e il bit più a sinistra (MSB) alla rappresentazione del segno. Figura 4 – Rappresentazione per segno e modulo La rappresentazione, detta per segno e modulo, consente di codificare tutti i numeri relativi appartenenti all’intervallo: [-2l -1 + 1, 2l -1 - 1] con 2l - 1 valori positivi e altrettanti negativi: per un totale di 2l valori diversi. Per l =8 sono rappresentabili tutti i numeri relativi appartenenti all’intervallo [127,127]. Si noti che, poiché sono presenti due configurazioni dello zero, lo “0” positivo (00000000) e lo “0” negativo (10000000), le operazioni di somma e sottrazione devono essere corrette nell’attraversamento dello zero. Ma il motivo che ha portato alla ricerca di una rappresentazione diversa per i numeri negativi, è che la rappresentazione per segno e modulo richiede un algoritmo complesso per effettuare somma e sottrazione in presenza delle diverse combinazioni dei segni degli operandi. Con la rappresentazione in complemento a due, somma e sottrazione si possono effettuare con lo stesso algoritmo, e quindi si possono affidare allo stesso circuito elettronico. In complemento a 2 le configurazioni che hanno il bit più significativo uguale a zero, cioè quelle comprese nell’intervallo [0, 2l - 1- 1], rappresentano se stesse (numeri positivi), mentre le configurazioni col bit più significativo uguale a uno, cioè quelle rientranti nell’intervallo [2l - 1,2l - 1], rappresentano i numeri negativi che si ottengono traslando a sinistra l’intervallo di 2l, cioè l’intervallo [-2l - 1,-1]. Figura 5: Rappresentazione per complemento a 2 L’informazione e le sue rappresentazioni 17 Nella rappresentazione per complemento a 2, i valori rappresentati sono compresi nell’intervallo: [-2l -1, 2l -1 - 1] sono sempre 2 l : - [0,2l -1] per i valori positivi [-2l - 1,-1] per i valori negativi. l’intervallo non è simmetrico: - 2l - 1 valore assoluto del minimo 2l -1 valore del massimo ed esiste una sola rappresentazione dello zero. Con 8 bit, ad esempio, si rappresentano i numeri naturali nell’intervallo [0, 281], cioè [0, 255], oppure i numeri relativi nell’intervallo [-27, 27-1], cioè [-128, 127]. Con 16 bit (2 byte) si rappresentano i numeri naturali nell’intervallo [0,216-1], cioè [0,65535], oppure i numeri relativi nell’intervallo [-215, 215-1], cioè [-32768, 32767]. Il complemento a due x” di un valore negativo x si calcola sottraendo il valore assoluto di x a 2l: x” = 2l – |x| Se si definisce il complemento alla base b di una cifra c come: c’ = b -1 - c allora il complemento a 2 si ottiene complementando alla base tutte le cifre del valore assoluto del numero x e sommando poi 1 al valore ottenuto, come l’esempio 4 mostra nel caso di l = 8. Si noti che il complemento alla cifra nel caso di b=10 si ottiene sottraendo la cifra c a 9, mentre nel caso di b=2 equivale alla sostituzione dello zero con l’uno e dell’uno con lo zero. Esempio 4 – Calcolo del complemento a 2 di un numero x negativo 18 Capitolo primo Viceversa, se si ha una sequenza di l bit che rappresenta un numero intero con segno, con i numeri negativi rappresentati in complemento a 2, allora, per ottenere il numero rappresentato, si procede nel seguente modo. - Si esamina il bit di segno. - Se esso è zero, il numero rappresentato è non negativo e lo si calcola con la normale conversione binario-decimale. - Se invece il bit di segno è uno, allora si tratta di un numero negativo, per cui, per ottenerne il valore assoluto, si applica lo stesso procedimento visto in precedenza complementando tutti i bit e sommando 1 al risultato. Un’altra interpretazione del complemento a 2 riporta che il bit di segno, quello più significativo nella stringa di bit, contribuisca con peso negativo alla determinazione del valore nel sistema di numerazione posizionale pesato; in altri termini, con l bit, con la prima posizione che parte da zero, si ha che il peso della cifra più significativa cl-1 è -2l - 1: cl - 1 ×(-2l -1 ) + cl - 2 ×(2l -2 ) + ………+ c1 × 21 + c0 × 20 come l’esempio che segue dimostra (sempre con l = 8). Esempio 5 – Interpretazione del complemento a 2 Il complemento a uno (x’) del numero x si differenzia dal complemento a 2 (x”) dello stesso numero per una unità: x’ = x” - 1 Dalla definizione si comprende che il complemento a 1 di un numero, detto anche complemento diminuito o complemento alla base, si ottiene semplicemente complementando tutte le cifre del numero. Il complemento a 1 è stato usato in alcuni calcolatori, ma è stato abbandonato perché alla semplicità di determinazione dei numeri negativi (nel caso di base 2 basta sostituire ad ogni uno lo zero ed ad ogni zero l’uno) accompagna una doppia rappresentazione dello zero che complica le operazioni di somma e sottrazione. Va notato che tale rappresentazione è simmetrica come quella per segno e modulo: con l bit l’intervallo rappresentato è [-(2l - 1-1), 2 l - 1-1]. Nella rappresentazione per eccesso 2l -1 i numeri negativi si determinano come somma di se stessi con 2l -1 dove l è il numero di bit utilizzati. Si noti che il sistema è identico al complemento a due con il bit di segno invertito. In pratica i numeri compresi in [-2l - 1, 2l - 1-1] sono mappati tra [0, 2 l -1]. L’informazione e le sue rappresentazioni 19 Figura 6: Rappresentazione per eccessi In tale rappresentazione, il numero binario che rappresenta 2l -1 sarà associato allo zero, mentre i valori minori di 2l - 1 ai numeri negativi e quelli maggiori a quelli positivi. Nel caso di n = 8 i numeri appartenenti a [–128, 127] sono mappati nell’intervallo [0, 255] (con i numeri da 0 a 127 considerati negativi, il valore 128 corrisponde allo 0 e quelli maggiori di 128 sono positivi). Tabella 12 – Esempi di codifica di numeri negativi nelle varie rappresentazioni Le rappresentazioni in complemento a due ed eccesso 2l -1 sono le più efficienti per svolgere operazioni in aritmetica binaria poiché permettono di trattare la sottrazione tra numeri come una somma tra numeri di segno opposto: (X - Y) = (X + (-Y)) Si noti che tale proprietà ha validità solo nel caso di rappresentazioni finite dei numeri come l’esempio 6 dimostra. È così possibile costruire dei circuiti che fanno solo addizioni. Esempio 6 – Operazioni in complementi a 2 20 1.2.3. Capitolo primo La rappresentazione dei numeri reali I numeri reali vengono rappresentati in binario attraverso la seguente notazione scientifica: con m numero frazionario detto mantissa, la base b numero naturale prefissato ed e numero intero chiamato esponente o caratteristica. L'esponente determina l’ampiezza dell'intervallo di valori preso in considerazione, mentre il numero di cifre della mantissa determina la precisione del numero (ossia con quante cifre significative sarà rappresentato). Tale notazione scientifica viene adottata per diversi motivi: - la sua indipendenza dalla posizione della virgola; - la possibilità di trascurare tutti gli zeri che precedono la prima cifra significativa con la normalizzazione della mantissa; - la possibilità di rappresentare con poche cifre numeri molto grandi oppure estremamente piccoli; - la dipendenza del valore rappresentato dalla mantissa e dall’esponente se si adottano specifiche convenzioni per la base e la mantissa. La rappresentazione in binario dei numeri reali si caratterizza rispetto alla notazione scientifica per alcune particolarità nel modo di rappresentare e utilizzare i numeri, dovute all'uso di rappresentazioni finite e definite sia per l'esponente che per la mantissa. I due fattori limitano quindi sia l'intervallo dell'insieme dei numeri reali che è possibile rappresentare, che il grado di precisione che essi avranno. Infatti in un intervallo reale comunque piccolo esistono infiniti valori (i numeri reali formano un continuo). I valori rappresentabili in binario appartengono invece ad un sottoinsieme che contiene un numero finito di valori reali ognuno dei quali rappresenta un intervallo del continuo. In altri termini, diviso l'insieme dei numeri reali in intervalli di fissata dimensione, si ha, come la figura mostra, che ogni x appartenente all'intervallo [Xi, Xi+1[ viene sostituito con Xi. Figura 7 – Finitezza della rappresentazione numeri reali L’informazione e le sue rappresentazioni 21 La sostituzione di un numero reale x con il valore X rappresentante l'intervallo a cui x appartiene, pone notevoli problemi di approssimazione in tutti i calcoli che usano valori del tipo reale. Per valutare gli effetti delle approssimazioni e gli errori che ne possono derivare, è nata la disciplina chiamata calcolo numerico, che si pone come obiettivo la ricerca di algoritmi appropriati per la soluzione di problemi matematici che fanno largo uso dei numeri reali. Difatti un qualsiasi calcolo numerico sarebbe privo di senso, qualora non si avesse un'idea del tipo e dell'entità degli errori che si possono commettere. In concreto i numeri reali rappresentabili in binario godono della seguente proprietà: x X Xi 1 Xi dove rappresenta l'errore che si commette sostituendo x con X, dove: - X = Xi se si approssima per difetto; - X = Xi+1 se si approssima per eccesso. Il valore dipende dalla rappresentazione finita (numero finito di cifre) utilizzata per i numeri reali. Ad esempio disponendo di una calcolatrice con una aritmetica a quattro cifre decimali che applica le note regole di arrotondamento sull'ultima cifra, si ha: NUMERO 0,00347 0,000348 0,00987 0,000987 ARROTONDAMENTO 0,0035 0,0003 0,0099 0,0010 ERRORE 3*10-5 = 0.3 *10-4 48*10-6 = 0.48*10-4 3*10-5 = 0.3 *10-4 13*10-6 = 0.13*10-4 Tabella 13 – Errori di arrotondamento con un errore massimo sull'ultima cifra di 0.5 (0.5 * 10-4) per le classiche regole dell'arrotondamento. In generale se -m è il peso della cifra meno significativa, l'errore massimo che si commette è: 1 10 2 m L’insieme R è anche costituito da infiniti valori (è definito dall’intervallo ], [). I numeri reali rappresentabili sono invece definiti in un insieme limitato con estremi predefiniti [-minreal, maxreal]. Si definiscono: - l’overflow come la condizione che si verifica quando i valori o sono più piccoli di minreal o più grandi di maxreal; - l’underflow come la condizione che si verifica quando un valore, per effetto delle approssimazioni, viene confuso con lo zero. Si noti che la rappresentazione in virgola mobile, fissata la base, consente di esprimere lo stesso valore con infinite coppie (mantissa, esponente), ad esempio: 48 103 è uguale a 4800 100, ma anche a 4,8 102 22 Capitolo primo È allora possibile scegliere, tra le infinite coppie quella che preserva il maggior numero di cifre significative con la normalizzazione della mantissa. Per esempio, per i numeri minori di uno quando la cifra più a sinistra è uno zero, si traslano (shift) verso sinistra le cifre diverse da zero (significative) decrementando l'esponente di tante cifre quante sono le posizioni scalate: in questo modo si ottiene un’altra coppia distinta, ma avente il medesimo valore del precedente (ad esempio 0,0025 *100 è equivalente 2,5000 * 10-3). La mantissa scalata in questo modo prende il nome di mantissa normalizzata e il numero in virgola mobile, il nome di numero normalizzato. In generale la forma normalizzata della mantissa obbliga che la sua prima cifra sia diversa da zero e che la sua parte intera sia in generale un numero minore dalla base. Ad esempio disponendo di una calcolatrice con le seguenti caratteristiche: - rappresentazione con b = 10, - cinque cifre per la mantissa considerata minore di 10, - due cifre per l'esponente, - rappresentazione normalizzata con la prima cifra diversa da zero. si hanno le seguenti rappresentazioni normalizzate: Numero 0,384 1345 64350 333 0,0048 0,0000001 Valore 3,8400 10-1 1,3450 103 6,4350 104 3,3300 102 4,8000 10-3 1,0000 10-8 Tabella 14 – Rappresentazioni Normalizzate e la condizione di overflow quando: x >9,9999 1099 x < 1,0000 10-99 e di underflow quando: Osservando la modalità di rappresentazione in virgola mobile, si può notare che gli intervalli [Xi, Xi+1] non hanno tutti la stessa ampiezza a causa della finitezza del numero di cifre della mantissa: man mano che ci si avvicina alla condizione di overflow gli intervalli si fanno sempre più ampi, mentre intorno alla condizione di underflow non solo si addensano ma diventano sempre più piccoli. Con la calcolatrice precedente è facile osservare il fenomeno confrontando gli intervalli [1.0000 10-99, 1.0001 10-99] e [9.9998 1099, 9.9999 1099]. Con la rappresentazione in virgola mobile le operazioni non solo si complicano ma possono generare errori di approssimazione. Ad esempio la somma e la sottrazione richiedono l'allineamento degli esponenti: 100 100 + 100 10-2 = 100 100 + 1 100 = 101 100 L’informazione e le sue rappresentazioni 23 mentre per il prodotto e la divisione servono operazioni separate sulle mantisse e sugli esponenti: 100 100 * 100 10-2 = (100 * 100) 10(0-2) = 10000 10-2 L'allineamento degli esponenti produce come effetto indesiderato quello di far scomparire alcune cifre rappresentative del numero. Ad esempio la somma dei numeri seguenti: 1,9099 101 + 5,9009 104 con la calcolatrice considerata prima, diventa: 0,0001 104 + 5,9009 104 con il troncamento delle cifre 9099 del numero con esponente più piccolo. Le operazioni che richiedono maggiore attenzione sono l'addizione e la sottrazione. Infatti, la causa principale degli errori di calcolo numerico risiede nella sottrazione di numeri di valore quasi uguale; in tal caso le cifre più significative si eliminano fra loro e la differenza risultante perde un certo numero di cifre significative o anche tutte (fenomeno detto cancellazione). Altra causa di errori è la divisione per valori molto piccoli, poichè il risultato può facilmente superare il valore di overflow: deve essere pertanto evitata non solo la divisione per lo zero ma anche per valori ad esso prossimi. Il grande vantaggio della rappresentazione in virgola mobile è che, se si conviene che le mantisse siano trasformate in valori minori di 10 con operazioni interne, un numero reale può essere rappresentato nella memoria di un calcolatore con un numero intero indicativo della parte decimale della mantissa e con un altro numero intero per l'esponente. Per esempio il numero 0,1230 10-9 viene rappresentato con la coppia di numeri interi (1230,-9) e gestito con operazioni interne che ne costruiscono l'effettivo valore. Nel caso binario la rappresentazione in virgola mobile normalizzata assume la forma: Nel 1980, i principali costruttori di elaboratori elettronici, producevano calcolatori che utilizzavano i numeri float, ognuno con un proprio formato numerico e con proprie convenzioni di calcolo. Nel 1985 divenne operativo lo Standard 754 IEEE per i numeri in virgola mobile, e i maggiori costruttori 24 Capitolo primo adottarono questo standard per i loro processori matematici. Lo standard 754 definisce principalmente tre formati numerici a virgola mobile: - singola precisione o precisione semplice (32 bit), - doppia precisione 64 bit), - precisione estesa (80 bit). Consideriamo i formati a 32 e 64 bit, che utilizzano la base 2 e la notazione in eccesso per l’esponente. Figura 8 – Formati a singola e doppia precisione Nella stringa di bit, si succedono nell’ordine da sinistra (MSB) a destra (LSB): - un bit per il segno del numero complessivo, (zero per positivo ed uno per negativo); - otto bit nel caso della singola precisione (11 per la doppia precisione) per l'esponente rappresentato per eccesso così da non doverne indicare il segno; - 23 bit nel caso della singola precisione (52 per la doppia) per la mantissa. La mantissa è normalizzata per cui comincia sempre con un 1 seguito da una virgola binaria, e poi a seguire il resto delle cifre. Lo standard prevede l’assenza sia del primo bit che del bit della virgola perché sono sempre presenti: l’insieme dell’uno implicito, della virgola implicita e delle cifre esplicite prende il nome di significante. Argomento Bit del segno Bit per l’esponente Bit per la mantissa Cifre decimali mantissa Esponente (rappresentazione) Esponente (valori) Precisione singola 32 Bit 1 8 23 Circa 7 (23/3.3) Base 2 ad eccesso 127 [-126, 127] Precisione doppia 64 bit 1 11 52 Circa 15 (52/3.3) base 2 ad eccesso 1023 [-1022, 1023] Tabella 15 – Caratteristiche formati singola e doppia precisione L’informazione e le sue rappresentazioni 25 L’esempio di seguito riepiloga alcuni dei procedimenti illustrati per la rappresentazione di numeri da binario a decimale e viceversa. Problema Si vuole convertire il numero 16 in binario utilizzando una rappresentazione binaria pura su 8 bit. Si vuole convertire il numero -12 in binario utilizzando una rappresentazione per complementi a 2 su 5 bit Si vuole convertire il numero -10 in binario utilizzando una rappresentazione per complementi a 2 su 4 bit. Soluzione Poiché la rappresentazione binaria pura consente la codifica di numeri interi postivi allora 16 è codificabile attraverso questa rappresentazione. Utilizzando 8 bit l’intervallo di rappresentazione è l’insieme dei numeri interi postivi compresi in [0,281], ovvero in [0,255]. Poiché 16 è racchiuso in tale intervallo è possibile passare alla sua codifica. Utilizzando l’algoritmo dell’esempio (1) si ha: 16:2 -> q1=8, r1=0 8:2 -> q2=4, r2=0 4:2 -> q3=4, r3=0 2:2 -> q4=1, r4=0 1:2 -> q5=0, r5=1 Considerando la successione dei resti invertita [r5,r4,…,r1] e inserendo gli zeri a sinistra del bit più siginificativo, si ha che il numero in binario corrispondente è: 00010000. Poiché la rappresentazione per complementi a 2 consente la codifica di numeri relativi allora -12 è codificabile attraverso questa rappresentazione. Utilizzando 5 bit l’intervallo di rappresentazione è l’insieme dei numeri interi compresi in [-25-1,25-1-1], ovvero in [-16,15]. Poiché -12 è racchiuso in tale intervallo è possibile passare alla sua codifica. Poiché -12 è negativo per ottenere il corrispettivo in binario si codifica il suo modulo su 5 bit, si complimentano le cifre ottenute e si aggiunge 1. Utilizzando l’algoritmo dell’esempio (1) si ha: 12:2 -> q1=6, r1=0 6:2 -> q2=3, r2=0 3:2 -> q3=1, r3=1 1:2 -> q4=0, r4=1 Considerando la successione dei resti invertita [r4,…,r1] e inserendo gli zeri a sinistra del bit più siginificativo, si ha che il numero in binario corrispondente è 01100. Complementando le cifre ottenute si ha 10011, infine aggiungendo 1 al numero complementato si ha: 10100 Poiché la rappresentazione per complementi a 2 consente la codifica di numeri relativi allora -10 è codificabile attraverso questa rappresentazione.. Utilizzando 4 bit l’intervallo di rappresentazione è l’insieme dei numeri interi compresi in [-24-1,24-1-1], 26 Capitolo primo Si vuole convertire il numero 111000 da complementi a 2 in decimale. Si vuole codificare il numero reale 8.25 utilizzando la rappresentazione in virgola mobile nel formato a singola precisione. ovvero in [-8,7]. Poiché -10 non è racchiuso in tale intervallo la sua codifica non è possibile. Innanzitutto si esamina il primo bit. Poiché esso è pari ad 1, il numero è negativo e quindi, per determinarne il modulo, si complementano ad 1 tutti i bit e si somma 1 al risultato. Il complemento è 000111, mentre sommando a tale numero 1, si ottiene 001000, il cui corrispettivo in decimale è 0*25 + 0*24 + 1*23+ 0*22 + 0*22 + 0*20 = 8. Il numero decimale corrispondente è quindi -8. Lo stesso risultato poteva essere ottenuto, in via alternativa, aggiungendo a -2l-1, ovvero a -25 = -32, la codifica in decimale di 11000, ovvero 1*24 + 1*23+ 0*22 + 0*22 + 0*20 = 24. Infatti -32+24=-8. Innanzitutto si codificano in binario la parte intera e frazionaria della mantissa e l’esponente (utilizzando la rappresentazione per eccessi a 2l-1) ottenendo 0 111.01 * 200000000. Normalizzando la mantissa, si ottiene: 0 1.1101 * 210000010 Esempio 7 – Alcuni esempi di conversione 1.3. Gli operatori booleani Sulle stringhe di bit sono anche definiti operatori che lavorano bit a bit (bitwise operator). Essi sono detti booleani e sono: - AND: dati due bit restituisce il valore 1 se e solo se i bit erano entrambi posti a 1, in tutti gli altri casi il risultato è 0; l’AND è detto anche prodotto logico. - OR: dati due bit restituisce il valore 0 se e solo se i bit erano entrambi posti a 0, in tutti gli altri casi il risultato è 1; l’OR è anche detto somma logica. - NOT: dato un bit restituisce il valore 0 se esso era posto a 1, restituisce invece 1 se il bit era posto a 0; il NOT viene anche detto operatore di negazione o di complementazione. La figura che segue riporta la definizione completa dei tre operatori: la tabella viene anche detta tavola di verità. L’informazione e le sue rappresentazioni 27 Tabella 16 – AND, OR, NOT Si noti che la somma logica di 1 OR 1 fa 1 e non 0 con riporto 1 come nel caso della somma aritmetica. Di seguito si riportano alcuni esempi di applicazione dei tre operatori a dei byte. Tabella 17 – Esempi di AND, OR, NOT 1.4. La convergenza digitale Negli ultimi anni tantissimi settori produttivi hanno migrato i loro sistemi realizzati con tecnologie, anche molto diverse tra loro, in sistemi capaci di trattare l’informazione in modo digitale. Lo sviluppo della tecnologia dei sistemi di comunicazione e di gestione delle informazioni digitali nei campi dell’editoria, della televisione, del cinema ha comportato non solo benefici economici notevoli, ma soprattutto la capacità di integrare e di elaborare informazioni provenienti da fonti diverse. E l’integrazione digitale è tanto più significativa quanto più consente di far condividere a soggetti differenti e distanti il proprio patrimonio informativo. Il fenomeno, detto convergenza digitale, si caratterizza per la concomitanza di diversi fattori: 28 Capitolo primo - la convergenza della codifica in quanto l’informazione in una sua qualsiasi forma viene rappresentata con un solo alfabeto di base: il codice binario; - la convergenza tecnologica perché è uno stesso strumento, il calcolatore elettronico, che elabora e trasmette informazioni differenti; - la convergenza del mercato che vede applicazioni diverse dotate di elementi comuni attraverso cui integrarsi. Oggi viviamo in un mondo caratterizzato da una straordinaria ricchezza di sorgenti di informazione (suoni, voci, rumori, musiche, immagini, fotografie, filmati) e differenti canali di comunicazione (radio, televisione, satellite, rete Internet, CD, DVD) e ritroviamo nel mondo digitale la stessa varietà di forme comunicative. La trasformazione della realtà analogica in forma digitale offre quindi immense e nuove opportunità: la più evidente è che ogni informazione può essere scambiata in processi di comunicazione. Secondo Marshall McLuhan, che negli anni ’60 diede forza agli studi sulla teoria della comunicazione, i concetti emergenti che nascono dall’analisi delle tecnologie digitali sono la globalità della comunicazione (il cosiddetto villaggio globale) e i medium. Sempre secondo McLuhan: - “La nuova interdipendenza elettronica ricrea il mondo come immagine di un villaggio globale” - “I media sono estensioni dell’uomo: tecnologie e prodotti che danno ai nostri sensi nuove possibilità di ricevere informazioni” Il termine medium deriva dal latino e significa mezzo: va pertanto inteso come mezzo di comunicazione. Con multimedialità si indica invece la combinazione di diversi codici espressivi (testo, audio, immagini, video) per realizzare un unico oggetto comunicativo che, grazie alle tecnologie digitali, si rappresenta con il medesimo linguaggio basato su stringhe di bit. La forza della multimedialità risiede nel convincimento che l’integrazione di diverse forme espressive migliori la comunicazione. Ma lo studio della comunicazione non può prescindere dallo studio delle tecnologie dei mezzi di comunicazione in quanto questi ultimi non sono neutri, ossia possono: - condizionare le modalità di rappresentazione delle informazioni; - alterare la percezione del messaggio, - spezzare la simmetria di comunicazione coinvolgendo diversi destinatari passivi (ad esempio radio, televisione), - alterare le relazioni temporali tra gli interlocutori (ad esempio i messaggi registrati). La trasformazione di testo, audio, immagini, video in oggetti digitali può avvenire in modo diretto o attraverso dispositivi capaci di effettuare la conversione che vengono detti ADC (Analogue to Digital converter). Nel primo caso esistono applicazioni informatiche (elaborazione di testi) o macchine elettroniche (macchine fotografiche o cineprese digitali) che forniscono direttamente le informazioni in binario. I dispositivi del secondo tipo, quali ad esempio gli scanner, servono al recupero in digitale di informazioni rappresentate nei loro formati tradizionali. In entrambi i casi la trasformazione dell’informazione analogica in digitale avviene con due processi distinti e disposti l’uno dopo l’altro: L’informazione e le sue rappresentazioni 29 - il campionamento e - la quantizzazione. Il campionamento è il processo che provvede a selezionare il sottoinsieme discreto di informazioni da rappresentare in digitale da quello più ampio offerto dalla realtà. È il partizionamento di un flusso continuo di informazione in quantità discrete, rispetto al tempo, allo spazio o ad entrambi. La scelta dei campioni viene effettuata in modo che l’informazione rappresentata sia utilizzabile. Ad esempio non ha molta importanza nel caso delle comunicazioni telefoniche, selezionare un numero di campioni molto elevato nell’unità di tempo della voce, perché l’orecchio umano non coglierebbe affatto le differenze. Figura 9 – Campionamento di un segnale con periodi differenti Nel caso di grandezze dipendenti dal tempo la frequenza di campionamento è il numero di campioni prelevati in un secondo (unità di tempo). La frequenza si misura in hertz (1 campione in un secondo) ed è l’inverso dell’intervallo di tempo che intercorre tra l’acquisizione di un campione e il suo successivo, che viene detto periodo di campionamento. La quantizzazione è invece il processo che misura le caratteristiche (ad esempio grandezza, intensità, colore) dei campioni selezionati attribuendo loro un valore numerico. Di norma, i campioni selezionati vengono codificati in binario 30 Capitolo primo sulla base dell’appartenenza a dati intervalli di valori, detti intervalli di quantizzazione. Figura 10 – Quantizzazione di un segnale Sia campionamento che quantizzazione introducono un’approssimazione della realtà: il primo processo seleziona un sottoinsieme discreto di un insieme di valori solitamente infinito, il secondo misura le caratteristiche scelte con un numero di cifre prefissato. Numero di campioni e numero di bit determinano anche la dimensione della rappresentazione della grandezza reale: ad esempio nella figura la grandezza G necessita in un caso di 88 bit, nell’altro di soli 66 bit. In tutti i processi di digitalizzazione si deve pertanto raggiungere un compromesso tra qualità e dimensione della rappresentazione: più sono i campioni e i bit utilizzati, maggiore è la fedeltà della rappresentazione e quindi anche la sua qualità. Ma quanto più si privilegia la qualità della rappresentazione, tanto più si rallentano le elaborazioni e lo scambio delle informazioni rappresentate. Per risolvere i problemi connessi con le dimensioni elevate sono stati introdotti processi di compressione che riducono lo spazio occupato mediante o la diminuzione del numero di bit necessari per codificare una singola informazione (compressione entropica) o la diminuzione del numero di informazioni da memorizzare o trasmettere (compressione differenziale, compressione semantica). La compressione può conservare integralmente o no il contenuto della rappresentazione originale secondo due tecniche principali: - la compressione senza perdita di informazione (lossless, reversibile) che sfrutta le ridondanze nella codifica del dato; L’informazione e le sue rappresentazioni 31 - la compressione con perdita di informazione (lossy, irreversibile) che invece sfrutta le ridondanze nella percezione dell’informazione. La compressione lossless avviene tramite una classe di algoritmi che consentono di ricostruire tutta l’informazione iniziale partendo da quella compressa. Non sempre si ottengono riduzioni significative. I metodi lossy comportano riduzioni notevoli delle dimensioni, ma la ricostruzione dell’informazione da quella compressa non è però identica a quella iniziale. Tali metodi rimuovono parti che possono non essere percepite come avviene nel caso di immagini, video e suoni. Ad esempio gli algoritmi di compressione usati nei formati GIF e JPEG per immagini fisse sfruttano la caratteristica dell’occhio umano di essere poco sensibile a lievi cambiamenti di colore in punti contigui, e quindi eliminano questi lievi cambiamenti appiattendo il colore dell’immagine. Tra le tecniche di compressione lossless si ricordano: - la Run-length encoding (RLE) che codifica sequenze di valori uguali premettendo un indicatore di ripetizioni al valore codificato; - la codifica di Huffman che assegna un numero inferiore di bit alle sequenze più probabili attraverso un vettore di codifica; - la compressione Lempel-Ziv-Welch (LZW) che costruisce dinamicamente una tabella di codifica con numero variabile di bit sulla base delle sequenze incontrate; - la codifica differenziale in cui ogni dato è rappresentato come differenza rispetto al dato precedente. - Tra le tecniche di compressione lossy si ricordano: - la compressione JPEG per le immagini che applica una trasformata nel dominio delle frequenze (Discrete Cosine Transform) che permette di sopprimere dettagli irrilevanti riducendo il numero di bit necessari per la codifica; - la compressione MPEG per i video che codifica parte dei frame come differenze rispetto ai valori previsti in base ad una interpolazione; - la compressione MP3 per l’audio che si basa alle proprietà psicoacustiche dell’udito umano per sopprimere le informazioni inutili. 1.4.1. La codifica delle informazioni testuali Il testo è uno degli oggetti digitali più diffuso nel mondo informatico. Molte sono le applicazioni che generano e manipolano i cosiddetti documenti elettronici. Un testo digitale è una stringa di simboli ad ognuno dei quali viene associato un codice binario secondo un prefissato standard. Figura 11 - Esempio di codifica di un testo Alla fine degli anni sessanta l'ente americano di standardizzazione ANSI (American National Standards Institute) decise di fissare un alfabeto che 32 Capitolo primo consentisse a tutti i calcolatori, anche di produttori diversi, di poter comunicare tra loro o con i dispositivi ad essi collegati. I simboli dell’alfabeto vennero elencati in una tabella per codificare, attraverso la posizione assunta da loro in essa, vari tipi di caratteri: alfabetici, numerici, di punteggiatura, simbolici, e anche alcuni codici da usare come controllo della comunicazione tra una macchina e l'altra (per esempio, per segnalare l’inizio o la fine di una trasmissione). Il trasferimento di un insieme di informazioni da un calcolatore all'altro su una rete poteva così essere effettuato attraverso un linguaggio comune, costituito da tale forma di codifica. La tabella fu chiamata ASCII, ossia American Standard Code for Information Interchange. Le prime 32 posizioni (da 0 a 31) sono occupate da codici di controllo. La posizione o codice 32 si riferisce allo spazio, dopo di esso seguono tutti caratteri visualizzabili. Per esempio, l'alfabeto (inglese) maiuscolo occupa le posizioni da 65 a 90, e quello minuscolo le posizioni da 97 a 122. Le cifre occupano le posizioni da 48 a 57. In totale ci sono 128 codici, da 0 a 127, e quindi sono sufficienti 7 bit per la codifica della tabella ASCII. La tabella è stata definita per rendere comoda l'elaborazione dei caratteri: per esempio, i codici di controllo hanno i due bit a sinistra uguali a zero, e quindi basta guardare tali due bit per capire se un carattere è di controllo o stampabile. Un'altra caratteristica è quella di aver disposto le cifre in sequenza, da 0 a 9, così che, se si sottrae il codice di '0' (che è 48) dal codice di qualunque altra cifra, si ottiene il valore numerico di tale cifra. Anche l'alfabeto è elencato in successione, così che sottraendo dal codice di qualunque lettera il codice della prima lettera dell'alfabeto (cioè 'A' per le maiuscole, codice 65, oppure 'a' per le minuscole, codice 97) si ottiene la posizione della lettera nell'alfabeto. Inoltre la posizione relativa delle lettere maiuscole e minuscole è fissa, e per la precisione ogni minuscola dista dalla corrispondente maiuscola esattamente 32 posizioni. Quindi passare da maiuscolo a minuscolo è molto semplice: basta sommare 32 al codice, e sottrarre 32 per la conversione inversa. Un testo è così rappresentato dalla sequenza di byte associati ai caratteri che lo compongono, nell'ordine in cui essi compaiono. Un testo di 1000 caratteri richiede 1000 byte (1 Kb) per essere rappresentato. La proposta iniziale dello standard ASCII ha come limite quello di essere stato pensato per la sola lingua inglese americana in quanto mancano tutte le lettere accentate in uso nei paesi europei. Per superare la localizzazione, cioé la dipendenza da una determinata area geografica e culturale, sono state proposte varianti della tabella ASCII. Lo standard ISO 8859 definisce un ASCII che usa 8 bit per estendere la tabella con ulteriori 128 caratteri. Le tabelle ASCII estese, quindi coincidono con il codice americano nei primi 127 caratteri. Ogni nazionalità, poi, può localizzare l'insieme dei caratteri disponibili aggiungendo quelli necessari alle proprie esigenze nel nuovo spazio disponibile, quello dei codici da 128 a 255. Esistono così diverse varianti dell'ISO-8859: la versione ISO-8859-1 è usata in Europa ed è detta anche ISO-latin-1, la sua configurazione col simbolo dell'euro è la ISO-885915. L’informazione e le sue rappresentazioni 33 Tabella 18 – Codice ASCII esteso Con la diffusione di Internet a livello mondiale, anche i codici ASCII estesi hanno però mostrato la loro inadeguatezza. Basta pensare alle lingue che usano ideogrammi anziché alfabeti per rendersi conto che un insieme di soli 127 simboli è largamente insufficiente. Uno standard che si propone di affrontare il problema del multilinguismo è Unicode (Universal Encoding). Unicode è un sistema di codifica che assegna un numero univoco ad ogni simbolo in maniera indipendente dal programma, dalla piattaforma e dalla lingua (e relativo alfabeto): il suo scopo è quello di creare una codifica delle scritture a livello universale. Unicode si basa sulla codifica ASCII, ma va oltre la limitazione dell'alfabeto latino potendo codificare caratteri scritti in tutte le lingue del mondo. Originariamente si basava su una codifica a 16 bit che dava la possibilità di codificare più di 65 mila caratteri. Per rappresentare qualunque carattere, compresi quelli cinesi e tutte le loro varianti, è stato proposto lo standard Unicode (ISO-10646) che utilizza l'UTF (Unicode Transformation Format) che supporta tre forme di codifica per rappresentare un repertorio comune di caratteri che può essere esteso fino a rappresentarne circa un milione. 34 Capitolo primo I formati UTF possono essere a 8, 16 e 32 bit. L'UTF-8 si basa su parole di 8 bit (1 byte) per la codifica dei caratteri; ed usa da 1 a 4 byte per carattere: i primi 128 valori, che iniziano col bit 0, utilizzano 1 byte per carattere e corrispondono all'ASCII, i successivi 1920 (dal 128 al 2047) utilizzano 2 bytes per codificare greco, cirillico, copto, armeno, ebraico, arabo. Per gli altri caratteri si usano 3 o 4 bytes. UTF-16 utilizza parole di 16 bit per codificare i caratteri, viene utilizzato da Java, ed è la rappresentazione interna usata da Windows e Mac OS-X. Una interessante conseguenza della gestione dei testi digitali è la possibilità di costruirne una strutturazione diversa da quella sequenziale dei documenti, alla quale i libri hanno abituato i lettori. La suddetta possibilità ha portato alla nascita dei cosiddetti ipertesti. Un ipertesto è un documento strutturato in cui un insieme di frammenti di testo detti nodi vengono collegati per mezzo di riferimenti detti link. In un ipertesto la fruizione del documento consiste nel muoversi da un nodo all’altro seguendo i collegamenti. L’idea formalizzata di ipertesto nasce nella metà del secolo scorso, la maturazione tecnologica avviene solo negli anni ’80. L’approccio ipertestuale è l’evoluzione di un approccio non sequenziale alla lettura dei documenti già utilizzato nella realizzazione di quotidiani, enciclopedie, libri con annotazioni e riferimenti, manuali, cataloghi. La digitalizzazione dei testi ne ha consentito una pervasiva applicazione come testimoniato dal mondo di Internet. Il testo digitale ha, tra i tanti vantaggi, quello di poter essere facilmente trasmesso in reti di calcolatori per essere mostrato sugli schermi o stampato su fogli di carta. Per garantire la presentazione elettronica o cartacea vengono aggiunte informazioni che fissano caratteristiche di formato quali il tipo e la dimensione del carattere, lo spazio tra una riga e l’altra, il formato della pagina. La tecnica più utilizzata per la codifica consiste nell'inserire, assieme al testo, informazioni speciali di formattazione che vengono interpretate in fase di presentazione finale. Il linguaggio HTML è l'esempio più diffuso per la codifica di testo formattato finalizzato ad una consultazione in rete. Il codice HTML è testuale e intuitivo. Infatti le specifiche di formato vengono racchiuse tra parentesi che prendono il nome di TAG. I TAG si distinguono dal resto del testo perché sono inclusi tra parentesi angolate: con <nometag> si apre una indicazione di formato e con </nometag> la si chiude. Ad esempio: <font color=”red”> che bello </font> prescrive la visualizzazione di “che bello” in rosso, mentre: <font size=”+1”>HTML</font> consente di scrivere HTML con caratteri di dimensioni più grandi. Il DOC è invece il formato più diffuso dei documenti formattati a causa dell’ampia diffusione dell’editor che lo gestisce (MicroSoft Word). Il Postscript è un altro linguaggio per la codifica di testo formattato finalizzato ad una presentazione cartacea. Una sua evoluzione è il PDF, sviluppato nel 1992 da Adobe Systems, che è stato pensato sia per la riproduzione cartacea che elettronica. Con tale formato i documenti consultati a video hanno lo stesso identico aspetto del documento stampato. L’informazione e le sue rappresentazioni 1.4.2. 35 La codifica delle immagini Le immagini sono informazioni continue in tre dimensioni, due spaziali ed una colorimetrica, e per codificarle occorre operare tre discretizzazioni. Le due discretizzazioni spaziali riducono l’immagine ad una matrice di punti detti pixel (da picture element) mentre la terza limita l’insieme di colori che ogni pixel può assumere ad un sottoinsieme definito. L’immagine digitale è quindi una matrice bidimensionale di numeri, ciascuno dei quali è la misura di una proprietà fisica (colore) di un’area elementare della scena rappresentata. Una immagine digitale può essere generata: - mediante acquisizione da immagini analogiche (ad esempio le fotografie, le diapositive) con dispositivi detti scanner; - da scene del mondo reale catturate con camere digitali; - da applicazioni di grafica. Le immagini digitali riproducono la scena dividendola in una griglia fatta di aree di cui viene misurata la luminosità o il colore. Ad ogni area viene fatto corrispondere un pixel la cui forma si discosta dalla superficie ripresa. Infatti per le caratteritiche della maggioranza dei dispositivi elettronici di acquisizione e visualizzazione delle immagini i pixel hanno la forma di un ellisse con l’asse verticale più lungo rispetto a quello orizzontale. Il rapporto tra i due assi viene detto rapporto di aspetto e serve, nelle applicazioni grafiche, per correggere eventuali deformazioni dovute a rappresentazioni diverse di uno stesso segmento nelle due direzioni ortogonali. Figura 12 - Immagine come griglia di pixel Il processo di campionamento applicato ad una immagine consiste quindi nel far corrispondere ad ogni pixel una porzione dell’immagine reale. Più la dimensione dei pixel è piccola, minore è l’approssimazione tra immagine digitale e quella reale. Con il termine di risoluzione si indica il numero di pixel per pollice (dpi - dot per inch) perché le dimensioni di un’immagine (larghezza e altezza) sono misurate in pollici. La dimensione dell’immagine viene anche espressa indicando separatamente il numero di pixel orizzontali e il numero di pixel verticali, ad esempio 600 x 800 pixel. Solitamente la risoluzione orizzontale è uguale a quella verticale. Ad ogni pixel viene poi assegnato un indirizzo che ne determina le coordinate verticali ed orizzontali (bit mapping). Il concetto di risoluzione è legato a quanto sono fitti i punti che visualizzano l’immagine. Maggiore è la risoluzione dell’immagine, maggiore è la possibilità di 36 Capitolo primo distinguere dettagli in essa presenti. Tutti i pixel contenuti in una immagine digitale hanno dimensioni identiche. La loro dimensione è determinata dalla risoluzione alla quale l’immagine viene digitalizzata: ad esempio la risoluzione di 600 dpi indica che ciascun pixel misura 1/600 di pollice. Ad ogni pixel dell’immagine vengono associati l bit che misurano caratteristiche di colore. Ma con l bit solo 2l sono le sfumature di colore rappresentate degli infiniti valori della realtà, e tutte le sfumature intermedie sono approssimate con il valore di luminosità più prossimo fra quelli codificati. Poiché la porzione di immagine associata ad un unico pixel ha una luminosità uniforme, senza che dettagli in detta porzione possano essere distinti, minore è il numero dei livelli di quantizzazione, minore è la qualità dell’immagine. La profondità di colore è la misura della capacità di rappresentare o distinguere varie sfumature di colore. Un’immagine rappresentata con una profondità di colore di 6 bit, riesce a distinguere tra 64 livelli di colore, e all’aumentare del numero di bit, aumenta il livello di dettaglio. Se l’immagine è in bianco e nero, basta associare un 1 ai pixel neri, e uno 0 a quelli bianchi. Per immagini a livelli di grigio si usano 4 o 8 bit mentre per quelle a colori 8, 24, 32 bit. Figura 13 - Immagini a diverse profondità di colore Si parla di colori veri quando ad un pixel corrispondono 24 bit per un totale di 16.7Mega colori diversi; con 48 bit viene oggi gestita l’alta definizione. La rappresentazione accurata di una immagine dipende dal numero di pixel e dalla profondità di colore. Una elevata qualità comporta una elevata quantità di informazione data proprio dal prodotto: numero pixel numero bit Per fare alcuni semplici esempi si passa dai 440KByte di una immagine televisiva con 256 colori (1 byte) per una risoluzione di 720x625 pixel, ai 440MByte di una foto con 16 milioni di colori (3 byte) per la risoluzione di ben 15.000x10.000 pixel. La misura del colore è oggetto di studio della colorimetria. Due sono i metodi usati per formare il colore: la sintesi del colore di tipo additiva e quella di tipo sottrattiva. L’informazione e le sue rappresentazioni 37 Figura 14 - Sintesi del colore Nel primo caso un colore può essere ottenuto attraverso la miscelazione di gradazioni dei tre colori primari: il rosso, il verde e il blu. Per i colori su cui si basa viene detto modello RGB (da red, green, blue). Unendo il rosso e il verde si ottengono i colori dal giallo all’arancio, unendo il rosso e il blu si ottengono dal porpora al viola. Se i colori primari sono sommati alla loro massima potenza producono il bianco per questo motivo il modello è di tipo additivo. Il secondo modello è detto invece CMY perché usa i colori ciano (Cyan), magenta, giallo (Yellow). Esso si usa soprattutto nei processi di stampa su carta perché si basa sulla capacità propria dell’inchiostro di assorbire la luce. Quando la luce bianca colpisce gli inchiostri, alcune lunghezze d’onda visibili vengono assorbite, mentre altre vengono riflesse e quindi viste: per questo motivo il modello di formazione del colore si dice sottrattivo. Anche se il nero può essere derivato direttamente dalla combinazione di ciano, magenta e giallo (ossia assorbendo tutti e tre i colori base), nelle stampanti, per motivi pratici, si una anche l’inchiostro nero (black) e il modello prende il nome di CMYK. In entrambi i casi i bit associati ai pixel di una immagine esprimono la misura de tre colori base. Nel caso di una profondità di 24 bit e codifica RGB, vengono assegnati 8 bit per la percentuale di rosso, 8 bit per quella di verde e 8 per quella di blu. Poiché si utilizzano 3 byte per rappresentare ogni pixel, una immagine a colori di 100x100 pixel avrà bisogno di 100 x 100 x 3 byte = 30.000 byte per essere rappresentata. Per ridurre la dimensione della rappresentazione si può utilizzare un sistema di codifica dei colori mediante tavolozza di colori (detta anche palette o CLUT da Color Look-Up Table). La palette è una codifica dei colori, solitamente con profondità di 8 bit per pixel, che consente di scegliere 256 colori diversi tra i milioni di colori esistenti. Si basa sull’ipotesi che difficilmente in una immagine sono presenti contemporaneamente 16 milioni di colori diversi. Consiste nell’utilizzare una tabella numerica in cui sono codificati solo i colori effettivamente presenti nell’immagine: ciascun pixel sarà codificato con un numero limitato di bit (da 4 a 8) che identifica la posizione in tabella del colore da usare. I colori della palette cambiano a seconda dell’immagine e dipendono dal suo contenuto: la tavolozza contiene infatti solo il sottoinsieme dei colori rappresentabili che compare nella foto. Possono essere anche modificati in 38 Capitolo primo funzione dell’utilizzo dell’immagine (stampa o visualizzazione). Se ad una immagine si cambia la CLUT, cambiano ovviamente le sue sfumature di colore. In tale codifica ogni immagine viene accompagnata dalla sua palette. Figura 15 - Codifica mediante palette Il formato di rappresentazione di immagini per punti è detto bitmap o raster. Esso è usato per riprodurre fotografie, dipinti e tutte le immagini per le quali non ha importanza l’individuazione degli elementi riprodotti. Nelle immagini bitmap è quindi il pixel con le sue caratteristiche l’elemento di riferimento. Il singolo pixel da solo detiene un contenuto informativo limitato, se viene invece considerato in combinazione all’insieme di pixel adiacenti si possono estrarre caratteristiche più significative. Elaborare un’immagine significa modificarne il contenuto allo scopo di evidenziare alcune caratteristiche. Analizzare un’immagine significa invece studiarne il contenuto allo scopo di inferire informazioni relative alla scena rappresentata. I settori della Computer Vision e della Computer Graphics studiano algoritmi che cercano di ricostruire dai tanti pixel di una immagine raster sia struttura che significato degli oggetti presenti in una scena. Ad esempio gli algoritmi OCR (optical character regognition) restituiscono il testo in formato digitale estraendolo dalla fotografia raster di un foglio dattiloscritto acquisita tramite scanner. I formati di rappresentazione più diffusi sono: L’informazione e le sue rappresentazioni - 39 il TIFF (Tagged Image File Format) che permette di gestire le immagini in varie modalità: bianco e nero, scala di grigio, colori RGB, colori CMYK. Il TIFF prevede un sistema di compressione chiamato LZW (Lempel-Ziv-Welch) non distruttiva, che non elimina alcuna informazione né degrada la qualità dell’immagine e che produce buone riduzioni della quantità di bit della rappresentazione; - il formato EPS (Encapsulated PostScript File) impiegato inizialmente per i disegni vettoriali si è poi diffuso come standard anche per le immagini raster e deve la sua importanza al linguaggio PostScript; - il JPEG (Joint Photographic Expert Group) è nato con lo scopo di standardizzare diversi formati per immagini con compressione di qualità. La sua principale caratteristica è quella di poter far scegliere il livello di compressione e di modulare quindi il rapporto tra la qualità dell’immagine e la quantità di bit; la compressione è con perdita e si possono raggiungere livelli di compressione alti (fino a 20:1 contro il 4:1 del GIF); - il GIF (Graphics Interchange Format) trova largo uso in Internet per la rappresentazione di elementi grafici come pulsanti, scritte, logo. Permette inoltre di rendere lo sfondo degli oggetti trasparente per integrarli nelle pagine del web. È un formato con poca quantità di bit, in quanto riduce a 256 la gamma dei colori, utilizzando una codifica che si basa sull’uso di una palette; - il PNG (Portable Network Graphics) è stato inventato per sostituirsi a GIF nella trasmissione di immagini sulla rete. Gestisce la trasparenza dello sfondo. Non supporta l’animazione. Presenta un algoritmo di compressione lossless migliore di quello del formato GIF; - il BMP (Bitmap) che è stato sviluppato per essere compatibile con tutte le applicazioni del mondo Windows per immagini in b/n, in scala di grigi, in scala di colore (RGB ma non in CMYK). Non prevede l’applicazione di metodi di compressione per cui la quantità di bit resta consistente. Quando le immagini hanno caratteristiche geometriche ben definite, come nel disegno tecnico, è possibile adottare una tecnica più efficiente per codificare le figure. Nel caso di progettazione architettonica, meccanica o elettronica, (CAD da Computer Aided Design) il disegno da memorizzare può essere facilmente scomposto in elementi base come una linea o un arco di circonferenza e non è conveniente rappresentare l’immagine in termini di pixel ma si procede descrivendo gli elementi che compongono la figura in termini matematici. Gli oggetti geometrici che compongono il disegno, quali punti, rette, linee, curve, cerchi, ellissi, rettangoli, vengono rappresentati secondo formule matematiche e parametri che li descrivono: ad esempio un punto tramite le coordinate, la retta tramite la sua equazione, il rettangolo mediante le coordinate dei quattro vertici; la circonferenza tramite le coordinate del vertice e la dimensione del raggio. Tale tipo di rappresentazione si definisce vettoriale. In essa è presente tutta l’informazione necessaria a riprodurre l’immagine, a prescindere dalle dimensioni del dispositivo di visualizzazione. Un grande vantaggio delle immagini vettoriali rispetto a quelle raster risiede nella minor quantità di bit usata per la loro rappresentazione e nella capacità di essere variate di dimensioni senza subire alcuna distorsione. 40 Capitolo primo Le immagini bitmap, invece, se ridimensionate rispetto alle dimensioni originali di acquisizione, hanno la tendenza a perdere di risoluzione risultando distorte o sfocate. Un ulteriore vantaggio della rappresentazione vettoriale risiede nel fatto che tutti gli oggetti che compaiono nella figura mantengono la loro identità in termini di caratteristiche descrittive per cui sono facilitate le operazioni di modifica dell’immagine come solitamente accade nei settori della progettazione. Ovviamente non è adatta per rappresentare immagini composte da continue variazioni di colore, quali ad esempio le fotografie. Figura 16 – Immagine vettoriale e raster 1.4.3. Immagini in movimento o video Il problema della rappresentazione delle immagini in movimento (o video) viene risolto allo stesso modo in cui il cinema o la televisione lo hanno affrontato: sfruttando un limite della capacità percettiva dell'occhio umano. La retina dell’occhio umano ha la caratteristica di mantenere impressa un’immagine per alcuni millisecondi prima che svanisca. Se si proietta una successione di immagini a determinate velocità, l’occhio non si accorge che ciò che vede è una sequenza discreta e percepisce il movimento come un continuo. Figura 17 – Video come sequenza di immagini Un video è una sequenza di immagini disposte in successione temporale, ossia una dopo l’altra. La sequenza continua di immagini della realtà viene quindi discretizzata ottenendo una serie di immagini (detti frame) che variano velocemente, ma a intervalli stabiliti. Il frame-rate è il numero di frame mostrati per secondo (fps). Al cinema il frame-rate è di 24 fps. Il sistema televisivo europeo (PAL) ha un frame-rate di 25 fps mentre quello americano (NTSC) ne prevede 30 fps. L’informazione e le sue rappresentazioni 41 Per digitalizzare l’immagine in movimento è necessario digitalizzare ogni singolo frame con la tecnica vista per le bit-map. In questo modo la quantità di bit usati per la rappresentazione dipende dalla risoluzione di ogni singola immagine, dalla sua profondità di colore e dalla durata del video che fissa il numero di frame complessivi. Ad esempio un minuto di trasmissione video con frame di 320x240 pixel e 256 colori richiede: 320 240 (pixel per frame) 25 (frame per sec) 1 (byte per pixel) 60 (secondi) = 115 Mbyte/min Un CD non conterebbe più di 5 minuti di video. È quindi necessario applicare tecniche di compressione. I CODEC (CODifica e la DECodifica) sono gli algoritmi (o le applicazioni che li realizzano) utilizzati per comprimere ed espandere i video digitali in modo da renderne più efficiente la gestione e la trasmissione. Lo standard MPEG (Moving Picture Experts Group), associa alla semplice codifica di ciascuna immagine anche tecniche per il suono e, soprattutto, modalità di compressione che sfruttano il fatto che la differenza tra un frame e il successivo è minima. Invece di conservare le informazioni di ogni frame, vengono conservate solo quelle essenziali a ricostruire la scena originaria e quelle che si modificano, tralasciando le variazioni impercettibili. Facendo riferimento all’esempio di figura 14 nella codifica MPEG vengono codificati solo il primo frame e le differenze tra ogni frame ed il successivo (zone delle immagine in cui si muove la pallina). E’ così immediato dato un frame ricostruire il successivo aggiungendo al precedente le sole differenze. MPEG-1 fu definito nel 1989 e rilasciato nel 1992. Questo sistema di compressione del segnale audio-video, ideato per i CD-Rom e per la visione di piccoli filmati su Internet, consente di vedere filmati televisivi con una qualità paragonabile a quella di un videoregistratore. Tra il 1992 e il 1994 si afferma MPEG-2, lo standard pensato per la trasmissione di contenuti multimediali sulla televisione digitale via satellite e via cavo. Oggi tutte le televisioni digitali degli Stati Uniti, buona parte di quelle europee, ed i DVD si basano sullo standard MPEG-2. Nel 1998 viene approvato MPEG-4 che rappresenta i frame a partire dagli oggetti di cui sono composti che mantengono una loro individualità sia nella fase di codifica che in quella di rappresentazione finale. Ad esempio, in un video composto da un paesaggio e da un motociclista che si muove, non è necessario ritrasmettere più volte le componenti invarianti del paesaggio, ma è sufficiente trasmettere solo quelle che cambiano legate agli spostamenti della moto e del suo guidatore. L’organizzazione ad oggetti è l'aspetto innovativo di MPEG-4: qualsiasi filmato può essere arricchito di ulteriori oggetti quali immagini fisse, videoclip, sorgenti audio, che vengono attivate grazie alla presenza di oggetti cliccabili e navigabili come su Internet. Si interviene quindi sul video rendendolo interattivo. Nel 2001 viene proposto MPEG-7 che può essere definito uno standard di descrizione piuttosto che di compressione. MPEG-7 non sostituisce le versioni precedenti, ma le affianca consentendo di estrarre informazioni da tutti gli oggetti audiovisivi esistenti per una indicizzazione e catalogazione sul modello di un motore di ricerca. 42 1.4.4. Capitolo primo La codifica del suono Il suono è un segnale analogico funzione del tempo consistente in vibrazioni che formano un’onda, la cui ampiezza misura l’altezza dell’onda e il periodo è la distanza tra due onde. Anche il suono deve essere campionato e discretizzato per poter essere digitalizzato. Se il campionamento è troppo rado e vengono usati pochi bit per misurare ogni valore istantaneo, la qualità del suono degrada nel senso che il suono riprodotto è diverso da quello originale. L'operazione di campionamento discretizza il segnale con una frequenza dell'ordine delle decine di KHz (migliaia di campioni al secondo) perché è dimostrato che l’orecchio umano percepisce fedelmente il suono originale se il suo campionamento è non inferiore a 30KHz. Particolare circuiti elettronici ricostruiscono il segnale originale e Nyquist ha dimostrato che per ottenere il segnale iniziale senza perdite è necessario campionare con una elevata frequenza di campionamento, pari ad almeno due volte la frequenza massima del segnale stesso. La quantizzazione è, diversamente da un buon campionamento, un processo irreversibile che conduce ad una sicura perdita di informazioni; tanto più l'operazione è accurata tanto più la qualità del suono è preservata riducendo al minimo quello che viene detto rumore di quantizzazione. La quantità di bit usati per rappresentare il suono dipende allora dal numero di bit usato per la quantizzazione, chiamato anche profondità del suono, dalla frequenza di campionamento e quindi dalla durata del suono. Ad esempio per una linea telefonica è sufficiente una frequenza di campionamento di soli 8KHz con una quantizzazione a 256 livelli (codificati con 8 bit) per riprodurre la voce umana ai due estremi del collegamento garantendo la comprensione del parlato. Nel caso di musica stereo la quantità di bit raddoppia perché vanno separatamente digitalizzati i segnali per il lato destro e per quello sinistro. Tipo Telefono Parlato Radio mono Radio stereo Audio Cassetta Compact Disk Frequenza di campionamento (Hz) 8.000 11.025 22.050 22.050 44.100 48.000 Profondità (bit) Mono/stereo Dimensione per un minuto 8 8 16 16 16 16 mono mono mono stereo stereo stereo 469,00 Kb 646,00 Kb 2,52 Mb 5,05 Mb 10,10 Mb 11,00 MB Tabella 19 – Frequenze di campionamento per il suono Da MPEG-1 ha avuto origine Mp3 (abbreviazione di MPEG-1 layer 3). Mp3 è una codifica del segnale audio che consente la compressione di brani musicali della durata di 3-6 minuti con pochi MB rendendone possibile la distribuzione nella rete Internet. L’informazione e le sue rappresentazioni 1.5. 43 Dati e metadati Nella realtà di tutti i giorni molte attività operano con informazioni di natura e forma diverse (testo, video, audio) e si costruiscono sistemi che gestiscono e elaborano informazioni. L’informazione è quindi un oggetto che ha un rapporto stretto con la realtà dalla quale può emergere se e solo se un determinato insieme o classe di oggetti assume stati o configurazioni differenti. In tale accezione l’informazione non è altro che la scelta di una delle possibili configurazioni in cui si trova un esemplare della classe di oggetti. Allora, il concetto di informazione è strettamente legato a quello di scelta di uno fra più oggetti di un particolare insieme e non esiste informazione se non si effettua una scelta. Ad esempio, la frase “sto studiando elementi di informatica”, fornisce un'informazione in quanto esprime la scelta della materia di studio e l'identificazione, quindi, della materia “elementi di informatica”, tra tutte le possibili materie del piano di studio. In informatica è stato introdotto il termine dato che deriva dal latino datum e significa letteralmente fatto. Mentre l’uomo tratta informazioni l’elaboratore tratta dati. Con dato si indica la rappresentazione di fatti e concetti in modo formale perché sia possibile una loro elaborazione da parte di strumenti automatici. Il dato da solo, senza un contesto, può non avere significato: uno stesso numero può esprimere cose diverse in situazioni diverse; così come una stessa parola può avere significato dipendente dal contesto. L’ambiguità può essere risolta dando al dato una interpretazione. L’informazione non è altro che la percezione del dato attraverso un processo di interpretazione. In altre parole l’informazione cattura il significato del dato. Quando l’elaborazione consente di trattare dati eterogenei in modo integrato si parla di elaborazione multimediale. Il termine metadato è apparso abbastanza recentemente anche se indica tipologie di informazioni diffuse da molto tempo. Nella vita reale i metadati vengono impiegati principalmente per organizzare informazioni o cose, e, ovviamente per effettuare ricerche tra informazioni o cose classificate, come avviene nei cataloghi delle biblioteche dove ogni libro viene etichettato con una scheda (riportante titolo, autore, anno di pubblicazione, casa editrice, argomenti trattati, etc.) di cui si conosce l’uso. I metadati indicano dati che descrivono altri dati riportandone struttura, significato o descrizione. Possono essere distinti nella categorie di metadati descrittivi finalizzati al recupero dei dati ed in metadati gestionali necessari alla gestione dei dati. Anche l’attributo di un dato è un altro esempio di metadato in quanto descrive il ruolo svolto da una variabile in un contesto applicativo. I metadati sono solitamente più facili da trattare dei dati che rappresentano perché hanno un formato prestabilito mentre il formato dei dati dipende da molti fattori. Un aspetto interessante dei metadati è che anch’essi sono dati e come tali possono essere gestiti nel senso che possono essere descritti da altri metadati, e così via. Il numero di metalivelli da specificare dipende dalle caratteristiche delle applicazioni e dalle specifiche esigenze. Secondo le nuove proposte del web semantico il metadato è l’informazione che da significato al dato rendendolo “machine understandable”, ossia comprensibile alle macchine. Esso costituisce lo strumento principale del Web 44 Capitolo primo Semantico in quanto permettono di introdurre la semantica per descrivere il contenuto dei documenti web. L’eXensible Markup Language o XML è oggi il linguaggio standard su cui si è innestato lo sviluppo del Web Semantico e la sua principale caratteristica consiste nel fatto che permette di definire delle strutture dati indipendenti da qualsiasi piattaforma e che possono essere elaborate in modo automatico. XML non è un linguaggio di programmazione ma un linguaggio di marcatura (markup) con una sintassi semplice per rendere agevole sia la lettura diretta che la elaborazione automatica Capitolo secondo Il modello di esecutore 2.1. Processi e processori L’informatica ha avviato alla fine del ventesimo secolo una rivoluzione che ha prodotto effetti analoghi a quelli osservati all'epoca della rivoluzione industriale. Ma mentre la rivoluzione industriale ha segnato il potenziamento della forza fisica dell'uomo (amplificazione dei suoi muscoli) la rivoluzione informatica ha portato ad un aumento della potenza della mente dell'uomo (amplificazione del suo cervello). Così come le macchine meccaniche sostituiscono l'uomo in azioni ripetitive o faticose, le macchine informatiche o computer sostituiscono l'uomo nelle attività ripetitive della sua mente. Un computer è un apparecchio elettronico che, strutturalmente, non ha niente di diverso da un televisore, uno stereo, un telefono cellulare o una calcolatrice elettronica, semplicemente è progettato per eseguire autonomamente attività diverse sia nello stesso tempo, che in tempi diversi. Come tutte le macchine, non ha nessuna capacità decisionale o discrezionale, ma si limita a compiere determinate azioni secondo procedure prestabilite. Si può anzi affermare, paradossalmente, che il computer è una macchina che in maniera automatica esegue operazioni “elementari” ad altissima velocità. L'altissima velocità di elaborazione (milioni di istruzioni per secondo) fa sì che operazioni complesse (espresse mediante un gran numero di operazioni semplici) siano eseguite in tempi ragionevoli per l'ambiente esterno. Come tutti gli esecutori di ordini anche il computer può compiere solo quei lavori che possono essere specificati mediante operazioni che è in grado di comprendere e mettere in pratica. L’algoritmo è la descrizione di un lavoro da svolgere. Allora se si vuole usare un computer bisogna non solo progettare preliminarmente un algoritmo, ma anche provvedere a comunicarglielo in modo che gli risulti comprensibile. L'esecuzione di un algoritmo da parte di un esecutore si traduce in una successione di azioni che vengono effettuate nel tempo. Si definisce processo il lavoro svolto eseguendo l'algoritmo, e processore il suo esecutore. Il processo non è altro che l’elenco delle azioni effettivamente svolte come si susseguono nel tempo. Ogni algoritmo evoca da uno a più processi, nel senso che, a seconda delle condizioni in cui il lavoro viene svolto, si possono verificare comportamenti diversi da parte dell’esecutore. 46 Capitolo secondo Il computer è un tipo speciale di processore che evolve in automatico (funziona senza l'intervento umano), ha un'alta velocità elaborativa (se confrontata con un esecutore uomo) ed è capace di eseguire processi differenti. 2.2. Modello di Von Neumann Per comprendere i motivi che rendono un computer diverso dalle altre macchine, si introduce uno schema di riferimento nel quale sono messi in evidenza tre blocchi fondamentali. Figura 1 – Modello di riferimento di Von Neumann Lo schema presentato è uno schema di principio ed è rappresentativo delle macchine tradizionali. Prende il nome da Von Neumann, il primo ricercatore che lo propose nel 1945. La Central Processing Unit (CPU) coordina l’esecuzione delle operazioni fondamentali; la memoria contiene l'algoritmo che descrive le operazioni da eseguire e i dati su cui l'algoritmo stesso opera; i dispositivi di input e output sono le interfacce della CPU nei confronti del mondo esterno, rispettivamente sono l’unità che consente l'inserimento di algoritmo e dati in memoria, e quella che presenta i risultati dell'attività della CPU. Queste unità fondamentali formano l'hardware del computer, ossia l'insieme di tutti i componenti elettronici, elettrici e meccanici che costituiscono un sistema elaboratore. Il prototipo proposto da Von Neumann era basato sul concetto di programma memorizzato: la macchina immagazzinava nella propria memoria i dati su cui lavorare e le istruzioni per il suo funzionamento. Una tale flessibilità operativa fece sì che macchine nate allo scopo di alleviare i problemi di calcolo per tecnici e scienziati, potessero essere in seguito impiegate nella risoluzione di problemi di natura completamente diversa come problemi di tipo amministrativo, gestionale e produttivo. Le caratteristiche che un sistema di tale tipo presenta, e che ne hanno decretato la rapida diffusione in molti campi applicativi, sono: - uno schema di funzionamento semplice nelle sue linee generali, Il modello di esecutore 47 - la velocità e l'affidabilità nella esecuzione degli algoritmi; - una adeguata capacità di memoria; - un costo vantaggioso. La velocità di esecuzione si aggira attualmente sui milioni di istruzioni svolte dalla CPU in un secondo e per tale motivo come unità di misura della capacità elaborativa dei computer è stato usato il MIPS (milioni di istruzioni per secondo). Vale la pena osservare che, nonostante la velocità dei computer tenda ad aumentare, esistono problemi che presentano soluzioni informatiche non pratiche poiché il loro tempo di esecuzione resta comunque lungo. Dal punto di vista dell’affidabilità si può affermare che un computer non commette errori e gli errori dovuti a guasti o a cattivi funzionamenti hardware sono subito riscontrabili, alcune volte persino in maniera automatica. Inoltre un computer non commette mai errori di algoritmo poiché è un esecutore obbediente dell'algoritmo, la cui esecuzione gli è stata affidata. Per memorizzazione delle informazioni si intende il compito della memoria di conservare informazioni per la CPU. La memorizzazione può essere temporanea, permanente o definitiva. Con capacità di memoria si fa riferimento al numero di informazioni che possono essere gestite. Tale numero varia in base al tipo di memoria usato, all'architettura della memoria stessa ed al tipo di informazione. La capacità è la misura del numero di informazioni immagazzinabili nella memoria ed oggi si misura in numero di byte. Per quanto riguarda il costo dei computer si può sicuramente considerare che esso è basso se paragonato ai tempi di lavoro necessari affinché esseri umani portino a termine gli stessi compiti. Ed è anche in conseguenza di ciò che i computer vanno sempre più diffondendosi nei settori produttivi della società. L’architettura di un computer nella realtà è molto più complessa. Nelle linee generali però il funzionamento interno di un qualsiasi computer si può ricondurre al semplice schema presentato che non è molto dissimile da uno antropomorfo che vede un essere umano sostituire con il suo cervello la CPU, fogli di carta alla memoria centrale, una calcolatrice con le operazioni fondamentali all'ALU (Aritmetic Logic Unit), ovvero il componente della CPU che effettua tutte le operazioni di calcolo logico e aritmetico. Per concludere si deve notare che i dispositivi di input e di output interfacciano la CPU con l'ambiente esterno provvedendo a tutte le trasformazioni necessarie a rendere comprensibili le informazioni sia alla CPU che agli utenti esterni del computer. Essi vengono progettati in modo confacente ai meccanismi di comunicazione delle informazioni dell'ambiente in cui il computer è immerso. Nella maggior parte delle applicazioni pratiche è l'uomo l'utente del computer, ma esistono applicazioni nelle quali il computer scambia informazioni con macchinari o sonde che rappresentano le informazioni sotto forma di segnali elettrici. Nelle comunicazioni con un utente umano i dispositivi di input ed output provvedono alla trasformazione della rappresentazione delle informazioni dal linguaggio naturale al linguaggio binario e viceversa. In tali casi il tipico organo di input è la tastiera mentre quello di output è uno speciale televisore detto monitor o video. La tastiera è fatta pertanto da tasti sui quali sono riportati lettere, cifre e simboli speciali: mediante la pressione dei tasti si inviano le informazioni in memoria. Il video o monitor riporta i risultati distribuendoli su un numero di righe limitate in modo che possano essere letti agevolmente. Si ricordano inoltre il 48 Capitolo secondo mouse, la penna ottica, la tavoletta grafica e lo scanner come altri esempi di dispositivi di input; mentre tra quelli di output il plotter e le stampanti ad aghi, a getto d’inchiostro o laser per riportare i risultati su foglio di carta. 2.2.1. Le memorie In generale le memorie possono essere viste come un insieme di contenitori fisici, detti anche registri, di dimensioni finite e fissate a cui si può far riferimento mediante la posizione occupata nell'insieme detta indirizzo di memoria. La dimensione di un registro si misura in numero di bit. Il bit è un dispositivo capace di assumere due sole condizioni: - nelle memorie di tipo elettronico sono circuiti detti flip-flop che mostrano un valore di tensione o uguale a 5 Volt o a 0 Volt; - nelle memorie di tipo magnetico è una sorta di calamita polarizzata o positivamente o negativamente; - nelle memorie di tipo ottico è una superficie con o senza un buco in modo da riflettere diversamente il raggio laser che la colpisce. In ogni caso il dispositivo di lettura deve essere in grado di associare allo stato del bit il valore 1 (ad esempio tensione a 5 volt, polo positivo, assenza di buco) o il valore 0 (tensione a 0 volt, polo negativo, presenza di buco). Memorie con registri di otto bit sono dette a byte o caratteri; con più di otto (solitamente 16 o 32) vengono invece dette a voce. I calcolatori moderni sono dotati di memorie a byte e le memorie a voce sono solo un ricordo del passato. Le operazioni consentite su un registro sono di lettura e di scrittura. Con la prima si preleva l'informazione contenuta nel registro senza però distruggerla; con la seconda si inserisce una informazione nel registro eliminando quella precedente. Per comprendere il funzionamento di un registro di memoria si può pensare ad una lavagna il cui uso può essere così esemplificato: - leggere informazioni a patto che vi siano state scritte; - la lettura non cancella quanto scritto; - la scrittura di nuove informazioni obbliga a cancellare quelle precedenti che pertanto vengono perse. La memoria è un sistema che assolve al compito di conservare il dato, depositandolo in un registro nel caso di operazione di scrittura, e di fornire il dato conservato in un registro, in caso contrario di operazione di lettura. Le due operazioni vengono anche dette di store (per la scrittura del dato) e di load (per la lettura). Il funzionamento della memoria in linea del tutto generale è alquanto semplice. La CPU indica preventivamente l’indirizzo del registro interessato dall’operazione; la memoria decodifica tale indirizzo abilitando solo il registro ad esso corrispondente affinché: - per uno store copi il dato del buffer nel registro; - per un load copi il dato del registro nel buffer. dove il buffer può essere vista come un’area di transito dei dati dalla CPU alla memoria e viceversa. Le operazioni di load e store richiedono tempi di attuazione che dipendono dalle tecnologie usate per la costruzione delle memorie e dalle modalità di accesso. Le prestazioni di un componente di memoria vengono misurate in termini di tempi di accesso. Le operazioni di load e store possono avere tempi di accesso differenti. Il modello di esecutore 49 Nel caso di load, il tempo di accesso misura il tempo che trascorre tra la selezione del registro di memoria e la disponibilità del suo contenuto nel registro di buffer. Il tempo di accesso nel caso dello store misura invece il tempo necessario alla selezione del registro e il deposito del contenuto del registro di buffer in esso. Le memorie devono mostrare tempi di accesso adeguati alle capacità della CPU, nel senso che non devono introdurre ritardi quando essa trasferisce dati. Per tale motivo gli sforzi tecnologici sono rivolti alla costruzione di memorie con tempi di accesso bassi anche se tale parametro contrasta con quello del costo degli stessi componenti. Figura 2 – Schema di funzionamento per le operazioni di load e store La selezione di un registro viene detta: - casuale quando il tempo di accesso non dipende dalla posizione: memorie di questo tipo vengono dette RAM (Random Access Memory); - sequenziale quando invece il tempo di accesso dipende dalla posizione come avviene nei nastri magnetici. Alcune memorie vengono realizzate in modo che sia possibile una sola scrittura di informazioni. Tali memorie vengono dette a sola lettura o ROM (da Read Only Memory). L'uso di queste memorie è necessario quando si desidera che alcune istruzioni o dati non siano mai alterati o persi. Le memorie composte da registri sui quali sono consentite le operazioni di lettura e scrittura vengono anche dette RAM per contrapposizione alle memorie ROM, anche se il termine non è molto appropriato. Infine si è soliti distinguere le memorie in base alla capacità di conservare le informazioni, anche quando i sistemi che le contengono non sono alimentati. Si dicono volatili le memorie che perdono le informazioni in esse registrate quando il 50 Capitolo secondo sistema viene spento; sono, di contro, permanenti gli altri tipi di memorie. Sono volatili le memorie elettroniche; sono invece permanenti le memorie di tipo magnetico, ottico e tutti i tipi di ROM. Lo schema iniziale di Von Neumann è stato nel tempo modificato per affiancare alla memoria centrale delle unità di memoria ausiliarie caratterizzate da una elevata capacità, dette per questo motivo memorie di massa. Figura 3 – Modello di Von Neumann modificato La differenza fondamentale fra la memoria centrale e quella di massa, dal punto di vista funzionale, risiede nel fatto che: - le informazioni contenute nella memoria centrale possono essere direttamente prelevate dalla CPU, mentre quelle contenute nella memoria di massa devono essere dapprima trasferite nella memoria centrale e successivamente elaborate; - le informazioni prodotte dalla CPU, viceversa, devono essere depositate in memoria centrale per poi essere conservate nelle memorie di massa. Il modello di esecutore 51 Figura 4 – Trasferimento di informazioni tra memorie Un’altra differenza tra la memoria centrale e quella di massa è la maggiore velocità della prima nella gestione dei dati. Le memorie di massa hanno tempi di accesso maggiori dovuti alle tecnologie impiegate per realizzarle. Solitamente queste ultime sono di natura magnetica e i maggiori tempi di accesso si giustificano pensando alle diverse attivazioni di componenti elettromeccanici necessari a portare i dispositivi di lettura e scrittura nelle posizioni selezionate. Per ovviare alla differenza di velocità tra i due dispositivi si impiegano tecniche che prevedono che la memoria centrale non solo contenga dati e istruzioni ma anche aree di accumulo dei dati in transito verso tutti i dispositivi esterni. Tali aree vengono dette, come già anticipato buffer. Un buffer di input ha quindi il compito di accumulare dati in memoria ricevendoli da un dispositivo lento prima che la CPU provveda ad elaborarli. Così la CPU, solitamente molto più veloce nelle sue elaborazioni di qualsiasi dispositivo di output, accumula tutti i dati prodotti in un buffer di uscita prima di abilitarne il trasferimento. Con i buffer si procede verso una separazione dei compiti tra i componenti del modello di Von Neumann nell’ottica di far cooperare dispositivi caratterizzati da velocità di trattamento dati diverse tra loro. I buffer non sono altro che magazzini di dati e svolgono le stesse funzioni dei magazzini di una fabbrica, che regolano i tempi diversi di produzione dei beni da quelli relativi alla loro vendita: quando la produzione è più veloce la merce si accumula dando tempo ai venditori di procedere con i loro tempi più lunghi. Solitamente le memorie di massa sono di tipo magnetico (nastri, dischi e tamburi) o ottico (CD, DVD) in modo da mantenere le informazioni in modo permanente, a differenza delle RAM di tipo elettronico che sono volatili. Le capacità delle memorie centrali, inoltre, si aggirano sui milioni di informazioni mentre quelle delle memorie di massa sono dell'ordine delle centinaia o milioni di milioni. 2.2.2. La CPU La CPU contiene i dispositivi elettronici in grado di acquisire, interpretare ed eseguire il programma contenuto nella memoria centrale operando la trasformazione dei dati. Il processore centrale è composto da tre parti fondamentali: l'Unità di Controllo o Control Unit (CU), l'Unità Logico Aritmetica (ALU) e un insieme di registri detti “interni” per distinguerli da quelli della memoria centrale. 52 Capitolo secondo Figura 5 – Schema interno di una CPU L'unità di controllo della CPU è l'organo preposto all'interpretazione delle singole istruzioni ed all’attivazione di tutti i meccanismi necessari al loro espletamento. In particolare la CU ha il compito di prelevare ogni istruzione dalla memoria centrale, di decodificarla, di prelevare i dati dalla memoria se servono all’istruzione, e infine di eseguire l’istruzione. Per esempio: se l'istruzione prelevata è di tipo aritmetico e richiede due operandi, la CU predispone dapprima il prelievo dalla memoria di tali operandi, attiva poi l'ALU perchè esegua l'operazione desiderata, ed infine deposita il risultato di nuovo in memoria. Al termine dell'esecuzione di una istruzione la CU procede al prelievo dalla memoria della successiva istruzione secondo un ordine rigidamente sequenziale: ossia l’esecuzione di una istruzione può avere inizio solo se la precedente è stata portata a termine. Perché l’intero sistema possa avere avvio la CU deve essere informata dell'indirizzo del registro di memoria che contiene la prima istruzione da eseguire. A partire da questa operazione iniziale detta di boot la CU esegue ininterrottamente l’algoritmo detto ciclo del processore fino allo spegnimento del sistema. Le tre fasi del ciclo vengono anche dette fase fetch, operand assembly ed execute. L’unità logico aritmetica esegue operazioni aritmetiche, di confronto o bitwise sui dati della memoria centrale o dei registri interni. L’esito dei suoi calcoli viene segnalato da appositi bit in un registro chiamato Condition Code. A seconda dei processori l’ALU può essere molto complessa. Nei sistemi attuali l’ALU viene affiancata da processori dedicati alle operazioni sui numeri in virgola mobile detti processori matematici. Il modello di esecutore 53 Figura 6– Ciclo del Processore Durante le sue elaborazioni la CU può depositare informazioni nei suoi registri interni in quanto sono più facilmente individuabili e hanno tempi di accesso inferiori a quelli dei registri della memoria centrale. Il numero e tipo di tali registri varia a seconda dell’architettura della CPU. Quelli che si trovano in molte CPU sono l'Istruction Register (IR), il Prossima Istruzione (PI), l'Accumulatore (ACC) e il Condition Code (CC). Il primo contiene l'istruzione prelevata dalla memoria e che l'unità di controllo sta eseguendo. Il PI invece ricorda alla CU la posizione in memoria della successiva istruzione da eseguire. Nei casi in cui ogni registro di memoria contenga un’intera istruzione, e l'insieme delle istruzioni del programma sia disposto ad indirizzi consecutivi, la CU incrementa di uno il valore contenuto in PI dopo ogni prelievo di una istruzione dalla memoria. L'ACC serve come deposito di dati da parte dell'ALU nel senso che contiene prima di un’operazione uno degli operandi, e al termine della stessa operazione il risultato calcolato. In questo caso i registri Op1 e Op2 diventano interni all’ALU. Il CC indica le condizioni che si verificano durante l'elaborazione, quali risultato nullo, negativo e overflow. 2.2.3. I bus La CPU comunica con la memoria e tutti i dispositivi di input ed output tramite tre canali detti anche bus. I bus collegano due unità alla volta abilitandone una alla trasmissione e l’altra alla ricezione: il trasferimento di informazioni avviene sotto il controllo della CPU. Il termine bus indica un canale di comunicazione condiviso da più utilizzatori. Esso è fisicamente costituito da uno o più fili su cui possono transitare uno o più bit contemporaneamente. A seconda delle informazioni trasportate si distinguono in: - bus dati (data bus) 54 Capitolo secondo - bus indirizzi (address bus) bus comandi o di controllo (command o control bus) Figura 7 – I bus Il control bus serve alla C.U. per indicare ai dispositivi cosa essi devono fare. Tipici segnali del control bus sono quelli di read e write mediante i quali la CU indica ai dispositivi se devono leggere un dato dal data bus (read) o scriverlo su di esso (write). Figura 8 – Control Bus Il data bus permette ai dati di fluire da CPU a registro di memoria selezionato per operazioni di store e viceversa per quelle di load. La CU controlla anche il flusso di informazioni con il mondo esterno abilitando il transito delle informazioni dalla memoria verso le risorse di output e viceversa da quelle di input. Nello schema le memorie di massa sono rappresentate in un blocco a parte solo per la loro importanza come memoria permanente del sistema, anche se il loro funzionamento è quello di un dispositivo che opera sia in input che in output. L’address bus serve alla CU per comunicare l'indirizzo del dispositivo interessato da una operazione di lettura o scrittura. In questa ottica anche i dispositivi di input Il modello di esecutore 55 od uno di output sono identificati da un indirizzo alla stessa stregua dei registri di memoria. Tutti i componenti del sistema (memoria, input, output, memoria di massa, etc.) devono essere dotati della capacità di riconoscere sull’address bus il proprio indirizzo. In altri termini attraverso l’address bus la CU effettua la selezione del dispositivo a cui sono rivolti i comandi e i dati. I bus realizzano lo scambio di informazioni tra tutti i componenti caratterizzato dalle seguenti regole: - la CPU è l’unico elemento che fornisce l’indirizzo all’address bus; - memorie e dispositivi di input ed output devono ascoltare l’address bus per attivarsi quando su di esso compare il proprio indirizzo identificativo; nel caso della memoria l’attivazione avviene quando viene riconosciuto l’indirizzo corrispondente ad uno dei registri di cui essa è composta; - il dispositivo attivo deve interpretare i segnali del control bus per eseguire i comandi della CU; - le memorie prelevano dati dal data bus o immettono dati in esso in funzione del comando impartito dalla CU; - i dispositivi di input possono solo immettere dati sul data bus; - viceversa i dispositivi di output possono solo prelevare dati dal data bus. Un bus costituito da un solo filo è chiamato bus seriale e su di esso i bit transitano uno dietro l’altro. Un bus costituito da n fili è chiamato bus parallelo perché su di esso transitano n bit alla volta. Tipici sono i bus a 8 e 32 fili sui quali si possono trasferire rispettivamente 8 e 32 bit (4 Byte) alla volta. L’address e il data bus sono paralleli e le loro dimensioni caratterizzano i sistemi di calcolo. Il numero di bit dell’address bus indica la capacità di indirizzamento della CPU: ossia la sua capacità di gestire la dimensione della memoria centrale e il numero di dispositivi di input ed output. Infatti un address bus con n bit consente di selezionare un registro tra 2n. La dimensione del data bus condiziona invece la velocità di scambio delle informazioni tra i diversi dispositivi in quanto con m fili solo m bit possono viaggiare contemporaneamente. 2.2.4. Il clock I componenti del modello di Von Neumann vengono coordinati dalla CU della CPU secondo sequenze prestabilite che corrispondono alle sue diverse capacità. Ad ogni operazioni che la CU è in grado di svolgere corrispondono ben prefissate sequenze di attivazione dei diversi dispositivi. Le attività di tutti i dispositivi non si svolgono però casualmente ma vengono sincronizzate tra loro mediante un orologio interno chiamato clock che scandisce i ritmi di lavoro. Il clock è un segnale periodico di periodo fisso, un’onda quadra caratterizzata da un periodo T (detto ciclo) e da una frequenza f (f=1/T) misurata in Hertz (Hz). Ad esempio un clock composto da 10 cicli al secondo ha la frequenza f = 10 Hz e il periodo T= 100ms. La frequenza dei clock presenti nei moderni sistemi spazia dai MHz (1 MHz corrisponde a un milione di battiti al secondo) ai GHz (1 GHz corrisponde a un miliardo di battiti al secondo). Il clock è un segnale che raggiunge tutti i dispositivi per fornire la cadenza temporale per l’esecuzione delle operazioni elementari. 56 Capitolo secondo Figura 9 – Clock La velocità di elaborazione di una CPU dipende dalla frequenza del suo clock come il suono prodotto da un musicista dipende dal metronomo: più accelerato è il battito del clock maggiore è la velocità di esecuzione. Alla frequenza del clock è legato il numero di operazioni elementari che vengono eseguite nell’unità di tempo dalla CU. Ad esempio, se si assume che ad ogni ciclo di clock corrisponde esattamente l’esecuzione di una sola operazione, allora la frequenza del clock indica il numero di operazioni che vengono eseguite nell’unità di tempo dalla CU. Per esempio con un clock a 3 GHz si ha che il processore è in grado di eseguire 3 miliardi di operazioni al secondo. In realtà tale ipotesi non è sempre vera in quanto l’esecuzione di una operazione può richiedere più cicli di clock sia per la complessità delle operazioni che per la lentezza dei dispositivi collegati alla CPU. Per comprendere l’importanza del clock si osservi il trasferimento di dati e istruzioni tra CPU e Memoria Centrale riportato in una forma semplificata in figura 10. Figura 10 – Trasferimento dati tra CPU e Memoria Il modello di esecutore 57 Entrambe le operazioni di load e store avvengono sotto lo stretto controllo della CU. Nel primo caso la CU procede eseguendo nell’ordine: - con il primo battito di clock ponendo l’indirizzo del registro di memoria di cui vuole leggere il contenuto sull’address bus; - con il secondo battito segnalando alla memoria che si tratta di una operazione di read; - con il terzo battito prendendo il dato dal data bus dove la memoria ha provveduto a depositarlo. Nel caso di una operazione di store la CU provvede: - con il primo battito di clock a porre l’indirizzo del registro di memoria di cui vuole leggere il contenuto sull’address bus; - con il secondo battito a depositare il dato sul data bus; - con il terzo battito a segnalare alla memoria che si tratta di una operazione di write e quindi che il dato è pronto per essere depositato nel registro selezionato. Nell’esempio si è fatta l’ipotesi che la memoria sia in grado di gestire sia l’inserimento del dato sul data bus che il prelievo del dato da esso in un solo ciclo di clock. Nella realtà le memorie possono essere più lente introducendo ritardi che portano la stessa operazione a completarsi con un maggior numero di clock. La memoria centrale è infatti realizzata mediante moduli che hanno prestazioni decisamente inferiori rispetto alla tecnologia utilizzata per costruire le CPU; per questo motivo si realizzano dei bus che rallentano la trasmissione di un fattore 10 rispetto al clock. 2.3. Firmware, software e middleware Lo scopo del modello di Von Neumann è quello di eseguire i comandi memorizzati nella sua memoria centrale. I comandi prendono anche il nome di istruzioni non solo perché istruiscono la CPU sul da farsi ma anche per effettuare una distinzione con i dati che sono gli oggetti rappresentati in memoria centrale su cui si svolgono le attività. L’insieme delle istruzioni prende il nome di programma. Tutti i programmi sono quindi formati da insiemi di istruzioni che la CU della CPU esegue mediante il coordinamento di tutti i componenti del modello di Von Neumann. Le istruzioni sono operazioni semplici quali: - trasferimento dati da un registro ad un altro (da memoria a memoria, da memoria a registri della CPU o viceversa, da memoria a output, da input a memoria); - operazioni aritmetiche o logiche eseguite dall’ALU; - controllo di condizioni riportate dal registro CC o deducibili dal confronto di due registri. La CU è un automa che ripete senza sosta il prelievo di una istruzione dalla memoria e la sua interpretazione con relativa esecuzione. L’esecuzione di una istruzione da parte della CU consiste nell’inoltro di una sequenza di abilitazioni dei dispositivi il cui effetto corrisponde alla operazione richiesta. Le prime CU erano realizzate con circuiti, detti a logica cablata, che evolvevano in tanti modi diversi quante erano le istruzioni che essa era in grado di svolgere. 58 Capitolo secondo Figura 11 – CU a logica cablata Le moderne UC sono invece realizzate in logica microprogrammata. Ad ogni istruzione corrisponde una sequenza di microistruzioni conservate in una memoria interna alla CU. La sequenza di microistruzioni ha il compito di generare le abilitazioni necessarie alla attuazione della istruzione. A tal fine un circuito interno alla UC provvede alla generazione di indirizzi per individuare una dopo l’altra le microistruzioni che un decodificatore trasforma in segnali di abilitazione. L’istruzione nel registro IR determina la posizione della prima microistruzione. Figura 12 – CU a logica micorprogrammata In effetti si ripropone in piccolo il modello di Von Neumann per la realizzazione del suo componente principale. L’insieme dei microprogrammi composti dalle microistruzioni memorizzate nella memoria interna alla CU prende il nome di firmware. L’esecuzione di un qualsiasi programma si traduce nel far applicare la CPU su un compito specifico interagendo con il mondo esterno tramite i dispositivi di input ed output. Tipiche applicazioni dei computer sono l’elaborazione di documenti, la ricerca di informazioni in Internet, il foglio elettronico, la gestione della segreteria studenti di una università, il sistema operativo a finestre. L’insieme di tutte le applicazioni del computer, quindi di tutti i programmi per computer, prende il nome di software. In una accezione più ampia il termine software può essere inteso Il modello di esecutore 59 come tutto quanto può essere preteso dall’hardware: basta infatti inserire in memoria un programma diverso perché il sistema cambi le sue attività. Tra tutte le macchine automatiche il computer è un sistema polifunzionale in quanto può eseguire infinite funzioni sempre che venga progettato un programma per ogni applicazione. Figura 12 – Hardware, Firmware e Software Nel variegato ed immenso mondo di applicazioni dei computer si è soliti fare una distinzione tra i programmi che servono a tutti gli utenti del sistema da quelli che risolvono problemi specifici. I primi vengono classificati come software di base e riguardano i sistemi operativi e i traduttori dei linguaggi di programmazione. I programmi che non rientrano in tale categoria vengono detti del software applicativo. Figura 13 – Software di base ed applicativo Tra le applicazioni del software di base più importanti si trova il sistema operativo. Senza di essa gli elaboratori non mostrerebbero quella semplicità di uso che ne sta caratterizzando la diffusione estesa nella società. Il sistema operativo è un insieme di programmi che deve garantire la gestione delle risorse hardware in modo semplice ed efficiente a tutti gli utenti del sistema, siano essi persone che interagiscono tramite tastiera e monitor che altre applicazioni. I primi calcolatori non avevano il sistema operativo. In essi il programmatore doveva prevedere tutto: dai calcoli alla gestione dei dispositivi di input ed output; doveva anche provvedere al caricamento del programma in memoria prima di attivare la CPU perché lo 60 Capitolo secondo eseguisse. Al termine dell’esecuzione del programma il programmatore o l’operatore del sistema doveva provvedere ad un nuovo caricamento in memoria ed ad una successiva attivazione. Con il sistema operativo il passaggio da una applicazione ad un’altra è svolto in automatico mediante l’interpretazione di comandi che l’utente inserisce da tastiera. La CPU si trova così ad eseguire i programmi del sistema operativo in alternanza con quelli applicativi. Figura 14 – Schema di funzionamento di un Sistema Operativo I programmi del sistema operativo vengono eseguiti all’avvio del sistema, quando termina un’applicazione o quando una applicazione ha bisogno di gestire una risorsa hardware. Altre applicazioni del software di base stanno sempre più assumendo un rilievo particolare, soprattutto in relazione al funzionamento di più sistemi tra loro interconnessi attraverso canali di comunicazione di cui Internet è il più importante esempio. Per far sì che i sistemi possano mostrarsi uguali tra loro si sovrappone al sistema operativo uno strato software chiamato middleware che ha il compito di interagire con l’applicazione utente. Il middleware è il software che fornisce un’astrazione di programmazione che maschera l’eterogeneità degli elementi sottostanti (reti, hardware, sistemi operativi, linguaggi di programmazione) e la loro distribuzione tra i diversi nodi della rete. Il middleware definisce una macchina generalizzata fissandone modalità di interazione con le applicazioni. Figura 15 – Middleware Il modello di esecutore 2.4. 61 Evoluzione del modello di Von Neumann Nel tempo sono state apportate modifiche al modello di Von Neumann con lo scopo di rendere il suo funzionamento più veloce. Il primo intervento ha riguardato la struttura dei dispositivi di input ed output. Nei primi sistemi di calcolo la CPU controllava tutti i componenti del sistema, anche quelli interni ai dispositivi periferici. Inoltre, data la natura rigidamente sequenziale del modello di Von Neumann, non era possibile sovrapporre i tempi delle operazioni di input con quelli dell’output. Per ovviare a tali limitazioni, sono stati realizzati sistemi dedicati il cui compito è scaricare la CPU della gestione di attività specifiche. I sistemi dedicati, detti anche canali, con la loro autonomia possono lavorare anche contemporaneamente con la CPU. I primi canali introdotti sono stati quelli di input ed output. Oggi esistono in un elaboratore processori dedicati alla grafica, alle operazioni sui numeri reali, alla acquisizione di segnali analogici. Da soli i canali non possono però garantire la piena autonomia di funzionamento. Per rendere indipendenti i processori dedicati è stato introdotto nell’architettura hardware un segnale detto delle interruzioni mediante il quale una qualsiasi entità esterna alla CPU può richiederle attenzione. Figura 16 - Interruzioni Con la presenza del segnale di interruzione la CPU può attivare un processore periferico e disinteressarsi delle sue attività. Quando un processore dedicato termina il suo compito, avanza una richiesta di interruzione al processore centrale e aspetta che gli venga rivolta attenzione. Mentre i processori periferici lavorano, la CPU può lavorare anch’essa a meno che non sia indispensabile quanto richiesto allo specifico processore: in questo caso la CPU aspetta che il processore concluda quanto richiesto. Solitamente le attività svolte dai processori dedicati sono lente, per cui la CPU può svolgere attività diverse dando l’impressione all’utente esterno di farle contemporaneamente. Le richieste di interruzione sono diverse, tante quanti sono i processori dedicati, e si verificano in momenti diversi ed in modo non sincrono con il lavoro della CPU. Per consentire alla CU di accorgersi del verificarsi di una interruzione il registro di condizione CC è stato dotato di un bit che diventa uguale ad uno quando arriva una interruzione. La CU controlla il bit al termine delle esecuzione di ogni istruzione: se è uguale zero procede normalmente con il prelievo dell’istruzione successiva; in caso contrario comincia l’esecuzione di un programma del sistema operativo, detto ISR (interrupt service routine) che ha come compito primario di capire la causa della interruzione, ossia quale dispositivo ha avanzato la richiesta. Nel caso si accorga della presenza di più richieste 62 Capitolo secondo stabilisce quale servire per prima secondo criteri di importanza o priorità di intervento. Figura 17 – Modifica del ciclo del processore Con i canali e il sistema delle interruzione si è introdotto una prima forma di parallelismo nello svolgere le diverse attività richieste da un programma. Ad esempio la CPU dopo aver attivato il canale di output affinché stampi una sequenza di dati, può attivare il canale di input perché prelevi da tastiera un insieme di dati. Nell’attesa che i due canali segnalino di aver terminato, la CPU può procedere in parallelo eseguendo altre operazioni. La gestione dei canali è svolta dai programmi del sistema operativo, come del resto la gestione di tutti i componenti del modello di Von Neumann, memoria compresa. Anche alla memoria centrale sono stati apportati cambiamenti per evitare che la CPU si dovesse adeguare ai tempi più lenti di gestione dei dati da essa garantiti. Si è fino ad oggi verificato che componenti di memoria veloci avessero costi che ne impedivano una significativa presenza all’interno dei sistemi. Per tale motivo, per ridurre i tempi di trasferimento dalla memoria centrale ai registri interni della CPU, viene replicata una porzione di memoria e posta tra memoria e CPU stessa. Tale memoria, molto veloce, viene chiamata cache e fa da buffer per il prelievo di informazioni dalla memoria centrale. Con operazioni particolari istruzioni e dati vengono trasferiti dalla memoria centrale nella cache secondo la capacità di quest’ultima. La CU procede nelle tre fasi del suo ciclo al prelievo di istruzioni e operandi dalla cache. Quando la CU si accorge che il prelievo non può avvenire scatta un nuovo travaso dalla memoria centrale. Poiché la cache è molto più veloce della memoria centrale il sistema ne guadagna complessivamente in efficienza. Dato l’elevato costo dei componenti con i quali si realizzano le cache, si è soliti riscontrare processori con 256k byte o 512 k byte di memoria cache. Solo sistemi che devono garantire elevate prestazioni in applicazioni critiche presentano cache da 1Mbyte ed oltre. Se la cache è interna alla CPU viene detta di primo livello (L1); le cache di secondo livello (L2) sono invece esterne e solitamente un pò più lente di quelle di primo livello ma sempre più veloci della memoria centrale. Infatti la cache L2 risulta 4 o 5 volte più lenta della cache L1 mentre la RAM lo è addirittura 20 o 30 volte. I due livelli possono coesistere. La memoria viene così ad essere strutturata in maniera gerarchica. La gerarchia consente di offrire ai programmi l’illusione di avere una memoria grande Il modello di esecutore 63 e veloce. Nella gerarchia i livelli più prossimi alla CPU sono anche quelli più veloci. Ma sono anche quelli con dimensioni più piccole visto il loro elevato costo. Invece, quelli più lontani sono quelli che mostrano una capacità massima ed anche tempi di accesso maggiori. Partendo dalla CPU ogni livello fa da buffer al livello successivo. Figura 18 – Gerarchia di memorie 2.5. Il modello astratto di esecutore La macchina di Von Neumann è il modello di riferimento che consente di comprendere le modalità con le quali un elaboratore esegue in modo automatico una qualsiasi applicazione pensata per esso. Il modello si basa sul concetto di automa capace di eseguire un programma residente nella memoria centrale. In generale un programma è un insieme di istruzioni la cui descrizione dipende dalle capacità di comprensione di un linguaggio da parte del componente che svolge le funzioni di CPU. La CPU, come automa capace di interpretare un prefissato linguaggio, esemplifica il comportamento di un qualsiasi esecutore di programmi, sia esso umano o macchina. Tra tutti i linguaggi di programmazione il linguaggio macchina è quello direttamente interpretabile da una CPU reale presente in un sistema informatico. Dato il suo basso potere espressivo è anche detto linguaggio di basso livello. L’insieme di istruzioni (repertorio) che si possono descrivere comprendono operazioni che: - spostano stringhe di bit da un registro all’altro di memoria; - attivano l’ALU per effettuare la somma aritmetica, l’AND, l’OR tra coppie di stringhe di bit, o la negazione (NOT) del contenuto dell’accumulatore; - eseguono lo scorrimento o la rotazione a destra o a sinistra dei bit contenuti nell’accumulatore; - provvedono ad interrogare i bit del registro di condizione (CC) per determinare come procedere nell’esecuzione del programma; - consentono di saltare ad un qualsiasi punto del programma. Tutte le istruzioni del programma devono essere allocate nei registri di memoria prima che la CPU possa procedere alla loro interpretazione ed esecuzione. Lo schema di allocazione deve essere definito dal programmatore rispettando un principio fondamentale secondo cui deve sempre esistere una 64 Capitolo secondo separazione netta tra le porzioni di memoria occupate dai dati da quelle occupate dalle istruzioni. Un tale principio serve a garantire che in ogni istante la CPU stia eseguendo effettivamente il programma progettato dal programmatore. Infatti l’allocazione in memoria di dati e programmi può essere statica o dinamica. Nel primo caso l’allocazione avviene prima dell’inizio dell’esecuzione del programma; nel secondo durante la sua esecuzione. Nel caso di allocazione dinamica di dati può avvenire che, per errori di programmazione, alcuni dati vadano ad allocarsi nell’area riservata alle istruzioni cambiando il contenuto dei registri di memoria e cambiando di fatto la struttura del programma. L’allocazione in memoria comporta un’associazione precisa tra istruzioni e dati e registri. In un modello di memoria a voce ad ogni istruzione o dato corrisponde un ed un solo registro di memoria. Nelle memorie a byte istruzioni o dati possono occupare più registri di memoria. Per semplicità si procederà pensando ad una memoria organizzata a voce. Il riferimento ad una istruzione o ad un dato avviene specificando l’indirizzo di memoria occupato. L’indicazione di un indirizzo di memoria contenente un dato si dirà puntatore a dato, il puntatore a istruzione è invece un indirizzo di un registro di memoria nel quale è collocata una istruzione. Si può allora definire una istruzione in linguaggio macchina come una quadrupla: i = (Cop, Pdi, Pdo, Pis), in cui: - Cop è il codice operativo, ossia il codice che indica alla UC della CPU l’operazione da compiere; l’insieme dei Cop prendere il nome di repertorio di istruzioni e dipende dalla specifica CPU; - Pdi sono i puntatori ai dati che servono per svolgere l’operazione Cop detti anche di input; si noti che esistono istruzioni che non hanno operandi di input; - Pdo sono i puntatori ai dati prodotti dall’operazione Cop detti anche di output; si noti che esistono istruzioni che non hanno operandi di output; - Pis è il puntatore all’istruzione da svolgere al termine dell’esecuzione di quella corrente. Il puntatore Pis serve a comprendere la corrispondenza tra struttura di un programma e schema di allocazione in memoria. Un programma è una sequenza di istruzioni da svolgere una dopo l’altra. Ogni istruzione occupa un registro di memoria. Il puntatore Pis serve a legare tra loro i registri di memoria contenenti istruzioni in modo che sia chiara la sequenza del programma. Il modello di esecutore 65 Figura 19 – Esecuzioni di programmi nel modello astratto di processore La definizione della struttura dell’istruzione e dello schema di allocazione dei programmi in memoria permettono di dettagliare le fasi del ciclo del processore. La fase fetch inizia con il prelievo dell’istruzione dalla memoria. Per farlo la CU comunica alla memoria il valore del puntatore ad istruzione presente nel registro PI. La risposta della memoria viene depositata nel registro IR così da consentire alla CU di: - interpretare il codice operativo dell’istruzione da eseguire; - conoscere i puntatori ai dati di input ed output; - ricevere il puntatore all’istruzione da eseguire successivamente. La fase fetch si conclude con l’aggiornamento del registro PI con il valore del puntatore all’istruzione successiva presente in IR. La fase operand assembly serve alla CU per predisporre gli operandi che servono al codice operativo. Per farlo la CU usa i puntatori ai dati contenuti nel registro IR. La fase execute consiste nel mettere in essere le azioni richieste con il codice operativo presente nel registro di IR. Nel caso vengano prodotti risultati, ne verrà effettuata la memorizzazione negli indirizzi specificati dai puntatori ai dati di output presenti nel registro IR. Figura 20 – Aggiornamento registri CPU 66 Capitolo secondo Al termine dell’esecuzione di una istruzione la CU riprende il suo cammino partendo dal contenuto del registro PI. Le istruzioni del programma vengono eseguite una dopo l’altra indipendentemente dalla loro disposizione in memoria. Si può allora introdurre una notevole semplificazione imponendo al programmatore di disporre le istruzioni ad indirizzi consecutivi di memoria e facendo in modo che la CU nella fase fetch aggiorni il PI semplicemente incrementando di uno il suo contenuto. In tale modalità le istruzioni non devono più riportare il Pis in quanto implicitamente il suo valore è dato dal valore del registro PI più uno. Figura 21 – Aggiornamento registri CPU con istruzioni consecutive in memoria Perchè il ciclo del processore possa avere inizio si deve predisporre in modo che il registro PI contenga l’indirizzo del registro di memoria contenente la prima istruzione da eseguire. La fase iniziale di boot ha solo il compito di inizializzare il PI con tale valore. Una volta avviato, il ciclo del processore non termina mai e quindi ad ogni istruzione deve sempre seguirne un’altra da eseguire successivamente. Questo spiega perchè quando termina un’applicazione un elaboratore torna ad eseguire i programmi del sistema operativo. Dai programmi del sistema operativo si passa ad un’altra applicazione in un’alternanza che fa sì che la CU possa procedere con il suo ciclo. Ma perché tutto ciò proceda nel rispetto del modello di Von Neumann, deve avvenire che in memoria siano sempre presenti i programmi e i dati del sistema operativo mentre quelli delle applicazioni vengono caricati in memoria dal sistema operativo su richiesta dell’utente prima che ne venga attivata la esecuzione. Nella memoria di un elaboratore moderno si possono pertanto individuare in ogni istante cinque aree distinte: - l’insieme dei registri nei quali si trovano i programmi del sistema operativo; - quelli occupati dai dati del sistema operativo; - quelli nei quali si trovano le applicazioni di utente; - quelli con i dati dei programmi di utente; - ed infine l’insieme dei registri che servono come buffer per il trasferimento dati da e verso i dispositivi di input ed output. Il modello di esecutore 67 Figura 22 – Programmi in memoria Il modello di Von Neumann può essere generalizzato. La memoria si può sostituire con un foglio di lavoro sul quale sono segnate le istruzioni. Il foglio serve anche per scrivere i dati. L’esecutore è un processore di Von Neumann se è in grado di: - leggere le istruzioni una alla volta dal foglio di lavoro; - interpretare il linguaggio con il quale le istruzioni sono scritte; - leggere e scrivere i dati sul foglio di lavoro; - compiere le azione prescritte dalle istruzioni. 2.6. I microprocessori Come già descritto, un processore centrale (CPU) conforme al modello di Von Neumann si compone logicamente di: - una unità di controllo (UC) capace di interpretare i comandi ad esso rivolti (detti anche istruzioni), di svolgere le azioni richieste da tali comandi e di interagire con l’ambiente esterno attraverso dispositivi periferici capaci di trasformare i segnali in modo tale che siano comprensibili dalla UC da una parte, e, dagli utenti del sistema dall’altra; - una unità logico aritmetica (ALU) per l'esecuzione delle operazioni di tipo aritmetico (solitamente somma) e logico (somma, prodotto e negazione logici). Figura 23 – Schema di una CPU 68 Capitolo secondo L'architettura di Von Neumann viene detta di tipo SISD (Single Istruction stream, Single Data stream) in quanto le istruzioni di un programma vengono eseguite una dopo l’altra serialmente con l’unità di controllo che interpreta le singole istruzioni generando comandi per tutti gli altri dispositivi, e con l'unità di elaborazione che esegue le operazioni di tipo aritmetico o logico. La disponibilità di registri interni come propri organi di memoria per compiti speciali e/o per contenere informazioni temporanee, consente di ridurre i tempi di esecuzione delle istruzioni non solo perché si riducono gli accessi alla memoria centrale ma anche perché i trasferimenti interni al processore sono più veloci. Il programma e i dati sono contenuti in memoria ed una singola istruzione viene eseguita mediante le seguenti fasi: 1. lettura dalla memoria dell'istruzione da eseguire; 2. determinazione dell'indirizzo della successiva istruzione da eseguire; 3. determinazione del significato del codice operativo per individuare l’azione da eseguire; 4. eventuale determinazione degli indirizzi degli operandi; 5. esecuzione delle sequenze di operazioni elementari richieste dalla istruzione. Il processore è una macchina sequenziale capace di svolgere un’azione alla volta: pertanto un’istruzione viene eseguita in passi successivi con azioni elementari abilitate dal segnale di tempificazione (clock) e il suo tempo di esecuzione dipende dal numero di passi per eseguirla e dalla frequenza del clock. I microprocessori sono dispositivi elettronici in grado di contenere all’interno di un unico circuito integrato le funzioni di un’intera CPU. Il microprocessore interagisce con tutti gli altri dispositivi attraverso i collegamenti dei bus di dati (data bus), di indirizzo (address bus) e di controllo (control bus). I microprocessori hanno bus dati a 8, 16, 32, 64 bit. La dimensione del bus dati esprime la capacità di elaborazione del processore, ossia la quantità di bit che possono essere elaborati in parallelo. Il bus indirizzi esprime, di contro, la capacità di memorizzazione del processore, ossia il numero di celle diverse a cui si può accedere (2m celle di memoria, se m è il numero dei bit del bus). La tabella riporta alcuni valori tipici del parallelismo esterno. Bit Data BUS 8 16 64 Bit Address BUS 16 20-24 64 Capacità di indirizzamento 64 KByte 1-16 MByte fino a circa 1019 Byte Tabella 1 – Capacità di indirizzamento Molti sono oggi i microprocessori presenti sul mercato ed una loro classificazione può essere fatta sulla base dei seguenti parametri: - parallelismo esterno espresso come numero di bit trasferiti o prelevati in un singolo accesso in memoria (8, 16, 32, 64,...) e caratterizzanti quindi il suo data bus; - capacità di indirizzamento legata alla dimensione in bit del suo address bus Il modello di esecutore - 69 numero, tipo e parallelismo dei registri interni; tecniche di indirizzamento intese come la modalità con la quale costruire l’indirizzo logico con il quale prelevare o salvare il valore dell’operando di una istruzione; - gestione delle periferiche di input ed output; - repertorio delle istruzioni inteso come numero e tipo di istruzioni costituenti il suo linguaggio macchina; - tempi necessari all’esecuzione di alcune istruzioni fondamentali come l'addizione da utilizzare per la valutazione del MIPS con il quale effettuare confronti sulle prestazioni. I microprocessori sono quindi caratterizzati anche dal numero e tipo di registri interni di cui sono dotati. Nel tempo il numero di registri interni è aumentato per rendere più veloce l’esecuzione delle istruzioni consentendo l’attivazione in parallelo di alcune microoperazioni della unità di controllo. L’insieme minimo di registri interni che sicuramente è presente in qualsiasi microprocessore moderno è formato da: - Prossima Istruzione (PI) o anche Program Counter: che nella fase fetch cambia il proprio contenuto passando da puntatore in memoria all’istruzione da prelevare all’inizio della fase a puntatore all’istruzione successiva da eseguire al termine della stessa; - Accumulatore (ACC): utile a conservare dati temporanei o necessario per attivare le operazioni logiche ed aritmetiche dell’ALU; infatti attraverso esso i dati vengono forniti all’ALU prima di una operazione e in esso si trova il risultato dell’operazione eseguita; alcuni microprocessori possono presentare anche due accumulatori; - Condition Code (CC): che riporta lo stato dell’elaborazione indicato dai diversi bit di cui si compone: o Bit Segno (S): indica con 1 la presenza in ACC di un valore negativo; con 0 il caso opposto; o Bit Zero (Z): indica con 1 la presenza in ACC di un valore uguale a zero; con 0 il caso opposto; o Bit Overflow (O): indica con 1 la presenza in ACC di un valore non corretto in quanto l’operazione aritmetica ha generato un risultato con più bit di quelli rappresentabili; con 0 il caso opposto; o Bit Riporto o Carry (C): indica con 1 la presenza in ACC di un risultato che ha generato un ulteriore bit nel calcolare una somma; con 0 il caso opposto; o Bit Interruzione (I): indica con 1 che il sistema delle interruzioni ha generato una richiesta di attenzione; con 0 il caso opposto; - Registo Indice (X): con il quale poter calcolare l’indirizzo dell’operando con quella che sarà chiamata tecnica di indirizzamento relativa; - Stack Pointer (SP): utile alla gestione del salti a sottoprogrammi per il suo modo di gestire gli indirizzi di memoria; lo stack pointer contiene infatti un puntatore alla memoria ad una area che viene chiamata stack; attraverso SP si accede a tale area con operazioni di inserzione (PUSH) o di estrazione (POP) con la tecnica LIFO (last in – first out), che comporta che l’ultimo elemento inserito sia anche il primo ad essere estratto. 70 Capitolo secondo L’accumulatore si interfaccia direttamente con il data bus presentando la stessa dimensione in numero di bit. I registri prossima istruzione, indice e stack pointer sono collegati invece con l’address bus al quale comunicano l’indirizzo dopo averlo costruito secondo quanto richiesto dalle diverse istruzioni. Figura 24 – Registri interni di un microprocessore La figura 24 illustra le dimensioni dei registri in un architettura classica che vede il parallelismo del data bus fissato ad 8 bit ed una capacità di indirizzamento (parallelismo dell’address bus) a 16 bit. I moderni processori operano con registri di accumulatore a 16 o 32 bit e parallelismo dell’address bus a 32 o 64 bit. A tali registri fondamentali si è soliti aggiungere tre ulteriori registri: - Istruction Register (IR): contenente al termine della fase fetch l’istruzione prelevata dalla memoria centrale completa di tutte le sue parti (codice operandi e puntatori ad operandi se necessari); - Data buffer (DB): interfaccia con il data bus del quale rappresenta lo stato, in altri termini il registro DB indica il valore che assume il data bus; - Address buffer (AB): interfaccia con l’address bus del quale rappresenta lo stato, in altri termini il registro AB indica il valore che assume l’address bus. Tali tre registri vengono introdotti per schematizzare il comportamento della CPU nella sua interazione con la memoria centrale. Essi riportano lo stato del processore durante le diverse fasi (fetch, operand assembly ed execute) del suo ciclo. Figura 25 – Schema di una CPU Il modello di esecutore 71 In figura 25 si riporta l’architettura di riferimento di un processore dotato dell’insieme di registri indicati con le principali connessioni tra essi. Nella costruzione dell’indirizzo di un operando di una istruzione le tecniche di indirizzamento più diffuse sono: - indirizzamento immediato che indica che il valore è contenuto già nell’istruzione; - indirizzamento diretto con il quale viene riportato nell’istruzione l’indirizzo del registro di memoria che contiene il valore o nel quale depositare il valore; - indirizzamento indiretto che riporta nell’istruzione l’indirizzo del registro di memoria al cui interno è specificato l’indirizzo del registro dal quale prelevare un valore o nel quale depositare un valore; - indirizzamento relativo con il quale l’indirizzo del registro di memoria che contiene il valore o nel quale depositare il valore è specificato nel registro interno del processore detto indice. Le diverse tecniche di indirizzamento vengono indicate nell’istruzione, o diversificando il codice operativo, o aggiungendo dei bit appositi il cui valore indica alla UC come costruire l’indirizzo. Ad esempio per la semplice istruzione per il caricamento dell’accumulatore si potrebbero avere in linguaggio macchina (rappresentato in esadecimale) le quattro istruzioni di tabella: Cod Op. Operando Tecnica 60 00ff Immediata 61 00ff Diretta 62 00ff Indiretta 63 Relativa Commento Accessi in memoria nella fase Operand Assembly LOAD ACC con il valore nessuno in quanto il dato è 255 prelevato nella fase fetch con l’istruzione LOAD ACC con il un solo accesso in contenuto del registro di memoria memoria di indirizzo 255 LOAD ACC con il due accessi in memoria: il contenuto del registro di primo per prelevare memoria il cui indirizzo è l’indirizzo alla posizione contenuto nel registro di indicata; il secondo per indirizzo 255 prelevare il dato LOAD ACC con il un solo accesso in contenuto del registro di memoria memoria il cui indirizzo è presente nel registro indice X Tabella 2 – Esempi di indirizzamento In uno stato del processore come quello riportato in figura 26, gli effetti delle diverse tecniche di indirizzamento per quanto attiene alla esecuzione della istruzione di caricamento dell’accumulatore sono mostrati in figura 27. 72 Capitolo secondo Figura 26 – Stato iniziale della CPU Immediato Diretto Indiretto Relativo Figura 26 – Effetti delle tecniche di indirzzamento Nella gestione delle periferiche di input ed output (I/O) i microprocessori si dividono in quelli che usano la tecnica memory-mapped e in quelli che invece adottano l’I/O-mapped. Con la tecnica memory mapped l’UC usa le stesse istruzioni, utilizzate per leggere e scrivere in memoria, anche per accedere ai dispositivi di I/O. I dispositivi di I/O hanno quindi dei propri indirizzi che devono essere riservati e non sovrapposti a quelli usati per la memoria. I dispositivi di I/O controllano il bus indirizzi e rispondono solo quando riconoscono un indirizzo a loro assegnato. Con l’I/O-mapped vengono invece usate istruzioni specifiche per l'esecuzione dell'input/output. I dispositivi di I/O hanno uno spazio indirizzi separato da quello della memoria, e un segnale del control bus serve alla UC per specificare se si tratta di un accesso alla memoria o ad un dispositivo periferico. Il vantaggio dell'uso del memory-mapped è che, non richiedendo da una parte hardware aggiuntivo per la gestione della periferia e dall’altra un insieme di Il modello di esecutore 73 istruzioni specifiche, consente la realizzazione di CPU con una complessità inferiore, più economiche, veloci e facili da costruire. Il repertorio delle istruzioni ha indirizzato i costruttori di microprocessori verso due distinte tecnologie: CISC (Complex Instruction Set Computer) e RISC (Reduced Instruction Set Computer). I processori CISC sono quelli nei quali il crescere delle potenzialità è stato accompagnato da un aumento delle operazioni che sono capaci di svolgere, inserendo nel linguaggio macchina istruzioni con potenza espressiva prossima a quella dei linguaggi di programmazione di alto livello. I processori CISC sono caratterizzati quindi da un ampio repertorio di istruzioni anche se molte di esse non risultano strettamente necessarie, potendosi ottenere con l’esecuzione di sequenze di istruzioni più semplici. Il gran numero di istruzioni di cui sono dotati i processori CISC ha comportato una loro maggiore complessità costruttiva. La caratteristica di un microprocessore RISC è quella di possedere un repertorio costituito da un ridotto ed essenziale insieme di istruzioni al fine di ottenere processori più veloci e di costo ridotto, data la minore complessità del loro progetto. Le due tecnologie hanno implicazioni diverse a seconda della tipologia di programmazione adottata. Nel caso di programmazione direttamente in linguaggio macchina, il ricco repertorio di istruzioni del CISC rende lo sviluppo di programmi più semplice. La maggiore complessità dello sviluppo dei programmi nell'architettura RISC è legata alla necessità di dovere realizzare con istruzioni semplici e in numero ridotto qualsiasi operazione più complessa: per tale motivo, diviene di primaria importanza la ottimizzazione del codice. Se però si analizza l'utilizzazione pratica di set di istruzioni estese dei CISC, si trova che statisticamente solo un numero molto ridotto di essi viene utilizzato, e ciò non solo perché il 90% di esse può essere sintetizzato in un restante sottoinsieme del 10%, ma anche perchè l'utilità di disporre di un ampio set di istruzioni è diminuito nel tempo a fronte dei progressi compiuti dalle tecniche di sviluppo dei compilatori. Se infatti si analizza il codice generato da un compilatore, si constata che solo una parte delle istruzioni vengono impiegate. L'obiettivo fondamentale dell'approccio RISC è, in definitiva, disporre di tale insieme fondamentale di istruzioni per ridurre al minimo il numero dei cicli di macchina (clock) necessari per loro esecuzione. Tutte le istruzioni RISC fondamentali hanno la stessa durata (un ciclo macchina), la stessa lunghezza e lo stesso formato. In questa accezione il RISC rappresenta un nuovo livello di ottimizzazione tra hardware e software, in cui il primo viene semplificato al massimo per raggiungere la massima velocità operativa, mentre il secondo si assume l'onere di compensare la rigidità introdotta nell'hardware. Relativamente alla tipologia di istruzioni esiste un ulteriore elemento caratterizzante i microprocessori e consistente nel numero di operandi espliciti presenti nell’istruzione. Si possono avere pertanto microprocessori il cui linguaggio macchina gestisce: - un solo operando; - due operandi; - tre operandi. Si noti che se da un lato il maggior numero di operandi semplifica l’attività di programmazione offrendo istruzioni più compatte, dall’altra rende anche il processore più complesso. 74 Capitolo secondo 2.7. Un modello di processore Da un’analisi dei processori esistenti, si può ricavare per fini didattici un modello capace di farne comprendere non solo il funzionamento. ma anche le modalità che ne permettono la programmazione in linguaggio macchina. Tale processore verrà da qui di seguito indicato con MP, o Modello di Processore. Per semplificare la presentazione dei concetti di base, al posto del linguaggio macchina in binario o in formato più compatto esadecimale, si introduce il linguaggio assemblativo. In un linguaggio assemblativo si mantiene la corrispondenza uno ad uno con il linguaggio macchina, ma al codice operativo si associa un codice mnemonico più semplice da comprendere e ricordare, ed al posto di indirizzi e valori da riportare in binario si introducono i valori nella loro rappresentazione esterna (i numeri in decimale, i caratteri nel formato ASCII). Ad ogni istruzione si può affiancare un’etichetta per far riferimento ad essa in altre istruzioni, e si può evidenziare nella istruzione la tecnica di indirizzamento scelta. L’assemblatore è un programma che esegue la traduzione di un programma, scritto in linguaggio assemblativo, in linguaggio macchina ed è il più semplice traduttore di linguaggi presente in informatica. Non deve far altro che: - far corrispondere ai codici mnemonici del codice operativo il rispettivo codice binario; - convertire da decimale a binario indirizzi e valori dei dati; - determinare gli indirizzi delle etichette associate alle istruzioni; - convertire dati alfanumerici nella loro rappresentazione binaria. Per la semplicità del linguaggio assemblativo, la struttura di una istruzione è rigida si può vedere composta delle seguenti parti: Label Codice Op. Tecnica Indirizzamento Operando Commento La label non sempre è presente così come il commento a fine frase che serve a spiegare le ragioni per le quali viene introdotta l’istruzione nel programma. Il campo Tecnica Indirizzamento indica la modalità di composizione dell’indirizzo; più precisamente valgono le corrispondenze in tabella 3. Notazione Tecnica indirizzamento = Immediata ^ Diretta (^) Indiretta (X) Relativa Tabella 3 – Modalità di indirizzamento e notazioni Non tutte le istruzioni prevedono un campo operando: è ovvio che se l’operando non è previsto, anche la notazione relativa alla tecnica di indirizzamento risulta essere assente. Il processore MP ha allora le seguenti caratteristiche: - è dotato dei registri interni PI, IR, SP, X, ACC e CC; - è I/O mapped; - è di tipo CISC; Il modello di esecutore - 75 gestisce la rappresentazione per complemento alla base considerando il bit di peso maggiore dell’ACC come indicatore del segno (1 per numeri negativi e zero in caso contrario); ha una struttura delle istruzioni ad un operando; ed è a sua volta parte di una architettura composta da: una memoria centrale di grandi dimensioni i cui registri di memoria sono contenitori di informazioni di qualsiasi tipo e dimensione; un canale di input (detto standard input) per gestire l’immissione dati da tastiera; un canale di output (detto standard output) per la gestione della presentazione dei risultati su di un monitor. - Figura 28 – Schema del processore MP La caratteristica dei registri di memoria di essere contenitori di informazioni di qualsiasi tipo e dimensione viene introdotta per esemplificare il funzionamento del modello, senza peraltro dover descrivere in dettaglio le operazioni sui diversi dati in funzione della specificità della loro rappresentazione. Si potrà sempre dimostrare che un registro di dimensione non specificata si riconduce ad una sequenza di registri posti ad indirizzi consecutivi contenenti tutti gli elementi posti nell’unico contenitore. Se ad esempio nel nostro modello si assume che un registro possa contenere il messaggio “Errore”, in un sistema reale fatto di una memoria organizzata a byte serviranno 6 registri contenenti ciascuno le lettere del messaggio. Anche l’accumulatore, per la stessa motivazione, sarà considerato un contenitore di informazioni di qualsiasi tipo e dimensione. Coerentemente con tale ipotesi si potrà assegnare ad un registro un valore numerico o una informazione alfanumerica: nel primo caso un $ davanti al numero ne indica una rappresentazione esadecimale ($F0AC); nel secondo caso le virgolette racchiuderanno la sequenza di caratteri (“questo è un esempio”). L’ipotesi semplificativa fatta impatta anche sulla allocazione delle istruzioni in quanto consente di assumere che una intera istruzione venga ad essere contenuta in un unico registro. Se una siffatta impostazione ha riguardato i primi calcolatori dotati di una struttura della memoria detta a voce, nelle architetture moderne organizzate a byte le istruzioni hanno una lunghezza variabile. Solitamente il repertorio delle istruzioni riesce ad essere gestito con un solo byte mentre l’operando, se presente può occupare più byte a seconda del valore espresso o della capacità di memoria nel caso si tratti di un indirizzo. Nel caso reale l’UC riconosce dal codice operativo dell’istruzione anche la sua lunghezza 76 Capitolo secondo e ne opera il prelievo ad indirizzi consecutivi a quello espresso dal PI: in tale caso, alla fine della fase fetch, il PI viene pertanto ad essere incrementato di un valore uguale proprio alla lunghezza dell’istruzione. Il repertorio di istruzioni di MP si compone di: - istruzione di lettura e modifica dei registri interni e di memoria; - istruzioni di tipo aritmetico; - istruzioni di tipo logico; - istruzioni di salto; - istruzioni di input ed output per la gestione della interazione con il mondo esterno. Nel primo sottoinsieme rientrano tutte le istruzioni di gestione dei registri di memoria con una delle tecniche di indirizzamento introdotte, dei registri interni ACC, X, CC e SP. Le istruzioni di tipo aritmetico e logico depositano il loro risultano nell’accumulatore modificando i bit del CC di segno, riporto, zero ed overflow. Le istruzioni di salto alterano il normale flusso di controllo del programma prescrivendo che la esecuzione non proceda con l’istruzione successiva il cui indirizzo è specificato in PI, ma con quella indicata dal valore dell’operando secondo modalità diverse. Per ogni istruzione verrà riportato il codice operativo (CodOP) in linguaggio assemblativo, l’operando se previsto, la operazione eseguita, una breve descrizione e le eventuali modifiche apportate ai bit del CC (0 o 1 a seconda dei casi, o A ad indicare la dipendenza dal valore assunto dall’accumulatore). Con la notazione [registro] si indicherà il contenuto del registro specificato (ad esempio [X] indica il contenuto del registro indice), mentre con M([registro]) si indicherà il registro di memoria il cui indirizzo è specificato in registro. La presenza nel campo operando di OP indica che l’istruzione richiede che venga determinato un valore sulla base di una delle tecniche di indirizzamento specificate: - è proprio OP per l’immediato, - è M(OP) per il diretto, - è M([M(OP)]) per l’indiretto - è M([X]) per il relativo. Per semplificare si indicherà con [OP] il valore determinato. Le istruzioni di gestione dell’accumulatore sono quelle riportate in tabella 4. CodOP Operando Operazione Descrizione copia in ACC il dato come determinato dalla LDA OP ACC = [OP] STA OP [OP] = ACC PSHA M([SP]) = ACC SP = SP - 1 PULA SP = SP + 1 ACC = M([SP]) CLRA NEGA ACC = 0 ACC=0 - [ACC] tecnica di indirizzamento copia nel registro di memoria, il cui indirizzato è determinato dalla tecnica di indirizzamento, il valore di ACC prima copia ACC nell’area stack di memoria, ossia all’indirizzo contenuto in SP; dopo viene decrementato il valore di SP per consentire un successivo inserimento nell’area dopo aver incrementato il contenuto del registro SP, copia in ACC il contenuto del registro di memoria indicato da SP azzera il contenuto di ACC calcola il complemento alla base del contenuto di ACC Tabella 4 – Istruzioni per la gestione dell’accumulatore Il modello di esecutore 77 Le istruzioni di gestione del registro X sono quelle in tabella 5. CodOP Operando Operazione Descrizione copia in X il dato come determinato LDX OP X = [OP] STX OP [OP] = X dalla tecnica di indirizzamento copia nel registro di memoria, il cui indirizzato è determinato dalla tecnica di indirizzamento, il valore di X Tabella 5 – Istruzioni per la gestione del registro indice Le istruzioni di gestione del registro SP sono quelle in tabella 6. CodOP Operando Operazione Descrizione copia in SP il dato come determinato LDSP OP SP = [OP] STSP OP [OP] = SP dalla tecnica di indirizzamento copia nel registro di memoria, il cui indirizzato è determinato dalla tecnica di indirizzamento, il valore di SP Tabella 6 – Istruzioni per la gestione del registro SP Le istruzioni di tipo aritmetico sono quelle in tabella 7. CodOP Operando Operazione Descrizione modifica il contenuto INCA ACC = [ACC] +1 DECA ACC = [ACC] -1 INCX X = [X] +1 DECX INCS X = [X] -1 SP = [SP] +1 DECS ADDA OP SP = [SP] -1 ACC = [ACC] + [OP] SUBA OP ACC = [ACC] - [OP] di ACC aggiungendo 1 modifica il contenuto di ACC sottraendo 1 modifica il contenuto di X aggiungendo 1 modifica il contenuto di X sottraendo 1 modifica il contenuto di SP aggiungendo 1 modifica il contenuto di X sottraendo 1 Somma al contenuto di ACC il valore determinato sulla base della tecnica di indirizzamento Sottrae al contenuto di ACC il valore determinato sulla base della tecnica di indirizzamento Tabella 7 – Istruzioni aritmetiche Le istruzioni di tipo logico in tabella 8. CodOP Operando Operazione NOTA ACC = not[ACC] ANDA OP ACC = [ACC] and [OP] ORA OP ACC = [ACC] or [OP] Descrizione complemento di ACC; i bit 1 vengono cambiati in 1 e quelli 0 in 1 Prodotto logico tra il contenuto di ACC e il valore determinato sulla base della tecnica di indirizzamento; l’and viene eseguito sui singoli bit coinvolgendo quelli che occupano la stessa posizione Somma logica tra il contenuto di ACC e il valore determinato sulla base della tecnica di indirizzamento; l’or viene eseguito sui singoli bit coinvolgendo quelli che occupano la stessa posizione Tabella 8 – Istruzioni di tipo logico Le istruzioni di confronto, utili a modificare i bit del CC, sono in tabella 9. 78 Capitolo secondo CodOP BITA Operando OP Operazione [ACC] and [OP] CMPA OP [ACC] - [OP] TST OP [OP] – 0 TSTA [ACC] – 0 Descrizione Se Z è diverso da zero, indica che ACC ha i bit uguali a 1 nelle stesse posizioni di OP Se Z è uguale a zero ACC è OP hanno la stessa configurazione di bit Se OP è uguale a zero Z è uguale ad 1 Se OP è negativo S è uguale ad 1 Se ACC è uguale a zero Z è uguale ad 1 Se ACC è negativo S è uguale ad 1 Tabella 9 – Istruzioni di confronto Le istruzioni di salto sono in tabella 10. CodOP Operando Operazione JMP Address PI = Address BRZ Address Z=1 PI = Address BRNZ Address Z=0 PI = Address BRS Address S=1 PI = Address BRNS Address S=0 PI = Address BRO Address O=1 PI = Address BRNO Address O=0 PI = Address BRC Address C=1 PI = Address BRNC Address C=0 PI = Address Descrizione Salto all’istruzione presente all’indirizzo specificato Se il bit Z è 1 salta all’istruzione all’indirizzo specificato altrimenti prosegui in sequenza Se il bit Z è 0 salta all’istruzione all’indirizzo specificato altrimenti prosegui in sequenza Se il bit S è 1 salta all’istruzione all’indirizzo specificato altrimenti prosegui in sequenza Se il bit S è 0 salta all’istruzione all’indirizzo specificato altrimenti prosegui in sequenza Se il bit O è 1 salta all’istruzione all’indirizzo specificato altrimenti prosegui in sequenza Se il bit O è 0 salta all’istruzione all’indirizzo specificato altrimenti prosegui in sequenza Se il bit C è 1 salta all’istruzione all’indirizzo specificato altrimenti prosegui in sequenza Se il bit C è 0 salta all’istruzione all’indirizzo specificato altrimenti prosegui in sequenza Tabella 10 – Istruzioni di salto Mentre il JMP (jump) è un salto incondizionato, tutti i BRANCH sono salti condizionati al valore del bit del CC interessato dall’istruzione. Se la condizione non viene verificata, il PI non viene modificato e l’esecuzione continua con l’istruzione successiva a quella di salto. Le istruzioni di gestione dei sottoprogrammi sono in tabella 11. CodOP Operando Operazione Descrizione (Jump to subroutine) JSR Address M([SP]) = PI Salto all’istruzione indicata da address SP = [SP] - 1 dopo aver salvato il valore di PI PI = Address nell’area stack RTS SP = [SP] + 1 PI = M([SP]) (Return from subroutine) Salta all’istruzione il cui indirizzo viene prelevato dall’area stack Tabella 11 – Istruzioni di gestione sottprogrammi Il modello di esecutore 79 Le istruzioni di gestione del CC sono in tabella 12. CodOP Operando Operazione Descrizione CLS S=0 pone a zero il bit S SETS S=1 pone a uno il bit S CLZ Z=0 pone a zero il bit Z SETZ Z=1 pone a uno il bit Z CLO O=0 pone a zero il bit O SETO O=1 pone a uno il bit O CLC C=0 pone a zero il bit C SETC C=1 pone a uno il bit C CLI I=0 pone a zero il bit I SETI I=1 pone a uno il bit I Tabella 12 – Istruzioni di gestione del registro CC Infine le istruzioni di input ed output sono quelle in tabella 13. CodOP Operando Operazione Descrizione IN ACC = [input Il dato prelevato dall’esterno (per inserimento da tastiera) dal canale di standard] OUT output [ACC] standard = input viene depositato in ACC Il valore di ACC viene fornito al canale di output per essere mostrato all’esterno (sul monitor) Tabella 13 – Istruzioni di I/O Si può osservare che non tutte le istruzioni sono realmente necessarie. Infatti molte di esse possono essere ricavate da altre. Non a caso il processore MP è stato classificato come CISC proprio perché è stata privilegiata la presenza di un ricco numero di istruzioni al fine di renderne più semplice la programmazione. In tabella 14 sono riportati alcuni esempi di ridondanza del repertorio introdotto. Codice Operativo Equivalenza INCA STA (^)2000 LDA (=)1 ADDA (^)2000 CLRA LDA (=)0 NEGA NOTA INCA Tabella 14 – Istruzioni Equivalenti 2.7.1. La programmazione in linguaggio assemblativo La programmazione nel linguaggio assemblativo è resa difficile non solo dalla bassa potenza espressiva delle istruzioni che devono essere utilizzate, ma anche dall’obbligo che ha il programmatore di pianificare la disposizione in memoria di dati ed istruzioni. Per ogni programma si deve provvedere a suddividere la memoria in tre aree fondamentali: - un’area istruzioni necessaria per contenere l’intero programma; - un’area dati per gestire i valori delle informazioni da trattare; - un’area stack importante per gestire sottoprogrammi e interruzioni. 80 Capitolo secondo Le tre aree devono essere adeguatamente dimensionate affinché non capiti che durante l’esecuzione del programma una di esse sconfini nelle altre. Se ciò accadesse si avrebbero effetti non prevedibili e di difficile diagnosi. Ad esempio, se durante l’esecuzione alcuni dati venissero memorizzati nell’area riservata alle istruzioni, accadrebbe che la UC incontrerebbe istruzioni diverse da quelle inserite dal programmatore. Altra causa di errore può generarsi per effetto di una espansione non controllata dell’area di stack a scapito dell’area dati: la sovrapposizione cambia il valore dei registri di memoria riservate a contenere i valori delle informazioni alterando quanto previsto dal programmatore. Se ad esempio in un registro di memoria è stato inserito un valore costante da cui dipende la correttezza dell’algoritmo, e tale registro viene ad essere modificato per effetto della espansione dell’area stack, si ha come naturale conseguenza la generazione di una condizione di errore. La memoria viene pertanto suddivisa ponendo: - l’area istruzioni ad indirizzi bassi in quanto l’allocazione delle istruzioni del programma deve essere fatta ad indirizzi consecutivi crescenti; - l’area stack ad indirizzi alti in quanto le operazioni di inserimento avvengono decrementando lo SP - l’area dati ad indirizzi intermedi tra le due precedenti aree. Supponendo quindi di disporre di una memoria con una capacità di 1000 registri, tutti i programmi dovrebbero almeno prevedere come prima istruzione quella di caricamento di SP con 999. Per comodità si assumerà anche che la prima istruzione sia collocata sempre nel registro di memoria di indirizzo uno. Con tale ipotesi perché il processore MP inizi l’esecuzione di un qualsiasi programma basterà forzare il PI ad azzerarsi in fase di boot. A seconda delle necessità, il programmatore dovrà disporre i dati in indirizzi compresi tra 1 e 999 dopo aver dimensionato lo spazio necessario a contenere le istruzioni del programma e l’area stack. Un primo e semplice programma per il nostro MP è quello in tabella 15. IM 1 2 Label CodOp LDSP LDA 3 4 5 6 OUT IN STA LDA 7 8 9 OUT IN ADDA 10 TI = = ^ = Operando 999 “Inserire dato” 500 “Inserire dato” Commento posizionamento iniziale di SP carica una stringa in accumulatore visualizza messaggio input di un dato da tastiera conserva primo dato carica una stringa in accumulatore visualizza messaggio input di un secondo dato da tastiera sommalo al primo dato prelevato da ^ 500 tastiera presenta il risultato al monitor OUT Tabella 15 – Esempio di programma per MP Per comodità è stata inserita la colonna IM (indirizzo di memoria) per indicare l’allocazione in memoria delle singole istruzioni. Per comprendere cosa il programma faccia se ne può tracciare la esecuzione elencando in successione non solo le azioni svolte, ma anche i suoi effetti sui registri interni e di memoria. Il modello di esecutore 81 Allo scopo viene inserita la tabella di trace (vedi tabella 16) che comprende tante colonne quanti sono i registri da osservare e tante righe quante sono le azioni svolte. Ogni riga riporta la fotografia dello stato in cui viene a trovarsi il sistema: l’insieme degli stati nell’ordine temporale di esecuzione rappresenta il processo svolto. Nella tabella di trace si aggiungono due colonne per elencare le interazioni del sistema con il mondo circostante: infatti i processi svolti dipendono fortemente da tale interazioni e sono casi particolari quelli nei quali ad un programma corrisponde un unico processo di esecuzione qualsiasi siano i dati immessi. Le due colonne aggiunte sono pertanto: - il file di output (FO) nel quale si elenca tutto ciò che viene presentato sull’output standard per effetto di operazioni d OUT; - il file di input (FI) dove sono elencati i dati di prova che possono essere inseriti dall’input standard per effetto di operazioni di IN. Nel caso in cui si fissi che i programmi siano caricati a partire dall’indirizzo 1, l’area dati inizi all’indirizzo 500, l’area stack parta da 999, si può strutturare la tabella di trace del programma precedente nel modo seguente riportando lo stato del sistema al termine della fase execute dell’UC. PI IR ACC SZOC X SP M(500) M(501) M(502) M(503) M(997) M(998) M(999) FO 2 LDSP 999 =999 3 LDA Inserire 0000 “=Inseri dato re dato” 4 OUT Inserire dato 5 IN 33 6 STA 33 ^500 7 LDA Inserire “=Inseri dato re dato” 8 OUT Inserire dato 9 IN 22 10 ADDA 55 ^500 11 OUT FI 33 22 55 Tabella 16 – Tabella di trace Si noti che per comodità di lettura nelle celle vengono riportati solo i cambiamenti sottintendendo che l’ultimo valore scritto è quello che rimane nel registro fin quando non interviene una successiva modifica. Il trace mostra che il programma esegue la somma di due numeri. Non si comprende però cosa accade dopo l’ultima istruzione di OUT soprattutto considerando che il ciclo del processore non ha mai termine e dopo la fase execute deve sempre occorrere una successiva fase di fetch. Per gestire tale situazione tutti i programmi che verranno presentati successivamente prevederanno come ultima l’istruzione un JMP sis_op che verrà illustrata solo alla fine del paragrafo. 82 Capitolo secondo Il programma seguente esegue lo scambio tra due registri di memoria nel senso che porta nel primo il valore del secondo e nel secondo quello del primo: per evitare che nello scambio uno dei due valori si perda deve essere usato un terzo registro che fa da buffer. Per semplificare la progettazione il programmatore può associare un nome ai registri di memoria usati per conservare dati. Ad esempio creata la seguente corrispondenza: Nome info01 info02 buffer Indirizzo di memoria 500 501 503 Tabella 17 – Corrispondenza nomi-indirizzi di memoria si potranno usare i nomi al posto degli indirizzi rendendo il programma più comprensibile. Un altro esempio di programma è quello in tabella 18. IM 1 2 Label CodOp LDSP LDA 3 4 5 6 OUT IN STA LDA 7 8 OUT IN 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 scambio Stampa Fine STA LDA STA LDA STA LDA STA LDA OUT LDA OUT LDA OUT LDA OUT JMP TI = = Operando 999 “Dato 1>” ^ = info01 “Dato 2>” ^ ^ ^ ^ ^ ^ ^ = info02 info1 buffer info2 info01 buffer info02 “Dato1:” ^ info01 = “Dato2:” ^ info02 sis_op Commento posizionamento iniziale di SP carica una stringa in accumulatore visualizza messaggio input di un dato da tastiera conserva primo dato carica una stringa in accumulatore visualizza messaggio input di un secondo dato da tastiera conserva secondo dato inizia scambio conserva il primo valore preleva il secondo valore copialo nel primo registro riprendi il primo valore portalo nel secondo registro carica messaggio visualizza messaggio visualizza valore carica messaggio visualizza messaggio visualizza valore salta all’indirizzo sis_op Tabella 18 – Esempio di programma per MP Il modello di esecutore 83 Il trace del programma è quello riportato nella tabella 19. PI 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 IR LDSP =999 LDA = “Dato1>” OUT ACC Dato 1> info 02 buffer M(997) M(998) M(999) FO FI Dato 1> IN 33 STA ^info01 Dato LDA 2> = “Dato 2>” OUT IN STA ^info02 LDA ^info01 STA ^buffer LDA ^info02 STA ^info01 LDA ^buffer STA ^info02 LDA =”Dato 1:” OUT SZO X SP info C 01 99 9 0000 33 33 Dato 2> 22 22 22 33 33 22 22 33 33 Dato 1: Dato 1: LDA 22 ^info01 OUT LDA Dato 2: =”Dato 2:” OUT LDA ^info02 OUT 22 Dato 2: 33 33 Tabella 19 – Tabella di trace Per evitare di dover ripetere sequenze identiche di istruzioni si possono introdurre i sottoprogrammi con le istruzioni JSR e RTS. Infatti la JSR esegue un salto all’indirizzo specificato dopo aver conservato nell’area di memoria gestita dallo stack il valore assunto dal PI: tale valore indica la posizione in memoria della istruzione successiva al JSR. Il salto fa procedere l’esecuzione con le istruzioni che compongono il sottoprogramma. L’istruzione RTS, posta al termine del sottprogramma, ripristina nel PI il valore che era stato conservato all’atto del JSR riprendendo in tale modo l’esecuzione del programma chiamante. 84 Capitolo secondo Nella figura seguente si è organizzato come sottoprogramma la sequenza formata dalla stampa il valore dell’accumulatore e dall’input di un nuovo dato da tastiera. Figura 29 – Gestione Sottoprogrammi Il trace in tabella 20 seguente mostra in dettaglio come avviene sia il salto al sottoprogramma che il ritorno da esso. PI IR LDSP =999 ACC SZOC X SP dato1 dato2 M(997) M(998) M(999) FO 99 9 Dato 0000 1> 102 LDA = “Dato 1>” JSR 301 OUT 302 102 IN RTS 103 105 STA ^dato1 LDA = “Dato 2>” JSR 301 OUT 302 IN 2 101 104 300 99 8 FI 102 Dato 1> 33 33 99 9 33 Dato 2> 300 99 8 105 Dato 1> 33 33 Il modello di esecutore 102 RTS 10 STA ^dato2 85 99 9 22 Tabella 20 – Tabella di trace L’introduzione dei sottoprogrammi obbliga il programmatore a pianificarne l’allocazione in memoria in area diversa da quella occupata dal programma principale. Per semplificare il lavoro del programmatore si potrà associare un nome all’indirizzo occupato dalla prima istruzione del sottoprogramma così come è stato già fatto per i dati. I programmi prodotti saranno molto più leggibili. Si scriva adesso un programma che: - carichi in memoria una sequenza di dati numerici di tipo intero, maggiori di zero prelevandoli da tastiera; - termini il prelievo quando viene inserito un valore nullo; - effettui la somma dei dati inseriti; - determini il valore minimo tra quelli inseriti; - determini il valore massimo tra quelli inseriti. Per prima cosa si organizzino i sottoprogrammi per: - LEGGI: per la stampa di un messaggio, l’inserimento da tastiera e il salvataggio in memoria; - CONTA: per incrementare una variabile contatore; - IN_VET: per leggere la sequenza dei numeri determinando quanti sono; - SOMMA: per effettuare la sommatoria dei dati inseriti; - MAX: per determinare il valore massimo e - MIN: per determinare il valore minimo degli stessi dati. Se si definiscono i nomi per le variabili e per le etichette di istruzioni come in tabella 21 (tabella dei simboli), Simbolo Vettore Indirizzo Memoria 600 Tipo variabile n i totale max min leggi conta in_vet somma minimo massimo 700 701 702 703 704 100 110 120 150 200 250 variabile variabile variabile variabile variabile label label label label label label Descrizione inizio sequenza dati (si assume che possano essere massimo 100) numero dati inseriti indice di ciclo valore somma valore massimo valore mnimo sottoprogramma sottoprogramma sottoprogramma sottoprogramma sottoprogramma sottoprogramma Tabella 21 – Tabella dei simboli si può procedere con la programmazione. 86 Capitolo secondo IM 100 101 102 103 Label LEGGI CodOp OUT IN STA RTS TI Operando (X) Commento stampa messaggio leggi dato memorizza dato termina Tabella 21 – Programma LEGGI Si noti che LEGGI prevede prima di iniziare che : - in ACC sia contenuto il messaggio da stampare - in X sia contenuto l’indirizzo di memoria dove conservare il dato preso da tastiera. Al suo termine il dato inserito da tastiera rimane in ACC e si trova anche all’indirizzo di memoria contenuto in X. IM Label CodOp TI Operando Commento 110 CONTA LDA ^ n carica valore contatore 111 INCA incrementa 112 STA ^ n aggiorna contatore 113 RTS termina Tabella 22 – Programma CONTA Si noti che CONTA preleva ed aggiorna il valore di n e non ha bisogno di scambiare dati attraverso gli atri registri. IM 120 121 122 123 124 125 Label IN_VET CICLO Cod Op LDX CLRA STA LDA JSR BRZ 126 INCX 127 128 129 JSR JMP RTS FINE_CICLO TI = Operando vettore ^ = n “Inserisci” LEGGI FINE_CICLO CONTA CICLO Commento carica in X indirizzo di vettore azzera contatore carica messaggio Salta a sottoprogramma salto condizionato alla presenzadi uno zero in ACC (terminazione sequenza di input) se il salto non avviene il dato inserito è diverso da zero e si deve procedere al successivo inserimento in una posizione di memoria seguente incrementa contatore riprendi l’inserimento termina Tabella 23 – Programma IN_VET Si noti che IN_VET restituisce il valore di n e le n locazioni di memoria che partono da vettore caricate con i dati inseriti da tastiera. Il modello di esecutore IM 150 CodOp LDX TI = Operando vettore Commento carica in X indirizzo di vettore CLRA STA STA LDA ADDA ^ ^ (X) ^ tot i 156 STA ^ tot 157 INCX 158 159 160 161 LDA INCA STA CMPA ^ i ^ ^ i n 162 BRNZ azzera somma salva valore indice preleva dato da sequenza aggiungi la somma precedente conserva il valore calcolato incrementa la posizione nella sequenza carica valore indice incrementa salva valore indice confronta con il numero di elementi se non sono uguali continua la somma 163 RTS 151 152 153 154 155 IM 200 201 202 203 204 205 Label SOMMA 87 CICLO_SOM Label MINIMO CICLO_MIN 206 207 208 209 210 211 212 213 214 CONTINA 215 216 IM 250 251 Label MASSIMO tot CICLO_SOM Tabella 24 – Programma SOMMA CodOp TI Operando Commento LDX = vettore carica in X indirizzo di vettore LDA (X) preleva primo dato da sequenza STA ^ min posiziona in valore min LDA = 1 STA ^ i posiziona ad 1 indice INX incrementa la posizione nella sequenza LDA (X) preleva dato da sequenza SUBA ^ min fai la differenza con min BRNS CONTINUA se la differenza non è negativa l’elemento èpiù grande del presunto minimo LDA (X) riprendi il dato STA ^ min aggiorna minimo LDA ^ i carica valore indice INCA incrementa STA ^ i salva valore indice CMPA ^ n confronta con il numero di elementi BRNZ CICLO_MIN se non sono uguali continua i confronti RTS Tabella 25 – Programma MINIMO CodOp TI Operando Commento LDX = vettore carica in X indirizzo di vettore LDA (X) preleva primo dato da sequenza 88 252 253 254 255 Capitolo secondo STA LDA STA INX ^ = ^ max 1 i 256 257 LDA SUBA ^ (X) max 258 BRNS 259 260 261 262 263 264 265 LDA STA LDA INCA STA CMPA BRNZ 266 CICLO_MAS PROSEGUI PROSEGUI (X) ^ ^ ^ ^ max i i n CICLO_MAS posiziona in valore max posiziona ad 1 indice incrementa la posizione nella sequenza preleva max fai la differenza con dato da sequenza se la differenza non è negativa il presunto max è più grande dell’elemento della sequenza riprendi il dato aggiorna massimo carica valore indice incrementa salva valore indice confronta con il numero di elementi se non sono uguali continua i confronti RTS Tabella 26 – Programma MASSIMO IM Si può adesso scrivere il programma principale. Label Cod TI Operando Commento Op 1 LDSP = 999 posizionamento iniziale di SP 2 JSR IN_VET lettura della sequenza 3 JSR SOMMA calcola la somma degli elementi 4 LDA ^ Totale 5 OUT stampa il risultato della somma 6 JSR MINIMO determina valore minimo 7 LDA ^ Min 8 OUT stampa valore minimo 6 JSR MASSIMO determina valore massimo 7 LDA ^ Max 8 OUT stampa valore massimo 9 JMP sist_op Tabella 27 – Programma MAIN Si noti che si è fatto ricorso ai sottoprogrammi anche e soprattutto per evidenziare sequenze di istruzioni con un compito preciso. In tale modo dal programma principale si evincono subito le operazioni fondamentali senza doverle scoprire attraverso il trace. Un altro aspetto che emerge dall’esempio è che i richiami dei sottoprogrammi possono essere nidificati: ossia all’interno di un sottoprogramma si possono richiamare altri sottoprogrammi. Il collegamento tra le diverse componenti del programma è garantito dalla gestione dell’area di memoria attraverso il registro SP. Ad ogni salto al sottoprogramma viene inserito (operazione di PUSH) l’indirizzo di ritorno nell’area stack di memoria. L’istruzione RTS preleva (operazione di POP) Il modello di esecutore 89 l’ultimo indirizzo inserito a cui tornare. I push conservano gli indirizzi di ritorno nell’ordine in cui si sono determinati; i pop li prelevano nell’ordine inverso. Figura 30 – Gestione sottoprogrammi nidificati L’assemblatore è stato uno dei primi programmi realizzati. Nel tempo si è sempre più arricchito di funzionalità che semplificassero la difficile attività di programmazione. Negli attuali assemblatori il programmatore non ha bisogno di definire la tabella dei simboli in quanto viene calcolata automaticamente durante la traduzione in linguaggio macchina. Per farlo l’assemblatore opera solitamente in due passi: nel primo costruisce la tabella dei simboli associando ad ognuno di essi posizione in memoria e caratteristica, nel secondo può procedere alla sostituzione: - dei codici mnemonici con quelli macchina, - e dei simboli con i valori determinati. Il programmatore può anche fornire all’assemblatore direttive con le quali guidare la traduzione. Per esempio può indicare all’assemblatore: - dove allocare le istruzioni: #AREAPROG AT address - dove allocare i dati definendone anche le dimensioni: #AREADATI AT address - le dimensioni dei singoli dati con direttive del tipo: #DATO nome SIZE dimensione - dove far iniziare l’area di stack: #AREASTACK AT address - dove è collocata la prima istruzione da eseguire che può essere diversa da quella allocata in prima posizione: #START AT address - quando termina l’esecuzione del programma #STOP 90 Capitolo secondo Un tale assemblatore consentirebbe di scrivere i programmi precedenti senza dover segnare a fianco di istruzioni e dati la loro allocazione in memoria come l’esempio seguente mostra. #START AT 200 #AREADATI AT 500 #DATO vettore SIZE 100 #DATO n SIZE 1 #DATO i SIZE 1 #DATO totale SIZE 1 #DATO max SIZE 1 #DATO min SIZE 1 #AREASTACK AT 999 #AREAPROG AT 200 JSR JSR LDA OUT JSR LDA OUT JSR LDA OUT ^ IN_VET SOMMA totale ^ MINIMO min ^ MASSIMO max #STOP AREAPROG AT 300 LEGGI CONTA OUT IN STA RTS LDA INCA STA RTS IN_VET LDX CLRA STA CICLO LDA JSR BRZ INCX JSR JMP END_CICLO RTS SOMMA LDX CLRA STA STA CICLO_SOM LDA ADDA (X) ^ n ^ n = vettore ^ = n “Inserisci” LEGGI END_CICLO CONTA CICLO = vettore ^ ^ (X) ^ tot i tot Il modello di esecutore STA INCX LDA INCA STA CMPA BRNZ RTS MINIMO LDX LDA STA LDA STA CICLO_MIN INX LDA SUBA BRNS LDA STA CONTINUA LDA INCA STA CMPA BRNZ RTS MASSIMO LDX LDA STA LDA STA CICLO_MAS INX LDA SUBA BRNS LDA STA PROSEGUI LDA INCA STA CMPA BRNZ RTS 91 ^ tot ^ i ^ ^ i n CICLO_SOM = (X) ^ = ^ vettore (X) ^ (X) ^ ^ min 1 i min CONTINUA min i ^ ^ i n CICLO_MIN = (X) ^ = ^ vettore ^ (X) (X) ^ ^ ^ ^ max 1 i max PROSEGUI max i i n CICLO_MAS L’assemblatore inserirà in automatico le istruzioni: LDS = address_area_stack JMP start_address all’inizio del programma, e: JMP sistema_operativo al posto della direttiva #STOP. 92 Capitolo secondo L’ultima direttiva è utile per comprendere due aspetti della architettura che sono stati fino a questo momento trascurati: - come inserire un programma in memoria prima che il processore MP possa eseguirlo; - cosa accade quanto tale programma termina coerentemente con l’ipotesi fatta sul ciclo del processore che prevede che dopo una fase execute ci sia sempre una fase fetch successiva. Per rispondere alla prima domanda si potrebbe far riferimento ai primi sistemi di calcolo che erano dotati di un lettore di nastro di carta perforato e di due pulsanti di LOAD e di RUN. Il programmatore era costretto a scrivere il proprio programma su di un nastro di carta con un sistema perforatore che faceva un buco ogni volta che un bit era uguale ad uno. Scritto il nastro lo caricava con il pulsante di LOAD nella memoria centrale del sistema di calcolo e solo al termine del travaso poteva avviare la CPU con il comando di RUN. Per eseguire un altro programma si doveva ripetere la procedura di caricamento e di avvio. La modifica ad un programma costringeva il programmatore alla riscrittura dell’intero nastro di carta. La CPU restava a lungo inutilizzata in quanto iniziava l’esecuzione solo quando il pulsante di RUN veniva premuto dall’operatore di sistema. E anche l’architettura del processore MP potrebbe funzionare nello stesso modo. Si può però costruire un programma che affidi proprio a MP l’onere di effettuare il caricamento in memoria delle istruzioni di un altro programma prelevandole dall’input standard: a caricamento avvenuto la MP può procedere alla sua esecuzione per poi ritornare al caricamento di un successivo programma. Si chiami LOADER il programma di caricamento e UTENTE il programma caricato. In ogni istante il sistema si trova o ad eseguire il LOADER o l’UTENTE. Figura 31: MP e caricamento programmi Ad esempio il LOADER per l’architettura MP è caratterizzato da: - occupazione delle prime posizioni di memoria sia per area codice che dati; - prima istruzione da eseguire collocata all’indirizzo uno; - caricamento delle programma UTENTE fissata con inizio a partire dall’indirizzo 100; Il modello di esecutore - 93 riconoscimento di MEM come posizione a partire dalla quale iniziare il caricamento; per semplicità si assume che l’indirizzo di caricamento venga inserito successivamente a MEM; riconoscimento di END come terminazione del caricamento. Si noti che MEM ed END sono dichiarazioni che vengono introdotte per guidare il LOADER e non corrispondono quindi a istruzioni che dovranno essere eseguite da MP. È comunque l’assemblatore ad introdurle nel programma tradotto in linguaggio macchina. Il codice del LOADER potrebbe essere allora il seguente. Caricando le istruzioni a partire dalla prima posizione, la label SIST_OP assume il valore 10 e tutto il codice del LOADER occupa solo 28 registri di memoria. Per comprendere il programma di LOADER si effettui il trace nell’ipotesi che il programma da caricare sia quello di somma di due numeri. Si supponga che tale programma sia prodotto dall’assemblatore che ha inserito le direttive MEM e END. Inoltre, per rendere più leggibile l’esempio si sono mantenute le istruzioni in linguaggio assemblativo invece di riportarne il codice binario prodotto dall’assemblatore. PI 2 IR ACC SZOC X SP M(80) M(81) M(82) M(83) M(84) M(85) M(100) M(101) M(200) 4 LDSP =99 LDA Prompt> 0000 =”Prompt>” STA ^80 5 LDA =100 6 STA ^81 3 99 Prompt> 100 100 FO FI 94 7 Capitolo secondo LDA =”MEM” STA ^82 MEM LDA =”END” 10 STA ^83 END 8 9 MEM END 11 LDX ^81 12 LDA ^80 100 Prompt> 13 OUT 14 IN 15 STA ^84 Prompt> LDSP ^999 16 CMPA ^83 LDSP^999 LDSP ^999 1000 17 BRZ 28 18 LDA ^84 19 CMPA ^82 LDSP ^999 0000 1000 20 BRNZ 24 25 LDA ^84 26 STA (X) LDSP ^999 0000 27 INCX LDSP ^999 101 28 JMP 11 12 LDA ^80 Prompt> 13 OUT 14 IN 15 STA ^84 Prompt> LDSP ^999 16 CMPA ^83 JMP100 LDSP ^999 1000 17 BRZ 28 18 LDA ^84 JMP100 0000 19 CMPA ^82 1000 20 BRNZ 24 25 LDA ^84 JMP100 0000 26 STA (X) 27 INCX JMP 100 102 28 JMP 11 12 LDA ^80 Prompt> 13 OUT 14 IN Prompt> MEM MEM 15 STA ^84 MEM 16 CMPA ^83 1000 17 BRZ 28 18 LDA ^84 19 CMPA ^82 MEM 0000 1100 Il modello di esecutore 95 20 BRNZ 24 21 IN 200 0000 200 22 STA ^85 MEM 23 LDX ^85 200 200 24 JMP 11 12 LDA ^80 Prompt> 13 OUT 14 IN Prompt> IN IN 15 STA ^84 IN 16 CMPA ^83 1000 17 BRZ 28 18 LDA ^84 IN 19 CMPA ^82 0000 1000 20 BRNZ 24 25 LDA ^84 IN 0000 26 STA (X) IN 27 INCX 201 28 JMP 11 12 LDA ^80 Prompt> 13 OUT 14 IN Prompt> END END 15 STA ^84 IN 16 CMPA ^83 1100 17 BRZ 28 29 JMP 100 101 LDSP =999 102 JMP 200 201 IN 206 JMP 10 11 LDX ^81 12 LDA ^80 100 Prompt> 13 OUT Prompt> Tabella 27 – Tabella di Trace del LOADER Nel trace sono state saltate alcune righe perché identiche alle precedenti se non nei dati inseriti e nei valori di X, aventi valori via via crescenti. Così non è 96 Capitolo secondo stato condotto il trace del programma UTENTE mostrando solo la sua terminazione con il salto al LOADER che fa riprendere il caricamento di un successivo programma in memoria. Il LOADER è stato il primo componente di un insieme di programmi che hanno consentito al sistema stesso di adempiere a tutta la sua gestione. Tali programmi sono detti Sistema Operativo. Se le istruzioni del LOADER sono conservate in una ROM e all’atto dell’accensione del sistema si forza il PI ad assumere il valore zero con circuiteria apposita, vengono ad essere soddisfatte le condizioni previste dalla fase di boot e necessarie affinché il ciclo del processore MP possa avere inizio. Da ultimo si vuole rimuovere l’ipotesi introdotta riguardante la strutturazione dei registri in contenitori di informazioni di qualsiasi tipo. L’esempio che segue confronta una istruzione di MP con un caso reale di processore che opera con registri di memoria a byte. Nell’esempio si è fatto ricorso ai codici ASCII dei caratteri che compongono il messaggio. Processore MP LDA =”CIAO” OUT Processore a Byte LDA =”C” OUT LDA =”I” OUT LDA =”A” OUT LDA =”O” OUT Si comprende dall’esempio che l’ipotesi è stata introdotta solo per la semplificazione degli esempi. Capitolo terzo Algoritmi e programmi 3.1. Informatica come studio di algoritmi Aver definito l’informatica come la scienza e la tecnica dell’elaborazione dei dati o, più in generale, del trattamento automatico delle informazioni, porta inevitabilmente a riflettere sul fatto che per elaborare l’informazione sia necessario preliminarmente comprendere il modo in cui procedere nella sua elaborazione. In un tale contesto l’informatica allarga i suoi orizzonti diventando lo studio sistematico dei processi che servono al trattamento delle informazioni o più in generale della definizione della soluzione di problemi assegnati. Obiettivo di tale studio è nell’ordine: - l’analisi dettagliata di ciò che serve al trattamento dell’informazione, - il progetto di una soluzione applicabile alla generazione di informazioni prodotte da altre informazioni, - la verifica della correttezza e della efficienza della soluzione pensata, - la manutenzione della soluzione nella fase di funzionamento in esercizio. Se si utilizza il termine algoritmo, introdotto nella matematica per specificare la sequenza precisa di operazioni il cui svolgimento è necessario per la soluzione di un problema assegnato, allora l’informatica diventa lo studio sistematico degli algoritmi. Va ribadito che tale definizione deve essere considerata in una accezione più ampia di quella che pone i calcolatori elettronici al centro dell’attenzione, sebbene l’interesse principale dell’informatica è quello di rendere comprensibili ed eseguibili gli algoritmi per un sistema di calcolo. Il calcolatore è tra tutti gli esecutori di algoritmi (compreso l’uomo) quello che si mostra più potente degli altri e con una potenza tale da permettere di gestire quantità di informazioni altrimenti non trattabili. Ed è questa, forse, una delle possibili chiavi di lettura del ruolo che l’informatica va sempre più a rivestire nelle società evolute. Lo studio dell’Informatica considera quindi il computer come uno scienziato utilizza il proprio microscopio: uno strumento per provare le proprie teorie e, nel caso specifico, verificare i propri ragionamenti o algoritmi. 3.1.1. La soluzione dei problemi In pochi anni l’informatica ha fatto passi da gigante, ed è naturale quindi chiedersi quali sono stati gli aspetti che ne hanno determinato la rapida evoluzione: una 98 Capitolo terzo evoluzione tanto rapida che oggi le “macchine informatiche” si incontrano in tutti i settori produttivi. Il primo e più importante aspetto è che le macchine informatiche risolvono un gran numero di problemi in modo più veloce e conveniente degli esseri umani e trovare soluzioni a problemi è una attività che impegna l’uomo quotidianamente. Senza entrare nel merito della definizione di problema, che si può considerare acquisita, è possibile per i seguenti noti problemi: a) preparare una torta alla frutta, b) trovare le radici di un'equazione di 2° grado conoscendone i coefficienti a,b e c, c) individuare il massimo tra tre numeri, d) calcolare tutte le cifre decimali di , e) inviare un invito ad un insieme di amici, f) individuare le tracce del passaggio di extraterrestri; osservare che: 1) la descrizione del problema non fornisce, in generale, indicazioni sul metodo risolvente; anzi in alcuni casi presenta imprecisioni e ambiguità che possono portare a soluzioni errate; 2) per alcuni problemi non esiste una soluzione; 3) alcuni problemi, invece, hanno più soluzioni possibili; e quindi bisogna studiare quale tra tutte le soluzioni ammissibili risulta quella più vantaggiosa sulla base di un insieme di parametri prefissati (costo della soluzione, tempi di attuazione, risorse necessarie alla sua realizzazione, etc.) 4) per alcuni problemi non esistono soluzioni eseguibili in tempi ragionevoli e quindi utili. Nel caso a), seppur ne sia noto il risultato, ossia la torta da gustare, non si riesce a ricavare alcuna indicazione sulla ricetta da seguire che, tra l’altro, non è facile individuare in un libro di cucina per la sua formulazione generica. Il caso b) è un noto e semplice problema di matematica per il quale si conosce chiaramente il procedimento risolvente. Il caso c) è un esempio di problema impreciso ed ambiguo in quanto non specifica se va cercato il valore massimo o la sua posizione all’interno della terna dei tre numeri assegnati. Il caso d) è invece indicativo di un problema con una soluzione nota che però non arriva a terminazione in quanto le cifre da calcolare sono infinite. Per il caso e) si può osservare che esistono sia soluzioni tradizionali basate sulla posta ordinaria, che soluzioni più moderne quali i messaggi SMS dei telefoni cellulari o i messaggi di posta elettronica. Bisogna quindi scegliere la soluzione più conveniente: ad esempio quella che presenta un costo più basso. Il caso f) è un chiaro esempio di problema che non ammette soluzione: o, come si dice, non risolvibile. Comunque tutti i problemi risolvibili presentano una caratteristica comune: prescrivono la produzione di un risultato a partire da fissate condizioni iniziali secondo lo schema di figura. Lo schema mette in luce gli elementi che concorrono alla soluzione dei problemi. Essi sono: - l'algoritmo, ossia un testo che prescrive un insieme di operazioni od azioni eseguendo le quali è possibile risolvere il problema assegnato. Se si indica Algoritmi e programmi 99 con istruzione la prescrizione di una singola operazione, allora l'algoritmo è un insieme di istruzioni da svolgere secondo un ordine prefissato; Figura 1 – Algoritmo ed esempi - - 3.1.2. l'esecutore, cioè l'uomo o la macchina in grado di risolvere il problema eseguendo l'algoritmo. Ma se un algoritmo è un insieme di istruzioni da eseguire secondo un ordine prefissato, allora l’esecutore non solo deve comprendere le singole istruzioni ma deve essere anche capace di eseguirle. E può eseguirle una dopo l’altra secondo un ordine rigidamente sequenziale che impone l’inizio dell’esecuzione di una nuova istruzione solo al termine di quella precedente; oppure può eseguire più istruzioni contemporaneamente svolgendole in parallelo; le informazioni di ingresso (anche dette input), ossia le informazioni che devono essere fornite affinché avvengano le trasformazioni desiderate; le informazioni di uscita (anche dette output), ossia i risultati prodotti dall'esecutore del dato algoritmo. La calcolabilità degli algoritmi L’algoritmo è composto da una sequenza finita di passi che portano alla risoluzione automatica di un problema. Il singolo passo è un’azione elaborativa che produce trasformazioni su alcuni dati del problema. Da un punto di vista generale il concetto di elaborazione può sempre essere ricondotto a quello matematico di funzione Y=F(X) in cui X sono i dati iniziali da elaborare, Y i dati finali o risultati e F è la regola di trasformazione. L’automa a stati finiti fornisce un’astrazione per il concetto di macchina che esegue algoritmi ed introduce il concetto di stato come particolare condizione di 100 Capitolo terzo funzionamento in cui può trovarsi la macchina. L’automa è uno dei modelli fondamentali dell’informatica ma è applicabile a qualsiasi sistema che evolve nel tempo per effetto di sollecitazioni esterne. Ogni sistema se soggetto a sollecitazioni in ingresso risponde in funzione della sua situazione attuale eventualmente emettendo dei segnali di uscita, l’effetto della sollecitazione in ingresso è il mutamento dello stato del sistema stesso. Il sistema ha sempre uno stato iniziale di partenza da cui inizia la sua evoluzione. Può terminare in uno stato finale dopo aver attraversato una serie di stati intermedi. Un automa M (a stati finiti) può essere definito da una quintupla di elementi (Q,I,U,t,w) dove: - Q è un insieme finito di stati interni caratterizzanti l’evoluzione del sistema; - I è un insieme finito di sollecitazioni in ingresso; - U è un insieme finito di uscite; - t è la funzione di transizione che trasforma il prodotto cartesiano Q I in Q (t: QxI Q) - w è la funzione di uscita che trasforma Q I in U (w: QxI U). Il modello degli automi a stati finiti torva larga applicazione soprattutto per la capacità di descrivere il comportamento dei sistemi. Alla formulazione matematica si accompagna una rappresentazione grafica immediata caratterizzata da un grafo composto da due semplici elementi: un cerchio per rappresentare gli stati del sistema e degli archi orientati ad indicare le transizioni. Nel grafo si individuano: - gli stati intermedi rappresentati da cerchi che hanno archi entranti ed uscenti, - lo stato iniziale come l’unico cerchio che non ha archi entranti; - lo stato finale, se esiste, come cerchio che non ha archi uscenti. Figura 2 – Rappresentazioni di un automa Al grafo può essere associata una rappresentazione più matematica in forma tabellare con tante righe quanti sono gli ingressi, e tante colonne quanti sono gli stati. Nella tabella si indicano gli stati nei quali il sistema si sposta per effetto delle sollecitazioni in ingresso con l’indicazione dell’eventuale uscita prodotta. Algoritmi e programmi 101 Il modello di automa fornisce un concetto generale di esecutore di azioni. È un modello generico nel quale non sono precisate la natura di ingressi ed uscite utili ad individuare un insieme di possibili azioni elaborative. Il modello di Macchina di Turing è un particolare automa per il quale sono definiti l’insieme degli ingressi e delle uscite come insiemi di simboli; inoltre è definito un particolare meccanismo di lettura e scrittura delle informazioni. È un modello fondamentale nella teoria dell’informatica, in quanto permette di raggiungere risultati teorici sulla calcolabilità e sulla complessità degli algoritmi. La macchina di Turing è un automa con testina di scrittura/lettura su nastro bidirezionale potenzialmente illimitato. Ad ogni istante la macchina si trova in uno stato appartenente ad un insieme finito e legge un simbolo sul nastro. La funzione di transizione in modo deterministico, fa scrivere un simbolo, fa spostare la testina in una direzione o nell'altra, fa cambiare lo stato. La macchina si compone di: a) una memoria costituita da un nastro di dimensione infinita diviso in celle; ogni cella contiene un simbolo oppure è vuota; b) una testina di lettura scrittura posizionabile sulle celle del nastro; c) un dispositivo di controllo che, per ogni coppia (stato, simbolo letto) determina il cambiamento di stato ed esegue un’azione elaborativa. Figura 3 – Macchina di Turing Formalmente la macchina di Turing è definita dalla quintupla: A, S, fm , fs, fd dove: - A è l’insieme finito dei simboli di ingresso e uscita; S è l’insieme finito degli stati (di cui uno è quello di terminazione); fm è la funzione di macchina definita come A × S A; fs è la funzione di stato A × S S; D = {Sinistra, Destra, fd è la funzione di direzione A × S Nessuna} La macchina è capace di: - leggere un simbolo dal nastro; - scrivere sul nastro il simbolo specificato dalla funzione di macchina; - transitare in un nuovo stato interno specificato dalla funzione di stato; - spostarsi sul nastro di una posizione nella direzione indicata dalla funzione di direzione. 102 Capitolo terzo La macchina si ferma quando raggiunge lo stato di terminazione. Definita la parte di controllo, la macchina è capace di risolvere un dato problema. Una macchina di Turing che si arresti e trasformi un nastro t in uno t’ rappresenta l’algoritmo per l’elaborazione Y=F(X), ove X e Y sono codificati rispettivamente in t e t’. Una macchina di Turing la cui parte di controllo è capace di leggere da un nastro anche la descrizione dell’algoritmo è una macchina universale capace di simulare il lavoro compiuto da un’altra macchina qualsiasi. Ma leggere dal nastro la descrizione dell’algoritmo richiede di saper interpretare il linguaggio con il quale esso è stato descritto. La Macchina di Turing Universale diventa così l’interprete di un linguaggio modellando il concetto di elaboratore di uso generale. Dalla macchina di Turing discendono alcune tesi, che seppur non dimostrate, restano fondamentali per lo studio degli algoritmi. La tesi di Church-Turing dice che non esiste alcun formalismo, per modellare una determinata computazione meccanica, che sia più potente della Macchina di Turing e dei formalismi ad essi equivalenti. Ogni algoritmo può essere codificato in termini di Macchina di Turing ed è quindi ciò che può essere eseguito da una macchina di Turing. Un problema è non risolubile algoritmicamente se nessuna Macchina di Turing è in grado di fornire la soluzione al problema in tempo finito. Se dunque esistono problemi che la macchina di Turing non può risolvere, si conclude che esistono algoritmi che non possono essere calcolati. Sono problemi decidibili quei problemi che possono essere meccanicamente risolvibili da una macchina di Turing; sono indecidibili tutti gli altri. Se la tesi di Church asserisce che non esiste un formalismo né una macchina concreta che possa calcolare una funzione non calcolabile secondo Turing, si conclude che: - se per problema esiste un algoritmo risolvente questo è indipendente dal sistema che lo esegue se è vero che per esso esiste una macchina di Turing; - l’algoritmo è indipendente dal linguaggio usato per descriverlo visto che per ogni linguaggio si può sempre definire una macchina di Turing universale. La teoria della calcolabilità cerca di comprendere quali funzioni ammettono un procedimento di calcolo automatico. Nella teoria della calcolabilità la tesi di Church-Turing è un’ipotesi che intuitivamente dice che se un problema si può calcolare, allora esisterà una macchina di Turing (o un dispositivo equivalente, come il computer) in grado di risolverlo (cioè di calcolarlo). Più formalmente possiamo dire che la classe delle funzioni calcolabili coincide con quella delle funzioni calcolabili da una macchina di Turing. La macchina di Turing e la macchina di von Neumann sono due modelli di calcolo fondamentali per caratterizzare la modalità di descrizione e di esecuzione degli algoritmi. La macchina di Von Neumann fu modellata dalla Macchina di Turing Universale per ciò che attiene alle sue modalità di computazione. La sua memoria è però limitata a differenza del nastro di Turing che ha lunghezza infinita. La macchina di Von Neumann, come modello di riferimento per sistemi non solo teorici, prevede anche la dimensione dell’interazione attraverso i suoi dispositivi di input ed output. Algoritmi e programmi 3.1.3. 103 La trattabilità degli algoritmi Calcolabilità e trattabilità sono due aspetti importanti dello studio degli algoritmi. Mentre la calcolabilità consente di dimostrare l’esistenza di un algoritmo risolvente un problema assegnato ed indipendente da qualsiasi automa, la trattabilità ne studia la eseguibilità da parte di un sistema informatico. Infatti esistono problemi classificati come risolvibili ma praticamente intrattabili non solo dagli attuali elaboratori ma anche da quelli, sicuramente più potenti, del futuro. Sono noti problemi che pur presentando un algoritmo di soluzione, non consentono di produrre risultati in tempi ragionevoli neppure se eseguiti dal calcolatore più veloce. Sono noti altri problemi la cui soluzione è di per sé lenta e quindi inaccettabile. Il concetto di trattabilità è legato a quello di complessità computazionale con la quale si studiano i costi intrinseci alla soluzione dei problemi, con l'obiettivo di comprendere le prestazioni massime raggiungibili da un algoritmo applicato a un problema. La complessità consente di individuare i problemi risolvibili che siano trattabili da un elaboratore con costi di risoluzione che crescano in modo ragionevole al crescere della dimensione del problema. Tali problemi vengono detti trattabili. Nello studio della trattabilità delle soluzioni dei problemi due sono i tipi di complessità computazionale fra loro interdipendenti che consentono di misurare gli algoritmi: - la spaziale intesa come la quantità di memoria necessaria alla rappresentazione dei dati necessari all’algoritmo per risolvere il problema; - la temporale intesa come il tempo richiesto per produrre la soluzione. Oggi la complessità spaziale non viene più presa in grande considerazione in quanto i sistemi moderni presentano memorie di grandi capacità. La complessità temporale è un indicatore della velocità di risposta di un elaboratore (detta anche efficienza). Si noti che l’efficienza non è un parametro che va considerato in assoluto ma nel relativo del contesto nel quale il problema viene risolto. Un algoritmo è trattabile se fornisce risposte in tempi che siano utili all’ambiente circostante. Tra due soluzioni di uno stesso problema una soluzione è più efficiente dell’altra se produce i risultati in minor tempo. La complessità di un algoritmo corrisponde quindi a una misura delle risorse di calcolo consumate durante la computazione ed è tanto più elevata quanto maggiori sono le risorse consumate. Le misure possono essere statiche se sono basate sulle caratteristiche strutturali (ad esempio il numero di istruzioni) dell’algoritmo e prescindono dai dati di input su cui esso opera. Sono invece misure dinamiche quelle che tengono conto sia delle caratteristiche strutturali dell’algoritmo che dei dati di input su cui esso opera. Un primo fattore che incide sul tempo impiegato dall’algoritmo per produrre un risultato, o tempo di esecuzione, è sicuramente la quantità di dati su cui l’algoritmo deve lavorare. Per questa ragione, il tempo di esecuzione è solitamente espresso come una funzione f(n) della dimensione n dei dati di input. Si dirà, ad esempio, che un algoritmo ha un tempo di esecuzione n2 se il tempo impiegato è pari al quadrato della dimensione dell’input. Ma da sola la dimensione dei dati di input non basta. Infatti il tempo di esecuzione dipende anche dalla configurazione dei dati in input oltre che dalla loro dimensione. Si possono allora fare tre tipi di analisi: 104 Capitolo terzo - analisi del caso migliore per calcolare il tempo di esecuzione quando la configurazione dei dati presenta difficoltà minime di trattamento. Spesso questo tipo di analisi non fornisce informazioni significative; - analisi del caso peggiore per calcolare il tempo di esecuzione quando la configurazione dei dati presenta difficoltà massime di trattamento. Si tratta di un’analisi molto utile, perché fornisce delle garanzie sul tempo massimo che l’algoritmo può impiegare; - analisi del caso medio per calcolare il tempo di esecuzione quando la configurazione presenta difficoltà medie di trattamento. In generale, nel decidere come caratterizzare il tempo di esecuzione di un algoritmo, si considerano solo quelle particolari operazioni che rappresentano il procedimento dominante della soluzione. Ad esse si attribuisce un tempo unitario di esecuzione per ottenere una misura indipendente da una macchina particolare. Inoltre ciò che veramente importa non è la quantità precisa di operazioni effettuate e di dati trattati, ma come questi crescono al crescere della dimensione dei dati. Interessa cioè sapere come l’ordine di grandezza del tempo di esecuzione cresce al limite, ossia per dimensioni dell’input sufficientemente grandi quando la funzione f(n) tende ai suoi asintoti. Lo studio della condizione asintotica della complessità di un algoritmo permette ulteriormente di trascurare in un algoritmo operazioni non significative concentrando l’attenzione solo su quelle predominanti. Così dati due algoritmi diversi che risolvono lo stesso problema e presentano due diverse complessità f(n) e g(n), se f(n) è asintoticamente inferiore a g(n) allora esiste una dimensione dell’input oltre la quale l’ordine di grandezza del tempo di esecuzione del primo algoritmo è inferiore all’ordine di grandezza del tempo di esecuzione del secondo. La complessità asintotica dipende solo dall’algoritmo, mentre la complessità esatta dipende da tanti fattori legati alla esecuzione dell’algoritmo. Per comprendere la dipendenza della trattabilità dalla complessità temporale si consideri un elaboratore capace di eseguire un milione istruzioni al secondo (1 MIPS). Per esso si costruisca una tabella in cui le colonne riportano possibili dimensioni di dati di input e le righe complessità di tipo polinomiale (n, n2, n3, n5) ed esponenziale (2n, 3n). n n2 n3 n5 2n 3n 10 0,00001 sec 0,0001 sec 0,001 sec 0,1 sec 0,001 sec 0,059 sec 20 0,00002 sec 0,0004 sec 0,008 sec 3,2 sec 1,0 sec 58 min 30 0,00003 sec 0,0009 sec 0,027 sec 24,3 sec 17,9 min 6,5 anni 40 0,00004 sec 0,0016 sec 0,064 sec 1,7 min 12,7 giorni 3,855 secoli 50 0,00005 sec 0,0025 sec 0,125 sec 5,2 min 35,7 anni 200.000.000 secoli Tabella 1 – Complessità temporale Si nota che gli algoritmi di tipo polinomiale si comportano meglio di quelli esponenziali. Ma l’aspetto più evidente è che gli algoritmi con complessità esponenziale possono operare con dati di piccole dimensione: se i dati crescono anche di poco essi diventano intrattabili. Algoritmi e programmi 3.2. 105 La descrizione degli algoritmi Quando si affronta un problema è di fondamentale importanza, qualunque sia l'esecutore (uomo o macchina), capire preliminarmente se il problema ammette soluzioni e successivamente, nel caso ne ammetta, individuare un metodo risolutivo (algoritmo) e, infine, esprimere tale metodo in un linguaggio comprensibile all'esecutore a cui è rivolto. Alcune di tali attività rientrano nelle consuetudini della vita quotidiana. Per rendersene conto, si può chiedere ad un amico di correggere gli errori lessicali presenti nel testo di figura 4a. Si può ad esempio suggerire di svolgere una dopo l'altra le azioni della figura 5. Ci si accorge allora di usare naturalmente costrutti che fissano l'ordine in cui le diverse azioni devono essere svolte. Il più semplice tra essi è quello che stabilisce che le azioni devono essere svolte una dopo l'altra. Nell'esempio, tale ordine, è fissato dalla numerazione delle frasi ma può anche coincidere con quello lessicografico, ossia quello che naturalmente si adotta leggendo un qualsiasi testo nel mondo occidentale: si scorrono i righi dall'alto verso il basso e si impone che ogni rigo contenga un comando o un’istruzione per l'esecutore. Si chiameranno tali costrutti col nome di costrutti di sequenza. Altri costrutti possono però stravolgere l'ordine sequenziale. Nell'esempio, un altro costrutto stabilisce che alcune azioni devono essere svolte solo se si verificano determinate condizioni (la frase “se la parola è sbagliata, allora correggila, altrimenti non fare niente” prescrive la correzione soltanto in presenza di un errore). Si chiameranno tali costrutti con il nome di costrutti di selezione. Infine, un ultimo costrutto usato in figura dice che alcune azioni devono essere ripetute un numero di volte prestabilito (“per ogni parola del rigo fai”) o determinato dal verificarsi di certe condizioni (“ripeti le azioni da 1) a 4) fino alla terminazione del testo”). Si chiameranno tali costrutti con il nome di “costrutti iterativi”. In tutti gli algoritmi si possono individuare due classi fondamentali di frasi: - quelle che prescrivono la esecuzione di determinate operazioni; - e quelle che indicano all'esecutore l'ordine in cui tali operazioni devono essere eseguite. Alle prime si darà il nome di istruzioni, mentre alle seconde quello di schemi di controllo, o istruzioni di controllo o anche strutture di controllo. L'esempio conferma che molti degli strumenti necessari per esprimere gli algoritmi sono noti essendo essi parte del linguaggio naturale. E tutti i linguaggi usati per comunicare algoritmi ai calcolatori, detti linguaggi di programmazione, avranno una parte della loro grammatica composta di costrutti del tipo di quelli illustrati nell’esempio precedente. Cambierà la sintassi della specifica frase ma non la tipologia dei costrutti disponibili anche perché, a differenza del linguaggio naturale, per il quale il significato delle frasi dipende fortemente dal contesto in cui vengono usate, i linguaggi di programmazione hanno una semantica indipendente dal contesto dovendo essere chiaro e deterministico l’effetto che ogni frase deve produrre. 106 Capitolo terzo Figura 4a - Testo da correggere Figura 4b - Testo corretto Figura 5 - Un algoritmo di correzione del testo di figura 2 Così nella vita quotidiana spesso ci si confronta con descrizioni di procedimenti da seguire (ad esempio una ricetta di cucina o il libretto di istruzione del telefonino) o si è portati a descrivere programmi di lavoro, come, in modo più specialistico, gli esperti dell’informatica fanno per prescrivere al calcolatore cosa deve fare per risolvere uno specifico problema. Nella soluzione di un problema la difficoltà maggiore consiste, allora, nell'individuare un algoritmo e nel dimensionarlo alle capacità dell'esecutore. Tra l’altro l’uso di un linguaggio per scrivere e comunicare algoritmi è tanto più difficile quanto minori sono le capacità dell'esecutore a cui è rivolto. Si possono allora classificare le istruzioni anche in funzione dello stretto legame esistente tra istruzioni e capacità dell’esecutore definendo: - elementari quelle istruzioni che l'esecutore è in grado di comprendere ed eseguire; - non elementari quelle non note all'esecutore. Ovviamente perché un'istruzione non elementare possa essere eseguita dall'esecutore a cui è rivolta, deve essere specificata in termini più semplici. Il procedimento che trasforma una istruzione non elementare in un insieme di istruzioni elementari, prende il nome di raffinamento o specificazione dell'istruzione non elementare. Il processo di raffinamento è molto importante. Senza di esso si dovrebbero esprimere gli algoritmi direttamente nel linguaggio di programmazione disponibile. E molte volte tale linguaggio ha una potenza espressiva tanto bassa o è talmente artificioso da concentrare l'attenzione più verso le difficoltà di uso del linguaggio, che verso l'individuazione dell'algoritmo Algoritmi e programmi 107 risolutivo. Invece, l'individuazione e la descrizione di un algoritmo deve avvenire per passi: prima lo si esprime usando il linguaggio naturale e poi si continua con un procedimento di raffinamenti che procede fino a che tutte le istruzioni non elementari contenute nell'algoritmo sono definite in termini di istruzioni appartenenti al "repertorio" dell'esecutore. Ovviamente tutti i raffinamenti devono essere integrati al posto delle istruzioni non elementari che li hanno generati perché si abbia l’algoritmo nella versione da affidare all’esecutore per l’esecuzione. 3.2.1. Sequenza statica e dinamica di algoritmi Le operazioni che compongono un algoritmo devono essere dotate delle seguenti caratteristiche: - finitezza: ossia devono avere termine entro un intervallo di tempo finito dall'inizio della loro esecuzione; - descrivibilità: ossia devono produrre, se eseguite, degli effetti descrivibili, per esempio fotografando lo stato degli oggetti coinvolti sia prima che dopo l’esecuzione dell’operazione; - riproducibilità: ossia devono produrre lo stesso effetto ogni volta che vengono eseguite nelle stesse condizioni iniziali; - comprensibilità: ossia devono essere espresse in una forma comprensibile all'esecutore che deve eseguirle. Ne discende che l’intero algoritmo, come insieme di operazioni, gode delle stesse proprietà di finitezza, descrivibilità, riproducibilità e comprensibilità. L'algoritmo serve a risolvere un problema permettendo all'esecutore (uomo o macchina) di eseguirlo senza essere necessariamente coinvolto nella sua definizione. L'esecuzione di un algoritmo da parte di un esecutore si traduce in una successione di azioni che vengono effettuate nel tempo. Si dice che l'esecuzione di un algoritmo evoca un processo sequenziale, cioè una serie di eventi che occorrono uno dopo l'altro, ciascuno con un inizio e una fine identificabili. Si definisce sequenza di esecuzione la descrizione del processo sequenziale. La sequenza di esecuzione è l'elenco di tutte le istruzioni eseguite, nell'ordine di esecuzione, e per questo motivo viene anche detta sequenza dinamica. Ad esempio, nel caso dell'algoritmo di calcolo delle radici di un'equazione di 2° grado viene evocato un unico processo sequenziale, descritto da una sola sequenza di esecuzione che coincide proprio con la descrizione dell'algoritmo. Calcolo d b 2 4ac Calcolo la prima radice come x1 b d 2a Calcolo la seconda radice come x2 b d 2a Esempio 1 – Algoritmo per il calcolo delle radici di un’equazione di 2° grado 108 Capitolo terzo L'esempio mostra un caso semplice con una sola sequenza di esecuzione. Solitamente un algoritmo evoca più sequenze di esecuzioni. In alcuni casi il numero di sequenze di esecuzione può essere infinito e il programma non ha una terminazione. L’analisi delle sequenze dinamiche serve proprio a capire se un dato algoritmo abbia o meno una terminazione. Se si modifica l’algoritmo di calcolo delle radici di una equazione di 2° grado per controllare la esistenza di radici reali, si comincia ad osservare come esso genera comportamenti diversi dipendenti dal valore dei dati di input a, b e c. Calcolo b2 4ac 0 Se Allora Calcolo d Calcolo la prima radice come b d 2a x1 Calcolo la seconda radice come b d 2a x2 Altrimenti Calcola radici complesse Esempio 2 – Algoritmo per il calcolo delle radici di un’equazione di 2° grado In questo caso il processo evocato non è più fisso, ma dipende dai dati iniziali da elaborare. Difatti sono possibili le due sequenze di esecuzione mostrate di seguito. Calcolo b2 4ac 0 è risultata vera Verifico che Calcolo d Calcolo la prima radice come b d 2a x1 Calcolo la seconda radice come x2 b d 2a Calcolo b2 4ac 0 è risultata non vera Verifico che Calcolo radici complesse Esempio 3 – Possibili sequenze di esecuzione per l’algoritmo per il calcolo delle radici di un’equazione di 2° grado Algoritmi e programmi 109 A seconda che i valori assegnati ai coefficienti forniscano un discriminante positivo o nullo e negativo. Uno stesso algoritmo ha quindi generato due sequenze di esecuzione tra loro diverse. Si consideri adesso l’algoritmo seguente che schematizza un gioco che si fa con le 52 carte francesi e che viene detto “solitario del carcerato” per il fatto che non riesce quasi mai o solo dopo molto tempo. Fintantoché (il mazzo ha ancora carte) ripeti: Mischia le carte Prendi 4 carte dal mazzo e disponile sul tavolo di gioco Se (le 4 carte hanno la stessa figura) allora levale dal tavolo di gioco Esempio 4 – Algoritmo per il solitario del carcerato Si comprende che il gioco ha termine quando tutte le carte sono state eliminate. Si osservi che se si è fortunati si eliminano le carte al primo tentativo evocando la sequenza di esecuzione nell’esempio 5, mentre, se sono uguali al secondo tentativo si ha a sequenza di esecuzione dell’esempio 6, o dopo n tentativi usando una notazione più sintetica si ha la sequenza nell’esempio 7. Infine, el caso sfortunato di non trovare mai quattro carte uguali si ha la sequenza di esecuzione nell’esempio 8. Mischiate le carte Prese le 4 carte dal mazzo e messe sul tavolo di gioco Verificato che (le 4 carte hanno la stessa figura) è risultata vera Tolte le 4 carte dal tavolo di gioco Esempio 5 – Sequenza di esecuzione per l’algoritmo per il solitario del carcerato Mischiate le carte Prese le 4 carte dal mazzo e messe sul tavolo di gioco Verificato che (le 4 carte hanno la stessa figura) è risultata falsa Mischiate le carte Prese le 4 carte dal mazzo e messe sul tavolo di gioco Verificato che (le 4 carte hanno la stessa figura) è risultata vera Tolte le 4 carte dal tavolo di gioco Esempio 6 – Sequenza di esecuzione per l’algoritmo per il solitario del carcerato ripetuto n-1 volte Mischiate le carte Prese le 4 carte dal mazzo e messe sul tavolo di gioco Verificato che (le 4 carte hanno la stessa figura) è risultata falsa Mischiate le carte Prese le 4 carte dal mazzo e messe sul tavolo di gioco Verificato che (le 4 carte hanno la stessa figura) è risultata vera Tolte le 4 carte dal tavolo di gioco Esempio 7 – Sequenza di esecuzione per l’algoritmo per il solitario del carcerato ripetuto infinite volte Mischiate le carte Prese le 4 carte dal mazzo e messe sul tavolo di gioco Verificato che (le 4 carte hanno la stessa figura) è risultata falsa Esempio 8 – Sequenza di esecuzione per l’algoritmo per il solitario del carcerato 110 Capitolo terzo L'esempio mostra che un algoritmo può prescrivere più di una sequenza di esecuzione. Se poi l'algoritmo prescrive un processo ciclico, ossia la ripetizione di un insieme di operazioni, allora può accadere che il numero di sequenze sia addirittura infinito. In questo caso l'algoritmo prescrive un processo che non ha mai termine. In un programma (che è la descrizione dell'algoritmo in un linguaggio di programmazione) la sequenza lessicografica (anche chiamata sequenza statica) delle istruzioni (descrizione delle azioni da svolgere nel linguaggio di programmazione) descrive una pluralità di sequenze dinamiche differenti. Il numero di sequenze dinamiche non è noto a priori e dipende dai dati da elaborare. La valutazione sia del tipo che del numero delle sequenze dinamiche è di fondamentale importanza per valutare soluzioni diverse dello stesso problema in modo da poter dire quale di esse presenta un tempo di esecuzione migliore, e per poter affermare se una soluzione ha terminazione o meno. 3.3. I linguaggi di programmazione Un programma è la descrizione di un algoritmo in un linguaggio comprensibile al processore macchina e linguaggio di programmazione tale linguaggio. Il linguaggio di programmazione è quindi una notazione formale per descrivere algoritmi e, come ogni linguaggio, è dotato di un alfabeto, un lessico, una sintassi ed una semantica. L'aspetto formale del linguaggio si manifesta soprattutto nella presenza di regole rigide per la composizione di programmi a partire dai semplici caratteri dell'alfabeto. L'insieme di queste regole costituisce la grammatica del linguaggio. Un limitato e fondamentale insieme di queste regole definisce la struttura lessicale del programma come formato da unità elementari, cioè le parole del linguaggio. Il lessico, quindi, permette di strutturare l'insieme limitato dei caratteri dell'alfabeto in uno più vasto, il vocabolario, fatto di simboli aventi una notazione più leggibile e comprensibile per chi li usa. L'organizzazione delle parole in frasi è invece guidata da regole che compongono la sintassi del linguaggio. Infine l'attribuzione di un significato alle frasi è oggetto delle regole semantiche. Un programma, così come l'algoritmo che esprime, è dunque una sequenza di frasi, ognuna delle quali specifica operazioni che l'esecutore (computer) può comprendere ed eseguire. La natura delle frasi o istruzioni dipende dal linguaggio di programmazione che si usa. Esistono oggi numerosi linguaggi di programmazione, ed ognuno ha un proprio repertorio di istruzioni. Ovviamente, quanto più è semplice il linguaggio, tanto più semplice è il processore che lo deve interpretare; invece quanto più è sintetico il linguaggio, tanto più semplice risulterà descrivere un qualsiasi algoritmo. In un computer il linguaggio più semplice è quello che è direttamente compreso dalla CPU. Chiameremo linguaggio macchina tale linguaggio per evidenziare il suo stretto legame con l'hardware. Il linguaggio macchina offre da un lato il vantaggio di un'alta velocità di esecuzione e di una ottimizzazione nell'uso delle risorse hardware, e dall'altro lo svantaggio di non permettere la trasportabilità dei programmi tra processori differenti, di non permettere cioè che un programma scritto per una data CPU possa essere eseguito da una CPU con caratteristiche diverse. Programmare in linguaggio macchina non è facile sia perché le istruzioni Algoritmi e programmi 111 esprimono funzionalità tanto elementari che la costruzione di ogni algoritmo richiede un gran numero di comandi, sia perché la CPU comprende istruzioni espresse sotto forma di sequenze di bit (ad esempio 1001100111111111 potrebbe indicare la somma di due numeri). Per superare questa ultima difficoltà al programmatore è offerta la possibilità di usare i linguaggi assemblativi che pur mantenendo uno stretto legame con le potenzialità offerte dal linguaggio macchina, rendono la programmazione a questo livello più agevole in quanto sostituiscono alle sequenze di bit dei codici mnemonici più facili da interpretare e ricordare. I linguaggi macchina e assemblativi sono anche comunemente detti linguaggi di basso livello, in quanto si pongono al livello della macchina. Essi, come già descritto, comunicano direttamente con la CPU, utilizzando i codici operativi (binari o mnemonici) dello stesso processore, comportando difficoltà di scrittura e di verifica del corretto funzionamento dei programmi. Per ovviare alla difficoltà di programmare in linguaggi di basso livello, sono nati dei linguaggi di programmazione che, con istruzioni più sintetiche, ossia con istruzioni più vicine al tradizionale modo di esprimere i procedimenti di calcolo da parte di un essere umano, rendono l'attività di programmazione più semplice; per il fatto che questi linguaggi sono più comprensibili dagli uomini che non dalle CPU, li chiameremo linguaggi di alto livello. Essi fanno uso di uno pseudo-linguaggio umano, utilizzando per il loro scopo parole-chiave o codici operativi ispirati quasi esclusivamente alla lingua inglese. Ovviamente ciò facilita molto sia la stesura che la rilettura di un programma, ma non mette il computer in condizione di capire direttamente il programma. Per ottenere il risultato finale è dunque necessario applicare un “interprete” che traduca il linguaggio simbolico e decisamente più sintetico, in reali istruzioni interpretabili dalla CPU. A tale scopo si potrebbe pensare di migliorare le capacità della CPU fino a fargli interpretare il linguaggio stesso. Ma questa risulterebbe una soluzione poco economica (per la complessità della CPU) e poco flessibile (un piccolo cambiamento nel linguaggio costringerebbe a buttar via la CPU). Allora la soluzione più adottata è quella di tradurre i programmi espressi nei linguaggi di alto livello, in programmi espressi in linguaggio macchina; la traduzione può essere svolta dallo stesso computer che poi esegue il programma, o addirittura da un computer completamente diverso. Così, la scrittura dei programmi in linguaggio di alto livello fa in modo che essi non dipendano più dalla CPU. E' difatti sufficiente che ciascuna CPU abbia il suo traduttore o interprete per garantire che lo stesso programma sia eseguito da macchine diverse. Il traduttore più semplice è chiaramente quello che mette in corrispondenza i codici mnemonici del linguaggio assemblativo con le sequenze di bit corrispondenti comprensibili dalla CPU: tale traduttore è detto assemblatore. Poiché quanto più di alto livello sono i linguaggi, ossia quanto più sono vicini all'uomo, tanto più facile è l'attività di programmazione (svolta dall'uomo) e più difficile la loro traduzione (svolta dalla macchina), a partire dagli anni cinquanta sono stati proposti e progettati, diversi linguaggi di programmazione. 3.4. I metalinguaggi I metalinguaggi sono particolari tipologie di linguaggi con i quali si presentano le regole della grammatica di un qualsiasi linguaggio. Un metalinguaggio descrive quindi le proprietà sintattiche e semantiche relative a un altro linguaggio o al 112 Capitolo terzo linguaggio stesso. I più diffusi sono la notazione Backus Normal Form o BNF e le carte sintattiche. La BNF, sviluppata da John Backus e Peter Naur nel 1960, prescrive che le frasi, che compongono un linguaggio, possono essere specificate a partire da un unico simbolo iniziale di alto livello. Ogni simbolo di alto livello (non terminale) può essere sostituito da una frase fra un insieme di sottofrasi, comprendenti simboli sia non terminali che di basso livello (terminali), mentre i simboli terminali non possono essere sostituiti. Il processo si arresta quando si ottiene una sequenza di soli simboli terminali. Il linguaggio definito da una BNF è costituito così da tutte le sequenze di simboli terminali ottenute applicando questo processo a partire dal simbolo iniziale di alto livello. I simboli non terminali sono racchiusi fra parentesi angolari (<), i terminali fra virgolette ("), le diverse alternative per un non terminale sono separate da una barra verticale (|) e il segno := indica le possibili sostituzioni per il non terminale. Ad esempio: <Frase> := <Soggetto> <Predicato verbale> <Complemento oggetto> dice che una frase si compone di un soggetto, di un predicato verbale e di un complemento oggetto. Le espressioni che seguono specificano quali sono i possibili valori dei simboli non terminali: <Soggetto> := “io” | “tu” | “mario” <Predicato verbale> := "mangia" | “beve” <Complemento oggetto> := “mela” | “l’acqua” Una frase costruita seguendo le indicazioni delle espressioni del BNF sono frasi corrette del linguaggio. Una estensione della BNF, detta Extendend BNF (EBNF), è stata introdotta da Niklaus Wirth per specificare la sintassi del linguaggio di programmazione Pascal. Nella EBNF i simboli {, }, (, ) e * possono comparire nel lato destro di una frase. Una sequenza di simboli racchiusa fra {…} indica che tale sequenza è opzionale, mentre un simbolo, o una sequenza racchiusa fra (…), seguito da *, indica un numero arbitrario di ripetizioni della sequenza, incluso 0. Le carte sintattiche sono invece uno strumento di tipo grafico usate per la descrizione delle regole del linguaggio. Una carta sintattica è un grafo in cui linee orientate uniscono delle scatole di formato differente: seguendo le frecce ed interpretando opportunamente il contenuto delle scatole si costruiscono frasi sintatticamente corrette. Le scatole rettangolari in figura sono dette simboli non terminali in quanto il loro contenuto non compare direttamente nelle frasi. Esse fanno riferimento ad altre carte sintattiche (quelle il cui nome coincide col contenuto della scatola) che virtualmente devono essere inserite nella posizione assunta dal nodo del grafo. Le scatole tonde della figura sono dette simboli terminali n quanto contengono caratteri o sequenze di caratteri che devono apparire direttamente nella frase, nella posizione assunta dal nodo del grafo. Algoritmi e programmi 113 Figura 6 – Simboli Terminali (a) e Non Terminali (b) per le carte sintattiche Le carte sintattiche non solo permettono un rapido apprendimento delle regole della grammatica del linguaggio grazie all’immediatezza fornita dalla rappresentazione di natura grafica, ma mettono anche nella condizione di scoprire facilmente eventuali errori presenti nella frase. Difatti così come data una carta si è in grado di costruire una frase corretta del linguaggio, partendo dalla frase si può controllarne la correttezza ed in caso negativo si può anche individuare la causa degli errori. 3.5. La programmazione strutturata Un esecutore esegue un processo se: a) è stato progettato l’algoritmo che descrive come il processo deve essere svolto; b) l'algoritmo è stato espresso con un opportuno linguaggio di programmazione (programma) ad esso comprensibile. Il ruolo degli algoritmi è fondamentale se si pensa che essi sono indipendenti sia dal linguaggio in cui sono espressi sia dal computer che li esegue. Si pensi ad una ricetta per una torta alla frutta: - il procedimento è lo stesso indipendentemente dall'idioma usato; - la torta prodotta è la stessa indipendentemente dal cuoco. Il linguaggio di programmazione e il computer hanno lo scopo di far comprendere l'algoritmo e di far eseguire il processo. Ovviamente anche i computer e i linguaggi di programmazione sono importanti. Gli sviluppi tecnologici consentono di realizzare computer capaci di eseguire gli algoritmi in modo più rapido, più economico e più affidabile, rendendo esplorabili nuove aree applicative. Linguaggi di programmazione più sintetici rendono più facile il compito di esprimere gli algoritmi, con conseguente diminuzione dello sforzo umano speso nella programmazione. Ma tra gli aspetti o le proprietà degli algoritmi da valutare con più attenzione sicuramente il più importante è proprio il progetto che si presenta come onerosa attività intellettuale (molto più onerosa di quella di esprimere l'algoritmo con un linguaggio di programmazione) che richiede creatività ed intuito. E si noti che non esiste un algoritmo per il progetto degli algoritmi. Un altro aspetto è la valutazione della complessità della soluzione individuata: se ci sono algoritmi diversi per descrivere lo stesso processo, la complessità ci dice quale di essi è “migliore”, ossia quale viene eseguito nel minor tempo con la minima occupazione di memoria, più in generale con il miglior utilizzo di tutte le risorse disponibili. Inoltre, lo studio della correttezza è un altro aspetto importante che ci consente di valutare l'aderenza della soluzione alle specifiche del problema. Essere sicuri della 114 Capitolo terzo correttezza è un'attività tanto più complessa quanto più complesso risulta il progetto dell'algoritmo. Negli ultimi anni, il settore informatico è stato caratterizzato da un notevole incremento della produzione del software nei riguardi dell'hardware, e questo si riflette sui costi di un sistema informatico. Con queste premesse risulta necessario evitare di produrre software in base alle esperienze e/o alle iniziative del programmatore: il processo di produzione del software non può essere un processo di tipo artigianale (negli anni '60 il programmatore usava mille trucchi per risparmiare memoria!), ma deve servirsi di metodologie e tecniche sistematiche di progettazione e programmazione con fissati parametri di qualità e in maniera standard. La software engineering (ingegneria del software) è la branca dell'Ingegneria Informatica che raccoglie il patrimonio di metodi e tecniche per la produzione del software. Il processo di industrializzazione del software, introducendo metodi e standard, riduce i margini di creatività e personalismo e consente una produzione che soddisfi i seguenti requisiti: - buon livello qualitativo; - produttività medio-alta; - impiego di personale non troppo specializzato (la specializzazione é fornita dallo standard); - riduzione sensibile del costo del prodotto. In questo modo la produzione del software non risulta dissimile da quella di un qualsiasi prodotto industriale e anche per esso si può introdurre il concetto di ciclo di vita. Con il termine “ciclo di vita” del software si intende l'insieme delle fasi attraverso le quali si sviluppa il prodotto software come schematizzato nella figura seguente. Figura 7 – Fasi del ciclo di vita di un software Le fasi del ciclo di vita del software ruotano intorno all'idea di prodotto concentrandosi sulla sua progettazione, la sua produzione e infine il suo mantenimento in vita. Nell'ambito della software engineering si sono sviluppate tecniche, metodi e metodologie. Le tecniche mettono a disposizione un linguaggio e degli standard Algoritmi e programmi 115 che supportano normalmente alcune fasi del processo di produzione del software. I metodi consistono in un insieme di tecniche il cui uso é guidato da indicazioni formali. Le metodologie comprendono un insieme di tecniche il cui uso é guidato da regole procedurali ben precise. Le metodologie, a partire dalla formulazione del problema, consentono: - di individuare gli oggetti da elaborare e l'algoritmo; - formulare oggetti e algoritmo in modo non ambiguo; - progettare oggetti e algoritmi nell'ambiente disponibile. L'obiettivo principale della programmazione strutturata consiste nella costruzione di programmi con le seguenti caratteristiche: - leggibilità: un programma non esaurisce la propria esistenza dopo poche esecuzioni. Pertanto deve essere comprensibile ad altri programmatori; - documentabilità: un programma deve contenere al suo interno il significato e la motivazione di tutte le scelte di progetto effettuate; - modificabilità: un programma deve essere strutturato in modo da permettere un rapido adattamento ad una eventuale variazione di alcuni parametri del progetto; - provabilità: un programma deve essere costruito in modo da facilitare le attività di testing e debugging (controllo della correttezza e correzione degli errori) fin dalle prime fasi del progetto software. Comunque, si noti che la regola fondamentale per ottenere programmi di buona qualità è di eliminare tutti quei particolari che inficiano la costruzione logica del programma e quindi la sua chiarezza. Le caratteristiche fondamentali della programmazione strutturata sono: - la modularità, - l'uso di strutture di controllo ad un ingresso e ad una uscita, - l'applicazione del metodo top-down o di quello bottom-up nella fase di progettazione. La modularizzazione ci dice che un programma deve essere composto di moduli funzionali. Ogni modulo funzionale deve possedere un singolo e ben precisato compito. Ogni modulo inoltre deve essere dotato di un solo ingresso e di una sola uscita. La modularizzazione comporta una regolarità della struttura dei programmi ed un valido supporto per la fase di progetto, in quanto rispecchia la limitazione degli esseri umani ad esaminare un solo aspetto di un problema alla volta. Le strutture di controllo sono da considerarsi degli schemi di composizione dei moduli costituenti il programma. Esse devono assolutamente avere un solo ingresso ed una sola uscita. Tale condizione discende dalla necessità che un modulo, composto dall'integrazione di altri moduli tramite le strutture di un controllo, abbia un solo punto di ingresso ed un solo punto di uscita così come i singoli moduli componenti. Un valido contributo alla scelta delle strutture di controllo è stato apportato dal teorema di Böhm e Jacopini. Il top-down e lo stepwise refinement costituiscono il modo procedurale di raggiungimento della soluzione. Tale approccio parte dalla considerazione che la complessità di un problema da risolvere non consente di tener conto contemporaneamente di tutte le decisioni realizzative. Sarà quindi necessario procedere per “raffinamenti successivi” procedendo dal generale al particolare. Ovvero: “si analizza il problema al più alto livello possibile di astrazione 116 Capitolo terzo individuandone gli elementi più importanti e supponendo di avere a disposizione un sistema adatto ad eseguire gli elementi funzionali individuati”. Ogni elemento, a sua volta, diventa il problema da analizzare subendo una suddivisione in problemi più elementari. Il procedimento continua fino a raggiungere un livello di scomposizione comprensibile all'esecutore (o software del sistema in uso). Figura 8 – Approccio Top Down In figura 8 si riporta un esempio di approccio top-down e step-wise refinement per il semplice problema del calcolo di una percentuale. Si può notare che si procede dal livello astratto definito dall'utente (liv. 1) fino alla procedura dettagliata prossima all'esecutore (liv. 3) attraverso raffinamenti successivi. In questo modo la soluzione ad un problema si presenta come un albero in cui : - la radice corrisponde al problema, - i nodi rappresentano le differenti decisioni di progetto, - le foglie, infine, vengono associate alla descrizione della soluzione in modo comprensibile all'esecutore. Figura 9 – Soluzione di un approccio Top-Down Esiste un altro approccio per il raggiungimento della soluzione. Il metodo bottom-up difatti parte considerando il sistema reale a disposizione e creando man mano moduli elementari che opportunamente integrati formano moduli capaci di compiere operazioni più complesse. Il procedimento continua fino a quando è stato creato un modulo che corrisponde proprio alla soluzione del problema. Ripensando alla struttura ad albero, è come se si procedesse dalle foglie verso la radice. Algoritmi e programmi 117 Figura 8 – Approccio Bottom-Up Senza entrare nel merito dei due metodi possiamo sicuramente affermare che mentre il top-down é un metodo deduttivo, quindi più adatto alla media degli esseri umani, il bottom-up é un metodo induttivo che richiede molta intuizione ed inventiva. 3.5.1. La progettazione dei programmi di piccole dimensioni La progettazione dei programmi costituisce un'attività complessa che richiede un'attenta fase di analisi del problema e un continuo processo di valutazione delle decisioni che in ogni momento vengono prese. Una delle esigenze maggiormente sentita è quella di una separazione netta, in fase progettuale, tra il “cosa” (analisi dei requisiti e specifiche funzionali) e il “come” (progetto a diversi livelli di dettaglio). In effetti la distinzione tra il “cosa” e il “come” è comune a qualsiasi tipo di progetto ed è un modo per esprimere con altre parole la separazione fra analisi e sintesi. Nell'ambito di un progetto software, l'analisi richiede la capacità di acquisire tutte le informazioni necessarie alla comprensione del problema e di strutturarle in un modello che esprima, in un linguaggio adeguato, una rappresentazione coerente e completa di cosa si deve fare. Molto spesso, in passato, queste informazioni erano frutto di conoscenze generiche dell'analista sui problemi da affrontare, proprio perché questa fase veniva ritenuta di poco peso e si passava subito a scelte di realizzazione. Le informazioni, viceversa, devono essere ricavate sia dai colloqui con gli utenti di qualsiasi livello, ovvero con coloro che sono coinvolti nell'uso del programma sia a livello direttivo che esecutivo, che da un esame dell'ambiente in cui il programma sarà utilizzato: queste attività costituiscono la cosiddetta fase di definizione dei requisiti. Il compito dell'analista è, a questo punto, quello di trarre, dall'insieme confuso e a volte contraddittorio di esigenze ed obiettivi, un modello che esprima il problema con un formalismo atto ad aumentarne la comprensibilità. Questo modello dovrebbe contenere sia i requisiti funzionali (cosa deve fare il programma 118 Capitolo terzo e su quali dati deve operare) sia i requisiti non funzionali (quali prestazioni il programma deve offrire) e dovrebbe fornire un'indicazione delle priorità con le quali i requisiti, funzionali o no, devono essere presi in considerazione. Il passaggio dall'acquisizione dei requisiti alla formulazione del modello rappresenta la cosiddetta fase di analisi di un problema. Il progetto, invece, richiede la capacità di prendere decisioni che permettano di realizzare un programma che soddisfi tutti i requisiti funzionali, nei limiti stabiliti dai requisiti non funzionali. Ai fini della manutenibilità del prodotto, è necessario evitare sconfinamenti dalla fase di analisi a quella di progetto, ovvero non si devono assumere decisioni atte a definire la soluzione di un problema (progetto), mentre si sta ancora definendo il problema stesso (analisi); tali scelte, infatti, sono difficilmente documentabili. In tal modo si opera una separazione, di tipo verticale, fra le attività di analisi e quelle di progettazione utile ad riduzione delle difficoltà. Ricapitolando, i motivi alla base di tale separazione sono: - la possibilità di documentare in maniera completa i requisiti del problema e quindi, indirettamente, le scelte della fase di progetto; - l'impossibilità d'inficiare i risultati dell'analisi a causa di scelte anticipate di progetto; - la possibilità di rendere indipendente l'analisi dai vincoli di realizzazione. Un'altra distinzione, in senso orizzontale, avviene fra dati e funzioni che rappresentano gli aspetti duali, ma strettamente legati, di ogni problema che si esamina; tale separazione è dovuta sia a ragioni intrinseche, in quanto dati e funzioni sono caratterizzati da proprietà differenti, che a ragioni metodologiche in quanto è opportuno trattare separatamente funzioni da un lato e dati dall'altro, in modo da ottimizzarne gli aspetti relativi in maniera indipendente dai condizionamenti che, viceversa, un'analisi complessiva comporterebbe. Ricapitolando, la separazione orizzontale tra dati e funzioni consente di ottenere: - l'esame accurato dell'aspetto semantico dei dati; - la completezza dell'analisi, in quanto essa viene compiuta da differenti punti di vista; - l'assicurazione che dati e funzioni assumano il loro giusto peso in analisi e non si enfatizzino gli uni rispetto agli altri. Le problematiche discusse riguardano sia i progetti software di grandi dimensioni che la stesura di un semplice programma. Pertanto verrà fornita una descrizione di criteri applicabili alla realizzazione di programmi di piccole dimensioni quali quelli che nel testo saranno presentati. L’insieme di tali criteri non può considerarsi come una metodologia di progettazione ma un primo approccio introduttivo utile alla comprensione delle problematiche di carattere generale. Nel seguito, i concetti introdotti verranno applicati come approccio metodologico alla progettazione dei programmi. In particolare le principali fasi dello sviluppo di un programma possono essere ricondotte a: - fase di analisi dei requisiti e delle funzioni - fase di progetto - analisi critica della soluzione La fase di analisi dei requisiti e delle funzioni ha lo scopo di definire in maniera non ambigua cosa il programma deve fare. è la fase più importante in quanto solitamente i problemi si presentano in una forma alquanto imprecisa ed è Algoritmi e programmi 119 fondamentale che le imprecisioni siano eliminate quanto prima per evitare che esse incidano sulle fasi successive del progetto. Infatti i costi di correzione degli errori aumentano quanto più si avanza nel progetto e sono massimi se erroneamente si risolve un problema diverso da quello desiderato. è allora necessario, per scoprire le ambiguità, verificare che ogni aspetto delle specifiche abbia un'unica interpretazione nel contesto delle applicazioni che si descrivono ed effettuare delle esemplificazioni degli effetti delle trasformazioni tra dati di ingresso e dati di uscita. Tali esemplificazioni costituiscono la base per un confronto del programma con l'utente che permette di verificare la correttezza delle specifiche. Sempre ai fini della correttezza occorrerà: - verificare che nelle specifiche non vi siano inconsistenze; - verificare che le specifiche coprano tutte le possibili situazioni coinvolte nel problema. Al termine della fase di analisi si deve disporre della documentazione su: - la definizione dei dati di ingresso al problema; - la definizione dei dati in uscita dal problema; - la descrizione di un metodo risolutivo che sulla carta risolva le specifiche del problema. La fase di progetto può iniziare solo dopo un'accurata fase di definizione dei requisiti e si articola nelle attività di raffinamento successivo dei dati e dell'algoritmo. Si è già detto che la scelta delle strutture dati con cui rappresentare gli oggetti coinvolti dal problema gioca un ruolo fondamentale nella scelta dell'algoritmo risolutivo. Inizialmente va scelta la struttura astratta che risponde in maniera più naturale alle caratteristiche del problema. Successivamente, tra tutte le realizzazioni possibili occorrerà scegliere quella che opera il compromesso ottimo dal punto di vista della efficienza e della comprensibilità. Per affrontare la scelta in modo adeguato è fondamentale la conoscenza del numero e tipo di operazioni sui dati previste dal problema. Costruire un algoritmo equivale ad esaminare una specifica realtà (il problema assegnato), costruirne un'astrazione, e infine rappresentare una tale astrazione in maniera formale in un linguaggio di programmazione. Se il problema è complesso, le varie scelte di astrazione e di rappresentazione si sovrappongono, cosicché non si riesce più a controllare il processo di progettazione dei programmi. In genere la mente umana non è in grado di tenere contemporaneamente sotto controllo gli effetti intrecciati di tutte le scelte che occorre effettuare. La tecnica top-down si presenta come un approccio guida con l'obiettivo preciso di ridurre la complessità e di offrire uno strumento di composizione di un'architettura modulare dei programmi. Il modo di procedere di tale approccio è una diretta applicazione del metodo analitico deduttivo che si è rilevato, storicamente, il più adatto alla media degli esseri umani. Una tale tecnica, come già visto. viene anche detta per raffinamenti successivi (step-wise refinement), in quanto consente di trasformare un problema in una gerarchia di problemi di difficoltà decrescente associando a tale gerarchia di problemi una gerarchia di linguaggi per la descrizione dell'algoritmo. Ogni passo di raffinamento implica alcune decisioni progettuali ed è importante che queste decisioni siano rese esplicite e che il programmatore sia conscio del criterio decisionale adottato e dell'esistenza di soluzioni alternative. Fra i criteri progettuali vanno menzionati la chiarezza e la regolarità della struttura del programma. 120 Capitolo terzo Un criterio guida nel processo di raffinamento per passi consiste nel decomporre quanto più possibile le decisioni per distinguere aspetti che sono soltanto formalmente interdipendenti e per ritardare più a lungo possibile quelle decisioni che concernono dettagli di rappresentazione. Un modo di procedere è quello per livelli di astrazione. L'astrazione consiste nell'estrazione di dettagli essenziali mentre vengono omessi i dettagli non essenziali. Ogni livello della decomposizione presenta una visione astratta dei livelli più bassi in cui i dettagli vengono spinti, quanto più possibile, ai livelli ancora più bassi. I moduli di livello più alto specificano gli obiettivi di qualche azione oppure cosa deve essere fatto, mentre quelli di livello più basso descrivono come l'obiettivo verrà raggiunto. Il modulo più basso nella gerarchia della struttura del programma dipende dal linguaggio con cui si lavora. Tale approccio aiuta così a rendere il programma più versatile, isolando la dipendenza dalla macchina in pochi moduli a basso livello. Il passo di raffinamento iterativo produce, a partire dal problema, una prima decomposizione dell'algoritmo e prosegue con successivi raffinamenti, sempre più vicini all'espressione finale dell'algoritmo nel linguaggio di programmazione. E durante le varie decomposizioni possono essere introdotte strutture dati necessarie alla definizione dell'algoritmo; per esse valgono le considerazioni fatte sul raffinamento dei dati. Ad ogni passo della decomposizione si devono organizzare i sottoproblemi in: - sequenza; quando dall'analisi del problema ci accorgiamo che l'insieme delle attività devono essere svolte una di seguito all'altra; - alternativa; quando si deve scegliere tra una o più attività; - iterativa; quando una o più attività devono essere eseguite più volte. Ogni sottoproblema che si individua può essere espresso con una frase del linguaggio naturale sintetica ed espressiva (se si vogliono ritardare decisioni di progetto relative all'implementazione della funzionalità individuata), o direttamente con un insieme di istruzioni del linguaggio di programmazione (se la funzionalità è facilmente descrivibile). Nel primo caso il sottoproblema diventa un nuovo problema da affrontare e per esso devono essere riapplicati tutti i criteri di decomposizione descritti. In particolare, devono essere definite e documentate le variabili di ingresso e di uscita e le specifiche funzionali di ogni sottoproblema. Inoltre deve essere posta particolare attenzione alle interfacce tra i vari sottoproblemi in modo che i risultati di un sottoproblema siano coerenti in numero e tipo con i dati di ingresso del sottoproblema posto in sequenza. L’analisi critica è l'ultima fase dell'approccio metodologico. In essa occorre effettuare una minuziosa valutazione della soluzione adottata. Dapprima si verifica la correttezza della versione dell'algoritmo, mediante ad esempio una simulazione della sua esecuzione con il trace fissando un set di dati in ingresso. Successivamente è possibile compiere una valutazione dell'efficienza della soluzione adottata confrontandola con altre soluzioni oppure studiando l'impatto di una particolare scelta di progetto. Ognuna delle azioni descritte deve essere opportunamente documentata in modo che unitamente alle specifiche iniziali si possa avere a disposizione un insieme esaustivo di specifiche utilizzabili per una futura ristrutturazione dell'algoritmo. Algoritmi e programmi 3.5.2. 121 La documentazione dei programmi La documentazione dei programmi è lo strumento fondamentale per la chiarezza e la leggibilità dei programmi. Tali caratteristiche di leggibilità consentono: 1) una più semplice comprensione di quale sia il problema che il programma risolve e quindi della correttezza del programma stesso; 2) una più semplice prosecuzione del progetto ogni qualvolta lo si sia interrotto; 3) una più elementare comunicazione delle scelte di progetto; 4) una più semplice modificabilità del programma al variare delle specifiche del problema. La regola principale della documentazione, e forse anche la più disattesa, è quella di produrre la documentazione nel corso stesso del progetto in quanto si può essere certi della efficacia e della completezza della documentazione soltanto se si documentano le scelte nel momento in cui esse vengono fatte. Inoltre, un'altra regola importante da seguire è quella di inserire la documentazione quanto più possibile all'interno del programma in modo che viva con il programma stesso, cambi e cresca con esso, e quindi sia sempre aggiornata. Comunque una buona documentazione deve essere articolata in due livelli: a) documentazione esterna del programma; b) documentazione interna del programma. La documentazione esterna è il primo livello di documentazione e va compilato preliminarmente nella fase di analisi dei requisiti. Essa costituisce lo strumento fondamentale per l'utente del programma in quanto descrive soltanto il "cosa" fa il programma e non il "come" lo fa. Inoltre, la documentazione esterna deve segnalare dettagli operativi tali da rendere autonomo l'utente del programma nell'uso dello stesso. Di essi i più importanti sono: a) funzionalità esterne; b) attivazione del programma; c) diagnostiche di errore; d) configurazione richiesta del sistema; e) indicazione sull'installazione del programma; f) versione e data di aggiornamento. La documentazione interna descrive la struttura interna del programma in termini di scelte sulle strutture dati e sull'algoritmo. Tra le principali tecniche a disposizione del progettista per generare la documentazione interna del programma si ricordano: 1) evidenziazione della struttura del programma mediante l'uso dell'indentazione (indentare un programma significa mettere in evidenza nel testo del programma, mediante opportuni incolonnamenti, blocchi di istruzioni che svolgono una funzionalità ben precisa); 2) documentazione top-down, ossia facendo comprendere come il programma è stato generato attraverso i vari raffinamenti; 3) uso di nomi di variabili autoesplicativi, ossia tali da spiegare il ruolo da esse svolte nel corpo del programma; 4) commento del programma attraverso le frasi di commento di cui tutti i linguaggi di programmazione sono dotati. Queste frasi consentono di descrivere il significato di un blocco di istruzioni (motivazioni) o 122 Capitolo terzo specificare lo stato in cui si trovano le variabili del programma tutte le volte che il controllo passa per il punto individuato dalla linea di commento (asserzioni). Le motivazioni vanno utilizzate all'inizio di ciascun frammento di programma che realizzi una funzionalità significativa. Infatti, hanno lo scopo di spiegare cosa il frammento di programma si accinge a calcolare indipendentemente da come lo calcola. Le motivazioni possono essere evidenziate anteponendo alla frase di commento la sigla “M:”. (*M: calcolo la somma degli n numeri*) (*M: inverto la matrice *) Particolarmente importante è il commento di motivazione globale posto all'inizio del programma in cui si descrive in modo succinto il problema che il programma risolve. Si osservi che l'applicazione della tecnica top-down comporta una generazione pressoché naturale dei commenti motivazione. Infatti le frasi del linguaggio naturale, introdotte durante le fasi di decomposizione, diventano le motivazioni delle istruzioni che le specificano nel raffinamento successivo. Le asserzioni devono essere inserite al termine di ciascun frammento di programma in quanto consentono di definire lo stato di una o più variabili a seguito dell'esecuzione delle istruzioni precedenti. Una frase di questo tipo ha lo scopo di chiarire le relazioni logiche che sussistono tra le variabili in quel punto del programma e può essere espressa anteponendo alla frase di commento la sigla "A:". (*A: X=A*2+C-1 con A>C*) Si noti che la derivazione delle relazioni logiche tra le variabili obbliga ad una comprensione profonda del significato del programma. Per questo motivo le asserzioni possono essere utilizzate per effettuare una prova qualitativa della correttezza del programma. Tra tutte le asserzioni particolare cura va posta su quelle riguardanti le variabili di input in quanto con esse si specificano le condizioni limite nelle quali la soluzione adottata si troverà a lavorare. Capitolo quarto La struttura dei programmi 4.1. Le frasi di un linguaggio di programmazione Tutti i linguaggi di alto livello prevedono quattro tipologie di frasi diverse: - le istruzioni che tradotte in linguaggio macchina indicano al processore le operazioni da svolgere; - le strutture di controllo che definiscono l’ordine di esecuzione delle istruzioni; - le frasi di commento che permettono l’introduzione di frasi in linguaggio naturale utili a rendere più comprensibili i programmi ad un lettore umano; le frasi di commento non vengono tradotte in linguaggio macchina e si distinguono in asserzioni e motivazioni; le asserzioni sono commenti destinati a fissare in un punto del programma lo stato di una o più informazioni. Per tale motivo permettono di chiarire “le condizioni nelle quali vengono scritte le istruzioni successive”. Le motivazioni sono invece commenti destinati a chiarire le ragioni e/o gli obiettivi per i quali il blocco di programma, successivo al commento, viene scritto. In altri termini, un'asserzione si riferisce alle conseguenze delle elaborazioni che la precedono, mentre la motivazione serve a dire a priori cosa si intende fare con le istruzioni successive; - le dichiarazioni con le quali il programmatore da ordini al traduttore del linguaggio di programmazione; anche le dichiarazioni non vengono tradotte in linguaggio macchina poiché servono solo a guidare il processo di traduzione. 4.1.1. Le dichiarazioni Tra le tante dichiarazioni le più importanti sono quelle con cui si definiscono le variabili sulle quali si svolgono le elaborazioni dell’algoritmo. Ad una variabile corrisponde una porzione di memoria la cui dimensione e le cui regole di uso dipendono dal suo tipo. La definizione di tipo consente di introdurre dei modelli degli oggetti della realtà in quanto ne fornisce le caratteristiche strutturali (cosa sono) e funzionali (a cosa servono). Una definizione di tipo di dato sufficientemente rigorosa e particolarmente adatta agli usi di tipo che verranno fatti in seguito è la seguente: “per tipo di dato si intende un insieme di valori associati a delle operazioni definite su di essi”. Con 124 Capitolo quarto le definizioni di tipo è possibile trattare l'informazione in maniera astratta, cioè prescindendo dal modo concreto con cui essa è rappresentata all'interno di una macchina. In molti linguaggi esistono dichiarazioni per la definizione di tipi: type t = T; dove t è il nome del nuovo tipo e T è un descrittore di tipo, cioè un costrutto sintattico del linguaggio atto a definire il tipo t. Il descrittore T può essere anche un tipo già definito o predefinito nel linguaggio, in modo da introdurre dei tipi sinonimi. L'istanziazione di variabile avviene poi semplicemente con frasi dichiarative come quella seguente: var v : T; 4.1.2. Le frasi di commento Le frasi di commento sono frasi in linguaggio naturale prive di ogni valore esecutivo o dichiarativo che consentono di migliorare la leggibilità e la chiarezza del programma. Si distinguono in asserzioni e motivazioni. Le asserzioni sono commenti destinati a fissare in un punto del programma lo stato di una o più variabile. Per tale motivo permettono di chiarire le condizioni nelle quali vengono ad operare le istruzioni successive. Le asserzioni sono molto importanti per controllare la correttezza dei programmi. Le motivazioni sono invece commenti destinati a chiarire le ragioni e/o gli obiettivi per i quali il blocco di programma, successivo al commento, viene scritto. Le motivazioni sono essenziali perché un programma possa essere compreso da altre persone. In altri termini, un'asserzione si riferisce alle conseguenze delle elaborazioni che la precedono, mentre la motivazione serve a dire a priori cosa si intende fare con le istruzioni successive. Per indicare le frasi di commento si usano delle sequenze di caratteri (marcatori) che indicano l’inizio (ad esempio /* in C o (* in Pascal) e la fine (rispettivamente */ e *) ) del commento che così può occupare più righi. In alcuni casi il commento occupa un solo rigo ed allora lo si indica con un solo marcatore iniziale (ad esempio // del C). Le due modalità possono coesistere nello stesso linguaggio. I linguaggi non forniscono strumenti per specificare il tipo di commento: per farlo il programmatore potrà anteporre alla frase la notazione “A:” per le asserzioni e “M:” per le motivazioni come gli esempi seguenti illustrano. (*A: alfa deve avere valore negativo *) /*A: i>=10 implica condizione di errore */ x= totale / numero_elementi //M: Calcolo della media 4.1.3. L’istruzione di calcolo ed assegnazione Con le istruzioni elementari di calcolo ed assegnazione si prescrive il calcolo di una qualche espressione con memorizzazione del risultato. In tutti i linguaggi di programmazione essa è presente in una forma del tipo: La struttura dei programmi v 125 espressione; che indica che l’esecutore deve prima risolvere l’espressione presente nel secondo membro e, solo quando ne ha prodotto il risultato, assegnare quest’ultimo alla variabile v posta a primo membro. L’istruzione viene anche scritta in una delle forme seguenti: v := espressione; v = espressione; L’esecuzione in due tempi distinti e successivi del calcolo dell’espressione e della memorizzazione del risultato consente di scrivere istruzioni del tipo: i = i + 1; Infatti l’esecutore prima opera sul secondo membro (somma uno al valore di i) e poi deposita il risultato in i: per cui se in i si trova 5 prima dello svolgimento dell’istruzione, al suo termine si troverà 6. 4.1.4. I costrutti di controllo I costrutti di controllo indicano all'esecutore l'ordine in cui le operazioni devono essere eseguite. Essi devono essere scelti in modo da rispecchiare quanto più possibile i meccanismi che naturalmente vengono usati quando si descrive (ad esempio in italiano) una qualsiasi successione di operazioni. Essi si dividono in costrutti di sequenza, selezione ed iterazione. Il costrutto di controllo più semplice è quello che specifica che due o più operazioni (elementari o no) devono essere eseguite una dopo l'altra. Tali costrutti sono detti costrutti di sequenza e vengono indicati nel seguente modo: PRIMA stacca il contatore POI sostituisci la presa QUINDI riattacca il contatore o anche con una notazione più compatta che usa il carattere punto e virgola come indicatore dell'istruzione successiva: stacca il contatore; sostituisci la presa; riattacca il contatore Il punto e virgola consente anche di scrivere più azioni della sequenza su di uno stesso rigo: stacca il contatore; sostituisci la presa; riattacca il contatore 126 Capitolo quarto Se poi si vuole marcare la funzionalità che l'intera sequenza rappresenta, indicandone precisamente l'inizio e la fine, si possono introdurre le due parentesi BEGIN ed END o le parentesi graffe come di seguito riportato. BEGIN stacca il contatore; sostituisci la presa; riattacca il contatore END stacca il contatore; sostituisci la presa; riattacca il contatore L'uso delle parentesi è importante per evidenziare le varie parti dell'algoritmo e non perdere traccia dell'eventuale processo di raffinamento, migliorando così la leggibilità del programma complessivo. Difatti il blocco presentato si può vedere come raffinamento dell'istruzione non elementare: (*M: operazioni di intervento su presa elettrica *) Inoltre un programma composto con blocchi di questo tipo è anche più facilmente gestibile perché la correzione, sostituzione o modifica di un blocco richiede un intervento locale alla coppia di parentesi. I costrutti condizionali consentono di subordinare l'esecuzione di una certa operazione al verificarsi o meno di una specificata condizione. La struttura di selezione più semplice è quella tra due alternative: SE (la carta scoperta è quadri) ALLORA vinci ALTRIMENTI perdi o anche, con un'altra notazione, tipica dei linguaggi di programmazione: IF (la carta scoperta è quadri) then vinci else perdi In alcuni casi i costrutti di sequenza possono prescrivere la selezione di una sola operazione: SE (hai fame) ALLORA mangia IF (hai fame) then mangia Altre volte, invece, si può voler selezionare un'operazione tra più di due alternative: NEL CASO (in cui il colore del semaforo) CASE (colore del semaforo) OF è ROSSO: fermati ROSSO: fermati è VERDE: passa l'incrocio VERDE: passa l'incrocio è GIALLO: passa con prudenza o fermati GIALLO: passa con prudenza o fermati END La struttura dei programmi 127 I costrutti iterativi prescrivono di ripetere l'esecuzione di una o più operazioni; tale ripetizione viene sospesa al verificarsi di un evento. Sono costrutti iterativi: RIPETI i compiti FINCHE' (suona la sveglia) MENTRE (piove) DEVI usare l'ombrello PER (10 giorni) DEVI non venire all'università REPEAT i compiti UNTIL (suona sveglia) WHILE (piove) DO usa l'ombrello FOR giorni:=1 TO 10 DO non venire all'università Si osservi che il primo costrutto ha termine quando diventa vera la condizione (la sveglia suona), il secondo invece quando la condizione diventa falsa (finisce di piovere). Infine nel terzo esempio il numero di ripetizioni è noto a priori: chiameremo queste iterazioni cicliche o iterazioni enumerative per distinguerle dalle altre due. Per poter confrontare costrutti con caratteristiche simili si introducono i concetti di equivalenza e di equivalenza funzionale. Si dicono equivalenti due programmi che evocano le stesse sequenze di esecuzione. Sono invece funzionalmente equivalenti due programmi che, sollecitati nello stesso modo, producono lo stesso risultato. Si noti che due programmi equivalenti sono anche funzionalmente equivalenti, mentre non è vero, in generale, che due programmi funzionalmente equivalenti sono anche equivalenti. Ad esempio i due programmi seguenti: assegna valore ad x e y; if (x<0) then stampa il valore di x+y else stampa il valore di x*y assegna valore ad x e y; if (y>0) then stampa il valore di x*y else stampa il valore di x/y producono lo stesso risultato (il prodotto di x per y) se le due variabili sono positive, e risultati diversi negli altri casi (la somma e la divisione se sono entrambe negative, la somma o il prodotto e il prodotto e il rapporto se il loro segno è discorde). L'equivalenza funzionale si riferisce così ad un ben preciso insieme di valori di ingresso: nel caso delle variabili x e y entrambe positive i due programmi sono funzionalmente equivalenti, mentre non lo sono negli altri casi. L'equivalenza funzionale è utile per comprendere le differenze tra le varie strutture di controllo e per giustificare da un punto di vista pratico la necessità di costrutti simili. Per quanto riguarda la selezione è sempre possibile ricondurre un costrutto ifthen-else ad una sequenza di soli if-then. Così il costrutto: if (condizione) then azione 1 else azione 2 128 Capitolo quarto che indica che in caso di verità della condizione deve essere svolta la prima azione, mentre in caso contrario (falsità della condizione) deve essere svolta la seconda, è funzionalmente equivalente alla sequenza dei due if-then: if (condizione) then azione 1 if (not condizione) then azione 2 Le due sequenze non sono equivalenti perché in azione 1 ci potrebbero essere operazioni che alterano il valore di condizione facendola diventare falsa generando una sequenza di esecuzione composta da entrambe le azioni. Comunque, disponendo della prima notazione non ha senso usare la seconda forma perché meno chiara e sintetica. Inoltre la riscrittura di “condizione” crea molti problemi di gestione quali errori che possono avvenire nella scrittura del testo, duplicazione delle eventuali correzioni, ed altri. La struttura case, che prescrive la valutazione di una espressione (anche detta selettore) e la scelta dell'azione a cui è stato associato il valore ottenuto da tale valutazione, può essere ricondotta ad un insieme di if disposti l'uno dentro l'altro. Così il seguente costrutto case: case (espressione) of a: azione 1 b: azione 2 c: azione 3 end; è equivalente a: if (espressione=a) then azione 1 else if (espressione=b) then azione 2 else if (espressione=c) then azione 3 che, come si nota, comporta la riscrittura dell'espressione più volte ed una notazione molto più pesante. In genere non vale il passaggio inverso: ossia non è detto che una struttura di if-then-else nidificati (inseriti uno dentro l'altro) è esprimibile con un case. Ad esempio quando le condizioni degli if sono diverse come nel caso che segue: if (espressione 1) then azione 1 else if (espressione 2) then azione 2 else if (espressione 3) then azione 3 La struttura dei programmi 129 Per quanto riguarda le due strutture iterative while e repeat si noti che è sempre possibile ricondurre l'una all'altra. Il while prescrive prima la valutazione della condizione e dopo l'esecuzione delle azioni qualora il valore ottenuto sia vero. Il ciclo termina quando la condizione diventa falsa. È evidente che il ciclo non avviene se la condizione è falsa la prima volta che è calcolata. Il repeat invece prescrive l'esecuzione delle azioni e, dopo, la valutazione della condizione per cui qualsiasi ne sia il valore, le azioni vengono eseguite almeno una volta. La ripetizione delle azioni termina quando la condizione diventa vera. Si noti che, in entrambi casi, quando il ciclo ha avuto inizio, si deve far in modo che le azioni eseguite ad ogni ripetizione alterino le variabili presenti nella condizione se si vuole che il ciclo termini. Da quanto detto discende che: repeat azione until (condizione) Azione while (not condizione) do azione sono equivalenti, così come lo sono: while (condizione) do azione repeat if (condizione) then azione until (not condizione) È comunque evidente come le strutture presentate per prime siano da preferire per la loro sinteticità. Da ultimo, anche la struttura ciclica è riconducibile ad una iterativa, e di conseguenza anche all'altra. Difatti un ciclo iterativo prescrive la ripetizione di azioni un numero di volte fissato a priori e determinato dal fatto che una variabile detta contatore di ciclo, a partire da un valore iniziale raggiunge un valore finale o per valori crescenti (indicato dal to) o decrescenti (indicato dal downto). Allora, fissato vp<vg (con vp Valore Piccolo e vg Valore Grande), si ha che le strutture nella colonna di sinistra sono funzionalmente equivalenti a quelle nella colonna di destra. for i:=vp to vg do azione i:= vp; while i<vg do begin azione; i:= i +1 end for i:=vg downto vp do azione i:= vg while i>vp do begin azione; i:=i-1 End 130 Capitolo quarto Si noti però come i costrutti ciclici sono: - più espressivi, perché mostrano su una sola linea tutti i parametri che governano l'iterazione; - più sintetici, perché risparmiano la scrittura delle frasi di inizializzazione, controllo e modifica della variabile; - più sicuri, in quanto garantiscono che non si verifichino cicli senza fine (dovuti ad errori di gestione della variabile contatore di ciclo). In generale i costrutti ciclici consentono anche di specificare il passo nel caso sia diverso da 1 con una notazione del tipo: for i:=vp to vg STEP v do azione for i:=vg downto vp STEP v do azione 4.2. La potenza espressiva Le strutture di controllo presentate sono state introdotte sulla base di motivazioni puramente intuitive. Ci si può chiedere se tale scelta può in qualche modo essere giustificata. In particolare possiamo domandarci se le strutture di controllo introdotte siano sufficienti ad esprimere tutti gli algoritmi che possono interessare e se siano tutte realmente necessarie. La risposta a queste domande è stata data da Böhm e Jacopini i quali, nel 1966, hanno dimostrato che in pratica tutti gli algoritmi possono essere espressi utilizzando: - una sola struttura di sequenza - una sola struttura selettiva - un sola struttura iterativa. Difatti non a caso queste strutture possono considerarsi i tre fondamentali “modelli di pensiero” mediante i quali siamo in grado di descrivere qualunque successione di eventi. Comunque si fa generalmente uso di più costrutti per ogni schema di controllo scegliendo in funzione della potenza espressiva e della praticità d'uso. Inoltre si può ulteriormente osservare che alcuni costrutti diventano non necessari se si introduce nel linguaggio un meccanismo di riferimento delle istruzioni. In alcuni linguaggi è difatti possibile etichettare con un numero o un nome le istruzioni. L'uso di tali etichette permette di prescrivere che l'istruzione successiva da eseguire non sia quella scritta nel rigo seguente ma quella identificata dalla etichetta specificata. Ad esempio nella sequenza: 100 azione 1; azione 2M if (condizione) then goto 100; azione 3 l'istruzione goto 100 prescrive che l'esecuzione continui con l'istruzione etichettata con il numero 100 e non con l'azione 3. Nei linguaggi che prevedono La struttura dei programmi 131 tali istruzioni, dette istruzioni di salto incondizionato, i costrutti sono esprimibili in termini di if-then opportunamente composto con istruzioni di salto come di seguito riportato: if (condizione) if (condizione) then azione 1 then begin else azione 2; azione 1; azione 3 goto 100 end; azione 2; 100 azione 3 while (condizione) 100 if (condizione) do azione then begin azione; goto 100 end repeat 100 azione; azione if (not condizione) until (condizione) then goto 100 case (espressione) of if (espressione=a) a : azione 1 then begin b : azione 2 azione 1; c : azione 3 goto 100 end; end azione 4 if (espressione=b) then begin azione 2: goto 100 end if (espressione=c) then begin azione 3; goto 100 end 100 azione 4 Per lunghi anni il goto è stato largamente usato dai programmatori. Poi Dijkstra, in un suo articolo del 1968, notando che la capacità dei programmatori era inversamente proporzionale all'uso che facevano del goto, propose con molto scalpore la sua abolizione dai linguaggi di programmazione. Egli fece notare come l'uso indiscriminato del goto portasse a dei programmi con molti intrecci, senza un filo logico, e quindi poco chiari. Difatti con i goto si possono creare blocchi che hanno più ingressi e più uscite, è possibile entrare nel mezzo di funzionalità ben evidenziate nel testo, è possibile fare tutto quanto inficia la costruzione logica di un programma. Ad esempio, nel programma riportato di seguito, si nota come ci siano tre punti di ingresso (istruzioni etichettate con 100, 200 e 300), due punti di uscita (goto 400 e la fine della sequenza), e l'interruzione del for (goto 300) nonostante tale costrutto debba essere usato quando il numero di volte sia fissato a priori. Tutto ciò non favorisce la chiarezza del testo, né una sua facile sostituzione o modifica ed è quindi da evitare. In genere l'uso di goto in avanti (verso istruzioni 132 Capitolo quarto disposte successivamente nel testo) è un modo poco ortodosso di realizzare delle forme di selezione, mentre salti all'indietro corrispondono alla realizzazione di forme di iterazione. 100 azione 1; azione 2; 200 azione 3; if (condizione 1) then goto 200 else goto 400; 300 azione 4; for i:=e1 to es do begin azione 5; if (condizione 2) then goto 300; azione 6 end In alcuni casi non si può fare a meno di usare il goto: o perché non sono presenti altri costrutti di controllo o perché, per ragioni dettate dal tipo di problemi risulta necessario l'abbandono completo di porzioni di programma (per esempio per segnalare subito errori catastrofici). In questi casi l'uso dell'istruzione di salto deve essere valutato con molta attenzione e segnalato con appositi commenti. In conclusione, l'uso dell'istruzione di salto non favorisce la costruzione logica dell'algoritmo, in quanto non è naturale esprimere i propri ragionamenti in termini di interruzioni in avanti o verso cose ancora da specificare. Per tale motivo il goto non verrà usato. È stato presentato in quanto in alcuni casi diventa indispensabile, nel processo di raffinamento, per la specificazione di quei costrutti che possono mancare nel linguaggio di programmazione usato per comunicare i programmi all'esecutore macchina. In tutti gli esempi che sono stati introdotti per illustrare le strutture di controllo, è stato usato il termine azione per sottolineare il fatto che in quel punto della struttura può comparire sia una istruzione semplice che una non elementare, ossia composta a sua volta da un insieme di altre istruzioni e strutture di controllo. In altre parole le strutture di controllo possono essere sia disposte in sequenza tra di loro che inserite le une dentro le altre con una modalità detta di innestamento o nidificazione. Se sono disposte l'una dopo l'altra, allora dopo aver espletato le azioni indicate da una struttura di controllo, si procede prendendo in considerazione quella ad essa successiva nella sequenza statica. Nel caso della seguente sequenza statica: I0; case (espressione) of 1: if (condizione1) then begin I1 ; I2 end La struttura dei programmi 133 else I3; 5: for i:=1 to 3 do I4 8: I5 end if (condizione2) then I6 I f; le sequenze di esecuzione (si) evocate sono: s1 I0 I1 I2 I6 If s2 I0 I3 I6 If s3 I0 I4 I4 I4 I6 If s4 I0 I5 I6 If s5 I0 I6 If s6 I0 I1 I2 If s7 I0 I3 If s8 I0 I4 I4 I4 If s9 I0 I5 If s10 I0 If Tabella 1 – Sequenze di esecuzioni evocate Una sequenza di istruzioni semplici e strutture di controllo è detta istruzione composta. Una istruzione composta può contenere al suo interno altre strutture di controllo con una gerarchia che a livello più alto termina nel programma. Poiché le istruzioni semplici (l'assegnazione di valore, le procedure di input e di output e le operazioni definite sulle strutture dati) presentano un solo punto di ingresso e di uscita, la loro composizione attraverso le strutture di controllo IF-THEN-ELSE, IFTHEN, CASE, REPEAT-UNTIL, WHILE-DO, FOR-DO, porta ad una istruzione composta che mostra complessivamente ancora un unico punto di ingresso e di uscita. Tale caratteristica viene anche indicata dicendo che la sequenza è di tipo one-in e one-out. L’uso di istruzioni di salto impedisce la costruzione di sequenze di tipo one-in e one-out. Il numero di sequenze di esecuzione evocate da una istruzione composta dipende dal tipo di strutture di controllo utilizzate e dal modo in cui queste sono composte tra loro. Se le strutture sono considerate singolarmente, allora entrambe le strutture di controllo IF-THEN-ELSE e IF-THEN generano due sequenze di esecuzione, il CASE tante sequenze di esecuzione quanti sono i valori previsti per il selettore, mentre non è possibile fare previsioni per le strutture iterative in quanto il numero di sequenze di esecuzione dipende dai valori che assumono le variabili di controllo dei cicli durante l'esecuzione. Le tabelle seguenti riportano alcuni semplici esempi di strutture di controllo selettive disposte in sequenza e innestate con a lato tutte le sequenze di esecuzione evocate. if ca then I1 else I2; if cb then I3 else I4; If ; s1 I1 I3 If s2 I1 I4 If s3 I2 I3 If s4 I2 I4 If 134 Capitolo quarto if ca then I1 else I2; if cb then I3 else I4; if cd then I5 else I6; If ; s1 I1 I3 I5 If s2 I1 I3 I6 If s3 I1 I4 I5 If s4 I1 I4 I6 If s5 I2 I3 I5 If s6 I2 I3 I6 If s7 I2 I4 I5 If s8 I2 I4 I6 If Tabelle 2,3 – Istruzioni selettive e sequenze di esecuzioni evocate Due sequenze per un solo if, quattro per due e otto per tre. Si comprende allora che una sequenza di n strutture selettive evoca 2n sequenze di esecuzione. if ca then if cb then I1 else I2; else I3; If ; if ca then if cb then if cd then I1 else I2; else I3; else I4; If ; s1 I1 If s1 I1 If s2 I2 If s3 I3 If s2 I2 If s3 I3 If s4 I4 If Tabelle 4,5 – Istruzioni selettive e sequenze di esecuzioni evocate Due sequenze per un solo if, tre per due e quattro per tre. Si comprende che un tale innesto di n strutture selettive evoca n+1 sequenze di esecuzione. Infine, la tabella seguente mostra come, nel caso di strutture iterative, il numero di sequenze di esecuzione vari in funzione dei valori assunti, durante l'esecuzione, dalle variabili che governano le condizioni della iterazione. I0; for i:=ei to es do I1; If ; s1 (ei=1 es=3) I0 I1 I1 I1 If s2 (ei=8 es=0) I0 If s3 (ei=50 es=50) I0 I1 If Tabelle 5 – Istruzioni iterative e sequenze di esecuzioni evocate La struttura dei programmi 4.3. 135 La modularità L'applicazione della tecnica top-down permette di ottenere un programma composto da moduli funzionali, ciascuno dei quali realizza un singolo e ben preciso compito e avente un solo punto di ingresso e di uscita. Una tale impostazione risulta un valido aiuto per l'attività di programmazione, in quanto rispetta la limitazione degli esseri umani che, solitamente, sono in grado di esaminare un solo aspetto di un problema alla volta. Inoltre, il processo di raffinamento iterativo produce una gerarchia di sottoproblemi di complessità decrescente associando ad essa una gerarchia di linguaggi per la descrizione dell'algoritmo. Difatti, ogni sottoproblema individuato viene espresso con una frase del linguaggio naturale sintetica ed espressiva per ritardare decisioni di progetto relative alla sua implementazione. Un tale approccio però non consente ancora di affrontare in modo adeguato i seguenti problemi: - evitare riscritture inutili di sottoproblemi dello stesso tipo in più punti del programma; - integrare soluzioni già disponibili di alcuni sottoproblemi; - non perdere la sinteticità delle frasi in linguaggio naturale introdotte per descrivere i vari sottoproblemi; - verificare la correttezza della soluzione del problema per passi: ossia verificando dapprima la correttezza dei singoli sottoproblemi e successivamente dell'insieme da essi costituito. Per ovviare a tutti questi inconvenienti, i linguaggi di programmazione prevedono l'uso dei sottoprogrammi: permettono cioè di assegnare ad una sequenza di istruzioni un nome che può essere utilizzato come sua abbreviazione e che può essere inserito al suo posto nel programma. Inoltre se il nome rappresenta anche un risultato, che può essere inserito in una espressione o in una istruzione, il sottoprogramma viene chiamato funzione: in tutti gli altri casi viene anche chiamato procedura. L'indicazione del nome di un sottoprogramma viene chiamata “indicazione della procedura” (o della funzione) corrispondente; l'impiego di una procedura in un programma viene detto “chiamata della procedura” (quello di una funzione, viene detto “chiamata della funzione”). L'indicazione del sottoprogramma si compone di due parti: il titolo e il corpo. Nel titolo del sottoprogramma vengono indicati il suo identificatore (il nome) e altre informazioni; il corpo si compone della sequenza di dichiarazioni e istruzioni denotata dal nome. sottoprogramma prepara_la crema; SP1: sbatti le uova con lo zucchero; SP2: stempera la farina nel latte; SP3: versa il latte nelle uova e nello zucchero; SP4: mette sul fuoco e porta ad ebollizione Queste convenzioni, osservando con attenzione il mondo che ci circonda, si presentano spesso anche nella realtà. Difatti, anche nella vita reale si fa 136 Capitolo quarto frequentemente ricorso ai riferimenti come mezzo per evitare inutili ripetizioni. Si consideri per esempio il testo della ricetta di una torta alla frutta ricopiato da un libro di cucina: programma torta; I1: impasta farina, burro, uova, sale e zucchero fino ad ottenere una pasta soffice; I2: inforna la pasta per una decina di minuti; I3: prepara_la_crema; I4: distribuisci la crema sulla sfoglia; I5: ricopri con frutta di stagione tagliata a dadini e gelatina Si può notare nella terza istruzione l'uso del riferimento ad un'altra ricetta che ha permesso allo scrittore di non perdere tempo a ricopiarla. L'esecutore della torta dovrà: - spostarsi sulla ricetta della crema quando trova il suo riferimento; - riportarsi sulla ricetta della torta nel momento in cui ha terminato la crema. In altre parole la chiamata di procedura può essere vista come una particolare istruzione di salto alla prima istruzione della procedura. La terminazione della procedura genera poi un ulteriore salto alla istruzione successiva a quella di chiamata. Figura 1: Chiamata a Sottoprogramma Così la sequenza dinamica evocata in figura risulta essere: I1;I2;SP1;SP2;SP3;SP4;I4;I5; L'uso dei sottoprogrammi non serve solo ad abbreviare il lavoro di scrittura, ma anche, e in modo essenziale, ad articolare, suddividere e strutturare un programma in componenti fra loro coerenti. Un'adeguata struttura è determinante per la comprensibilità di un programma, soprattutto quando esso è complicato e il La struttura dei programmi 137 testo ha dimensioni che non permettono di scorrerlo con un unico sguardo. Un'appropriata articolazione in sottoprogrammi è indispensabile per una documentazione comprensibile e per una facile verifica. Difatti un sottoprogramma rappresenta una precisa e definita funzionalità ed è dotato di una altrettanto ben definita interfaccia con l'esterno. Entrambe queste proprietà ne garantiscono il controllo di correttezza a prescindere dal resto del programma che ne fa uso. Così un programma organizzato in sottoprogrammi può essere più facilmente controllato di un programma senza sottoprogrammi per il fatto che l'attività di controllo viene dapprima esercitata sui singoli sottoprogrammi e successivamente sulla loro integrazione. Per questi motivi è utile indicare una sequenza di istruzioni con un sottoprogramma (assegnandole quindi un nome) anche se essa compare in un sol punto del programma e l'introduzione del sottoprogramma non porta ad un testo più breve. L'adozione del metodo top down comporta un uso naturale dei sottoprogrammi. Difatti nel passaggio da una frase del linguaggio naturale, o istruzione non direttamente esprimibile nel linguaggio di programmazione, al suo raffinamento si può far ricorso all'uso di procedure o funzioni ogni qualvolta la funzionalità individuata: 1 viene utilizzata in varie sezioni del programma; 2 è già disponibile in una libreria di sottoprogrammi (si pensi ad una funzione matematica); 3 può essere utile inserirla in una libreria per poterla riutilizzare in altri progetti; 4 dipende fortemente dal particolare elaboratore che si sta usando (si pensi a funzioni di gestioni del video); 5 deve nascondere le modalità con cui vengono effettuate le elaborazioni al suo interno, al fine di eliminare turbative all'ambiente esterno per modifiche interne. Ciò viene realizzato mediante comunicazioni con l'ambiente esterno che avvengono solo attraverso un numero ristretto di parametri; 6 deve contenere astrazioni sui dati in modo che si possa interagire con un particolare tipo di dato soltanto attraverso le operazioni di accesso al tipo senza interessarsi della reale rappresentazione del dato stesso. L'applicazione di questi criteri consente non solo di migliorare la comprensibilità del programma finale ma anche e soprattutto la verificabilità (in particolare i punti 1, 2, 5 e 6), la manutenibilità (in particolare 1, 5 e 6) e la portabilità del prodotto software (punto 4). Inoltre procedendo in tale modo è più facile pensare alla suddivisione del lavoro, nel caso in cui il progetto deve essere realizzato da più persone. 4.3.1. La parametrizzazione del codice Spesso accade che una sequenza di istruzioni compaia in punti diversi non esattamente nella stessa forma. Particolare attenzione merita il caso in cui la differenza consiste nell'uso di operandi diversi. Si considerino ad esempio le due sequenze: 138 Capitolo quarto stampa('Dammi riempimento:'); leggi(riemp) stampa('Numero fogli :'); leggi(num_fogli) esse differiscono per le costanti da stampare ('Dammi riempimento :' e 'Numero fogli :') e per le variabili di cui si devono leggere i valori (riemp e num_fogli). Allora è possibile estrarre uno schema comune: stampa(messaggio); leggi(x) definendo parametricamente le variabili su cui operare. In questo modo è possibile ricondurre le due sequenze allo schema, a patto che si crei un meccanismo che associ rispettivamente: 'Dammi riempimento :' x => messaggio => riemp e 'Numero fogli :' x => messaggio => num_fogli Difatti messaggio deve ricevere la stringa da stampare mentre x deve restituire il valore letto. Si noti che messaggio è un parametro di input alla procedura mentre x è di output per essa. Si definiscono parametri formali gli oggetti utilizzati nel corpo della procedura. Essi devono essere indicati nel titolo della procedura. Gli oggetti da sostituire al posto dei parametri formali, prima di ogni esecuzione, sono detti parametri attuali (o effettivi) e devono essere specificati in ogni chiamata della procedura o della funzione. Si osservi che esistono vari modi di sostituire i parametri formali in quelli effettivi. In altre parole, i parametri formali indicano genericamente su quali oggetti il sottoprogramma deve agire (servono quindi alla sua formulazione), mentre quelli effettivi precisano gli oggetti sui quali il sottoprogramma deve effettivamente operare. I parametri formali devono riferirsi, durante l'esecuzione del sottoprogramma, ai parametri effettivi e, perché ciò possa avvenire, devono essere compatibili con essi (devono essere dello stesso tipo). Nel titolo della procedura si specificano sia il tipo dei parametri che il meccanismo di sostituzione. Inoltre, tra il titolo della procedura e la sua chiamata si stabilisce una corrispondenza tra la lista dei parametri effettivi e formali di tipo posizionale: cioè al primo parametro effettivo viene fatto corrispondere il primo parametro formale, e così via per i successivi. I parametri attuali devono quindi essere forniti con rispetto di numero, tipo e ordine rispetto a quelli formali. La struttura dei programmi 139 I meccanismi di sostituzione, in generale ricadono in una delle tre seguenti tipologie: - per valore - per riferimento (o per variabile) - per nome. La sostituzione per valore assegna al parametro formale del sottoprogramma il valore del corrispondente parametro effettivo. Il parametro effettivo può essere un'espressione e come casi particolari di espressione una costante o una variabile. Se è un'espressione, se ne calcola il valore e lo si assegna al corrispondente parametro formale. Nel caso di variabili, la sostituzione per valore garantisce che la variabile passata come parametro effettivo non venga alterata dal sottoprogramma. Difatti il sottoprogramma dopo averne copiato il valore nella corrispondente variabile formale al momento della chiamata, lavora in sostanza su una sua copia (il parametro formale) e tutte le modifiche effettuate su tale copia non riguardano in alcun modo il corrispondente parametro effettivo. Questo significa che, se il parametro formale è di tipo strutturato, all'atto della chiamata l'intera struttura del parametro effettivo viene ricopiata in esso, con conseguente consumo di tempo (per la copiatura) e di spazio (per l'occupazione di memoria della struttura). La sostituzione per riferimento fornisce al parametro formale del sottoprogramma l'indirizzo di memoria del corrispondente parametro effettivo. Per tale motivo il parametro effettivo può essere soltanto una variabile. A differenza della sostituzione per valore, il parametro formale non è una copia ma un'informazione attraverso cui si lavora direttamente sul parametro effettivo (tipo puntatore) e la sua occupazione di memoria è esclusivamente quella necessaria per contenere l'indirizzo del parametro attuale. Esiste un ultimo meccanismo di sostituzione detto per nome che citiamo soltanto per motivi di completezza, in quanto, ormai, lo si ritrova soltanto in pochissimi linguaggi. Secondo tale modalità il parametro formale viene “testualmente” sostituito dai parametri effettivi senza alcuna valutazione. Questa modalità differisce sostanzialmente dai precedenti meccanismi in quanto la sostituzione non avviene all'atto della chiamata del sottoprogramma ma durante la sua esecuzione, ogni qualvolta che esso fa uso del parametro formale. In altre parole è come se avvenisse una riscrittura del sottoprogramma ad ogni sua attivazione in modo da sostituire l'identificatore del parametro formale con quello del corrispondente effettivo. Per quanto concerne la scelta del metodo di sostituzione dei parametri si possono seguire le seguenti regole base: - quando un parametro rappresenta un argomento di una procedura, si sceglie la sostituzione per valore; - quando un parametro rappresenta invece un risultato, occorre usare la sostituzione per riferimento. Osservando tale problema da una visuale differente, possiamo classificare i parametri formali in: - parametri di ingresso al sottoprogramma; - parametri di uscita dal sottoprogramma; - parametri modificabili dal sottoprogramma. 140 Capitolo quarto I primi sono parametri che forniscono i valori a partire dai quali il sottoprogramma inizia le proprie elaborazioni; i secondi rappresentano i risultati da esso prodotti; infine i terzi sono quelli che contemporaneamente possono considerarsi di ingresso e di uscita. Una tale classificazione ci torna utile per la scelta del meccanismo di sostituzione. Difatti, se i parametri sono solo di ingresso al sottoprogramma conviene scegliere la sostituzione per valore che impedisce l'alterazione dei parametri effettivi da parte del sottoprogramma. Invece, negli altri due casi, affinché dopo la chiamata possano essere visibili gli effetti del sottoprogramma, si deve scegliere la sostituzione per riferimento che permette alle istruzioni del sottoprogramma di lavorare direttamente sui parametri effettivi. Per comprendere il modo di scegliere il meccanismo di sostituzione si pensi, ad esempio, ad uno scrittore che per svolgere la sua attività può aver bisogno di un correttore di bozze per eliminare errori di stampa e di un traduttore per la traduzione del libro in una lingua diversa dalla sua. Mentre il primo deve produrre il libro corretto, il secondo deve fornire anche un nuovo libro: quello tradotto. Se l'autore è accorto, prima di fornire il libro al traduttore, ne effettua una fotocopia in modo che l'originale non sia alterato da qualsiasi cosa il traduttore possa aver intenzione di fare. Invece, nel caso del libro da correggere, l'autore deve lavorare sullo stesso libro del correttore se vuole controllare il lavoro svolto. In altri termini, la sostituzione per riferimento permette a due unità di condividere una stessa area di lavoro, mentre quella per valore le separa anche se assumono lo stesso valore solo all'atto dell'attivazione del sottoprogramma. 4.3.2. Le funzioni Ricapitolando, le procedure sono caratterizzate dalle seguenti proprietà: - sono unità di programma subordinate che sono eseguite solo mediante una esplicita attivazione (chiamata); - svolgono compiti ben definiti; - si interfacciano con il programma chiamante scambiando con esso informazioni mediante il meccanismo di sostituzione per valore o per riferimento (accettando così le informazioni da elaborare e restituendo i risultati della loro elaborazione). Da un punto di vista sintattico le procedure sono caratterizzate da: - dichiarazione del titolo e del corpo; - esplicitazione nel titolo del meccanismo di sostituzione e del tipo dei parametri formali; - meccanismo di attivazione. Quando una procedura fornisce un unico risultato, può essere organizzata in funzione, in modo che il suo nome corrisponda proprio a tale risultato. Per esempio, si organizzano in funzione le funzioni matematiche che devono essere utilizzate all'interno del programma, o le espressioni o porzioni di esse che devono essere usate più volte all'interno di uno stesso programma. In tali casi si ha che: - il nome della funzione è l'unico risultato della procedura; - poiché il nome della procedura è una variabile a tutti gli effetti, deve essere dotata di un tipo che deve essere indicato nell'intestazione; - si può assegnare valore a tale variabile soltanto nel corpo della funzione stessa; La struttura dei programmi 141 - si può usare il valore di tale variabile inserendone il nome in espressioni che si trovano all'esterno del corpo della funzione. In altri termini la presenza all'interno della funzione del suo stesso nome nella parte sinistra di un'assegnazione permette di trasmettere al programma chiamante il risultato delle sue elaborazioni, mentre il richiamo della funzione avviene inserendone semplicemente il nome in una espressione. Per esempio, una funzione che calcola la somma di due numeri potrebbe essere definita da: funzione somma(a,b:interi):intera somma:=a+b ed essere utilizzata in uno dei seguenti modi: x:=a+somma(3,y)/3; stampa(somma(alfa,beta)); Ad esempio, considerate le seguenti istruzioni: Y:=3*XA+LN(XB-1); K:=SIN(Y)+3*Z+LN(M-1)+X; WRITELN(SQR(X)-9*X+SQR(X1)-10*X1); Y:=SIN(X0-Y0/2)*9+5*(SIN(X0+1-(Y0+1)/2)*3); se vengono definite le tre funzioni riportate di seguito: funzione F1(x,y:reale):reale; funzione F2(x,y:reale):reale; funzione F3(x,y:reale):reale; F1:=3*x+LN(y-1); F2:=SQR(x)-9*x ; SIN(x-y/2)*3; si possono riscrivere nella forma: Y:=F1(XA,XB); K:=SIN(Y)+F1(Z,M)+X; WRITELN(F2(X)+F2(X1)-1); Y:=F3(X0,Y0)*3+5*F3(X0+1,Y0+1); con un notevole miglioramento della leggibilità del programma. Tutti i linguaggi di programmazione mettono a disposizione del programmatore alcune procedure o funzioni che sono già definite all'interno del linguaggio stesso. Esse non richiedono una definizione esplicita e per questo motivo vengono anche dette implicite o standard. Solitamente sono funzioni standard le funzioni matematiche per il calcolo del seno, coseno, logaritmo naturale, l'esponenziale, il valore assoluto di un numero, la radice quadrata, il quadrato, ed altre ancora. In genere si ritrovano come funzioni o procedure predefinite quei moduli che hanno un'alta probabilità di essere impiegati in alcuni campi di applicazione, per cui lasciarne la definizione all'utente risulterebbe dispendioso in termini di efficienza complessiva, o che sono indispensabili per la completa definizione e utilizzazione di costrutti del linguaggio, come per esempio si può osservare a proposito dei meccanismi di ingresso e uscita. D'altro canto si può notare come l'uso dei sottoprogrammi sia un potente strumento di espansione del linguaggio in quanto consente al programmatore l'inserimento in esso di nuove parole chiave, i nomi delle procedure e delle 142 Capitolo quarto funzioni appunto, la cui definizione è indipendente dagli algoritmi che ne fanno uso. Così è possibile costruire linguaggi con potenze espressive sempre maggiori con una notevole facilitazione dell'attività di programmazione. Si noti infine che tale modo di procedere è alla base del metodo induttivo di risolvere i problemi, che è stato chiamato approccio bottom-up. 4.3.3. La visibilità L'uso dei sottoprogrammi permette di costruire programmi non costituiti da una unica sequenza di istruzioni ma da una gerarchia di unità di programma (o anche macchine astratte se si vuole dare risalto al cosa fanno e non al modo in cui sono realizzate), ciascuna delle quali realizza una particolare istruzione in modo autonomo, con proprie definizioni di tipi, dichiarazioni di variabili e istruzioni. Ogni unità forma la base per il livello superiore (le unità che la chiamano) e si appoggia eventualmente su un livello di macchina inferiore (le procedure che sono chiamate al suo interno). Si chiamerà programma principale quella unità di programma che si interfaccia direttamente con il sistema operativo. Nella figura, per esempio, A può essere il programma principale, A.1 e A.2 il livello su cui A si appoggia, e A.1.1 e A.1.2 quello su cui si appoggia A.1. A.1.1 A.1 A.1.2 A A.2 Figura 2: Struttura di un prgramma Evidenzieremo il programma principale racchiudendone gli elementi fondamentali (dichiarazioni ed istruzioni) nella struttura: program .......; ..... begin ..... end. mentre abbiamo racchiuso gli elementi di una procedura nella struttura: La struttura dei programmi 143 procedure .... (...); ..... begin ..... end; o in: function ..... (....): ....; ..... begin ..... end; se si tratta di una funzione. In tale ambito, un altro concetto fondamentale sottolinea il ruolo delle procedure dal punto di vista della strutturazione dei programmi. Spesso, certe variabili vengono impiegate soltanto all'interno di una sequenza di istruzioni, mentre non hanno influenza nel resto del programma. La comprensione del programma aumenta considerevolmente quando la “localizzazione” delle variabili è posta in chiara evidenza. In ogni caso, i campi di influenza delle variabili (cioè, dove il loro valore influenza l'esecuzione) devono risultare con chiarezza dalla struttura del programma. Con il termine regole di scope (visibilità) si fa riferimento a quelle regole che consentono di determinare i campi di influenza di una variabile (e più in generale di un qualunque oggetto) in tutte le varie parti costituenti un programma. In altre parole si dice scope di un oggetto la porzione di programma che è in grado di usarla. L'organizzazione in procedure è il modo più opportuno per porre in risalto lo scope degli oggetti. Se un oggetto - una costante, un tipo, una variabile, una procedura o una funzione - è definito ed usato solo all'interno di una determinata procedura, allora viene detto locale a quella procedura. Se, viceversa, è definito nell'unità di programma chiamante, ma risulta visibile alla procedura chiamata attraverso qualche meccanismo, allora viene detto non locale alla procedura. Infine viene detto globale se risulta definito nel programma principale, perché tale unità è l'unica che può rendere visibili i propri oggetti a tutte le altre in quanto rappresenta la radice della gerarchia di unità di programma (in altre parole è l'unica unità che chiama tutte le altre e che non è chiamata da nessun'altra). Si noti che non esiste alcun inconveniente se si sceglie per un oggetto locale ad una unità di programma lo stesso identificatore utilizzato per un altro oggetto di una diversa unità di programma. Tale caratteristica è molto importante poiché senza di essa non soltanto i rischi di errori sarebbero molto grandi, ma sarebbe praticamente impossibile impiegare procedure di biblioteca o procedure scritte da altri. Tale proprietà deriva dal fatto che un oggetto locale ad una unità di programma è visibile soltanto dalle istruzioni di tale unità e pertanto qualsiasi modifica del suo valore non è visibile all'esterno di essa. Per quanto riguarda invece gli oggetti che genericamente abbiamo detto non locali o globali esistono dei meccanismi espliciti o impliciti che consentono di estendere la visibilità di un oggetto a più unità di programma. Tali meccanismi 144 Capitolo quarto differiscono in funzione del modo in cui, nei vari linguaggi, è possibile organizzare il programma nella gerarchia di unità di programma illustrata precedentemente. A tale proposito esistono dei linguaggi (es. Pascal, Basic) in cui i vari sottoprogrammi (e funzioni) possono essere inseriti nel corpo del programma principale. Si definiscono sottoprogrammi interni tale tipo di sottoprogrammi. Viceversa, esistono altri linguaggi (ad esempio il FORTRAN) in cui ogni unità di programma risulta staccata da tutte le altre unità di programma. Si definiscono sottoprogrammi esterni tale tipo di sottoprogrammi. Nei sottoprogrammi interni esistono dei meccanismi impliciti tali che, se un identificatore è definito in una unità di programma, è visibile, allora, da tutte le unità interne ad essa, salvo il caso in cui esso sia ridefinito localmente in una di queste unità interne. Ad esempio nel programma: Procedure A; var x,y:integer; ...... procedure B; var x:integer; ...... ...... begin ...... ...... end; ...... begin ...... B; ...... end; la variabile y di A è visibile da B, mentre la x di A non lo è in quanto la dichiarazione di una variabile in B con lo stesso nome ha creato una variabile locale che nasconde quella definita nella procedura chiamante. In altre parole, tutte le modifiche fatte da B su y influenzano anche A, mentre quelle effettuate su x da B non si propagano sulla x di A (in quanto sono due variabili differenti). Nei sottoprogrammi esterni esistono invece dei meccanismi espliciti che consentono a più unità di programma di condividere gli stessi oggetti. Ad esempio in FORTRAN è possibile, con opportune dichiarazioni, raggruppare un insieme di variabili in un'area di memoria prestabilita. Tale area viene chiamata common (comune) perché è condivisibile da più unità di programma differenti. L'uso del common in due unità di programma ha l'effetto di assegnare le medesime locazioni di memoria (quelle dell'area common appunto) a variabili dell'uno e dell'altro. Ad esempio l'uso del common nelle quattro unità di programma di seguito riportate: La struttura dei programmi 145 program main integer a,b,c,d common a,b,c,d ...... end subroutine sub1 integer x,y common x,y ...... end subroutine sub2 integer i common i ...... end subroutine sub3 integer k,f,v,z common k,f,v,z ...... end comporta che a, x, i, e k sono associati alla prima locazione di memoria dell'area common; b, y, e f alla seconda; c e v alla terza; e infine d e z alla quarta. La globalità di una variabile risulta così definita dalla posizione assunta nella zona comune e non dal nome delle variabili. Ricapitolando, una procedura può usare una variabile dell'unità di programma chiamante o attraverso la corrispondenza parametri formali ed effettivi o mediante l'uso delle regole di visibilità collegate ai meccanismi di comunanza. Senza pretendere in questa sede di discutere a fondo i vantaggi e gli inconvenienti dei due metodi, è bene fare le osservazioni generali, riportate di seguito. - A priori, se alcuni argomenti devono essere trasmessi in molte chiamate, la trasmissione esplicita può essere pesante e fastidiosa. Un meccanismo di comunanza dati è preferibile soprattutto per le procedure chiamate frequentemente perché più veloce. - Per altro si deve notare che l'utilizzazione del meccanismo di comunanza dati corrisponde a passare implicitamente dei dati del tipo “dati modificabili”. Cresce così il rischio di introdurre nella procedura chiamante errori indesiderati e difficilmente controllabili se la visibilità delle variabili è molto estesa: errori che derivano dalla possibilità offerta alle procedure chiamate di modificare le variabili non locali senza tener conto di come esse siano usate nel programma chiamante. Per tale motivo l'utilizzazione di un identificatore non locale è una pratica pericolosa e poco raccomandabile. Inoltre, nei linguaggi con sottoprogrammi esterni, esistono anche altri inconvenienti: non è possibile eliminare le variabili inutili (o giudicate tali dopo un rapido esame) dall’area comune e quindi il rischio di generare errori per l'inserzione di modifiche non previste è reale 146 Capitolo quarto se si pensa che l'eliminazione di una variabile da una dichiarazione fa saltare la corrispondenza delle altre nell'area comune. In funzione di queste osservazioni possiamo formulare alcuni consigli. - è meglio riservare la comunanza implicita al caso in cui il programma principale e i suoi sottoprogrammi manipolano un gruppo di variabili veramente comuni (cioè sarebbe arbitrario per queste distinguere argomenti reali e argomenti formali). - quando le variabili sono in comune è meglio che abbiano lo stesso nome ovunque usate. Si eviterà così nei linguaggi con sottoprogrammi interni di ridefinire in una procedura gli identificatori già definiti in un blocco esterno. Analogamente, se si utilizzano delle aree comuni, sarà opportuno mantenere sempre le stesse liste di identificatori. - se per motivi di efficienza si sceglie il meccanismo di comunanza, si deve documentare in modo preciso sia dove l'identificatore è definito sia tutte le unità di programma che ne fanno uso, in modo da effettuare velocemente i controlli incrociati necessari per valutare gli effetti complessivi che la modifica di una variabile non locale comporta. 4.3.4. L'allocazione dinamica Tutti i sottoprogrammi sono, come abbiamo visto, unità di programma con oggetti locali, cioè validi nell'ambito ristretto dell'unità, che possono essere usati anche da altre unità o mediante la sostituzione dei parametri o mediante un meccanismo di comunanza. Una volta terminata l'esecuzione del sottoprogramma, tutte le sue variabili locali diventano inaccessibili fino a quando il sottoprogramma non viene richiamato; ma, nella chiamata successiva, i valori iniziali di queste variabili saranno diversi da quelli finali della chiamata precedente. Questo perché l'allocazione in memoria centrale delle variabili locali avviene all'atto della chiamata del sottoprogramma per cercare di ridurre l'occupazione complessiva di memoria. Difatti alcuni sottoprogrammi potrebbero non essere mai attivati, e l'allocazione in memoria delle rispettive variabili locali risulterebbe alquanto inutile. Risulterebbe altrettanto inutile allocare le variabili di quei sottoprogrammi non eseguiti mai contemporaneamente. Così, al termine di un sottoprogramma l'area di memoria occupata dalle sue variabili locali viene resa disponibile per l'allocazione delle variabili locali di un diverso sottoprogramma. In altre parole, la corrispondenza tra identificatore di variabile e indirizzo del registro di memoria viene determinata dinamicamente durante l'esecuzione. Diremo questo schema di allocazione dinamico per distinguerlo da quello statico che prevede che l'allocazione venga fissata all'inizio dell'esecuzione del programma e non cambi finché il programma non sia terminato. Nella gerarchia di unità di programma, le variabili globali (quelle definite nel programma principale) sono statiche per ovvie ragioni (è l'unità che chiama tutte le altre), mentre quelle locali ai vari sottoprogrammi sono dinamiche. Per una completa comprensione degli effetti della gestione differenziata dell'allocazione delle variabili si osservi l'output del seguente esempio: La struttura dei programmi 147 program P; var I,A : integer; procedure Y; var B: integer; begin A:=A+1; B:=A*A writeln('B =',B:2) end; procedure Z; var C: integer; begin A:=A+1; C:=C+1; writeln('C =',C:2) end; begin A:=0; for i:=1 to 2 do begin Y; Z; end; writeln('A =',A:2) end. OUTPUT: B=1 C=2 B=9 C =10 A=4 Lo schema che segue è uno schema di principio che mostra, passo dopo passo, come ciò avvenga mettendo in evidenza l'allocazione delle variabili subito dopo la chiamata delle procedure e poco prima della loro terminazione. I passi sono: 1) prima dell'esecuzione del ciclo for del programma principale; I A 0 2) prima chiamata di Y; I A B 1 0 3) terminazione di Y; I A B 1 1 1 148 Capitolo quarto 4) prima chiamata di Z; I 1 5) terminazione di Z; I 1 6) seconda chiamata di Y; I 2 7) terminazione di Y; I 2 8) seconda chiamata di Z; I 2 9) terminazione di Z. I 2 A C 1 A 1 C 2 A 2 B 2 A 2 B 3 A 9 C 3 A 9 C 4 10 Se invece tutte le variabili, senza distinguere tra locali e non, fossero state allocate staticamente all'atto dell'attivazione del programma lo schema risulterebbe uguale a: I A B C permettendo di generare il seguente output: B= 1 C =98 B= 9 C =99 A= 4 come facilmente si può dimostrare. Si noti il valore strano assunto la prima volta da C. Esso si spiega osservando che l'allocazione delle variabili associa ad esse un indirizzo di uno o più registri di memoria senza interessarsi minimamente del loro contenuto. Alcuni linguaggi, per limitare i danni legati all'indeterminatezza del valore iniziale delle variabili, fanno seguire all'allocazione una inizializzazione delle celle di memoria compatibile con il tipo di dato. È comunque buona norma non fare affidamento su tali inizializzazioni perché possono, tra l'altro, variare da macchina a macchina, ed inizializzare con esplicite istruzioni di assegnazione tutte le variabili che si usano. Ma, tornando alla differenza tra allocazione statica e dinamica, resta solo da dire che in alcuni linguaggi (ad esempio il FORTRAN) è possibile rendere statiche le variabili locali dei sottoprogrammi con opportune dichiarazioni (save). La struttura dei programmi 4.3.5. 149 La ricorsione La ricorsione è una proprietà dei sottoprogrammi e delle funzioni che consente l'uso, all'interno della definizione di un'unità di programma, della chiamata ad essa stessa. È una proprietà molto importante in quanto molto problemi possono essere definiti in modo semplice solo usando la ricorsione. La definizione di un oggetto si dice ricorsiva se contiene un riferimento all'oggetto stesso. Tale principio è usato con naturalezza nella vita comune. Nel linguaggio matematico numerosi oggetti sono definiti ricorsivamente, per esempio il fattoriale di un numero n è dato dal numero n moltiplicato per il fattoriale di (n-1) con eccezione del fattoriale di 0 che è uguale a 1: 0! = 1 n! = n * (n-1)! per ogni n>0. Non tutti i linguaggi però prevedono la ricorsione: laddove prevista si fa ricorso ad essa inserendo nel corpo della procedura o della funzione il richiamo alla procedura stessa. Per esempio, sempre per il fattoriale di n: function fatt(n:integer):integer; begin if n=0 then fatt:=1 else fatt:=n*fatt(n-1) end; Comunque la ricorsione è sempre riconducibile ad una iterazione, e ciò fa comprendere quanto sia importante studiare la condizione di terminazione degli algoritmi ricorsivi. 4.3.6. Gli effetti collaterali L'uso dei sottoprogrammi può introdurre errori che non sono facilmente individuabili e che sono essenzialmente legati ai meccanismi con i quali si espande la visibilità di una informazione tra le diverse componenti di un programma. A tale riguardo si osservi, per esempio, che la sostituzione per riferimento fa sì che una stessa variabile si presenti sotto differenti denominazioni in diverse unità di programma. Difatti, fissata la seguente procedura: procedure quadrato(var x:integer); begin x:=x*x; end; se la si richiama nel contesto seguente: for i:=1 to n do begin ..... quadrato(i); ..... end; 150 Capitolo quarto si genera la modifica della variabile contatore del ciclo, operazione pericolosa se non addirittura vietata in alcuni linguaggi, in quanto, per effetto del meccanismo di sostituzione adottato, le operazioni svolte dal sottoprogramma su x si svolgono di fatto su i. Chiameremo side effect (effetto collaterale) l'effetto non desiderato che si genera a causa della modifica di un parametro formale nel corpo di un sottoprogramma che si ripercuote anche all'interno del programma chiamante. Un'altra fonte di effetto collaterale è l'uso della sostituzione per riferimento per uno dei parametri formali di una funzione. Difatti se il parametro in questione viene modificato nel corpo della funzione si ha come effetto collaterale che la procedura genera più di un risultato contravvenendo alla definizione stessa di funzione. Per esempio la funzione: function radice(var x:real):real; begin x:=abs(x); radice:=sqrt(x) end; che effettua il calcolo della radice quadrata del valore assoluto di un numero assegnato, fornisce due risultati: il valore assoluto del numero, tramite l'unico parametro formale, e il valore della radice, attraverso il nome della funzione. In casi del genere si può facilmente ovviare all'effetto collaterale sostituendo il meccanismo di sostituzione per riferimento con quello per valore. Alcune volte però, si fa volutamente ricorso al meccanismo di sostituzione per riferimento sia per motivi di efficienza (si evita la ricopiatura) che per non aumentare l'occupazione di memoria (non si duplicano le informazioni). Si pensi ad esempio ad un sottoprogramma per il calcolo del determinante di una matrice quadra: la scelta della sostituzione per valore comporterebbe la copia della matrice nel parametro formale con una perdita di tempo e di spazio proporzionale alle dimensioni di tutta la matrice. La scelta della sostituzione per riferimento laddove dovrebbe essere usata quella per valore, può essere fatta solo quando, a progetto terminato, ci si accorge che il sottoprogramma non opera sul parametro formale e, quindi, non introduce degli effetti collaterali non desiderati. Per la loro pericolosità, tali scelte vanno documentate in modo chiaro ed evidente nel corpo del sottoprogramma soprattutto perché un programma non cessa di essere utilizzato nello stesso stato in cui è stato creato la prima volta. Difatti durante la sua vita è molto probabile che dovrà subire diverse modifiche sia per rimuovere eventuali errori di programmazione sia per adattarsi a cambiamenti delle specifiche iniziali. A questo riguardo si pensi ad un semplice calcolo della ritenuta IVA su una fattura: fino a qualche anno fa l'aliquota da applicare era del 18 per cento mentre oggi è del 19 per cento. Intervenendo allora su di un sottoprogramma in cui non sono state documentate le ipotesi di correttezza, si rischia di alterarne i rapporti con l'esterno introducendo effetti collaterali che nella versione di partenza non esistevano. La struttura dei programmi 4.3.7. 151 Il riuso dei sottoprogrammi In genere quanto maggiore è lo sforzo investito nella realizzazione di un programma, tanto maggiore sarà il desiderio da parte di chi lo ha realizzato di prolungarne la vita e quindi maggiore sarà la probabilità di intervenire per correggerlo o modificarlo. Tale attività prende il nome di manutenzione e, ormai, è opinione diffusa che la manutenzione di un grande progetto software è un problema addirittura più complesso e costoso della sua realizzazione. Senza entrare nel merito di tale affermazione, diciamo soltanto che l'unico modo per diminuire la complessità della manutenzione è progettare un software ben organizzato e documentato: i sottoprogrammi sono un utile meccanismo per arrivare ad un progetto con questi requisiti sia perché sono un potente strumento di astrazione sia perché godono di una certa autonomia che ne consente l'organizzazione in librerie utilizzabili in progetti diversi. L'autonomia è di fondamentale importanza non solo per la riutilizzabilità dei programmi, ma anche per ridurre i tempi di sviluppo e quelli di manutenzione di un progetto software. Difatti, per quanto potenti oggi siano i calcolatori attuali, la compilazione di programmi molto lunghi può risultare assai onerosa e quindi lenta e costosa. Per questo motivo alcuni linguaggi introducono il concetto di sottoprogrammi esterni, ossia di sottoprogrammi compilabili separatamente dal loro contesto. Così, durante lo sviluppo di un progetto, si compilano soltanto i moduli nuovi o quelli che sono stati oggetto di modifiche, con un notevole risparmio di tempo. Inoltre l'autonomia favorisce la manutenzione in quanto permette di verificare separatamente la correttezza di ogni singolo modulo e successivamente quella del programma che li integra. Ma l'autonomia di un sottoprogramma non è però illimitata. Gli effetti collaterali precedentemente illustrati ne sono un esempio. In molti casi un sottoprogramma risulta completamente definito solo se immerso nel contesto che lo usa. Tanto per fare un ulteriore esempio, si pensi ad un sottoprogramma i cui calcoli dipendano da una (o anche più di una) variabile non locale in un ambiente come il Pascal o da una variabile inserita in area comune per ambienti con sottoprogrammi di tipo esterno: per provare la correttezza del sottoprogramma si deve disporre anche della parte di programma che dà valore alla variabile prima della chiamata del sottoprogramma stesso. Per favorire quindi l'autonomia dei sottoprogrammi è auspicabile ridurre l'uso di meccanismi che possono introdurre effetti collaterali o quantomeno giustificarne l'adozione, con una dettagliata documentazione, in tutti quei casi in cui si ottenga una stesura più semplice del programma. 4.3.8. L'information hiding Un altro modo di aumentare l'autonomia dei sottoprogrammi è cercare di non usare informazioni al di fuori dei moduli che sono stati creati apposta per trattarli. In altre parole, si deve cercare di nascondere le informazioni (dall'inglese information hiding) che vengono usate a certi livelli di astrazione del metodo top down ai livelli superiori, se si vuole aumentare l'indipendenza dei singoli moduli. In altri termini l’intestazione di un sottoprogramma deve contenere tutto quanto serve alla comprensione di come esso può essere usato. Per comprendere l'importanza del problema si consideri il rapporto che esiste tra i sottoprogrammi e 152 Capitolo quarto i tipi astratti. Si è visto che non sempre il linguaggio mette a disposizione del programmatore strumenti che permettono di modellare gli oggetti della realtà, e ciò non deve rappresentare un impedimento durante la progettazione. Difatti, se una informazione è essenzialmente caratterizzata da struttura ed operazioni, si può sempre raffinare il tipo astratto usando una struttura che si presta ad una sua realizzazione e costruendo intorno ad essa funzioni e sottoprogrammi che arricchiscono il linguaggio con parole chiavi tali da dare la sensazione di disporre proprio di un tale tipo. Per esempio un numero complesso può essere raffinato introducendo il seguente tipo: type num_complex = record parte_reale, parte_immaginaria : real end; e le seguenti operazioni: function somma_complex(x,y:num_complex):num_complex; function prodotto_complex(x,y:num_complex):num_complex; È noto che i numeri complessi possono essere rappresentati in modo alternativo indicando modulo ed argomento (un angolo). Così, se per qualche motivo si sceglie di cambiare la rappresentazione del tipo, si potranno modificare solo la dichiarazione di tipo e il corpo delle funzioni somma_complex e prodotto_complex nel caso in cui non siano state disseminate all'interno del programma istruzioni del tipo: if x.parte_reale then z:=num.parte_reale*3 ossia se è stata nascosta la struttura del tipo num_complex al programma stesso. Si noti che nessun linguaggio costringe al rispetto del principio dell’information hiding all'interno di un programma. Di conseguenza è compito del buon progettista cercare di disciplinarsi o disciplinare gli altri quando il progetto è condotto da più persone, se vuole ottenere un progetto in cui tutti moduli godano di quella reale autonomia così fondamentale per un più semplice controllo. Capitolo quinto I dati 5.1. Informazione e dato Nella realtà di tutti i giorni molte attività operano con informazioni di natura e forma diverse (testo, video, audio) e si costruiscono sistemi che gestiscono ed elaborano tale tipologie di informazioni. L’informazione è quindi un oggetto che ha un rapporto stretto con la realtà dalla quale può emergere se e solo se un determinato insieme o classe di oggetti assume stati o configurazioni differenti. In tale accezione l’informazione non è altro che la scelta di una delle possibili configurazioni in cui si trova un esemplare della classe di oggetti. Allora, il concetto di informazione è strettamente legato a quello di scelta di uno fra più oggetti di un particolare insieme e non esiste informazione se non si effettua una scelta. Ad esempio, la frase “sto studiando elementi di informatica”, fornisce un'informazione in quanto esprime la scelta della materia di studio e l'identificazione, quindi, della materia “elementi di informatica”, tra tutte le possibili materie del piano di studio. Mentre l’uomo tratta informazioni, l’elaboratore tratta dati. Come le informazioni anche i dati che le rappresentano sono di tipo numerico, alfanumerico, alfabetico, immagine, audio. Quando l’elaborazione consente di trattare dati eterogenei in modo integrato si parla di elaborazione multimediale. Con dato si indica una rappresentazione di fatti e concetti in modo formale perché sia possibile una loro elaborazione da parte di strumenti automatici. Il dato da solo, senza un contesto, può non avere significato: uno stesso numero può esprimere cose diverse in situazioni diverse; così come una stessa parola può avere significato diverso dipendente dal contesto. L’ambiguità viene risolta dando al dato una interpretazione. L’informazione non è altro che la percezione del dato attraverso un processo di interpretazione. In altre parole l’informazione cattura il significato del dato. Perché un dato diventi informazione si devono pertanto specificare: - un tipo, ossia l'insieme degli elementi in cui si effettua la scelta; - un valore, ossia il particolare elemento scelto; - l'attributo, ossia l'identificatore (o anche metadato) che conferisce significato all'elemento scelto. Il tipo è l'insieme degli oggetti entro cui si effettua la scelta. Nella prassi quotidiana tale insieme non sempre è esplicitamente definito. Tra l’altro l'attribuzione di un dato ad un tipo piuttosto che ad un altro dipende, dal contesto: 154 Capitolo quinto ad esempio, può non avere interesse in alcuni casi trattare separatamente con i tipi giorno, mese ed anno piuttosto che con un unico tipo data che li accorpa. Ciascun tipo di dato è caratterizzato poi dal numero di elementi di cui è composto. Tale numero è detto cardinalità del tipo. In un tipo con cardinalità N, la scelta di un elemento avviene tra tutti gli N; ne discende che la scelta più elementare che si possa effettuare è quella fra due soli oggetti e pertanto il dato più elementare è quello che appartiene ad un tipo con cardinalità 2. Un tale dato viene detto binario se i valori di un tale insieme sono [0,1], ma anche vero e falso; e si è soliti usare il termine bit per indicare sia l'informazione binaria sia i valori 0 e 1 da essa assunti. Da un punto di vista teorico la cardinalità di un tipo può essere finita o infinita. Da un punto di vista pratico i processi elaborativi trattano solo tipi a cardinalità finita. Difatti, per poter essere gestiti da un elaboratore, i dati, opportunamente rappresentati, devono essere inseriti in contenitori (memoria dell'elaboratore) con dimensioni proporzionali alla cardinalità dei tipi: è chiaro che a cardinalità infinita corrisponderebbero contenitori di dimensione infinita. Il valore di un dato è un elemento di un tipo. Tale definizione risulta però generica se non viene collegata alla definizione di tipo. Si osservi che il concetto di valore è un concetto che comporta una certa astrazione e non deve essere confuso con il problema della rappresentazione dell'informazione che può essere diversa in contesti differenti: basti pensare al modo di identificare un oggetto in lingue diverse (“padre” e “father”) o alle differenti rappresentazioni dei valori dei numeri (1/10 o 0.1). L'attributo conferisce il significato al valore specificato dal dato. Il tipo e il valore da soli non sono sufficienti alla caratterizzazione di una informazione. Ad esempio il valore 1000 del tipo numeri interi non ci dice nulla di significativo: potrebbe benissimo essere il prezzo di un biglietto del tram o anche il numero di matricola di uno studente. Il concetto di attributo è analogo a quello matematico di variabile e, come questo, si può rappresentare mediante simboli letterali o nomi da associare alle informazioni per specificarne il significato. Si può così chiamare rad la radice di una equazione e totale_fattura l'ammontare di una fattura. L'individuazione dei tipi di dato con cui trattare è strettamente legata alla natura delle elaborazioni da effettuare e può variare dunque da elaborazione ad elaborazione. La definizione di tipo consente di introdurre dei modelli degli oggetti della realtà in quanto ne fornisce le caratteristiche strutturali (cosa sono) e funzionali (a cosa servono). Una definizione di tipo di dato sufficientemente rigorosa e particolarmente adatta agli usi di tipo che verranno fatti in seguito è la seguente: “per tipo di dato si intende un insieme di valori associati a delle operazioni definite su di essi”. Con le definizioni di tipo è possibile trattare l'informazione in maniera astratta, cioè prescindendo dal modo concreto con cui essa è rappresentata all'interno di una macchina. Un tipo si definisce alla stessa stregua degli insiemi, ossia per: 1) enumerazione degli elementi {Lunedi, Martedi, Mercoledi, Giovedi, Venerdi, Sabato, Domenica} 2) elencando le proprietà di cui godono i suoi elementi {C: centravanti di una squadra nazionale di calcio di serie A e B} Elementi fondamentali di una elaborazione non sono soltanto i tipi su cui essa agisce, ma anche le operazioni che si possono effettuare sui rispettivi valori di un I dati 155 tipo. Tali operazioni possono essere definite assiomaticamente con il tipo e possono essere interne oppure esterne ad esso. Le prime producono un risultato di tipo uguale a quello degli operandi mentre le seconde di tipo diverso. Ad esempio la somma di due numeri interi è di tipo interno mentre è esterna la divisione tra due numeri interi in quanto fornisce sia risultati di tipo intero (8:4 => 2) che di tipo reale (5:2 => 2.5). Fra i valori di un tipo può essere definita una relazione d'ordine che fissa che uno specifico valore ha alcuni altri valori che lo precedono e altri ancora che lo seguono: la relazione d’ordine può essere totale (se coinvolge tutti gli elementi) o in alcuni casi parziale (se è definita su sottoinsiemi). Nella pratica tale relazione è implicita nella natura del tipo e corrisponde alla posizione che le sue costanti occupano nella sua definizione per enumerazione. Sono ad esempio ordinati gli insiemi dei numeri interi, delle lettere e delle cifre. I tipi ordinati sono anche detti scalari. Tra due costanti di un tipo ordinato sono definite a priori, oltre ai confronti di uguaglianza e disuguaglianza, validi anche per tipi non ordinati, le relazioni d'ordine di minore, maggiore, minore o uguale e maggiore o uguale. Le diverse relazioni vengono indicati con opportuni operatori (vedi tabella 1). Confronto uguale diverso minore maggiore minore o uguale maggiore o uguale Operatore = <> < > <= >= Operatore == != < > <= >= Tabella 1 - Relazioni su tipi ordinati Tra le costanti di un tipo sono possibili operazioni di relazione (confronto): operazioni, cioè, che trasformano valori di un qualsiasi tipo nei valori logici VERO e FALSO (e pertanto sono operazioni esterne). Confronto 3>1 7=9 Relazione Verificata Non verificata Valore determinato VERO FALSO Tabella 2 - Operazioni di confronto Infine, sui tipi ordinati sono definite le seguenti funzioni: - SUCC(x) che fornisce il successore di x nell'insieme; - PRED(x) che fornisce il predecessore di x nell'insieme; - ORD(x) che fornisce la posizione di x nell'insieme (la prima posizione si assume per convenzione essere uguale a 0). Si noti che le funzioni succ e pred sono interne al tipo a cui sono applicate, mentre ord è esterna perchè fornisce una posizione intera qualsiasi sia il tipo di x. Ad esempio, fissato l'insieme delle lettere, si ha che SUCC(A)=B, PRED(B)=A, ORD(A)=0 e ORD(B)=1. Le prime due sono operazioni interne mentre le ultime due sono esterne. 156 5.2. Capitolo quinto La classificazione dei tipi Nei linguaggi di programmazione esistono dichiarazioni per la definizione di tipo: type t = T; dove t è il nome del nuovo tipo e T è un descrittore di tipo, cioè un costrutto sintattico del linguaggio atto a definire il tipo t. Il descrittore T può essere anche un tipo già definito o predefinito nel linguaggio, in modo da introdurre dei tipi sinonimi. L'istanziazione di variabile, ossia l’introduzione di una variabile da poter utilizzare nel programma, avviene poi semplicemente con frasi dichiarative come quella seguente: var v : T; All’instanziazione di variabile corrisponde l’allocazione in memoria della variabile, ossia l’associazione al nome della variabile dell’indirizzo del registro di memoria nel quale verranno depositati i valori della variabile e da dove gli stessi valori verranno letti. Il numero di bit, e quindi il numero di registri associati al nome della variabile saranno determinati sulla base della rappresentazione dei valori in memoria. I tipi di dato si possono classificare in funzione della modalità con la quale è possibile definirli o in funzione della loro struttura. Se il tipo è predefinito nel linguaggio, allora lo si dirà primitivo: in caso contrario, quando è il programmatore che ne deve definire la composizione e in alcuni casi anche le operazioni ammesse sui suoi valori, si dirà che è un tipo derivato. Tutti i linguaggi di programmazione comprendono nella loro grammatica dei costruttori di tipo. Un tipo si dice atomico quando i valori di cui si compone non possono essere scomposti in elementi più semplici; si dice invece strutturato quando presenta valori che sono aggregazioni di altri valori che possono a loro volta essere semplici o strutturati. Il colore di un oggetto è una informazione di tipo atomico; la data di nascita è invece strutturata perché in essa si ritrovano aggregati le informazioni del giorno, del mese e dell’anno. La classificazione di un tipo in atomico e strutturato dipende fortemente dall’uso dell’informazione. La stessa informazione per alcune elaborazioni può essere considerata atomica mentre per altre strutturata. Ad esempio, se si considera un libro, si può notare che per il libraio che lo deve vendere è un oggetto di tipo atomico, ma per tante altre persone (il lettore, il tipografo, l’editore) può essere visto organizzato in pagine, le pagine in righe, le righe in parole e le parole in caratteri, a secondo dello specifico punto di vista. Sono tipi atomici: - il tipo logico o booleano - i tipi numerici intero e reale - il tipo carattere - il tipo puntatore sono tipi strutturati: - il tipo array ad una o a più dimensioni - il tipo record I dati 157 - il tipo file - la pila o stack - la coda - la tabella In tutti i linguaggi di programmazione esistono i costruttori per definire array, record e file. 5.3. I tipi atomici I tipi si dicono atomici quando i valori di cui sono composti non sono scomponibili in elementi più semplici dalle applicazioni che ne fanno uso. Per tale motivo è facile fornire una loro definizione mediante l’enumerazione delle costanti appartenenti al tipo o definendo le proprietà di cui le stesse costanti godono. Tra le proprietà vanno considerate le operazioni interne ed esterne al tipo. 5.3.1. Il tipo booleano Il tipo booleano indica l'insieme dei due valori di verità, true e false, ed il suo nome deriva dall'ideatore del calcolo logico, George Boole (1915-1964). Sui valori del tipo logico sono definiti i seguenti operatori: - OR: equivalente alla disgiunzione inclusiva (O logico), anche detto somma logica; non si fa la lezione se il docente è malato O gli studenti non vengono - AND: equivalente alla congiunzione (E logico), anche detto prodotto logico; l'esame si supera se si fa bene l'esercizio E si risponde bene all'orale - NOT: equivalente alla negazione (NON logico). oggi NON piove La tabella 3 riporta come gli operatori logici vengono indicati in diversi linguaggi. Operazione OR AND NOT Operatore || && ! Tabella 3 - Operatori logici 158 Capitolo quinto Per capire gli operatori logici definiamo proposizione una espressione linguistica della quale si può dire se è vera o falsa. Ad esempio “Parigi è la capitale della Francia” è una frase che universalmente è riconosciuta essere vera. Invece definiamo proposizioni variabili o predicati le espressioni linguistiche delle quali si può dire che sono vere solo se si assegna un valore alle informazioni in esse contenute. Ad esempio per la frase: oggi piove è possibile pronunciarsi solo dopo aver osservato il cielo. Si dice valutazione del predicato l'operazione che, noti i valori delle informazioni che vi compaiono, consente di stabilirne il valore di verità (cioè di dire se il predicato è vero o falso). Allora dato un predicato P se ne può costruire un altro facendolo precedere dal simbolo NOT: il predicato che si ottiene si dice opposto o negazione del primo ed ha i valori di verità opposti rispetto al primo. Cioè se P è vero allora NOT P è falso e viceversa. Si noti che ovviamente che: NOT(NOT P1) = P1. Dati invece due predicati P1 e P2, se ne può costruire un terzo mediante la congiunzione logica AND. Il predicato ottenuto P1 AND P2 risulterà vero solo quando sia P1 che P2, sono veri mentre sarà falso in tutti gli altri casi. Infine, dati due predicati P1 e P2, se ne può costruire un terzo mediante la disgiunzione logica O. Il predicato ottenuto P1 OR P2 risulterà falso solo quando sia P1 che P2, sono falsi mentre sarà vero in tutti gli altri casi. Dalle definizioni date è possibile riempire le seguenti tabelle che vengono dette tavole di verità (la prima utilizzando false e true, la seconda 0 e 1). P1 FALSE FALSE TRUE TRUE P1 0 0 1 1 P2 FALSE TRUE FALSE TRUE P2 0 1 0 1 P1 AND P2 FALSE FALSE FALSE TRUE P1 AND P2 0 0 0 1 P1 OR P2 FALSE TRUE TRUE TRUE P1 OR P2 0 1 1 1 NOT P1 TRUE FALSE NOT P1 1 0 Tabelle 4,5 – Tavole di verità Inoltre valgono le seguenti proprietà commutativa:: X OR Y = Y OR X associativa: (X OR Y) AND Z=(X AND Z) OR (Y AND Z) nonché le due relazioni di DE MORGAN: NOT (X OR Y)=(NOT X) AND (NOT Y) NOT (X AND Y)=(NOT X) OR (NOT Y) I dati 159 che si dimostrano con le tavole di verità in tabella 6. X Y X OR Y NOT (X OR Y) NOT X NOT Y FALSE FALSE TRUE TRUE FALSE TRUE FALSE TRUE FALSE TRUE TRUE TRUE TRUE FALSE FALSE FALSE TRUE TRUE FALSE FALSE TRUE FALSE TRUE FALSE (NOT X) AND (NOT Y) TRUE FALSE FALSE FALSE Tabella 6 – Tavole di verità Inoltre, si noti che gli operatori di relazione, per definizione stessa di confronto, trasformano due o più valori di un tipo in un valore del tipo logico. Difatti “A confronto con B” assume il valore TRUE se il confronto ha successo e il valore FALSE in caso contrario. Si noti che l’insieme degli operatori di relazione è riconducibile a due soli operatori di relazione (l'uguale e il minore ad esempio) e ai tre operatori logici. Operazione Equivalenza X<>Y NOT (X = Y) X<=Y (X < Y) OR (X = Y) X>=Y NOT (X < Y) X>Y NOT ((X < Y) OR (X = Y)) Tabella 6 – Equivalenza tra operatori Se in una espressione logica sono presenti operatori logici e di relazione e sono assenti le parentesi devono essere eseguite nell'ordine il NOT, l'AND, l'OR, e infine gli operatori di relazione. 5.3.2. Il tipo carattere Il tipo carattere è un tipo di fondamentale importanza in quanto contiene l'insieme dei simboli medianti i quali un calcolatore comunica con l'esterno attraverso i dispositivi di ingresso e uscita. Se si considera come unione degli insiemi delle lettere, delle cifre e dei simboli di interpunzione, matematici e speciali, allora si presenta come parzialmente ordinato poiché non ha senso definire un ordinamento tra le lettere e le cifre o tra i caratteri di interpunzione. Da un punto di vista pratico, però, si presenta come ordinato. Inoltre, sempre per motivi pratici, legati alla necessità di differenziazione in fase di visualizzazione, nell'insieme dei caratteri si ritrovano sia le lettere minuscole che maiuscole. I motivi che hanno condotto all'ordinamento del tipo carattere sono legati alla necessità di definire un insieme di caratteri che fosse accettato da tutte le case costruttrici di computer e di periferiche quali stampanti e video. Difatti solo definendo un insieme standard di caratteri è stato possibile far comunicare sistemi diversi tra loro o adattare periferiche di una casa a sistemi di altre case. Lo standard ormai universalmente accettato è quello definito dall'American Standard Code for Information Interchange (ASCII). Si basa su 128 codici di cui i primi trentadue vengono anche chiamati Control per la loro caratteristica di servire come comandi di gestione delle periferiche. Ai codici di controllo non corrisponde 160 Capitolo quinto un simbolo visibile, ma l'esecuzione di un comando da parte della periferica che lo riceve. Di essi ricordiamo: - Cr (detto anche RETURN o anche IMMISS) che serve al computer per comunicare alla stampante o al video di spostare l'elemento di stampa ad inizio rigo (la levetta di una macchina da scrivere per andare daccapo); - Lf o Line Feed, che serve al computer per comunicare alla stampante o al video di spostare l'elemento di stampa sul rigo seguente; - BLANK o Space per introdurre spazi di separazione tra le parole. L'ASCII introduce un ordinamento tra i simboli che è funzione della loro posizione nella tabella di codifica. Per tale ordinamento: - le cifre precedono le lettere; - le lettere maiuscole precedono quelle minuscole. Sul tipo carattere ordinato sono solitamente definite le funzioni (ad esempio CHR e ORD) che pongono in corrispondenza biunivoca l'insieme dei simboli con la loro posizione all'interno del tipo. In particolare la funzione CHR restituisce il simbolo della posizione specificata e, viceversa, la ORD restituisce la posizione di un dato simbolo (le posizioni partono da zero e arrivano a 127). Ad esempio, osservando la tabella ASCII si può verificare che ORD(B)=66 e CHR(65)=A; ovviamente CHR(ORD(A))=A e ORD(CHR(66))=66. 5.3.3. Il tipo intero Il tipo intero (o integer) si definisce come un sottoinsieme finito e definito dell'insieme dei numeri interi: type integer = { n | n appartiene [- m, M] } Spesso si ha che m = M. In questo caso tale valore viene detto MAXINT e si ha che i valori rappresentabili dal tipo intero sono compresi nell'intervallo: [-MAXINT,MAXINT] Le costanti del tipo intero sono sequenze di cifre eventualmente precedute da un segno. Ad esempio sono numeri interi 30000, +400, 500, -678 e -1. La tabella che segue indica le operazioni definite sul tipo intero e gli operatori solitamente usati in diversi linguaggi per indicarle. Nella tabella x e y sono due variabili di tipo intero. Operazione Operatore Operatore Esempi + + x+y Somma x–y Sottrazione * * x*y Moltiplicazione / / x/y Divisione (esterna) DIV / x DIV y Divisione intera x/y MOD % x MOD y Resto x%y Tabella 7 – Operatori su tipo intero Si noti che se si definisce insieme di overflow l'insieme dei valori interi tali che: x : |x| > MAXINT I dati 161 (ossia l'insieme dei numeri interi esterni al tipo intero), si ha che le operazioni elementari della aritmetica applicati a valori del TIPO INTERO valgono se e solo se (a op b) non assume valori nell'insieme di overflow. Ossia se e solo se: | a op b | MAXINT dove op = {+,-,*}, In pratica la condizione di overflow può tradursi in vario modo all'interno di una macchina: per esempio può generare una condizione di errore con interruzione del programma. 5.3.4. Il tipo reale Il tipo reale ( o real) è diverso dall'insieme dei numeri reali poiché in un intervallo reale comunque piccolo esistono infiniti valori (i numeri reali formano un continuo). Allora il tipo reale si definisce come l'insieme che contiene un numero finito di valori reali ognuno dei quali rappresenta un intervallo del continuo. In altri termini, diviso l'insieme dei numeri reali in intervalli di fissata dimensione, si ha, come la figura mostra, che ogni x appartenente all'intervallo [Xi,Xi+1[ viene sostituito con Xi. Figura 1 – Il tipo reale La sostituzione di un numero reale x con il valore X rappresentante l'intervallo a cui x appartiene, pone notevoli problemi di approssimazione in tutti i calcoli che usano valori del tipo reale. Il tipo reale si definisce come un sottoinsieme DISCRETO dei numeri reali. In concreto il tipo reale si definisce come l'insieme dei numeri reali che godono della seguente proprietà: 162 Capitolo quinto X: x X X dove rappresenta l'errore relativo che si commette sostituendo x con X. dipende dalla rappresentazione finita (numero finito di cifre) utilizzata per i numeri reali. Ad esempio disponendo di una calcolatrice con una aritmetica a quattro cifre decimali che applica le note regole di arrotondamento sull'ultima cifra, si hanno gli errori di rappresentazione in tabella 8. NUMERO 0.00347 0.000347 0.00987 0.000987 ARROTONDAMENTO 0.0035 0.0003 0.0099 0.0010 ERRORE 3*10-5 =0.3 *10-4 47*10-6 =0.47*10-4 3*10-5 =0.3 *10-4 13*10-6 =0.13*10-4 Tabella 8 – Errori di rappresentazione In generale se n sono le cifre decimali l'errore che si commette è: 1 10 2 n Per il tipo reale, si definisce insieme di overflow l'insieme costituito da tutti i valori: x non appartenenti a [-m , M ] ossia dei valori più grandi di quelli rappresentabili; e insieme di underflow quello costituito da tutti i valori: x appartenenti a ]- , [ tali che X = 0 ossia l'insieme di valori che vengono confusi con lo zero. Le operazioni definite sul tipo reale sono la moltiplicazione (*), la divisione (/), l’addizione (+) e la sottrazione (-). Le operazioni che richiedono maggiore attenzione sono l'addizione e la sottrazione. Infatti, la causa principale degli errori di calcolo numerico risiede nella sottrazione di numeri di valore quasi uguale; in tal caso le cifre più significative si eliminano fra loro e la differenza risultante perde un certo numero di cifre significative o anche tutte (fenomeno detto cancellazione). Altra causa di errori è la divisione per valori molto piccoli, poiché il risultato può facilmente superare il valore di overflow. Quindi deve essere evitata non solo la divisione per lo zero ma anche per valori ad esso prossimi. Nei linguaggi di programmazione il tipo intero e il tipo reale sono due tipi distinti, anche se il primo è un sottoinsieme del secondo, in quanto le operazioni sui numeri interi forniscono risultati esatti e sono più veloci. Comunque, si conviene definire funzioni di conversione esplicite ed implicite tra i due tipi. In una espressione con valori reali, un qualsiasi operando intero viene implicitamente convertito in reale. Viceversa le funzioni di troncamento e arrotondamento (ad esempio trunc e round) consentono la conversione esplicita da reale ad intero. La funzione trunc(X) restituisce il numero intero che si ottiene troncando le cifre frazionarie di X. La funzione round(X) arrotonda X troncando il valore dopo avergli aggiunto 0.5. I dati 5.3.5. 163 Il tipo per enumerazione Il tipo per enumerazione è un tipo di utente in quanto viene definito elencando l'insieme dei valori che lo costituiscono. Ad esempio: type GIORNO = (lun,mar,mer,gio,ven,sab,dom) type SESSO = (maschio,femmina) Anche se a priori è una raccolta disordinata di valori, solitamente si conviene di definire un ordinamento al suo interno in funzione dell'ordine di apparizione delle costanti nella definizione di tipo. Per tale motivo: lun<mar<mer<gio<ven<sab<dom maschio<femmina In questo modo, per le costanti di un tipo per enumerazione sono definite le funzioni PRED(), SUCC() e ORD() ed è possibile applicare ad esse tutti gli operatori di relazione. 5.3.6. Il tipo subrange Il tipo subrange si definisce specificando un sottoinsieme di un tipo predefinito ed ordinato e per questo motivo è un tipo di utente. La costruzione del tipo avviene specificando due estremi di un tipo ordinato (ossia i tipi integer, char, per enumerazione con esclusione del tipo real) separati da una coppia di punti come mostrato di seguito: type FERIALE = lun..ven type CODICI_MESE = 1..12 La definizione è corretta se e solo se limite inferiore < limite superiore Il tipo così generato conterrà tutti gli elementi compresi tra i due estremi (estremi inclusi) ed erediterà le operazioni del tipo genitore (anche detto base), cioè quello di cui si è scelto l'intervallo. L'introduzione del tipo subrange è importante perché aumenta la chiarezza del modo in cui è analizzata la realtà del problema. Difatti in taluni casi un tipo contiene un numero eccessivo di valori per rappresentare alcune informazioni. Ad esempio, se risulta naturale rappresentare l'informazione anno come un intero (i numeri negativi potrebbero servire per gli anni prima di Cristo), è eccessivo attribuire all'informazione giorno tutti i valori dello stesso tipo. Allora definire: type giorno = 1..31 consente di introdurre dei vincoli sulle informazioni rappresentanti i giorni, vincoli che si concretizzano in una serie di controlli introdotti implicitamente dal traduttore del linguaggio che vietano l'assunzione errata di valori esterni all'intervallo di definizione. 164 5.4. Capitolo quinto I tipi strutturati Si dice che un'informazione è di tipo strutturato se è composta da altre informazioni più semplici. Quindi un tipo è strutturato se è un'aggregazione di informazioni. I tipi strutturati sono di fondamentale importanza perché tramite essi vengono gestite aggregazioni di informazioni della realtà individuate da un unico attributo. Ad esempio, la data indica il giorno, il mese, l'anno; un mazzo di carte indica la prima carta, la seconda, e così via fino all'ultima carta. Le strutture di informazioni che è possibile osservare si presentano in modi diversi e gli elementi caratterizzanti sono fondamentalmente: - il tipo degli elementi che compongono la struttura; - il modo in cui vengono aggregati; - le operazioni che consentono di estrarre una componente o insiemi di componenti da tutta la struttura; - le operazioni che si possono fare su tutta la struttura. Per tali ragioni un tipo strutturato si può definire: 1. specificando il tipo delle informazioni componenti (si noti che i componenti del tipo strutturato possono essere sia atomici che, a loro volta, strutturati creando così strutture di strutture); 2. indicando il costruttore necessario alla creazione della struttura, ossia le operazioni da fare sui componenti per formare il tipo strutturato; 3. indicando il selettore (anche detto funzione di accesso) necessario alla estrazione di un componente dalla struttura, ossia le operazioni che sul tipo strutturato possono essere fatte per individuare uno specifico componente della struttura 4. elencando le operazioni che sull'informazione di tipo strutturato possono essere fatte; operazioni definite o assiomaticamente sul tipo o mediante operazioni sui valori componenti la struttura (quasi sempre assegnazione di valori e test di uguaglianza). Da quanto detto ne consegue che i tipi strutturati sono tipi di utente in quanto è l'utente che ne specifica i componenti, mentre i linguaggi di programmazione mettono a disposizione i costruttori e i selettori. Il prodotto cartesiano e la sequenza sono i costruttori più importanti. Assegnati gli insiemi A e B (distinti o non), si dice prodotto cartesiano di A per B, l'insieme costituito da tutte le coppie: (a,b) con a appartenente ad A e b a B Ad esempio, assegnati gli insiemi A {1,2,3} e B {4,5} si ha che il prodotto cartesiano di AxB è: {(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)} Il prodotto cartesiano tra n insiemi (I1xI2x…xIn) produce una ennupla (v1,v2,…vn) con v1 appartenente a I1, v2 ad I2 e vn ad In. I dati 165 Si riportano qui di seguito alcuni esempi pratici. - Le coordinate dei punti del piano sono, detto R l'insieme dei numeri reali, il prodotto cartesiano RxR, per cui un punto P è individuato dalla coppia (x,y) con x e y appartenenti a R: x è detta ascissa mentre y ordinata del punto. - Detto I l'insieme dei numeri interi e M l'insieme dei nomi dei mesi, una data è rappresentata dal prodotto cartesiano I x M x I secondo la seguente convenzione DATA=(giorno,mese,anno). Esempi di date sono (02,gennaio,1954), (10,dicembre,2000). - Definito il tipo carta come l'insieme dei quattro semi {C,Q,F,P} (ossia Cuori, Quadri, Fiori Picche), un mazzo di carte francesi è il prodotto cartesiano: Carta1 x Carta2 x ... x Carta52. Un mazzo può essere descritto come (prima carta, seconda carta, ... , cinquantaduesima carta) e possibili valori sono (C,C,F,F,P,...,Q), (F,F,P,Q,Q,...,F). Si osservi che per quanto riguarda gli insiemi che compongono il prodotto cartesiano si ha che: - il loro tipo può essere distinto o uguale; - il loro numero è definito e finito; - i loro attributi possono variare o meno a seconda della posizione all'interno della ennupla, e nel caso in cui sono tutti uguali, la individuazione di un elemento è fatta attraverso la sua posizione. La sequenza è un costruttore che genera successioni (anche dette stringhe ordinate) di elementi tutti dello stesso tipo e in numero non definito a priori. Le sequenze hanno quindi una lunghezza K che può assumere valori maggiori od uguali a zero. Se la sequenza ha lunghezza nulla si parla allora di sequenza vuota o stringa nulla. Sul piano pratico non è possibile avere stringhe a lunghezza infinita: pertanto si ha una limitazione superiore K < MAX con MAX che dipende dall'ambiente e dal tipo costruito con la sequenza. Sulle sequenze è sempre definita l'operazione di concatenazione di due o più stringhe: nel caso di due stringhe la concatenazione è definita come l'accodamento della seconda alla prima, mentre nel caso di più stringhe si definisce come l'accodamento della seconda alla prima, quindi della terza alla stringa così ottenuta, e si continua in questo modo fino all'ultima stringa. La concatenazione si può indicare con +,&,//. Ad esempio, date le due stringhe: s1 = abc s2 = efg la concatenazione s1 + s2 definisce la stringa abcefg. Sulle sequenze sono anche definite le operazioni di estrazione del primo elemento first(s1) = a, e dell'ultimo elemento last(s1) = c. I tipi strutturati che sono presenti nella maggioranza dei linguaggi di programmazione (nel senso che il linguaggio offre i costruttori per la loro definizione) sono: array monodimensionale, bidimensionale e pluridimensionale, record, stringa di caratteri, file sequenziale. Per i tipi: pila o stack, coda, tabella, anche se assenti dai linguaggi di programmazione, verrà comunque data la definizione per il ruolo importante che essi assumono in una molteplicità di elaborazioni. 166 Capitolo quinto In generale l'assenza di un costruttore di tipo, non deve condizionare il programmatore dall'uso di un tipo astratto. Difatti solo la definizione precisa (struttura e operazioni) del tipo può portare ad una sua reale definizione o simulazione coi meccanismi presenti nel linguaggio usato. 5.4.1. Gli array Con l'array monodimensionale, anche detto vettore, si assegna un nome collettivo ad un insieme ordinato e finito di oggetti dello stesso tipo. L'insieme è ordinato nel senso che ogni elemento occupa una posizione ben precisa ed è finito perché il numero di elementi che lo compongono è per motivi pratici limitato. L'array può anche essere definito come il prodotto cartesiano di elementi tutti dello stesso tipo e con lo stesso attributo. La funzione di accesso è ottenuta specificando la posizione della componente nella ennupla del prodotto cartesiano. Però, se si fissa una corrispondenza tra un insieme di valori e l'insieme delle posizioni nella ennupla, allora si può utilizzare tale insieme di valori come funzione di accesso. Questo insieme è detto insieme indice ed ha la proprietà che ad ogni suo valore è associato un solo elemento dell'array secondo il seguente schema. Figura 2: Mapping tra indici ed elementi di un array I tipi subrange dei tipi intero, carattere e di quello definito per enumerazione sono usabili come tipi indice. Difatti la corrispondenza tra il valore del tipo e la posizione nell'array è definita implicitamente dalla posizione del valore stesso nell'intervallo. Il tipo indice definisce così l'intervallo di valori che può essere specificato nella selezione di un componente dell'array. Per quanto riguarda le operazioni sull'array, esse sono tutte quelle definite sul tipo delle informazioni componenti. Allora un tipo strutturato si definisce come array quando sono specificati: - l'attributo dell'array; - il tipo dei componenti; I dati 167 - il tipo dell'informazione indice; il numero dei componenti chiamato anche cardinalità dell’array e implicitamente definito dalla cardinalità del tipo indice. Si noti che solitamente si usano intervalli del tipo intero come tipo per l'indice. In questo caso il numero di componenti dell'array è dato da estremo superiore meno estremo inferiore più uno perché entrambi gli estremi devono essere considerati. In alcuni linguaggi l'estremo inferiore si può omettere quando coincide con il valore uno, e si deve quindi indicare soltanto l'estremo superiore. In questi casi le due definizioni di array di 10 numeri reali: type vettore=array [1..10] of integer e type vettore=array [10] of integer sono equivalenti. La selezione di un componente si effettua facendo seguire al nome dell'array il valore dell'indice racchiuso tra parentesi quadre: con vettore[1] si accede al primo elemento dell’array se l’indice parte da uno. È anche possibile selezionare un elemento dell'array mediante espressioni o variabili il cui valore sia un valore appartenente al tipo indice. In questo caso la posizione viene calcolata dinamicamente durante l'elaborazione. Ad esempio vettore[I-1] comporta la determinazione del valore della variabile I e il suo decremento di una unità per individuare la posizione nell'array. Allora se I ha il valore 4 vettore[I-1] coincide con vettore[3], mentre vettore[I] coincide con vettore[4]. Per questo motivo la posizione è in genere un parametro d'uso nel senso che dipende dalla elaborazione in corso: in altre parole la selezione dell'elemento dell'array è fatta dopo aver valutato il valore della variabile o dell'espressione. Come già detto, le operazioni sull'array sono tutte quelle definite sul tipo delle informazioni componenti. Qualche volta è possibile l'assegnazione del valore di tutti i componenti di un vettore B a quelli di un altro vettore A dello stesso tipo. In questi casi A:=B corrisponde a: A[1]:=B[1] A[2]:=B[2] : :: : :: A[n]:=B[n] o addirittura sono consentite operazioni di somma, prodotto, divisione, per cui, ad esempio, A:=B+C corrisponde a: A[1]:=B[1]+C[1] A[2]:=B[2]+C[2] : :: :: A[n]:=B[n]+C[n] ma sono casi molto rari. In una elaborazione non è detto che tutti gli elementi di un array vengono necessariamente utilizzati. Difatti avviene spesso che solo una parte del numero totale di elementi è utile alla elaborazione in corso. Per tali motivi si è soliti 168 Capitolo quinto accompagnare la struttura array con una ulteriore informazione di tipo intero che conta quante sono le posizioni occupate durante l’elaborazione. A tale informazione viene dato il nome di grado di riempimento dell’array o semplicemente riempimento. Il riempimento rappresenta il numero di componenti effettivamente usato nella elaborazione in corso e non va confuso con il numero di componenti dell'array (cardinalità dell'array). Solitamente il riempimento è minore o al massimo uguale alla cardinalità. All’inizio del programma il riempimento deve assumere il valore zero per indicare che nell’array non sono stati inseriti valori. Diverse possono essere le strategie per disporre valori diversi nelle diverse posizioni dell’array. Il modo più semplice è di inserirli in posizioni successive a partire dalla prima cosicché ad ogni inserimento il riempimento non solo indica quanti elementi sono stati allocati ma anche quale è l’ultima posizione occupata. Nella tabella 9 che segue si presentano diversi algoritmi che inseriscono nell’array vet gli elementi prodotti da una funzione generica produci_valore; in tutti i casi il riempimento è indicato dalla variabile n mentre la cardinalità dalla costante cmax. Negli esempi si distinguono i casi in cui la prima posizione dell’array è zero da quella in cui è uno. Nei primi due esempi il numero di elementi prodotti è predeterminato. Caricamento vettore con posizione iniziale uguale a zero e numero elementi predeterminato Caricamento vettore con posizione iniziale uguale ad uno e numero elementi predeterminato Caricamento vettore con posizione iniziale uguale a zero e numero elementi non predeterminato Caricamento vettore con posizione iniziale uguale ad uno e numero elementi non predeterminato n := fissa_riemp; for i:=0 to n-1 do vet[i]:=produci_valore; n := fissa_riemp; for i:=1 to n do vet[i]:=produci_valore; n:=0; while (termina AND n<cmax) do begin n:=n+1; vet[n-1]:=produci_valore; termina:= verifica_terminazione; end n:=0; while (termina AND n<cmax) do begin n:=n+1; vet[n]:=produci_valore; termina:= verifica_terminazione; end Tabella 9 - Caricamento di vettori In tutti i casi n indica il riempimento del vettore: nel caso di uso della posizione zero n indica la posizione successiva a quella occupata per ultima; negli altri casi indica proprio l’ultima posizione occupata. Si noti che la scelta della cardinalità di un array (cmax negli esempi) è di estrema importanza in quanto deve essere fatta pensando che lo stesso programma può essere eseguito in condizioni diverse: cmax deve comprenderle tutte in quanto un suo sottodimensionamento porta il programma in una condizione di errore gestita in modi diversi dal compilatore del linguaggio. Negli esempi l’uscita dal ciclo per n<cmax non più verificata, comporta che alcuni dati prodotti dall’ambiente non vengano presi in considerazione. In definitiva non si deve sovradimensionare l'array introducendo componenti mai utilizzati occupando inutilmente la memoria centrale, e non si deve neppure introdurre un array troppo piccolo per le diverse richieste dell'elaborazione. I dati 169 Un array si dice bidimensionale se i suoi componenti sono array monodimensionali. Una possibile definizione di un tipo array bidimensionale di interi è la seguente: type matrice=array [1..10,1..10] of integer In questo caso la funzione di accesso è composta da due indici: - il primo che individua l'array componente; - il secondo che individua il componente dell'array selezionato con il primo indice. Ad esempio matrice[I,J] è il componente di posto J dell'array I-esimo. Gli array bidimensionali vengono usati nei problemi di analisi per la rappresentazioni delle matrici. Gli esempi che seguono riportano alcune modalità di uso degli array bidimensionali. Caricamento matrice quadrata per righe n:=fissa_ordine; for I:=1 to n do begin for J:=1 to n do begin matrice[I,J]:=determina_valore; end end Caricamento matrice non quadrata per colonne n:=fissa_ordine; m:=fissa_ordine; for J:=1 to m do begin for I:=1 to n do begin matrice[I,J]:=determina_valore; end End Copia di una matrice in un’altra invertendo righe e colonne (entrambe quadrate di ordine n) for I:=1 to n do begin for J:=1 to n do begin A[J,I]:= B[I,J]; end End Tabella 10 - Caricamento di matrici Gli array pluridimensionali sono array di array di array con funzione di accesso A[I,J,K,...]. Solitamente il numero di dimensioni ammesse per un array è finito e definito per motivi pratici. 5.4.2. Il tipo stringa di caratteri Il tipo stringa di caratteri si definisce come sequenza di caratteri. Per motivi pratici la lunghezza della sequenza è finita. È possibile avere stringhe senza caratteri, ossia con lunghezza nulla. Tali stringhe sono dette stringhe nulle o vuote. Tra i valori del tipo stringa di caratteri si definisce un ordinamento se il tipo carattere è ordinato e il confronto tra due stringhe è il risultato del confronto dei caratteri in egual posizione. Si ha allora che ‘BIANCO' segue 'BIANCA' (è maggiore di), e 'caso' precede 'casolare' (è minore di). In particolare, per le stringhe che presentano la medesima lunghezza l'ordinamento è quello lessicografico: due parole si confrontano carattere per carattere a partire da sinistra (carattere in prima posizione) e l'ordinamento viene deciso dalla prima occorrenza di caratteri diversi e si dice che la parola che ha il carattere più piccolo precede l'altra. Per le stringhe con lunghezza diversa si estende l'ordinamento lessicografico accodando alla stringa più corta il numero di spazi necessari a renderla lunga quanto l'altra. Si noti 170 Capitolo quinto però che l'ordinamento tra le stringhe è diverso da quello del dizionario per il quale non si fa differenza tra lettere maiuscole e minuscole e sono assenti i caratteri speciali e le cifre. Qualche volta è definita una funzione di accesso che consente di estrarre da una stringa assegnata la sottosequenza che inizia in una posizione specificata ed è costituita da un numero fissato di caratteri, oppure termina in una data posizione. Un'operazione interna al tipo stringa di caratteri (come già visto applicabile a tutte le sequenze) è la concatenazione, che applicata a due sequenze, la prima di lunghezza M, la seconda di lunghezza N, genera una sequenza di lunghezza M+N avente come primi M simboli quelli della prima sequenza e come successivi N simboli quelli della seconda sequenza. Di solito si indica con &,//,+, ed ad esempio, se s1='CA' e s2='SA' allora si ha che s1 + s2 'CASA'. Si ricordi che per definire una stringa sono necessari: - l'attributo della stringa; - il numero massimo di caratteri che la stringa può contenere (solitamente 0 < K < MAX, con MAX che dipende dal linguaggio di programmazione). Ad esempio: type PAROLE = string[10] type TELEFONO = string[12] Per le stringhe di caratteri valgono considerazioni analoghe a quelle fatte per il riempimento di un array. La lunghezza della stringa dipende dalla elaborazione in corso e risulta sempre minore od uguale alla massima dimensione. I linguaggi di programmazione però gestiscono in modi molto diversi la lunghezza della stringa. In particolare alcuni consentono di utilizzare solo una parte di quella massima dichiarata altri no. Allora si dice che una stringa ha una lunghezza variabile se è possibile usare solo una parte dei K caratteri previsti nella definizione. Sulle stringhe gestite in questo modo è definita la funzione LEN(x) che fornisce il numero di caratteri presenti nella stringa (valore compreso in [0,K]). Si dice invece che una stringa ha una lunghezza non variabile se vengono usati sempre tutti i K caratteri previsti nella definizione. Per comprendere la differenza fissiamo che s1 sia string[10] e s2 sia string[5] e osserviamo il funzionamento della concatenazione s1 + s2 dopo aver effettuato le seguenti assegnazioni: s1:='abc' e s2:='efg' 1. caso variabile: i len(s2) caratteri di s2 vengono accodati ai len(s1) caratteri di s1 dando la stringa 'abcefg' 2. caso non variabile: i 5 caratteri di s2 vengono accodati ai 10 caratteri di s1 fornendo la stringa 'abc efg ' Si noti che assegnare ad una stringa un'altra stringa di lunghezza minore della sua dimensione non crea alcun problema nel caso variabile mentre in quello a lunghezza fissa comporta che i restanti caratteri vengono solitamente riempiti con il carattere spazio. Ad esempio l'assegnazione s1:='abc' assegna ad s1 definita come string[10] la stringa 'abc' nel caso a lunghezza variabile e 'abc ' nell'altro caso. Invece l'assegnazione ad una stringa di un'altra di dimensione maggiore non presenta alcuna differenza nei due casi e si traduce in un troncamento dei caratteri I dati 171 eccedenti. Ad esempio l'assegnazione ad s1 di 'abcdefghilmnopqrstuvz' corrisponde all'assegnazione della sola stringa 'abcdefghil' (primi 10 caratteri). Per il caso a lunghezza fissa bisogna però fare attenzione agli effetti combinati della concatenazione e dell'assegnazione: difatti l'assegnazione s1:= s1 + s2 risulta inutile in quanto per effetto della concatenazione vengono presi tutti i caratteri di s1 e composti con quelli di s2, ma per effetto della assegnazione la stringa ottenuta verrà troncata proprio dei caratteri di s2. 5.4.3. Il record Il tipo strutturato record è il prodotto cartesiano di informazioni dette campi: i tipi dei campi possono essere omogenei o disomogenei. Per definire un record sono necessari: - l'attributo del record; - e l'elenco degli attributi dei campi con relativi tipi. Ad esempio: type NOME = record of NOME_PRIMO_CAMPO: TIPO_A; NOME_SECONDO_CAMPO : TIPO_B; : NOME_I-esimo_CAMPO: TIPO_A; : NOME_N-esimo_CAMPO: TIPO_X end; type COORDINATE = record of ASCISSA: real; ORDINATA: real; end; type DATA = record of GIORNO: integer; MESE: integer; ANNO: integer; end; La funzione di accesso del record consente la selezione di un campo. Si realizza specificando il nome del record e quello del campo separati da un punto. Ad esempio se A è una variabile del tipo: type ANAGRAFICA = record of NOME: string[40]; INDIRIZZO: string[30]; ETA: 1..100; ESAMI_SOSTENUTI: integer end 172 Capitolo quinto allora per selezionare il campo NOME si deve scrivere A.NOME, mentre per ETA, A.ETA e così via. Le operazioni sul record sono quelle definite sul tipo delle informazioni componenti. Ad esempio: A.NOME *4 non è possibile, mentre: A.ESAMI_SOSTENUTI +30 è ovviamente possibile. In alcuni linguaggi è possibile l'assegnazione ad un record del valore di un altro record dello stesso tipo. In questo caso il valore di tutti i campi del record a secondo membro vengono assegnati ai campi con medesimo nome del record a primo membro. Ad esempio, se dat1 e daT2 sono due record del tipo DATA, allora l'assegnazione dat1:=dat2 corrisponde all'insieme delle assegnazioni: dat1.giorno:=dat2.giorno dat1.mese:=dat2.mese dat1.anno:=dat2.anno 5.5. I puntatori Una variabile è detta puntatore se il suo valore individua la posizione di un'altra informazione. Per posizione di una informazione si intende la posizione da essa assunta o in memoria o all'interno di una struttura dati. In tal senso sono puntatori i riferimenti ad un elemento di un array, di un carattere all'interno di una stringa, e così via. Nei linguaggi attuali, nei quali è possibile la creazione e la distruzione di variabili dinamicamente, durante l'esecuzione del programma sotto il diretto controllo del programmatore, i puntatori sono l’unico strumento per poter operare su tali variabili. Con comandi specifici (ad esempio new) si istanziano variabili quando servono nel corso della elaborazione; con altri comandi (ad esempio dispose) si recupera lo spazio di memoria occorso per l’allocazione della variabile. La istanziazione della variabile restituisce l’indirizzo di memoria attraverso cui si accede ad essa per tutte le operazioni consentite compatibili con il tipo di appartenenza. Per poter far uso di un tale meccanismo di allocazione dinamico, le variabili devono essere opportunamente dichiarate per consentire non solo la gestione dell’indirizzo di memoria attraverso cui operare sulla variabile, ma anche la sua effettiva occupazione in memoria. La definizione di una variabile puntatore si effettua quindi indicando a quale tipo di variabile essa deve puntare come l’esempio seguente schematizza. type tipo_puntato = record of campo1: integer; I dati 173 campo2: real; end; type puntatore =^tipo_puntato; Con essa si definisce il tipo puntatore come riferimento al record di tipo tipo_puntato. Allora la dichiarazione: var punt : puntatore; crea una variabile il cui contenuto sarà l'indirizzo dell’area di memoria gestita secondo le indicazioni di tipo della definizione e allocata dinamicamente. In altre parole, l'inserimento all'interno del programma della seguente chiamata: new(punt); provoca l'allocazione in memoria di una variabile di tipo tipo_puntato (un record quindi) e l'assegnazione alla variabile puntatore dell'indirizzo di allocazione. Senza tale chiamata la variabile di tipo record, riportata nell'esempio, non può essere usata e solo dopo di essa si può fare riferimento alla variabile creata tramite la variabile puntatore. Per tali motivi si ha che: - punt denota il valore del puntatore (ossia l'indirizzo di memoria dove il record è stato allocato per effetto della procedura new); - punt^ denota invece la variabile puntata (quindi il record). Così la seguente assegnazione: punt^.campo1 := 1000; assegna il valore 1000 al primo campo del record. Se infine si dichiara un’altra variabile dello stesso tipo puntatore: var punt1: puntatore; ed ad essa si assegna il valore del precedente puntatore: punt1 := punt; si ha che l'indirizzo di memoria associato a punt viene assegnato anche a punt1 in modo che entrambi fanno riferimento alla stessa zona di memoria. Con un meccanismo opposto al new, la procedura predefinita dispose restituisce lo spazio di memoria associato al puntatore specificato nella chiamata della procedura stessa. Così: dispose(punt) rende disponibile per altre allocazioni l'area di memoria a cui fa riferimento punt. 174 Capitolo quinto I linguaggi che dispongono di puntatori di questo tipo, ossia contenenti direttamente indirizzi di memoria, devono fornire anche un valore che indichi che il puntatore non punta a nulla. Solitamente tale costante è predefinita e si indica con NIL. 5.6. I file Il file si definisce come una sequenza di elementi tutti dello stesso tipo. Si possono così avere file di interi, di caratteri, di record, di array, di reali, etc. L'importanza di tale struttura è che di essa non se ne deve indicare la dimensione all'atto della definizione in quanto i suoi valori vengono allocati in una delle memorie di massa disponibili nel sistema informatico. Tra l’altro, mentre la dimensione della memoria centrale è da considerare definita e limitata, e quindi sono limitati gli oggetti in essa inseribili, le memorie di massa hanno una capacità così grande da non rappresentare, solitamente, una limitazione al numero di elementi gestibili. Comunque, può accadere che durante l'aggiunta di informazioni ad un file si generi un errore per la saturazione della memoria di massa non essendo essa di fatto illimitata. Poichè le informazioni contenute nel file devono essere gestite nel rispetto del modello di Von Neumann, al file viene fatto corrispondere un buffer in memoria centrale capace di garantire il transito dei valori da e verso la memoria di massa. Per operazioni di scrittura il buffer viene caricato del valore che successivamente viene registrato sulla memoria di massa, per quelle di lettura si procede in modo opposto, il valore viene letto dalla memoria di massa e quindi inserito nel buffer dove può essere poi elaborato. Il file è quindi composto da una sequenza di elementi tutti dello stesso tipo T residenti sulla memoria di massa e da una ulteriore informazione di tipo T che svolge le funzioni di buffer in memoria centrale. I file possono essere ad accesso diretto o sequenziale. Nel primo caso si può accedere direttamente ad un particolare dato nel file nota la posizione; nel secondo caso l’accesso al file avviene in modo rigorosamente sequenziale (cioè prelevando un valore dopo l'altro), indipendentemente dal particolare supporto impiegato per la sua memorizzazione. 5.6.1. I file sequenziali Le operazioni definite su un file sequenziale di nome F sono: - l'inserimento di un elemento PUT(F); - l'estrazione di un elemento GET(F); - l'operazione di riavvolgimento RESET(F) per predisporre il file alle estrazioni; - la cancellazione o azzeramento REWRITE(F) per predisporre il file alle inserzioni - e il predicato logico EOF(F) che segnala durante le estrazioni se il file è terminato (End Of File). Il buffer viene inserito automaticamente all'atto della definizione, lo si indica con F^ se F è una variabile di tipo file ed è di tipo T se il file è composto da elementi di tipo T. Il buffer è l'unica informazione del file in memoria centrale che I dati 175 consente di leggere o aggiungere componenti ad F permettendo il transito da e verso la memoria ausiliaria. In ogni momento il file può essere visto come la concatenazione di una parte destra Fd di una parte sinistra Fs (prendendo come riferimento l'organo di lettura e scrittura): F:=Fs+Fd Figura 3: Parte destra e sinistra di un file In tali condizioni EOF(F) assume il valore true se Fd è vuoto, negli altri casi è false. Se EOF(F) è falso, ossia la parte destra non è vuota, si definisce il buffer come: F^:=first(Fd) Nel caso di un file composto dalla sequenza di elementi (abcd) si ha che l'esecuzione di un RESET(F) comporta l'inizializzazione del file (riavvolgimento) e il posizionamento nel buffer del primo elemento secondo lo schema: Fs :=<file vuoto> Fd :=abcd (*intero file*) e, quindi, per definizione: F^:=first(Fd) ossia F^=a perchè EOF(F) è falso L'esecuzione di un GET(F) porta invece nel buffer l'elemento del file successivo a quello precedentemente prelevato per effetto di un RESET o di un altro GET secondo lo schema: se Fs:=ab e Fd:=cd allora la GET produce Fs:=abc e Fd:=d e, quindi, per definizione 176 Capitolo quinto F^:=first(Fd) ossia d (in quanto EOF(F) è ancora falso). L'esecuzione di un REWRITE(F) cancella gli elementi del file svuotandolo: F=<file vuoto> L'esecuzione di un PUT(F) accoda il contenuto del buffer agli elementi del file secondo lo schema: se F:=abcd e F^:=f allora put produce F:=F+F^, ottenendo F:=abcdf solitamente il buffer F^ è indefinito dopo un PUT. Nei file sequenziali l'ordine in cui le operazioni possono susseguirsi è molto rigido. Dopo un reset sono possibili solo operazioni di get, mentre dopo un rewrite solo quelle di put. Si definisce generazione di un file l'insieme di operazioni necessarie alla scrittura di un file su memoria di massa: esse sono un rewrite e una o più operazioni di put. Si definisce, invece, ispezione di un file l'insieme di operazioni necessarie alla lettura di un file: esse sono un reset e tante operazioni di get fino a che non si incontra la fine del file (eof diventa vero). Si osservi che: - la definizione delle operazioni ci impone che l'accesso ad un elemento è possibile solo dopo l'accesso a tutti gli elementi che lo precedono; - non è possibile mischiare operazioni di put e get; - durante l'ispezione del file le componenti vengono lette secondo la disposizione sequenziale fissata durante la scrittura. I linguaggi di programmazione mettono a disposizione procedure di lettura e scrittura per i file più semplici da usare. Sono procedure, ad esempio di read e di write, in cui il buffer risulta nascosto e in cui si fa riferimento direttamente alla variabile il cui valore deve essere letto o scritto su file. Ovviamente le variabili che compaiono nelle istruzioni di read e write devono essere dello stesso tipo delle informazioni che compongono il file. Procedura read(F,var); read(F,var1,var2,..,varN) write(F,var); write(F,var1,var2,..,varN) Sequenza equivalente var:=F^ get(F); read(F,var1); read(F,var2); ……… read(F,varN); F^:=var; Put(F); write(F,var1); write(F,var2); ............ write(F,varN); Tabella 12 – Operazioni di lettura e scrittura su file I dati 177 Con tali operazioni, l'ispezione di un file con elaborazione dei valori si presenta nel modo seguente: (*M: posizionamento all'inizio del file *) reset(f); (*M: scorrimento fino alla fine di esso *) while not eof(f) do begin (*M: leggo dal file *) read(f,info); elabora(info) end La generazione del file si presenta invece nella forma: (*M: predisposizione del file alla scrittura *) rewrite(f); repeat (*M: produci dati con una prefissata elaborazione*) produci(info); if info <> terminazione then (*M: scrittura su file *) write(f,info) until info = terminazione 5.6.2. I file di caratteri o textfile Un tipo di file molto comuni sono i file di caratteri, detti anche textfile. Fra tutti i file, i textfile svolgono un ruolo molto importante in quanto, nella maggioranza dei casi, i dati di ingresso e di uscita dei programmi sono costituiti da textfile. Un processo di calcolo che interagisce con il mondo esterno può essere considerato come un processo di trasformazione di textfile. Difatti, l'insieme di dati prelevati da una tastiera sono di fatto un textfile (detto di input) così come è un textfile (detto di output) l'insieme dei risultati presentati sul video o sul foglio di carta di una stampante. L'uso di alcuni caratteri di controllo ASCII permette di organizzare i textfile in linee o righe. Ciascuna riga contiene al suo interno le rappresentazioni (sequenze di caratteri) che servono all'elaborazione nel caso dell'input o che da essa vengono generate nel caso dell'output. In particolare, si conviene di usare il CR (Carriage Return) come separatore di righi e lo spazio (Space) come separatore di informazioni. Il prelievo da parte del processore di informazioni dal file di input prende il nome di operazione di lettura mentre l'inserzione di informazioni in quello di output viene indicata come operazione di scrittura. Il passaggio dal tipo dell'informazione letta (o scritta) alla sequenza di caratteri che la rappresentano nel file di input (o di output) prende il nome di trasformazione di formato ed avviene durante l'elaborazione. Opportune indicazioni guidano la trasformazione. Tali indicazioni possono essere di tipo esplicite o implicito. Quelle esplicite sono fornite al processore dal programmatore 178 Capitolo quinto mentre le implicite intervengono in assenza delle esplicite e sono definite nel linguaggio di programmazione usato. In definitiva i file di input e di output sono file particolari. Il file di input è un file che può essere solo ispezionato e per tale motivo è già generato e resettato all'esterno del processo di elaborazione ed ammette come unica operazione il get. Identificheremo l'operazione di lettura del valore di una informazione dal file di input con: read(nome_variabile) Essa corrisponde, detto INPUT il nome del file di input, alla sequenza di operazioni: nome_variabile:=INPUT^ get(INPUT) ossia all'assegnazione alla variabile del contenuto del buffer associato al file di input e ad un successivo get. Il file di output è un file che può essere solo generato e per tale motivo è generato all'interno del processo di elaborazione ed ammette come unica operazione il put. Identificheremo l'operazione di scrittura del valore di una informazione nel file di output con: write(nome_variabile) Essa corrisponde, detto OUTPUT il nome del file di output, alla sequenza di operazioni: OUTPUT^:=nome_variabile put(OUTPUT) ossia all'assegnazione al buffer associato al file di output del valore della variabile e ad un successivo put. Comunque, non solo il mondo esterno (file INPUT per la tastiera e OUTPUT per il video) è organizzato come textfile. Anche altri file possono essere organizzati in tale modo. Le informazioni che da essi vengono prelevate richiedono trasformazioni di formato identiche a quelle viste a proposito dell'immissione dei dati da tastiera, mentre quelle che in essi vengono registrate richiedono le stesse trasformazioni di formato che governano la presentazione dei risultati al video. Il vantaggio di organizzare i dati in textfile risiede nel fatto che la rappresentazione in caratteri ne garantisce la comprensione da parte dell'uomo anche all'esterno del programma che li genera o li ispeziona. In ogni caso, il lavoro richiesto per la conversione di valori dalla loro rappresentazione interna (stringhe di bit) nella forma di sequenze leggibili di caratteri per le operazioni di read e, viceversa, per quelle di write, richiede del tempo di elaborazione. Se le informazioni devono essere scritte su un file che lo stesso o un altro programma deve rileggere, non c'è alcuna evidente necessità di convertile dapprima in caratteri e successivamente riconvertirle nel formato interno di memoria. Inoltre, la conversione e riconversione oltre a far perdere del tempo può, nel caso di informazioni di tipo numerico, generare errori per la differenza di precisione tra la rappresentazione esterna e quella interna: se ad esempio una variabile numerica I dati 179 viene scritta sul file con un formato che tronca alcune sue cifre significative, quando se ne rilegge successivamente il valore, lo si troverà diverso da quello iniziale. Tutti i file di tipo non testo memorizzano i valori delle informazioni così come sono rappresentati in memoria centrale, quindi senza alcuna ulteriore elaborazione. Essi dipendono fortemente dall’ambiente di programmazione che li genera e pertanto richiedono un legame molto stretto tra la loro generazione e ispezione, nel senso che non solo la successione delle informazioni lette deve corrispondere a quella in cui le stesse informazioni sono state scritte ma anche che la lettura deve avvenire nello stesso ambiente in cui sono stati generati. Nei casi in cui i file vengono generati in un ambiente ed elaborati in un altro, si preferisce utilizzare per essi il tipo textfile per garantire la portabilità dei dati. 5.6.3. La connessione e sconnessione dei file La gestione delle memorie di massa è affidata al Sistema Operativo. Molti sono i modi di organizzare le memorie di massa sia dal punto di vista logico che da quello fisico. In questa sede è sufficiente ricordare che, da un punto di vista logico, le memorie di massa sono organizzate in aggregati di informazioni tra loro omogenee a cui viene dato il nome di file. Il file system è la componente del sistema operativo a cui è affidato il compito di organizzare e gestire i file. Le sue funzioni specifiche variano da computer a computer, ma tipicamente includono: - la creazione e la cancellazione dei file; - la gestione degli accessi alle informazioni del file; - la gestione e l’organizzazione dello spazio delle memorie di massa; - la protezione dei file per impedire l'accesso da parte di persone non autorizzate a farlo. Poiché su una memoria di massa possono risiedere più file contemporaneamente, il file system richiede che a ciascun file, all'atto della sua creazione, venga assegnato un nome. Tale nome viene poi usato come suo riferimento per effettuare una qualsiasi altra operazione di gestione come l'aggiornamento, l'ispezione o la cancellazione. Due file diversi non possono avere ovviamente lo stesso nome. Il file system gestisce per ogni file, oltre al nome, anche le seguenti informazioni: - l'allocazione del file sul supporto magnetico; - la dimensione del file; - la data di creazione o dell'ultimo aggiornamento del file; - le informazioni di controllo sui diritti di accesso quali quelli che abilitano operazioni di modifica del contenuto del file, la lettura del file o la sua eventuale eliminazione. L'informazione fondamentale che contraddistingue il file è il nome attribuitogli al momento della sua creazione. I file system dei vari sistemi operativi prevedono sintassi molto diverse per la costruzione di tali nomi. Solitamente sono stringhe di caratteri e cifre di lunghezza fissata attribuite al file in modo analogo a quello in cui si sceglie un identificatore per una variabile: ossia in modo da ricordarne il contenuto. 180 Capitolo quinto Al nome può essere aggiunta una estensione facoltativa: per esempio nella stringa “filename.est”, filename è il nome e est è l'estensione. L'estensione comincia con il punto (.), ed è formata da 1, 2 o 3 caratteri. Più file possono essere raggruppati in cartelle o direttori. Se sulla memoria di massa sono state organizzate più cartelle, prevedendo anche che alcune di esse siano interne ad altre, il nome del file da solo non basta ad identificarlo e si deve specificare il percorso da seguire per raggiungerlo nella cartella in cui è collocato. Ad esempio “\programmi\corso2007\c\ordina.c” indica che il file ordina.c si trova nella cartella c che è interna a corso2007 che a sua volta è interna a programmi. Infine un sistema può disporre di più memorie di massa identificate da lettere o da nomi. Se ad esempio il sistema dispone di una unità minidisco e di una unità disco fisso, l'unità minidisco viene identificata dalla lettera A e il disco fisso dalla lettera C. Pertanto per individuare un file, si devono specificare: - la lettera o il nome identificativi dell'unità, - il percorso da seguire tra le cartelle per raggiungere quella che contiene il file - il nome di file - e l'estensione, se esiste. L’insieme dei quattro elementi è detto specificazione del file. I due punti (:) separano l’identificativo dell’unità dal resto; le cartelle del percorso sono separate tra di loro dal carattere “\” e l’estensione è preceduta dal punto. In un programma, prima di poter operare su un file si deve interagire con il file system del sistema operativo fornendogli le indicazioni del file capaci di individuarlo univocamente. La connessione permette all'interno di un programma di richiedere al sistema operativo il file sul quale operare. Tutti i linguaggi prevedono a questo scopo procedure predefinite per consentire la connessione tra la variabile file e il nome da esso assunto nella memoria di massa gestita dal file system del sistema operativo. In modo molto semplice la connessione può avvenire tramite una procedura che associa alla variabile di tipo f il suo nome sulla memoria di massa. Ad esempio con una procedura assign del tipo: assign(F, “c:\programmi\corso2007\ordina.c”) si attiva la connessione della variabile F con il file ordina.c residente nella cartella corso2007, a sua volta interna alla cartella programmi presente sull’unità c:. L'operazione di connessione deve ovviamente avvenire prima di un reset o di un rewrite. A partire da essa il programma potrà operare in lettura (reset) o scrittura (rewrite) sul file che sulla memoria di massa ha il nome specificato. In alcuni linguaggi alla connessione si accompagna anche l’indicazione della modalità di accesso. In essi, con procedure di tipo OPEN, si forniscono le indicazioni di connessione per ispezione o per generazione come la tabella illustra. I dati 181 Procedura OPEN(F,”nome_file”,”r”) Modalità Ispezione OPEN(F,”nome_file”,”w”) Generazione Operazioni equivalenti Assign(F, ”nome_file”) Reset(F) Assign(F, ”nome_file”) Rewrite(F) Tabella 13 – Procedura OPEN In alcuni casi si può anche attivare la scrittura in modalità di accodamento (append) per aggiungere alla fine di un file esistente. Così come si stabilisce la connessione con un particolare file, si può in ogni momento sancire la sconnessione tra programma e file. Dopo che è stata stabilita la sconnessione con il file, non è più possibile effettuare operazioni su di esso. Solo una ulteriore riconnessione ne permette l'uso. Per consentire la sconnessione esistono procedure tipo close(F). Se non specificata esplicitamente, la sconnessione avviene in modo automatico all'atto della terminazione del programma. L'uso della connessione e della sconnessione consente di utilizzare le stesse procedure di gestione dei file per file diversi all'interno di uno stesso programma. 5.6.4. I file ad accesso diretto (RANDOM) Esistono numerosi problemi di gestione dei file per i quali l'accesso sequenziale non è adeguato. Si pensi alla gestione di un magazzino dove l'operazione di registrazione degli articoli avviene in genere una sola volta ma la modifica del numero di pezzi e del prezzo, per esempio, sono invece operazioni che avvengono con una elevata frequenza interessando un solo elemento del file. In questi casi serve una modalità di gestione dei file con la quale posizionarsi velocemente sull’elemento in una data posizione per poterlo modificare senza dover seguire le procedure rigide di scorrimento tipiche dell'accesso sequenziale. Oggi, tutti i linguaggi mettono a disposizione del programmatore file ad accesso diretto (o anche casuale o random) nel senso che è possibile operare su un elemento in una posizione specificata senza dover processare tutti quelli che lo precedono, come l'accesso sequenziale impone di fare. È così possibile leggere o scrivere una qualsiasi delle componenti del file, anche se l'aggiunta di componenti nuove deve avvenire sempre in coda al file stesso. In altre parole i file ad accesso diretto possono essere visti come una estensione degli array su memoria di massa: ad ogni elemento del file è associata una posizione mediante la quale operare su di esso con una particolare funzione di accesso. A differenza degli array non si deve definire la dimensione massima. Per la connessione dei file ad accesso diretto non è richiesta l’indicazione della modalità di accesso. Dopo il posizionamento, mediante l’indicazione della posizione dell’elemento su cui operare, si può operare indifferentemente sia in lettura con la procedura read che in scrittura con la procedura write. 5.7. L’astrazione sui dati L'importanza di definire le strutture dati pila, coda e tabella risiede nel fatto che parecchie applicazioni ne possono richiedere l'uso a prescindere dalla loro effettiva disponibilità nei linguaggi di programmazione. Infatti, proprio per il già 182 Capitolo quinto discusso legame tra algoritmo e struttura dati in esso impiegata, è importante poter trattare ad un primo livello di formulazione della soluzione di un problema con l'informazione astratta, definita cioè tramite la struttura e le modalità di funzionamento, rinviando, quindi, la definizione dettagliata del tipo con gli strumenti messi a disposizione dal linguaggio dopo la verifica della correttezza della soluzione stessa. In questo modo si otterranno soluzioni più aderenti alla realtà del problema e quindi più facilmente interpretabili e gestibili nel tempo. In altri termini l'astrazione sui dati è un utile metodo per posporre i dettagli di implementazione in modo da risolvere i problemi di rappresentazione nella maniera più adeguata e quanto più indipendente dall'algoritmo. L'uso di tale metodo, peraltro, è favorita dalla presenza in alcuni linguaggi più moderni di meccanismi appositi di definizione. Invece, nei linguaggi tradizionali, si deve dapprima scegliere tra quelle disponibili la struttura dati che più è simile per composizione e modalità d'uso a quella astratta e quindi renderla equivalente, da un punto di vista funzionale, attraverso i meccanismi del linguaggio, quali le funzioni o i sottoprogrammi. 5.7.1. Il tipo pila La pila viene anche detta stack. E' una struttura che possiamo definire come composta da oggetti tutti dello stesso tipo sui quali è possibile operare in modo che il primo oggetto che dalla struttura si può estrarre è l'ultimo che in essa è stato inserito. Per questa modalità di gestione la pila è anche detta struttura LIFO dalle parole inglesi Last In - First Out (ultimo entrato - primo uscente). Le operazioni definite sulla pila sono: - PUSH: per aggiungere un elemento alla pila, - POP: per togliere alla pila l'elemento che è stato aggiunto per ultimo, - TOP: per osservare l'elemento che è stato aggiunto per ultimo. Inoltre sulla pila è definito il predicato: - EMPTY: che quando assume il valore TRUE indica che la pila è vuota, ossia non ha elementi. Per motivi pratici, legati all'impossibilità di trattare tipi a cardinalità infinita, viene introdotto anche il predicato: - FULL: che con il valore TRUE indica quando la pila si è riempita. Come si può notare, un tale tipo si presta a modellare l'organizzazione di oggetti della vita quotidiana. Infatti, come una pila di piatti, di libri, di fogli esso è caratterizzato dal fatto che il primo oggetto prelevabile è quello che si trova in testa a tutti gli altri ed è proprio quello che è stato posato su di essi per ultimo. Si noti infine che: - POP e TOP sono possibili se la pila non è vuota; - PUSH è possibile se la pila non è piena. 5.7.2. Il tipo coda La coda è una struttura composta da oggetti tutti dello stesso tipo sui quali è possibile operare in modo che il primo oggetto estraibile dalla struttura è il primo che in essa è stato inserito. Per questo modalità di gestione la coda è anche detta struttura FIFO dalle parole inglesi First In - First Out (primo entrato - primo uscente). I dati 183 Le operazioni definite sulla coda sono: - PUSH: per aggiungere un elemento alla coda, - POP: per togliere alla coda l'elemento che è stato aggiunto per primo. Inoltre sulla pila è definito il predicato: - EMPTY: che quando assume il valore TRUE indica che la coda è vuota, ossia non ha elementi. Per motivi pratici, legati all'impossibilità di trattare tipi a cardinalità infinita, viene introdotto anche il predicato: - FULL: che con il valore TRUE indica quando la coda si è riempita. Per comprendere questa organizzazione di oggetti possiamo pensare ad una reale “coda” che si forma davanti ad uno sportello di un ufficio: la persona che è servita per prima è proprio quella che è arrivata prima nella coda e che si trova quindi alla sua testa, e le persone che via via arrivano vanno a disporsi sempre dopo l'ultimo della coda. Si noti infine che: - POP è possibile se la coda non è vuota; - PUSH è possibile se la coda non è piena. 5.7.3. Il tipo tabella La tabella è una struttura costituita da un insieme finito di coppie di valori (a,b), con a e b appartenenti a tipi diversi o anche uguali, tra loro corrispondenti secondo l'applicazione F: b:=F(a) I tipi dei valori possono essere qualsiasi, sia atomici che strutturati. Inoltre il primo elemento della coppia viene detto chiave della tabella per il fatto che è il mezzo per entrare nella struttura e prelevare il secondo elemento della coppia. Le operazioni definite sulla tabella sono: - INSERIMENTO: per aggiungere una coppia alla tabella; - RICERCA: per estrarre, se esiste, l'elemento b corrispondente al valore della chiave a specificata; - CANCELLAZIONE: per eliminare la coppia che ha il primo elemento uguale al valore della chiave specificata. Inoltre sulla tabella sono definiti i predicati: - EMPTY: che quando assume il valore TRUE indica che la tabella è vuota, ossia non ha elementi. - FAIL: il cui valore TRUE indica il fallimento della ricerca sia per operazioni di RICERCA che di CANCELLAZIONE, ossia indica che non esiste una coppia con il valore della chiave specificata. Per motivi pratici, legati all'impossibilità di trattare tipi a cardinalità infinita, viene introdotto anche il predicato: - FULL: che con il valore TRUE indica quando la tabella si è riempita. La tabella è una struttura astratta di grande interesse poiché permette di trattare corrispondenze fra insiemi di qualsiasi natura. Ad esempio, si può pensare all'elenco telefonico che mette in corrispondenza un nominativo (la chiave) con un 184 Capitolo quinto numero telefonico, all'indice di un libro (dove il titolo del paragrafo è la chiave di accesso alla pagina), alla rappresentazione di una funzione per punti, e così via. Si noti infine che: - la RICERCA e la CANCELLAZIONE sono possibili se la tabella non è vuota; - l' INSERIMENTO è possibile se la tabella non è piena. Capitolo sesto Il linguaggio C 6.1. Introduzione Il C è un linguaggio di programmazione molto usato in diversi campi applicativi. Realizzato nel 1972 da Dennis Ritchie, il linguaggio porta un nome che deriva dal suo predecessore, il linguaggio B, progettato da Ken Thomson per implementare il sistema operativo Unix. Ritchie e Thomson, successivamente, riscrissero il sistema Unix interamente in C. Sebbene il sistema operativo Unix, il compilatore C e quasi tutti i programmi applicativi di Unix siano scritti in C, il linguaggio non è legato ad alcun sistema operativo o ad alcuna macchina particolare: viene comunque considerato da molti come un linguaggio di programmazione di sistema per la sua utilità nella scrittura dei sistemi operativi. Il C è un linguaggio di “alto livello” che possiede un ristretto insieme di costrutti di controllo e di parole chiave ed un ricco insieme di operatori; esso permette, inoltre, di programmare in modo modulare, usando funzioni e macro. Nel contempo, il C viene anche considerato un linguaggio relativamente “di basso livello”, in quanto prevede operazioni tipiche di un linguaggio macchina, come l’indirizzamento in memoria. Per scelta di progetto il C non contiene istruzioni di ingresso/uscita (non esistono istruzioni di READ e WRITE dedicate, e neppure istruzioni per l’accesso ai file) né istruzioni particolari per le operazioni matematiche; mancano anche operazioni per trattare direttamente oggetti strutturati come stringhe di caratteri, insiemi, liste ed array, ed infine non esistono operatori con i quali manipolare interi array o intere stringhe. Il C affida l’implementazione di tali tipologie di operazioni a librerie esterne così da avere un linguaggio compatto le cui funzionalità si possono ampliare facilmente sia sviluppando ulteriori librerie che modificando quelle standard. 6.2. Le caratteristiche generali del linguaggio C Le caratteristiche principali del linguaggio C sono: - presenza dei fondamentali costrutti di controllo di flusso: o sequenza di istruzioni organizzate in blocchi; o decisioni o costrutti selettivi (if, switch); o cicli o costrutti iterativi (while, for, do). 186 Capitolo sesto - disponibilità di operazioni su tipi fondamentali (come i caratteri, gli interi, i reali in virgola mobile) e su tipi derivati, costruiti a partire dai tipi fondamentali o da altri tipi derivati, (come l’array, le funzioni, le strutture e le unioni). - gestione dei puntatori con una aritmetica sugli indirizzi. - invocazione ricorsiva di una funzione, con chiamate nidificate. - linguaggio “case sensitive”, nel senso che gli identificatori dipendono dal modo in cui sono scritti in quanto lettere maiuscole e minuscole sono considerate diverse. Il linguaggio C è un linguaggio “strutturato” con precise regole di visibilità dei dati e del codice. Un programma C è composto da un insieme di sottoprogrammi, che possono essere visti come unità disgiunte, senza però la possibilità di innesto (ovvero di dichiarare sottoprogammi all’interno di altri sottoprogrammi in forma annidata). Le unità di programma sono tutte esterne le une alle altre e sono dotate di propri oggetti locali; é possibile tuttavia ricorrere ad oggetti globali, esterni alle unità ma da esse visibili. Ogni unità è costituita da una sequenza di frasi terminate da punto e virgola che può essere suddivisa in una sezione dichiarativa, dove si dichiarano tutti gli oggetti necessari, ed in una sezione esecutiva dove si trovano le istruzioni vere e proprie atte ad implementare l’algoritmo associato all’unità stessa. 6.2.1. Il vocabolario del linguaggio Il vocabolario del linguaggio C è costituito da sequenze di lunghezza finita di caratteri trattate come singole entità logiche (è quindi l'insieme delle parole costruite secondo le regole lessicali). In C ci sono sei classi di simboli: - separatori ed identificatori, - simboli speciali - parole chiave, - costanti, - stringhe. 6.2.2. Separatori ed identificatori Nel linguaggio le diverse entità lessicale sono separate tra loro mediante opportuni separatori. I caratteri “spazio” ed ENTER (fine linea) sono considerati separatori espliciti, mentre gli operatori (aritmetici e di relazione), il punto e virgola e l'operatore dell'assegnazione (=) vengono implicitamente identificati come separatori. Così le frasi che seguono presentano due entità lessicali distinte: alfa = 10 if contatore mentre le frasi seguenti ne presentano soltanto una: ifcontatore whilecondizione Il linguaggio C 187 Gli identificatori permettono di indicare i nomi di programmi, costanti, variabili e funzioni. Un identificatore è una stringa di caratteri che deve soddisfare i seguenti vincoli: - deve essere composta da lettere, cifre e dal carattere ‘_’; - il primo carattere deve essere una lettera; - deve essere costituito da un numero limitato di caratteri; - non deve ovviamente contenere al suo interno lo spazio. Figura 1 – Carta sintattica per identificatori Sono ad esempio identificatori diversi : A a alpha ALPHA Alpha Sol1 Pi_Greco eq2Grado Nella costruzione dell’identificatore si può usare una delle lettere dell’alfabeto inglese ed una delle dieci cifre arabe. Per quanto riguarda le lettere, si possono usare sia maiuscole che minuscole, con significato però diverso (alpha, ALPHA, Alpha ad esempio sono tre identificatori diversi). 6.2.3. I simboli speciali I simboli speciali sono o semplici caratteri o coppie adiacenti di essi ed indicano le operazioni presenti nel linguaggio e tutta la punteggiatura richiesta dalla sintassi. Essi sono: + * / (operatori aritmetici) < > == <= (operatori di confronto) >= , ; . ‘ (delimitatori e punteggiatura) “ ~= : .. … 188 Capitolo sesto ( ) (parentesi) [ ] { } && || ! (operatori logici) // /* (commento) */ Si vuole notare come il C distingua l’operatore di assegnazione ‘=’ da quello di confronto logico ‘==’. 6.2.4. Parole chiavi Alcuni identificatori hanno un ben preciso e congelato significato nell'ambito del linguaggio e pertanto prendono il nome di parole riservate o chiavi. Tali parole sono riservate per usi particolari e non sono ridefinibili in altro modo dal programmatore in quanto sono “le chiavi” che guidano nella costruzione delle frasi del linguaggio. I seguenti identificatori sono esempi di parole chiavi. int char float double struct union long short unsigned auto 6.2.5. extern register typedef static goto return break continue if else for do while switch case default I delimitatori Il linguaggio C usa il carattere punto e virgola (‘;’) come delimitatore esplicito per terminare tutte le frasi ad eccezione di quelle di commento. 6.2.6. Le frasi di commento Come tutti gli altri linguaggi di programmazione anche il C consente di introdurre frasi prive di valore esecutivo o dichiarativo al fine di migliorare la leggibilità e la chiarezza del programma. Esse servono unicamente ad uno scambio di messaggi tra le persone e prendono il nome di frasi di commento. Le frasi di commento iniziano con i caratteri “/*” e terminano con i caratteri “*/”; la presenza di una apertura e di una chiusura del commento permette di distribuire la frase su più righe. Ad esempio sono frasi si commento: /* questo è un possibile Commento del linguaggio C Il linguaggio C 189 */ e /************************************************ Nome programma: radice quadrata Autore: Paolo Rossi Versione: 1.0 Data: 12/19/2008 *************************************************/ È possibile anche introdurre un commento in una unica riga, usando i caratteri “//”, in questo caso è considerato commento tutto quello che inizia con “//” e termina con la fine del rigo (“/n” nella carta sintattica). I commenti non possono essere nidificati. int x_quadro; //definizione della radice quadrata //segue implementazione algoritmo risolutivo /* frase */ // frase /n Figura 2 – Carta sintattica per frasi di commento 6.2.7. Le costanti Le costanti del linguaggio si dividono in costanti numeriche e costanti di tipo carattere. Per ciò che attiene le prime, i numeri trattati dal linguaggio sono di due tipi: interi e reali. Un numero intero è una sequenza di cifre eventualmente preceduta da un segno. Un numero reale ha in aggiunta una parte decimale ed eventualmente un fattore di scala. In particolare una costante reale in virgola mobile si compone di una parte intera, un punto decimale, ed una parte decimale, ad esempio 12.14. È possibile inoltre usare il carattere ‘e’ oppure ‘E’ per l’esponente intero con segno come fattore di scala, come in 12.14 E-10. Sono costanti intere: 10 300 -1000 Sono costanti reali: 3.400 10.20 190 Capitolo sesto +1.99E+30 -30.008 -0.7E-10 9.02E3 Inoltre per le costanti intere, se la sequenza inizia con ‘0’ (cifra zero), la costante è un numero ottale, altrimenti è decimale. Una sequenza di cifre precedute da “0x” o “0X” rappresenta invece un intero esadecimale. Una costante intera che supera il più grande intero rappresentabile dalla macchina, è considerata long. Una costante intera seguita da lettera “l” oppure “L” è una costante long. Ad esempio: 012 rappresenta il numero ottale 12, mentre 012L rappresenta il numero ottale 12 di tipo long. Figura 3 – Carta sintattica per costanti numeriche Una costante di tipo carattere è racchiusa tra singoli apici. Il valore di una costante carattere è il valore numerico del carattere nel set di caratteri della macchina. Alcuni caratteri non grafici sono rappresentati come di seguito: newline \n tab orizzontale \t backspace \b carriage return \r form feed \f backslash \\ apice singolo \’ Figura 4 – Costanti di tipo carattere semplice Il linguaggio C 6.2.8. 191 Le stringhe Le costanti stringa di caratteri sono sequenze di caratteri racchiuse da una coppia di caratteri doppi apici ("), come in ''salve''. Il valore della stringa è dato dalla sequenza di caratteri esclusi i doppi apici che fungono da parentesi. Una costante senza caratteri (una coppia di doppi apici) rappresenta la stringa a lunghezza nulla. Tutte le stringhe, anche quando sono stringhe identiche, sono in realtà diverse. Il compilatore immette un byte nullo,“\0”, alla fine di ogni stringa per individuarne la fine. Sono costanti stringhe di caratteri: ''ALFABETO'' ''ciao'' ''Minuscolo'' Figura 5 – Carta sintattica per stringhe 6.3. Il programma e la gestione dei tipi in C Un programma C è un insieme di uno o più sottoprogrammi o funzioni. Più nel dettaglio la struttura di un programma C prevede: - un insieme di “direttive” di compilazione (inclusione di librerie esterne, definizione costanti, etc…) - la dichiarazione di eventuali variabili globali (visibili a tutte le funzioni del programma), di eventuali alias di tipi definiti dall’utente e dei prototipi delle funzioni - la specifica di un insieme di funzioni f1(), f2(),…,fN() ognuna costituita da una sezione dichiarativa (variabili locali alla funzione) ed una esecutiva (sequenza di istruzioni). L’unica funzione che deve essere sempre presente all’interno di un programma C è chiamata main(), ed è la prima funzione a cui viene ceduto il controllo quando inizia l’esecuzione di un programma. Di solito la funzione main() si occupa del controllo generale delle varie attività del programma attivando nel giusto ordine le funzioni di cui il programma si compone. Di seguito è riportata la struttura generale di un programma C. direttive di compilazione dichiarazione di variabili globali dichiarazione alias di tipi dichiarazione prototipi di funzione f1() { variabili locali sequenza di istruzioni } 192 Capitolo sesto f2() { variabili locali sequenza di istruzioni } . . . fN-1() { variabili locali sequenza di istruzioni } main() { variabili locali sequenza di istruzioni } Un programma C è quindi l’insieme di una o più funzioni, tra cui la main(); presenta un preambolo nel quale si elencano le direttive di compilazione, le dichiarazioni di variabili globali, gli alias di tipi, i prototipi di funzioni. Figura 7 – Carta Sintattica per la rappresentazione di un programma Una funzione C è un blocco di frasi a cui viene assegnato una intestazione che contiene la dichiarazione del tipo della funzione stessa, il nome e la lista dei parametri di ingresso/uscita. Il blocco è poi iniziato da una parentesi graffa aperta ed è chiuso da una parentesi graffa chiusa come mostrato di seguito. <tipo della funzione> nome_funzione (parametri) { blocco } Figura 8 – Carta Sintattica per la rappresentazione di un sottoprogramma o funzione Ad un programma C è infine associato un nome che coincide con il nome del file del codice sorgente (con estensione .c). Il seguente esempio mostra un Il linguaggio C 193 semplice programma C, di nome “salve.c”, che effettua la stampa di una frase a video. #include <stdio.h> int main() { printf("Salve!\n"); return 0; } La funzione printf() scrive a video la sequenza di caratteri, racchiusa tra virgolette, specificata tra le parentesi tonde. Come già visto, una sequenza di caratteri tra virgolette è una stringa. Quella dell'esempio si chiude con i caratteri '\' e 'n', che in coppia hanno, nel linguaggio C, il significato particolare di "vai a capo". La funzione return serve a ritornare un valore (nel caso in oggetto 0) alla fine dell’esecuzione del programma main. Infatti, avendo dichiarato che il valore di ritorno della funzione deve essere di tipo intero, il compilatore si aspetta che la funzione main restituisca un valore intero alla fine dell’esecuzione. Il programma, pur nella sua semplicità, permette di ricavare un certo numero di informazioni molto utili sulla struttura generale di un programma in linguaggio C. Ogni riga, contenente istruzioni o chiamate a funzioni o definizioni di dati, si chiude con un punto e virgola (carattere che segnala al compilatore il termine dell’istruzione). La prima riga del preambolo è una direttiva al compilatore (#include): con essa si chiede l’inserimento del file “stdio.h” all’interno del programma, a partire dalla riga in cui si trova la direttiva stessa. Nel file “stdio.h” ci sono altre direttive e definizioni che servono al compilatore per tradurre correttamente il programma. In particolare “stdio.h” contiene non solo la definizione della funzione printf() utilizzata nel corpo del programma, ma di altre funzioni di input ed output. In generale tutti i programmi C includono alcune direttive di inclusione di file .h, detti include file o header file, il cui contenuto è necessario per un corretto utilizzo delle funzioni di libreria. Il nome dell'include file è, in questo caso, racchiuso tra parentesi angolari (‘<’ e ‘>’): ciò significa che il compilatore deve ricercarlo solo nelle directory specificate nella configurazione del compilatore. Se il nome viene invece racchiuso tra virgolette (ad esempio: "mialibreria.h"), il compilatore lo cerca prima nella directory corrente, e poi in quelle indicate nella configurazione. Da non dimenticare che le direttive del compilatore non sono mai chiuse dal punto e virgola. Tutto quello che si trova tra le due parentesi graffe costituisce invece il corpo della funzione (function body) e definisce le azioni svolte dalla funzione stessa: può comporsi di definizioni di variabili, di istruzioni e di chiamate a funzione. Come già ricordato, tutti i programmi C hanno bisogno di una funzione principale main che fa partire l’esecuzione del programma. Un programma termina quando: - si raggiunge la fine del main; - si effettua una chiamata ad una funzione particolare detta exit(); - il programma è interrotto in qualche modo 194 Capitolo sesto - 6.3.1. il programma va in una condizione di errore detta anche crash. L’intestazione di una funzione L'intestazione di una funzione è costituita dai seguenti elementi: - il tipo della funzione corrispondente al valore da essa restituito alla sua terminazione; se la funzione non restituisce valori al posto della dichiarazione di tipo deve comparire void; - il nome mnemonico ad essa assegnato; - la lista di parametri di ingresso/uscita contenente la specifica (nome e tipo) dei parametri formali mediante i quali la funzione è capace scambiare informazioni con gli altri sottoprogrammi. Come si vedrà in seguito, nella specifica dei parametri formali può essere usato o meno l’operatore *, a seconda che il parametro deve essere scambiato all’atto dell’attivazione della funzione per riferimento o per valore. Sono ad esempio liste di parametri formali i seguenti esempi: int a, int b int a, int b, int *c Nella specifica dei parametri che vanno passati per riferimento bisogna utilizzare l’operatore *. In realtà in C lo scambio dei parametri avviene sempre per copia e nel caso di passaggio per riferimento viene copiato l’indirizzo anziché il valore di una variabile. Di norma i parametri di ingresso di una funzione sono passati per valore, mentre quelli di uscita o di ingresso/uscita per riferimento. Figura 9 – Carta Sintattica per l’intestazione di un sottoprogramma Sono esempi intestazioni di funzioni: int main() void somma (int a, int b, int *c) int somma (int a, int b) Si vuole fare notare come grazie all’esistenza della dichiarazione void (vuoto) in C esiste solo la dichiarazione di funzione e le procedure vengono viste come particolari funzioni che non ritornano alcun valore. 6.3.2. Il blocco di una funzione Il blocco è una entità sintattica che contiene la parte elaborativa di una qualsiasi unità di programma (programma principale, procedure e funzioni). Esso consiste di una serie di frasi del linguaggio costituenti la specifica dell’algoritmo racchiuse tra parentesi graffe. { specfica algoritmo } Figura 10 – Carta Sintattica per il blocco di un programma Il linguaggio C 6.3.3. 195 I tipi semplici Il linguaggio C prevede tre dichiarazioni di tipo elementare per le variabili: uno per le variabili di tipo alfanumerico, uno per le variabili di tipo numerico, ed uno per le gestione delle “variabili void”. Figura 10 – Carta sintattica per i tipi elementari del linguaggio Per ciò che concerne le variabili alfanumeriche il tipo utilizzato dal C è il tipo char, cioè carattere, corrispondente ad 1 byte. Esso può assumere 256 valori diversi, in quanto rappresentato su 8 bit. Si distinguono due tipi di char: il signed char, con l'ottavo bit usato come indicatore di segno, e l’unsigned char, che utilizza invece tutti gli 8 bit per esprimere il valore, e può dunque esclusivamente assumere valori positivi. Per quanto riguarda le variabili numeriche, il tipo di dato utilizzato per la rappresentazione degli interi e corrispondente ad una “word” è il tipo int. Anche il tipo intero può essere signed o unsigned. Il numero di bit costituenti una word varia da macchina a macchina. Nell’ipotesi di una macchina con una word pari a 16 bit, l’intero con segno assume valori che vanno da -32768 a 32767, mentre quelli dell'intero senza segno variano da 0 a 65535. Per evitare la differenza di rappresentazione tra macchina e macchina, lo standard definisce anche un tipo particolare detto short int che occupa 16 bit: spesso short int e int sono equivalenti. Per esprimere valori interi che variano in un range maggiore, si può usare il long int, che occupa 32 bit. Anche il long int può essere signed o unsigned. I tipi usati per rappresentare tipi interi sono anche detti integral types. Per quanto riguarda i tipi reali, il C permette di gestire numeri in virgola mobile in floating point, a precisione singola e doppia mediante l’uso rispettivamente delle parole chiave float e double. Il float occupa 32 bit secondo lo standard IEEE 754, il double occupa 64 bit ed il long double 80 bit. A causa della loro funzione le parole chiave signed, unsigned e long sono anche indicate col nome di modificatori di tipo. La tabella seguente riassume le caratteristiche dei tipi di dato sin qui descritti. 196 Capitolo sesto TIPO character unsigned character short integer unsigned short integer integer unsigned integer long integer unsigned long integer floating point double precision long double precision BIT 8 8 16 16 16 16 32 32 32 64 80 VALORI AMMESSI da -128 a 127 da 0 a 255 da -32768 a 32767 da 0 a 65535 da -32768 a 32767 da 0 a 65535 da -2147483648 a 2147483647 da 0 a 4294967295 da 3.4*10-38 a 3.4*1038 da 1.7*10-308 a 1.7*10308 da 3.4*10-4932 a 1.1*104932 Tabella 1: Valori ammessi per tipi numerici Infine per la gestione di variabili booleane o logiche, pur non essendo previsto nelle prime versioni del linguaggio, è stato introdotto il tipo bool. Le variabili di tipo bool possono assumere i soli due valori logici “vero” e “falso” (true e false). 6.3.4. Dichiarazione di variabili In C è necessario dichiarare una variabile prima di poterla utilizzare. La dichiarazione delle variabili avviene dapprima specificando l’eventuale modificatore di tipo, poi il tipo, e, quindi l’identificatore. Figura 12 – Carta sintattica per la dichiarazione di una variabile Inoltre, in C, le variabili possono essere classificate, oltre che secondo il tipo di dato, in base alla loro accessibilità e alla loro durata. In particolare, a seconda del contesto in cui sono dichiarate, le variabili di un programma C assumono per default determinate caratteristiche di accessibilità e durata; in molti casi, però, queste possono essere modificate mediante l'utilizzo di apposite parole chiave, in gergo note come classi di memorizzazione, applicabili alla dichiarazione delle variabili stesse. In termini generali, possiamo dire che la durata di una variabile si estende dal momento in cui le viene effettivamente assegnata un'area di memoria fino a quello in cui quell’area è riutilizzata per altri scopi. Dal punto di vista dell'accessibilità ha invece rilevanza se sia o no possibile leggere o modificare, da parti del programma diverse da quella in cui la variabile è stata dichiarata, il contenuto dell'area di memoria riservata alla variabile stessa. Il linguaggio C 197 Figura 13 – Carta sintattica per la dichiarazione di una variabile Le classi si memorizzazione delle variabili sono riportate di seguito. - Automatic – Qualsiasi variabile dichiarata all'interno di un blocco di codice appartiene per default a tale classe. Non è dunque necessario, anche se è possibile farlo, utilizzare la parola chiave auto; la durata e la visibilità della variabile sono entrambe limitate al blocco di codice in cui essa è dichiarata. - Register – Dichiarando una variabile con la parola chiave register si forza il compilatore ad allocarla direttamente in un registro della CPU, con notevole incremento di efficienza nell'elaborazione del valore in essa contenuto. - Static – Come nel caso delle variabili automatic, le variabili static sono locali al blocco di codice in cui sono dichiarate ma hanno durata estesa a tutto il tempo di esecuzione del programma. Esse, pertanto, esistono già prima che il blocco in cui sono dichiarate sia eseguito e continuano ad esistere anche dopo il termine dell'esecuzione del medesimo. Ne segue che i valori in esse contenuti sono persistenti; quindi se il blocco di codice viene nuovamente eseguito esse si presentano con il valore posseduto al termine dell'esecuzione precedente. - External – Le variabili external sono variabili dichiarate al di fuori delle funzioni. Esse hanno durata estesa a tutto il tempo di esecuzione del programma, ed in ciò appaiono analoghe alle variabili static, ma differiscono da queste ultime in quanto la loro accessibilità è globale a tutto il codice del programma. In altre parole, è possibile leggere o modificare il contenuto di una variabile external in qualsiasi funzione. Infine il linguaggio C possiede il modificatore d’accesso const utilizzato in fase di assegnazione di una variabile per indicare che il valore della variabile stessa non può essere modificato durante l’esecuzione del programma. Ad esempio: const float versione=3.1; ha l’effetto di mantenere costante a 3.1 il valore della variabile reale “versione”. La visibilità delle variabili, e più in generale di un identificatore opportunamente dichiarato, è governata da regole precise che possono riassumersi dicendo che quanto dichiarato all’interno di un blocco non è usabile al suo esterno, mentre vale che se un oggetto è dichiarato all’esterno di un blocco esso è visibile all’interno di tutti i blocchi che seguono lessicograficamente la dichiarazione. Nella struttura del programma C, allora gli oggetti dichiarati nel preambolo sono visibili a tutte le unità di programma e pertanto sono globali; gli oggetti dichiarati nella sezione dichiarativa di una funzione sono locali ad essa; gli oggetti dichiarati 198 Capitolo sesto all’interno di un blocco sono ad essi locali e visibili alle frasi che seguono la dichiarazione stessa. 6.3.5. Alias di tipi Il C non permette di definire un nuovo tipo all’interno di un programma: tuttavia permette di introdurre un nome (pseudonimo o alias) che corrisponde ad uno dei tipi definiti. typedef nomeTipo nomeNuovoTipo; Il meccanismo degli alias può essere molto utile soprattutto per aumentare la leggibilità di un programma e per evitare espressioni complesse. 6.3.6. Il tipo enumerativo Tra i tipi fondamentali del linguaggio C va infine annoverato il cosiddetto tipo enumerativo. L’enumerazione è un insieme di costanti intere rappresentate da identificatori. Le costanti sono dette anche costanti di enumerazione e sono delle costanti simboliche i cui valori sono impostati automaticamente: iniziano da 0 e sono incrementati di solito di 1. Un esempio di enumerazione è il seguente: enum GIORNI {LUN, MAR, MERC, GIOV, VEN, SAB, DOM } creando un nuovo tipo GIORNI i cui identificatori sono associati con gli interi compresi tra 0 e 6, ovvero: enum GIORNI {LUN=0, MAR=1, MERC=2, GIOV=3, VEN=4, SAB=5, DOM=6 } 6.3.7. I tipi derivati Come noto, in un dato linguaggio, a patire dai tipi fondamentali è possibile costruire nuovi tipi, detti tipi derivati. In C i tipi derivati sono vari: tra essi si prenderanno in considerazione gli array, le strutture, le unioni ed i campi. 6.3.8. Il tipo array La struttura array (o vettore) è composta da un insieme di elementi tutti dello stesso tipo e con un unico nome collettivo. Può avere una o più dimensioni fino ad un massimo prefissato. Ad ogni elemento dell'array è possibile accedere mediante un indice (o un numero di indici uguale alle dimensioni stabilite) che ne individua la posizione all'interno della struttura. Il numero di elementi dell’array è definito all’atto della dichiarazione e resta inalterato durante il corso del programma. Figura 14 – Carte sintattiche per la costruzione di array Il linguaggio C 199 La definizione di un array monodimensionale vet con cinque elementi di tipo intero è il seguente: int vet[5]; Prima si dichiara il tipo (int), poi il nome dell’array (vet) e successivamente il numero massimo degli elementi dell’array. Per accedere ad un singolo elemento di un array, il linguaggio C mette a disposizione una funzione d’accesso consistente nel nome dell’array seguito dalla posizione dell’elemento (o coppia di posizioni) racchiusa tra parentesi quadre. La posizione è anche detta indice di accesso. Nel caso dell’array vet composto da 5 elementi l’indice può assumere i valori 0,1,2,3,4. Ad esempio le istruzioni: vet[1]=70; vet[3]=1; assegnano al secondo elemento dell’array vet il valore 70 e al quarto elemento dello stesso array il valore 1. Figura 15 – Carta sintattica per l’accesso agli elementi di un array Un esempio di definizione di array con più dimensioni (matrice) è il seguente: int mat[10][15]; float matReal[15][15]; L’accesso agli elementi di un array a due dimensioni è simile a quello di un array ad una sola dimensione, bisogna però in tal caso specificare due indici: l’indice di riga e quello di colonna. Ad esempio l’istruzione: mat[1][3]=3; assegna il valore 3 all’elemento della matrice che si torva in corrispondenza della prima riga e della terza colonna. 6.3.9. Il tipo struct Il linguaggio C mette a disposizione il tipo struct per la definizione dei tipi record. La struttura record è composta da un numero prefissato di componenti, anche di tipo differente. Ogni componente, detto anche campo del record, ha un suo nome e tipo. La definizione di un record prevede, allora, l'elencazione delle variabili, sia semplici che a loro volta strutturate, costituenti i campi componenti. Le operazioni consentite sul record sono quelle che si possono fare sui campi. 200 Capitolo sesto Figura 16 – Carta sintattica per il tipo struct Nell’esempio che segue si definiscono il tipo DATA e la variabile data_mese di tipo record. struct DATA int giorno; int mede; int anno; ; struct DATA data_mese; In maniera più sintetica si può anche utilizzare la seguente definizione. struct DATA int giorno; int mede; int anno; data_mese; L’accesso ai valori dei singoli campi del record avviene indicando nome del record e nome del campo separati da un punto: nome_struct.nomecampo ovvero se si vuole ad esempio accedere al giorno della data: data_mese.giorno 6.3.10. Il tipo unione Il concetto di unione deriva direttamente da quello di struttura, ma con una importante differenza: i campi di una union, a differenza di quelli di una struct, hanno lo stesso indirizzo di memoria e vanno ad occupare, quindi, tutte le medesime locazioni di memoria. Questo implica che l’occupazione di memoria di una union coincide con quella del campo dell’unione di dimensione maggiore. Ad esempio: union ghost int a; long b; char c; ; union ghost x; ha una dimensione che coincide con quella della variabile b (di tipo long, in alcune implementazioni – come visto – pari a 4 byte). Le unioni dunque sono da preferire alle struct variabili quando servono variabili che possono assumere diverse dimensioni a seconda delle circostanze. 6.3.11. I campi I campi sono una particolare struttura C che consente di far riferimento simbolicamente ai singoli bit di una variabile. Nell’ esempio seguente: Il linguaggio C 201 struct unsigned a:1; insigned b:7; variabileBit; la variabile variabileBit occupa uno spazio in memoria pari alla somma dei bit utilizzati da ogni campo della struttura. Così su una word di 16 bit, il campo a occupa 1 bit; b occupa 7 bit; i restanti 8 sono non utilizzati. 6.3.12. Stringhe di caratteri Il C non prevede la presenza di un tipo stringa predefinito. Tuttavia, è possibile utilizzare un array di caratteri con alcune convenzioni. L’array: char stringa[10]; dichiara un array di 10 caratteri, mentre la dichiarazione: char sentenza[ ]= “Salve mondo”; dichiara un array di caratteri in cui numero di elementi è dato dalla quantità di caratteri presenti nella stringa, più uno, un carattere speciale che indica la fine della stringa, detto carattere null (“\0”): S a l v e M o n D o \0 Il carattere “\0” è il primo carattere del codice ASCII (NUL) e corrisponde al codice “00000000”. Le operazioni classiche sulle stringhe, quali la copia, la concatenazione ed il confronto tra stringhe, sono implementate da opportune funzioni standard di libreria “string.h” e verranno mostrate in seguito. 6.3.13. I Puntatori La comprensione e l’uso corretto dei puntatori sono fondamentali per la creazione di programmi C. Con essi si possono modificare gli argomenti delle funzioni quando vengono chiamate ed inoltre consentono l’allocazione dinamica della memoria. Un puntatore è una variabile che contiene un indirizzo di memoria: tale indirizzo rappresenta la posizione in memoria di un’altra variabile. Quando una variabile contiene l’indirizzo di un’altra, la prima è detta puntare alla seconda. Poiché una variabile possa essere usata come puntatore, bisogna che sia prima dichiarata come tale, attraverso la dichiarazione: tipo *var; dove tipo è un tipo base del linguaggio e var è la variabile puntatore. 202 6.4. Capitolo sesto Gli operatori del linguaggio Di seguito viene riportata una carrellata degli operatori maggiormente diffusi nel linguaggio C. 6.4.1. Operatori Aritmetici Gli operatori aritmetici sono i classici operatori binari +,- ,*,/ e l’operatore di modulo %. La divisione tra interi tronca la parte frazionaria, mentre l’espressione x%y fornisce il resto della divisione di x per y. L’operatore di modulo può essere applicato solo a tipi integrali. Gli operatori + e – hanno la stessa priorità, priorità inferiore a * e / (che invece hanno identica priorità). 6.4.2. Operatori Relazionali Gli operatori relazionali sono: > >= < <= mentre gli operatori di eguaglianza (e disuguaglianza) sono: == != Gli operatori di relazione hanno priorità maggiore rispetto a quelli di eguaglianza /disuguaglianza. Gli operatori relazionali hanno invece priorità minore rispetto agli operatori aritmetici. 6.4.3. Operatori Logici Gli operatori logici sono: && (and), || (or) e ! (not) && ha priorità maggiore di || ed entrambi hanno priorità inferiore agli operatori relazionali e di uguaglianza. Si noti che non esistendo un tipo logico predefinito, l’operatore ! converte un operando non zero in un operando zero. 6.4.4. Operatori di incremento e decremento Il C fornisce due operatori inusuali nei linguaggi ad alto livello, utili per incrementare e per decrementare le variabili, ovvero l’operatore ++ e l’operatore -, con notazione “prefissa” o “postfissa”, come in esempio: x++ ++x x- - -x Sia nella notazione prefissa che postfissa si ha come effetto quello di incrementare (o decrementare) una variabile. Tuttavia ++x (- -x) incrementa (decrementa) x prima di usarne il valore, mentre x++ (x- -) incrementa (decrementa ) x dopo averne usato il valore. Il linguaggio C 6.4.5. 203 Operatori sui puntatori Esistono due operatori per la manipolazione dei puntatori: & e *. Il primo è un operatore unario che restituisce l’indirizzo di memoria dell’operando cui è applicato. Ad esempio: indirizzo_x=&x; pone nella variabile indirizzo_x l’indirizzo di memoria della variabile x. Il secondo è un operatore unario che restituisce il valore della variabile che si trova all’indirizzo indicato nella variabile che lo segue. Ad esempio: valore_x=*indirizzo_x; ha l’effetto di porre nella variabile valore_x il valore della variabile che si trova in memoria all’indirizzo contenuto nella variabile indirizzo_x, ovvero il valore di x. Il seguente esempio cerca di chiarire i concetti prima esposti. int x; int* indx=&x; int valx=*indx; printf("Benvenuti al corso di Fondamenti di Informatica\n"); printf("Esempio di programma sui puntatori\n"); printf("Inserisci il numero x: "); scanf("%d",&x); printf("\nIl valore di x e': %d\n",x); printf("\nL'indirizzo di memoria (in esadecimale) a cui si trova x e': %x\n",indx); printf("\nIl valore di x mediante puntatore e': %d\n",x); L’uso dei puntatori risulta essenziale nello scambio di parametri all’atto dell’attivazione di funzioni. Se ad esempio si vuole realizzare una funzione che effettua la somma e la differenza tra due numeri, allora i parametri, che alla fine dell’elaborazione dovranno contenere somma e differenza, dovranno essere passati per riferimento attraverso l’utilizzo dell’operatore &, mentre nell’intestazione e corpo della funzione, essi dovranno essere gestiti attraverso l’operatore * per accedere al valore puntato. In particolare si ha per la funzione suddetta: void sommadiff(int a, int b, int *d, int *s) { *d=a-b; *s=a+b; } mentre l’attivazione della funzione avverrà come segue: int a,b,d,s; sommadiff (a,b,&d,&s); L’attivazione della funzione ha come effetto quello di passare una copia dei valori delle variabili a ed b alla funzione (passaggio per valore) e quello di passare 204 Capitolo sesto per le variabili d e s una copia dell’indirizzo di memoria (passaggio per riferimento) in cui esse si trovano per potere accedere al loro contenuto mediante l’operatore * all’interno della funzione. Si vuole notare che il tipo array viene sempre passato per riferimento, e quindi all’atto dell’attivazione di una funzione non viene utilizzato l’operatore & per la variabile ad esso relativo. Inoltre si ha che la sequenza: char str[80], *p1; p1=str; ha l’effetto di copiare nel puntatore p1 l’indirizzo del primo elemento dell’array str. Ciò comporta che gli elementi di un array possono essere acceduti (in maniera sequenziale ) anche mediante l’utilizzo dei puntatori. Ad esempio: str[4] e *(p1+4) sono equivalenti in quanto ogni elemento del vettore occupa un solo byte di memoria. 6.4.6. Operatori logici bit oriented In C sono presenti operatori che consentono la manipolazione dei bit. Essi sono elencati di seguito. & | ^ << >> - 6.5. AND bit a bit OR bit a bit XOR shift a sinistra shift a destra complemento ad uno La specifica dell’algoritmo in C La specificazione delle azioni elaborative di un sottoprogramma in C si traduce in un insieme di enunciati o statement composti. Lo statement composto è formato da una sequenza di statement semplici terminati dal carattere punto e virgola, mentre gli statement semplici che compongono la specificazione dell'algoritmo denotano le azioni elaborative vere e proprie che devono essere svolte dall'esecutore macchina. Tali statement si possono classificare in: - istruzioni di assegnazione - richiamo di funzioni - costrutti selettivi ( if … else, switch … case) - costrutti iterativi (while, for) 6.5.1. Istruzioni di assegnazione L'enunciato o istruzione di assegnazione è la più elementare forma di enunciato. Esso prescrive che venga dapprima calcolato il valore dell'espressione che si trova a destra del segno di ‘=’, e poi che tale valore venga poi assegnato alla variabile Il linguaggio C 205 che si trova a sinistra dello stesso simbolo. La variabile e il valore dell'espressione devono essere di norma dello stesso tipo con alcune eccezioni relativamente alle variabili numeriche (e.g., se la variabile è reale, allora il tipo dell'espressione può anche essere intero). L’istruzione di assegnazione termina con il delimitatore punto e virgola. Figura 16 – Carta sintattica per un’istruzione di assegnazione Un’espressione, di cui per semplicità non è riportata alcuna carta sintattica, è una formula o regola di calcolo che specifica sempre un valore detto risultato dell'espressione. È composta da operandi ed operatori. Gli operandi possono essere variabili, costanti o valori restituiti da una funzione. Gli operatori coincidono con quelli del linguaggio e sono classificati in monadici o unari e in diadici o binari a seconda che prevedano uno o due operandi rispettivamente. Se in una espressione sono presenti più operandi, occorre ricordare la sequenza della loro esecuzione nel caso in cui non sia esplicitata mediante l'introduzione nell'espressione delle parentesi. Esempi di una istruzione di assegnazione sono: x = 1; x = x+1; x = a+b; In particolare, nel primo caso si assegna una costante numerica alla variabile x, nel secondo caso si incrementa di 1 la variabile x, nel terzo caso ad x si assegna la somma del valore di a con il valore di b. Si noti che in C l’istruzione del tipo: Espressione1 = Espressione1 operando Espressione2 può essere anche espressa con: Espressione1 operando= Espressione2 Ad esempio, l’espressione x = x+1 diventa: x + = 1; mentre l’espressione y *= y+1 è in realtà la forma compatta di: y = y * (y+1); Una espressione particolare del C è la cosiddetta espressione condizionale, del tipo: espressione1 ?espressione2 :espressione3 In questo tipo di espressione viene valutata l’espressione1: se essa è diversa da zero (vera) allora viene valutata l’espressione2, altrimenti viene valutata l’espressione3. Ad esempio l’espressione: z=(x>y)? x:y; mette in z il valore maggiore tra x ed y. 6.5.2. Richiamo di funzioni Il richiamo della funzione determina l'esecuzione della funzione indicata. Affinché il richiamo sia corretto, si deve fornire una lista di parametri di I/O uguale, in numero e tipo, a quella specificata nella dichiarazione della funzione. La corrispondenza si stabilisce per ordine, nel senso che al primo parametro attuale è 206 Capitolo sesto associato il primo formale, e così via. Per i sottoprogrammi di tipo funzioni bisogna inoltre specificare la variabile in cui verrà memorizzato il parametro d’uscita. Figura 18 – Carta sintattica per richiamo di funzioni La forma di richiamo per una funzione è la seguente: variabile=nome_fnzione(par_effettivo1,par_effettivo2,…., par_effettivon); mente per una procedura: nome_procedura(par_effettivo1,par_effettivo2,…., par_effettivon); I parametri attuali devono essere forniti con rispetto del meccanismo di sostituzione e del tipo dei parametri indicati nella dichiarazione della funzione o della procedura. In particolare, se si è fissato una sostituzione per valore, si possono usare come parametri attuali variabili, costanti ed espressioni di un tipo compatibile, secondo le regole viste a proposito dell'assegnazione di valore, con quello del parametro formale (si può ad esempio usare un parametro attuale di tipo sia intero che reale nel caso di un parametro formale di tipo reale, mentre deve essere assolutamente di tipo intero nel caso di parametro formale dichiarato intero). Nel caso di sostituzione per riferimento, si devono impiegare solo variabili e i tipi dei due parametri devono coincidere. Esempi di istruzioni di richiamo di funzioni sono le seguenti: x=somma(a,b); somma(a,b,&c); 6.5.3. Costrutti selettivi Gli enunciati selettivi permettono la selezione di azioni elaborative in funzione del valore di un'espressione booleana e non: ossia si valuta l'espressione e si sceglie l'istruzione successiva in funzione del valore trovato. If-else L'enunciato if-else (se - altrimenti) permette di effettuare la scelta tra due alternative in funzione del valore di una espressione booleana. Se il valore dell'espressione è TRUE, si sceglie l'alternativa (insiemi di enunciati racchiusi tra parentesi graffe) introdotta dopo l’if, in caso contrario (insiemi di enunciati Il linguaggio C 207 racchiusi tra parentesi graffe) quella aperta dall'else. In entrambi i casi, dopo l'esecuzione del blocco selezionato, l'esecuzione stessa continua con l'enunciato successivo all'if. È anche possibile usare una notazione abbreviata nel caso in cui non esista una delle due alternative, non specificando l'else dell'enunciato. Sono esempi di if: if (n>10) { n=n+1; } else { n=n-1; } if (n>10) n=0; Si noti che, nel caso che l’azione elaborativa dell’if o dell’else sia composta da un solo statement semplice, le parentesi graffe non sono obbligatorie. Inoltre si può notare come l'uso appropriato dell'incolonnamento (o anche indentazione) delle strutture innestate l'una dentro l'altra migliori notevolmente la chiarezza del programma in quanto evidenzia l'ordine di esecuzione dei vari blocchi. È allora da usare tale accorgimento anche quando la disposizione delle istruzioni sui righi non è importante, come nel nostro caso. if espressione booleana ( ) statement { statement composto else } statement { statement composto } Figura 19 – Carta sintattica per if-else Il linguaggio C permette anche di realizzare if annidati, ovvero costrutti di selezione che appartengono al ramo if o al ramo else di un’ulteriore istruzione di selezione. Nel caso di costrutti innestati il C dispone di una regola che assegna ogni else all’if più vicino che non ne possiede già uno. Un esempio è il seguente, dove l’else è relativo al secondo if: 208 Capitolo sesto if (x>0) { if (y>0) printf (“x e y maggiori di 0”); else printf(“solo x maggiore di 0”); } Switch-case L’istruzione switch permette di scegliere tra più di due alternative (decisioni multiple) verificando se il valore di una espressione è uguale ad un valore tra quelli specificati in una lista. In particolare consiste di un'espressione (di tipo numerico o carattere), detta selettore, e di una lista di enunciati, ciascuno dei quali identificato da uno o più valori costanti appartenenti al tipo del selettore. L'enunciato scelto è quello identificato dalla costante che è uguale al valore calcolato del selettore. Solitamente la scansione degli identificatori, per cercare quello con valore uguale al selettore, avviene in modo sequenziale. Per tale motivo si consiglia di disporre per ultimi gli identificatori che hanno la minore probabilità di essere scelti. Un esempio di switch è il seguente: switch(x) case 0: case 1: n++; break; case 2: n- -; break; default: n *=2; Si noti che l’istruzione break causa l’uscita dallo switch. Il caso chiamato default viene eseguito quando non sono stati soddisfatti gli altri casi dello switch. Un altro esempio è il seguente: SWITCH(numero_mese) { CASE {1,3,5,7,8,10,12} printf(“mese di 31 giorni”); break; CASE {4,6,9,11} printf(“mese di 30 giorni”); break; CASE 2 printf(“mese di 28 o 29 giorni”); break; DEFAULT: printf(“mese non valido”); } In questo caso viene verificata l’appartenenza del valore della variabile dello switch ad un tipo enumerativo. Si noti che la non introduzione del break porta a confrontare il valore del selettore con tutti i casi anche quando è uguale ad uno di essi. Il linguaggio C 209 Figura 20 – Carta sintattica per costrutto SWITCH 6.5.4. Costrutti Iterativi Gli enunciati iterativi permettono l'esecuzione di un blocco di istruzioni un certo numero di volte. La terminazione della ripetizione avviene quando sono verificate certe condizioni che sono calcolate internamente al blocco. Se il numero di ripetizioni è noto a priori, la struttura viene anche detta ciclica o enumerativa. Il ciclo while L'enunciato while è composto da una espressione logica e da uno o più enunciati da ripetere in funzione del valore di tale espressione. L'esecuzione del while comporta la valutazione dell'espressione e l'esecuzione degli enunciati nel caso in cui il valore calcolato dell'espressione sia TRUE. Il ciclo ha termine quando l'espressione assume il valore FALSE per cui, se l'espressione risulta subito falsa, gli enunciati non vengono mai eseguiti. Un esempio di ciclo while è: while(x > 0) a = a+x; x- -; Si osservi che una volta iniziato, il ciclo può terminare solo se all'interno di esso vi sono degli enunciati che modificano il valore di verità dell'espressione: cioè operano sulle variabili che ne fanno parte. È comunque possibile uscire da un ciclo 210 Capitolo sesto while in maniera incondizionata, cioè, in maniera indipendente dal valore di verità dell’espressione, attraverso la parola chiave break. while ( espressione booleana statement composto { ) } statement Figura 21 – Carta sintattica per costrutto while Il ciclo do-while A differenza dei ciclo while, che verifica la condizione all’inizio del ciclo (loop) stesso, il costrutto do-while la verifica alla fine, con la conseguenza che esso viene eseguito almeno una volta. In particolare, l’istruzione viene eseguita, poi viene valutata l’espressione: se è vera, l’istruzione viene ancora eseguita e così via. Il ciclo termina quando l’istruzione diventa falsa. do statement composto { } statement while ( espressione booleana ) Figura 22 - Carta sintattica del costrutto do-while Ad esempio, il seguente programma legge i numeri da tastiera finché si introduce un numero con valore minore di zero. do scanf(“%d”,&n); while (n>0) Il linguaggio C 211 Il ciclo for Il costrutto for è un enunciato iterativo enumerativo o ciclico. Esso deve essere usato ogni qualvolta il numero di ripetizioni è noto a priori. Figura 23- Carta sintattica per il costrutto for Lo statement di inzializzazione coincide, nella sua forma più semplice, con un’istruzione di assegnazione con la quale viene fornito il valore iniziale alla variabile di controllo del ciclo. Lo statement di condizione rappresenta un’espressione booleana che serve a controllare la terminazione del ciclo, finché è TRUE il ciclo prosegue. Lo statement di aggiornamento definisce il modo in cui la variabile di controllo cambia il suo valore ad ogni ripetizione del ciclo. Di norma l’aggiornamento consiste in un incremento o decremento del valore della variabile di controllo. Esempi di for sono: for(int i=0; i<=100; i++) printf(“%d “,i); for(int i=100; i>=0; i- -) printf(“%d “,i); somma = somma+i; Nel primo caso vengono visualizzati a video tutti gli interi compresi tra 0 e 100 (incremento), nel secondo caso gli stessi interi vengono visualizzati ma in ordine inverso tra 100 e 0 (decremento) e ne viene effettuata la somma. In altri termini, per il primo for, viene posta inizialmente la variabile di controllo i al valore 0 e viene richiamata la funzione printf() per la visualizzazione a video di i, dopodichè, al ritorno dalla funzione, viene applicata la condizione di aggiornamento (in questo caso di incremento) della variabile i e si verifica la condizione (i<=100). Poiché la condizione è TRUE si ripete l’istruzione di visualizzazione sul nuovo valore della variabile di controllo. Analogamente a quanto già descritto, il ciclo si ripete finchè i non diventa maggiore di 100. Un 212 Capitolo sesto discorso simile, ma con aggiornamento a decremento, vale per il secondo for. L’istruzione: for( ; ;) implementa di contro un ciclo infinito. Per una piena comprensione dei due costrutti iterativi for e while, si presentano di seguito due costrutti equivalenti. for(int i=0; i<=100; i++) printf(“%d “,i); int i=0; while (i<=100) { printf(“%d “,i); i++; } 6.6. Le librerie di funzioni Come già anticipato il C, per scelta di progetto, non supporta direttamente istruzioni di ingresso/uscita, né istruzioni particolari per le operazioni matematiche; non esistono nemmeno operazioni per trattare direttamente oggetti strutturati come stringhe di caratteri, insiemi, liste ed array. Il C affida tali tipologie di operazioni a librerie esterne di funzioni. È però possibile servirsi delle funzioni di una particolare libreria includendone il corrispettivo file header all’interno del programma, come già visto, mediante la direttiva di compilazione: #include <file header della libreria> Dopo l’inclusione è possibile servirsi di tutte le funzioni della libreria nota solo la loro interfaccia, ovvero noti i parametri di ingresso/uscita. Ad esempio, se il seguente programma vuole utilizzare la funzione “getch()” della libreria “conio.h” per attendere la digitazione di un carattere da tastiera, allora il suo codice dovrà avere la seguente forma: #include <conio.h> int main () { getch(); return 0; } Un moderno compilatore C mette a disposizione una vasta gamma di librerie contenenti: - funzioni di uso generale (“stdlib.h”), - gestione dell’I/O (“stdio.h”), - calcolo matematico (“math.h”), - gestione di stringhe (“string.h”). Il linguaggio C 213 Di seguito si mostreranno le funzioni principali delle librerie più comunente utilizzate, rimandando ad appositi manuali la descrizione approfondita delle varie librerie del linguaggio. 6.6.1. La gestione dell’I/O Le funzioni comprese nel sistema di input/output del C possono essere raggruppate in tre grandi categorie: I/O su console (tastiera e monitor), I/O su file bufferizzato e I/O su file non bufferizzato (Unix-like). Per l’utilizzo di tutte le funzioni suddette è richiesta l’inclusione del file header “stdio.h”. Tale libreria fornisce alcune funzioni per le operazioni di lettura e scrittura dei valori delle variabili dai file standard INPUT e OUTPUT. In genere il file di INPUT rappresenta la tastiera del sistema di calcolo; quello di OUTPUT il monitor. Essi possono essere visti come organizzati in un testo costituito da sequenze di RIGHI separati tra loro dal carattere di fine rigo CR (Carriage Return). La sequenza di righi è terminata dal carattere di fine file. All'interno di ogni rigo sono distribuite le rappresentazioni delle informazioni opportunamente separate usando il carattere spazio. Apertura file In C la lettura da un file avviene aprendo un canale di comunicazione con la memoria di massa attraverso la connessione del file. In altre parole un file deve essere sempre connesso o aperto prima di prelevare dati da esso. L’apertura di un file avviene attraverso la funzione fopen() della libreria “stdio.h”: FILE* fopen (char* filename, char* permission) Tale funzione apre un file il cui nome (path su disco del file) è un insieme di caratteri “puntato” dalla variabile filename, mentre la variabile permission definisce la modalità di apertura del file (e.g., sola lettura, solo scrittura, lettura/scrittura). La funzione suddetta restituisce poi un identificatore del file noto anche con nome di puntatore a file, attraverso il quale il file viene referenziato. Se per vari motivi il file non può essere aperto viene restituito un puntatore nullo. Di seguito è riportato un esempio dell’apertura in sola lettura di un file pippo.txt contenente un array di numeri interi (i numeri si trovano su righi differenti di testo separati tra loro dal carattere di fine rigo). La modalità di apertura a solo lettura utilizza la stringa “r”, quella a sola scrittura l’opzione “w”, infine, quella in scrittura/lettura “rw”. FILE *fid; fid=fopen (“c:\pippo,txt”, “r”); if (!fid) printf(“FILE NON APERTO”); Di seguito è riportato un esempio di apertura di un file in scrittura: FILE *fid; fid=fopen (“c:\pippo,txt”, “w”); if (!fid) printf(“FILE NON APERTO”); 214 Capitolo sesto Lettura da file Dopo la connessione del file in modalità lettura, attraverso l’identificatore del file è possibile eseguire le operazioni di lettura che avvengono con l’utilizzo della funzione fscanf() della libreria “stdio.h”: int fscanf(FILE*fid, char*format, var) dove fid è l’identificativo (puntatore) del file, format indica il tipo degli elementi da leggere e var è la variabile in cui si inseriscono gli elementi letti. La funzione ritorna il numero di elementi effettivamente letti, se viene restituito EOF significa che è stata tentata la lettura dopo la fine del file. La variabile format può assumere i seguenti valori a seconda del tipo di dato che si vuole leggere: %d %f,%g %c %s interi reali caratteri stringhe Di seguito è riportato un esempio di lettura del file dove è memorizzato un array di interi (sul primo rigo del file di testo c’è il riempimento del vettore): FILE * fp; register int i; int vettore[100]; int riemp, ret; if (!(fp=fopen("pippo.txt","r"))) { printf("\n Il file non puo' essere caricato\n"); } else { ret=fscanf(fp,"%d",&riemp); if (ret!=EOF) { printf("\nNumero di elementi nel vettore: %d\n",riemp); if (riemp>0) { for (i=0; i<riemp; i++) fscanf(fp,"%d",vettore[i]); } } else printf("\nfile vuoto!!!\n"); } La lettura di informazioni da standard input (tastiere) avviene invece, come già descritto, attraverso la funzione scanf() (senza bisogno di aprire alcun file) della libreria “stdio.h”: int scanf(char*format, var) Il linguaggio C 215 dove var rappresenta la variabile in cui verrà memorizzato il valore immesso da tastiera, mentre format ha lo stesso significato per la funzione fscanf. Di seguito e riportato l’esempio della lettura di un intero da tastiera: int a; scanf(“%d”, &a); Scrittura su file Dopo la connessione del file in modalità scrttura, attraverso l’identificatore del file è possibile eseguire le operazioni di lettura che avvengono con l’utilizzo della funzione fprintf() della libreria “stdio.h”: int fscanf(FILE*fid, char*format, var) dove fid è l’identificativo del file, format indica il tipo degli elementi da scrivere e var è la variabile in cui si trovano gli elementi da scrivere su file. La funzione ritorna il numero di elementi effettivamente scritti, se viene restituito un valore negativo significa che si sono verificati problemi di scrittura su file. La variabile format può assumere gli stessi valori illustrati per la lettura dei file. Di seguito è riportato un esempio di scrittura su file di un array di interi (sul primo rigo del file di testo viene inserito il riempimento del vettore): register int i; FILE*fp; int count; int tot=0; int riemp=3; int vettore[]= {1,2,3}; if (!(fp=fopen("pippo.txt","w"))) { printf("\n Il file non puo' essere salvato\n"); } else { count=fprintf(fp,"%d\n",riemp); tot=tot+count; for (i=0; i<riemp; i++) { count=fprintf(fp,"%d\n",vettore[i]); tot=tot+count; } } if (tot==riemp+1) printf(“Scrittura avvenuta con successo”); La scrittura di informazioni su standard output (monitor) avviene invece, come già descritto, attraverso la funzione pritnf() della libreria “stdio.h”: int printf(char*format, var) dove var rappresenta la variabile il cui contenuto sarà visualizzato a video, mentre format ha lo stesso significato dell’omonima variabile della funzione printf. Di seguito e riportato l’esempio di scrittura di un intero su monitor: int a; printf(“%d”, a); 216 Capitolo sesto La gestione delle stringhe Il C possiede un ampio insieme di funzioni per il trattamento delle stringhe e dei caratteri. Si ricorda che una stringa coincide con un array di caratteri terminato col carattere ‘\0’. Tutte le dichiarazioni richieste dalle funzioni per il trattamento delle stringhe sono contenute nel file header “string.h”. Di seguito si mostrerà l’utilizzo solo di alcune delle funzioni per la gestione delle stringhe, rimandando ad appositi manuali una trattazione più approfondita. La funzione strcat() permette il concatenamento di due stringhe: char *strcat(char *str1, char *str2) Tale funzione concatena una copia della stringa str2 alla stringa str1, concludendo la stringa str1 (che conterrà la stringa risultante) con un carattere di terminazione. Il primo carattere di str2 si sovrappone al terminatore originale di str1, mentre la stringa str2 rimane inalterata. La funzione restituisce un puntatore nullo. Di seguito è riportato un esempio per la scrittura a video della concatenazione delle stringhe “ciao…” e “come stai?”. char s1[]={‘c’,’i’,’a’,’o’,’.’,’.’,’.’, ‘\0’}; char s2[]={‘c’,’o’,’m’,’e’,’ ’,’s’,’t’,’a’,’i’,’?’, ‘\0’}; strcat(s1,s2); printf(“%s”,s1); La funzione strcpy() permette la copia di stringhe: char *strcpy (char *str1, char *str2) Tale funzione copia il contenuto della stringa str2 nella stringa str1, restituendo un puntatore a str1. Di seguito è riportato un esempio per la scrittura a video della copia di una stringa. char str[100]; strcpy(str,”ciao”); printf(“%s”,str); La funzione strcmp() permette di verificare l’uguaglianza tra stringhe: int strcmp(char *str1, char *str2) Tale funzione confronta secondo le regole lessicografiche due stringhe str1 e str2 terminate da carattere nullo e restituisce un intero il cui valore è determinato sulla base delle seguenti regole: - 0 le stringhe sono uguali; - minore di 0 le stringhe sono diverse ed in più str1 ha meno caratteri di str2; - maggiore di 0 le stringhe sono diverse e str1 ha più caratteri di str2. Il linguaggio C 217 Di seguito è riportato un esempio per la verifica se due stringhe sono uguali. char s1[100]; char s2[100]; scanf(“%s”,s1); scanf(“%s”,s2); if (strcmp(s1,s2)==0) printf(“stringhe uguali”); La funzione strlen() permette di determinare la lunghezze di una stringa: int strlen(char *str1) Tale funzione conta il numero di caratteri da cui è composta una stringa str1 che termina col carattere di terminazione, escludendo tale carattere dal conteggio. Di seguito è riportato un esempio di conteggio di caratteri di una stringa. char s1[100]; scanf(“%s”,s1); int len=strlen(s1); printf(“stringa lunga %d caratteri”,len); Infine, le due funzioni: char*strlwr(char *str1) char*strupr(char *str1) convertono rispettivamente una stringa str1 in minuscolo e maiuscolo, restituendo entrambe un puntatore alla nuova stringa. Funzioni per il calcolo matematico Il C possiede un’ampia gamma di funzioni matematiche presenti all’interno della libreria “math.h”. Tali funzioni hanno come argomenti di ingresso variabili di tipo double e restituiscono come argomenti di uscita ancora variabili di tipo double. Le funzioni si possono dividere nelle seguenti categorie: - funzioni trigonometriche o double sin(double x) per il calcolo del seno di un numero reale x o double cos(double x) per il calcolo del coseno di un numero reale x o double tan(double x) per il calcolo del tangente di un numero reale x - funzioni iperboliche o double sinh(double x) per il calcolo del seno iperbolico di un numero reale x o double cosh(double x) per il calcolo del coseno iperbolico di un numero reale x o double tanh(double x) per il calcolo del tangente iperbolica di un numero reale x - funzioni esponenziali e logaritmiche 218 Capitolo sesto double exp(double x) per il calcolo dell’esponenziale di un numero reale x o double log10(double x) per il calcolo del logaritmo in base 10 di un numero reale x o double log2(double x) per il calcolo del logaritmo naturale di un numero reale x altre funzioni o double sqrt (double x) per il calcolo della radice quadrata di un numero reale x o double fabs(double x) per il calcolo del valore assoluto di un numero reale x o double ceil (double x) per il calcolo dell’intero più piccolo non inferiore ad un numero reale x o double floor(double x) per il calcolo dell’intero più grande non superiore ad un numero reale x o double fmod(double x, double y) per il calcolo del resto in modulo della divsione di x per y o double pow(double base, double exp) per il calcolo di baseexp o - 6.7. Gli algoritmi di base in C Si vogliono ora di seguito mostrare degli algoritmi base dell’informatica realizzati con l’ausilio del linguaggio C. 6.7.1. Lo scambio di valore Descrizione del problema : Scrivere una funzione che effettui lo scambio di valore tra due informazioni di un tipo T. Esempio: input: x=3, y=5 output: x=5, y=3 Descrizione dell’algoritmo : Se si associano alle due informazioni due contenitori di liquidi, il problema diventa quello del loro trasferimento da un contenitore all’altro senza che i liquidi si mischiano. Risulta allora evidente che non c’è modo di effettuare tale trasferimento se non introducendo un terzo contenitore che permette di svuotare uno degli altri due contenitori. Si può così travasare il liquido il liquido dell’altro in quello che si è svuotato e infine ritrasferire in esso il liquido che è stato spostato per primo nel terzo contenitore. Si comprende allora che per scambiare due informazioni dello stesso tipo è necessario introdurre una terza variabile, ovviamente dello stesso tipo. Ad esempio date le tre informazioni (tra parentesi quadre è riportato il valore delle variabili): x [3] y [5] z[] si copia dapprima x in z: Il linguaggio C 219 x [3] y [5] z[3] poi y in x: x [5] y [5] z[3] ed infine z in y: x [5] y [3] z[3] utilizzando la sintassi del C, l’algoritmo assume la seguente forma: z=x; x=y; y=z; Descrizione delle funzioni: Per la realizzazione dell’algoritmo si utilizza una sola funzione scambia con le seguenti caratteristiche: - Paramentri di input: [x,y] informazioni prima dello scambio - Parametri di output: [x,y] informazioni dopo lo scambio - Variabili locali: [z] informazione necessaria allo scambio Implementazione // FUNZIONE PER LO SCAMBIO DI VALORE // La funzione void scambia(x,y) // permette di scambiare il valore delle variabili x ed y void scambia(float*x, float*y) { // dichiarazione variabile locale di appoggio utilizzata per lo scambio float z; // effettua scambia per mezzo della variabile z z=*x; *x=*y; *y=z; } Esempio d’uso: Per testare la funzione sviluppata può essere utilizzato il seguente main in cui è inserita anche la gestione dell’I/O. // Funzione main int main () { int x,y; printf("Benvenuti al corso di Fondamenti di Informatica\n"); printf("Esempio di programma che effettua lo scambio di valore tra 2 numeri\n"); printf("Inserisci il primo numero x: "); scanf("%f",&x); printf("Inserisci il secondo numero y: "); scanf("%f",&y); printf("\nIl valore di x prima dello scambio e': %f\n",x); printf("\nIl valore di y prima dello scambio e': %f\n",y); printf("\nElaborazione in corso....\n"); scambia(&x,&y); system("Pause"); printf("\nIl valore di x dopo lo scambio e': %f\n",x); printf("\nIl valore di y dopo lo scambio e': %f\n",y); system("Pause"); return 0; } 220 Capitolo sesto 6.7.2. Inserimento in un vettore Descrizione del problema: Scrivere una funzione per l’inserimento dell’informazione info nella posizione posiz di un vettore di n elementi di un certo tipo T. Esempio: input: n=5, v= [10 50 20 40 35], info=66, posiz=3 output: n=6, v=[10 50 66 20 40 35] Descrizione dell’algoritmo: Si può pensare al vettore come ad uno scaffale di libri allineati verso sinistra. Per far posto ad un nuovo libro in una posizione assegnata, bisogna spostare tutti i libri che si trovano in posizione successiva a quella data di una posizione verso destra. Lo spostamento può essere fatto un libro alla volta, cominciando da quello in ultima posizione. Dopo l’inserimento, il numero di libri o riempimento dello scaffale è aumentato di una unità. Si fa l’ipotesi che lo scaffale non sia mai pieno. Nel caso in cui si debba inserire la lettera a nella seconda posizione di un vettore di 4 elementi la procedura per l’inserimento è riportata di seguito: [x, y, z, f] si effettua lo spostamento (shift) di tutti gli elementi del vettore, a partire dall’ultimo fino a quello in seconda posizione, di un posto a destra: [x, , y, z, f] si copia il nuovo elemento nella seconda posizione e si incrementa di una unità il numero di elementi all’interno del vettore: [x, a, y, z, f] Utilizzando la sintassi del C, l’algoritmo assume la seguente forma : for (i=n-1;i>=posiz;i--) { v[i+1]=v[i]; } v[posiz]=info; n=n+1; Descrizione delle funzioni: Per la realizzazione dell’algoritmo si utilizza una sola funzione inserisci con le seguenti caratteristiche: - Parametri di Input: [v,info,posiz,n] rispettivamente vettore, elemento da inserire e rispettiva posizione, e riempimento del vettore - Parametri di Output: [v,n] vettore e riempimento dopo l’inserimento - Variabili locali: [i] contatore di ciclo Implementazione // FUNZIONE PER L'INSERIMENTO DI UN ELEMENTO IN UN VETTORE // La funzione void inserisci(v,info,posiz,n) // permette di inserire l'elemento info al posto posiz nel vettore v di riempimento n // Vettore è un alias di un tipo array di float void inserisci(Vettore v, float info, int posiz, int* n) { // contatore di ciclo register int i; // effettua l'inserimento mediante shift Il linguaggio C 221 for (i=*n-1;i>=posiz;i--) { v[i+1]=v[i]; } v[posiz]=info; // aggiorna il riempimento *n=*n+1; } Esempio d’uso: Per testare la funzione sviluppata può essere utilizzato il seguente main in cui è inserita anche la gestione dell’I/O. // Funzione main int main () { Vettore v; register int j; int posiz,n; float info; printf("Benvenuti al corso di Fondamenti di Informatica\n"); printf("Esempio di programma di inserimento di un elemento in un vettore\n"); n=-1; while ((n<0) || (n>NMAX-1)) { printf("Inserisci il numero delle componenti del tuo vettore(>=0 <=50): \n"); scanf("%d",&n); }; printf("Inserisci elementi del Vettore (dopo ogni un numero premere INVIO)\n"); for (j=0; j<n; j++) { printf("elemento[%d]=",j); scanf("%g",&v[j]); } printf("\nVettore v prima dell'inserimento\n"); printf("Vetore = ["); for (j=0; j<n; j++) { if (j<n-1) printf("%g,",v[j]); else printf("%g",v[j]); } printf("]\n"); printf("Inserisci l'elemento che vuoi inserire: "); scanf("%f",&info); posiz=-1; while ((posiz<0) || (posiz>n-1)) { printf("Inserisci la posizione dove inserirlo (>=0 <=%d): \n",n-1); scanf("%d",&posiz); }; printf("\nElaborazione in corso....\n"); inserisci(v,info,posiz,&n); system("Pause"); printf("\nVettore v dopo l'inserimento\n"); printf("Vetore = ["); for (j=0; j<n; j++) { if (j<n-1) printf("%g,",v[j]); else printf("%g",v[j]); } 222 Capitolo sesto printf("]\n"); system("Pause"); return 0; } 6.7.3. Eliminazione in un vettore Descrizione del problema: Scrivere una funzione per l’eliminazione dell’elemento in una posizione posiz data di un vettore di n elementi di un certo tipo T. Esempio: input: n=5, v= [22 50 30 16 10], posiz=2 output: n=4, v=[22 30 16 10] Descrizione dell’algoritmo: Si può pensare al vettore come ad uno scaffale di libri allineati verso sinistra. Dopo avere tolto il libro dalla posizione assegnata, bisogna spostare tutti quelli che si trovano alla sua destra di un posto verso sinistra per recuperare lo spazio resosi disponibile. Lo spostamento viene fatto un libro alla volta, cominciando da quello successivo al libro nella posizione assegnata e si termina quando si è arrivati all’ultimo libro. Dopo l’eliminazione, il numero di libri o riempimento dello scaffale è diminuito di una unità. Si fa l’ipotesi che lo scaffale contenga almeno un elemento. Nel caso in cui si voglia eliminare l’elemento in seconda posizione di un vettore di 4 elementi, la procedura per l’eliminazione è riportata di seguito: [x, y, z, f] si effettua lo spostamento (shift) di tutti gli elementi del vettore, a partire da quello in terza posizione fino all’ultimo, di un posto a sinistra: [x, z, f] si diminuisce di una unità il numero di elementi all’interno del vettore. Utilizzando la sintassi del C, l’algoritmo assume la seguente forma : for (i=posiz+1;i<n;i++) { v[i-1]=v[i]; } n=n-1; Descrizione delle funzioni: Per la realizzazione dell’algoritmo si utilizza una sola funzione elimina con le seguenti caratteristiche: - Parametri di Input: [v,posiz,n] vettore di partenza, posizione dell’elemento da eliminare e riempimento del vettore - Parametri di Output: [v,n] vettore e suo riempimento dopo l’eliminazione - Variabili locali: i contatore di ciclo Implementazione // FUNZIONE PER L'ELIMINAZIONE DI UN ELEMENTO IN UN VETTORE // La funzione void elimina(v, posiz) // permette di eliminare l'elemento al posto posiz nel vettore v di riempimento n // Vettore è un alias di un tipo array di float void elimina(Vettore v, int posiz, int *n) { // contatore di ciclo register int i; Il linguaggio C 223 // effettua l'eliminazione mediante shift for (i=posiz+1;i<*n;i++) { v[i-1]=v[i]; } // aggiorna il riempimento *n=*n-1; } Esempio d’uso: Per testare la funzione sviluppata può essere utilizzato il seguente main in cui è inserita anche la gestione dell’I/O. // Funzione main int main () { Vettore v; register int j; int posiz,n; printf("Benvenuti al corso di Fondamenti di Informatica\n"); printf("Esempio di programma di eliminazione di un elemento da un vettore\n"); n=-1; while ((n<0) || (n>NMAX-1)) { printf("Inserisci il numero delle componenti del tuo vettore(>=0 <=50): \n"); scanf("%d",&n); }; printf("Inserisci gli elementi del Vettore (dopo ogni numero premere INVIO)\n"); for (j=0; j<n; j++) { printf("elemento[%d]=",j); scanf("%g",&v[j]); } printf("\nVettore v prima dell'eliminazione\n"); printf("Vetore = ["); for (j=0; j<n; j++) { if (j<n-1) printf("%g,",v[j]); else printf("%g",v[j]); } printf("]\n"); posiz=-1; while ((posiz<0) || (posiz>n-1)) { printf("Inserisci posizione dell’elemento da eliminare (>=0 <=%d): \n",n-1); scanf("%d",&posiz); }; printf("\nElaborazione in corso....\n"); elimina(v,posiz,&n); system("Pause"); printf("\nVettore v dopo l'eliminazione\n"); printf("Vetore = ["); for (j=0; j<n; j++) { if (j<n-1) printf("%g,",v[j]); else printf("%g",v[j]); } printf("]\n"); system("Pause"); return 0; 224 Capitolo sesto } 6.7.4. Eliminazione di una colonna da una matrice Descrizione del problema: Scrivere una funzione per l’eliminazione dell’elemento in una data colonna col di una matrice di n righe e m colonne di elementi di un certo tipo T. Esempio: input: n=3, m=4, A= [10 22 33 50; 20 54 80 41; 30 10 23 31], col=2 output: n=3, m=3, A=[10 33 50; 20 80 41; 30 23 31] Descrizione dell’algoritmo: L’eliminazione di una colonna in una matrice si effettua applicando l’algoritmo di eliminazione di un elemento da un vettore a tutte le righe della matrice. Utilizzando la sintassi del C, l’algoritmo assume la seguente forma: for (i=0;i<n;i++) { for (j=col+1;j<m;j++) { A[i][j-1]=A[i][j]; } } m=m-1; Descrizione delle funzioni: Per la realizzazione dell’algoritmo si utilizza una sola funzione eliminacol con le seguenti caratteristiche: - Parametri di Input: [A,n,m,col], matrice, numero di righe, numero di colonne ed indice della colonna da eliminare - Parametri di Output: [A,m] matrice e numero di colonne dopo l’eliminazione - Variabili locali: i, j indici di riga e colonna Implementazione // FUNZIONE PER L'ELIMINAZIONE DI UNA COLONNA DA UNA MATRICE // La funzione void eliminacol(A,n,m,col) // permette di eliminare gli elementi della colonna col dalla matrice A // Matrice è un alias di un tipo array bidimensionale di int void eliminacol(Matrice A, int n, int* m, int col) { // contatori di ciclo register int i,j; // effettua l'eliminazione mediante shift for (i=0;i<n;i++) { for (j=col+1;j<*m;j++) { A[i][j-1]=A[i][j]; } } // aggiorna il riempimento relativo alle colonne *m=*m-1; } Esempio d’uso: Per testare la funzione sviluppata può essere utilizzato il seguente main in cui è inserita anche la gestione dell’I/O. Il linguaggio C 225 // Funzione main int main () { Matrice A; register int k,z; int n,m,col; printf("Benvenuti al corso di Fondamenti di Informatica\n"); printf("Esempio di programma che elimina una colonna da una matrice\n"); n=-1; while ((n<0) || (n>NMAX-1)) { printf("Inserisci il numero di righe della matrice(>=0 <=50): \n"); scanf("%d",&n); }; m=-1; while ((m<0) || (m>MMAX-1)) { printf("Inserisci il numero di colonne della matrice(>=0 <=50): \n"); scanf("%d",&m); }; printf("Inserisci gli elementi della Matrice (dopo ogni elem premere INVIO)\n"); for (k=0; k<n; k++) { for (z=0; z<m; z++) { printf("elemento[%d,%d]=",k,z); scanf("%d",&A[k][z]); } } printf("\nMatrice A prima dell'eliminazione\n"); printf("A=\n|"); for (k=0; k<n; k++) { for (z=0; z<m; z++) { if (z<m-1) printf("%d ",A[k][z]); else printf("%d|",A[k][z]); } if (k<n-1) printf("\n|"); else printf("\n"); } col=-1; while ((col<0) || (col>m-1)) { printf("Inserisci l'indice di colonna da eliminare (>=0 <=%d): \n",m-1); scanf("%d",&col); }; printf("\nElaborazione in corso....\n"); eliminacol(A,n,&m,col); system("Pause"); printf("\nMatrice A dopo l'eliminazione\n"); printf("A=\n|"); for (k=0; k<n; k++) { for (z=0; z<m; z++) { if (z<m-1) printf("%d ",A[k][z]); else printf("%d|",A[k][z]); } if (k<n-1) printf("\n|"); else printf("\n"); } system("Pause"); return 0; } 226 Capitolo sesto 6.7.5. Eliminazione di una riga da una matrice Descrizione del problema Scrivere una funzione per l’eliminazione dell’elemento in una data riga riga di una matrice di n righe e m colonne di elementi di un certo tipo T. Esempio: input: n=4, m=3, A= [10 22 33; 20 54 80; 30 10 23; 22 10 8], riga=2 output: n=3, m=3, A=[10 22 33; 30 19 23; 22 10 8] Descrizione dell’algoritmo: L’eliminazione di una colonna in una matrice si effettua applicando l’algoritmo di eliminazione di un elemento da un vettore a tutte le colonne della matrice. Utilizzando la sintassi del C, l’algoritmo assume la seguente forma: for (j=0;j<m;j++) { for (i=riga+1;i<n;i++) { A[i-1][j]=A[i][j]; } } n=n-1; Descrizione delle funzioni: Per la realizzazione dell’algoritmo si utilizza una sola funzione eliminariga con le seguenti caratteristiche: - Parametri di Input: [A,n,m,riga] matrice, numero di righe, numero di colonne ed indice della riga da eliminare - Parametri di Output: [A,n] matrice e numero di righe dopo l’eliminazione - Variabili locali: i, j indici di riga e colonna Implementazione // FUNZIONE PER L'ELIMINAZIONE DI UNA RIGA DA UNA MATRICE // La funzione void eliminacol(A,n,m,riga) // permette di eliminare gli elementi di una data riga dalla matrice A // Matrice è un alias di un tipo array bidimensionale di int void eliminariga(Matrice A, int* n, int m, int riga) { // contatori di ciclo register int i,j; // effettua l'eliminazione mediante shift for (j=0;j<m;j++) { for (i=riga+1;i<*n;i++) { A[i-1][j]=A[i][j]; } } // aggiorna il riempimento relativo alle colonne *n=*n-1; } Esempio d’uso: Per testare la funzione sviluppata può essere utilizzato il seguente main in cui è inserita anche la gestione dell’I/O. // Funzione main Il linguaggio C 227 int main () { Matrice A; register int k,z; int n,m,riga; printf("Benvenuti al corso di Fondamenti di Informatica\n"); printf("Esempio di programma di eliminazione di una riga da una matrice\n"); n=-1; while ((n<0) || (n>NMAX-1)) { printf("Inserisci il numero di righe della matrice(>=0 <=50): \n"); scanf("%d",&n); }; m=-1; while ((m<0) || (m>MMAX-1)) { printf("Inserisci il numero di colonne della matrice(>=0 <=50): \n"); scanf("%d",&m); }; printf("Inserisci gli elementi della Matrice (dopo ogni elem premere INVIO)\n"); for (k=0; k<n; k++) { for (z=0; z<m; z++) { printf("elemento[%d,%d]=",k,z); scanf("%d",&A[k][z]); } } printf("\nMatrice A prima dell'eliminazione\n"); printf("A=\n|"); for (k=0; k<n; k++) { for (z=0; z<m; z++) { if (z<m-1) printf("%d ",A[k][z]); else printf("%d|",A[k][z]); } if (k<n-1) printf("\n|"); else printf("\n"); } riga=-1; while ((riga<0) || (riga>m-1)) { printf("Inserisci l'indice di riga da eliminare (>=0 <=%d): \n",m-1); scanf("%d",&riga); }; printf("\nElaborazione in corso....\n"); eliminacol(A,&n,m,riga); system("Pause"); printf("\nMatrice A dopo l'eliminazione\n"); printf("A=\n|"); for (k=0; k<n; k++) { for (z=0; z<m; z++) { if (z<m-1) printf("%d ",A[k][z]); else printf("%d|",A[k][z]); } if (k<n-1) printf("\n|"); else printf("\n"); } system("Pause"); return 0; } 228 6.7.6. Capitolo sesto Ricerca sequenziale Descrizione del problema: Scrivere una funzione per la ricerca dell’informazione info in un vettore non ordinato avente n elementi di un certo tipo T. In particolare la funzione deve produrre una indicazione sull’esistenza dell’informazione nel vettore e, nel caso esista, la posizione. Si fa l’ipotesi che nel caso in cui l’elemento non sia presente nel vettore la sua posizione è -1. Esempio: input: v= [9 5 6 8 7], info=8 output: msg=elemento presente, posiz=4 Descrizione dell’algoritmo: La ricerca di un oggetto in un insieme si effettua fissando la strategia con la quale si possono effettuare i confronti dell’oggetto cercato con quelli dell’insieme. Se non si ha una conoscenza dell’insieme, l’unico modo di procedere è quello di prendere un oggetto alla volta e confrontarlo con quello dato fino a quando non se ne trova uno uguale ad esso o è stato visionato l’intero insieme. Tale tipo di ricerca è detto sequenziale. La ricerca di un’informazione in un vettore che contiene valori tra loro non ordinati procede in maniera sequenziale. Si comincia a confrontare il primo elemento con quello ricercato. Poi il secondo, poi il terzo e così via fin quando il confronto non risulta verificato. In questo modo si verifica l’assenza dell’elemento dopo averlo confrontato con tutti gli elementi del vettore. Nel caso contrario, quando lo si incontra, si ferma l’indagine facendo l’ipotesi che nel vettore non esistano valori tra di loro uguali. Facciamo ora un esempio. Sia assegnato il vettore: v=[9 5 6 8 7] (1 2 3 4 5) (tra le parentesi tonde sono indicate le posizioni degli elementi nel vettore) e sia 4 il valore da ricercare. Allora l’algoritmo procede iterativamente nel seguente modo: passo confronto corrente trovato fine vettore (posizione nel vettore) 1 4==9 no no 2 4==5 no no 3 4==6 no no 4 4==8 no no 5 4==9 no si e la ricerca termina con l’indicazione che 4 non è presente nel vettore. Cerchiamo quindi il valore 8: passo (posizione nel vettore) 1 2 3 4 confronto corrente trovato fine vettore 8==9 8==5 8==6 8==8 no no no si no no no no Il linguaggio C 229 e la ricerca termina con l’indicazione che il valore 8 è presente all’interno del vettore nella posizione 4. Utilizzando la sintassi del C, l’algoritmo assume la seguente forma: trovato=0; i=0; posiz=-1; while (!trovato && i<n) { if (v[i]==info) { trovato=1; posiz=i; } i++; } Descrizione delle funzioni: Per la realizzazione dell’algoritmo si utilizza una sola funzione ricercaseq con le seguenti caratteristiche: - Parametri di Input: [v,n,info] vettore d’ingresso, suo riempimento ed informazione da ricercare - Parametri di Output: [posiz] posizione dell’elemento all’interno del vettore - Variabili locali: [i,trovato,posiz] contatore di ciclo, variabile binaria che indica se il confronto ha avuto successo e posizione dell’elemento all’interno del vettore Implementazione // FUNZIONE PER LA RICERCA DI UN ELEMENTO IN UN VETTORE // La funzione int ricercaseq(v,n,info) // permette di ricerca la posizione dell’elemento info nel vettore v // Vettore è un alias di un tipo array monodimensionale di int int ricercaseq(Vettore v, int n, int info) { // contatore di ciclo, posizione dell’elemento e variabile d’arresto register int i=0; int posiz=-1; int trovato=0; // effettua la ricerca sequenziale scorrendo gli elementi del vettore while (!trovato && i<n) { if (v[i]==info) { trovato=1; posiz=i; } i++; } // ritorna la posizione dell’elemento return posiz; } Esempio d’uso: Per testare la funzione sviluppata può essere utilizzato il seguente main in cui è inserita anche la gestione dell’I/O. // Funzione main 230 Capitolo sesto int main () { Vettore v; register int j; int posiz,info,n; printf("Benvenuti al corso di Fondamenti di Informatica\n"); printf("Esempio di programma per la ricerca di un elemento da un vettore\n"); n=-1; while ((n<0) || (n>NMAX-1)) { printf("Inserisci il numero delle componenti del tuo vettore(>=0 <=50): \n"); scanf("%d",&n); }; printf("Inserisci gli elementi del Vettore (dopo ogni numero premere INVIO)\n"); for (j=0; j<n; j++) { printf("elemento[%d]=",j); scanf("%d",&v[j]); } printf("\nVettore v prima della ricerca\n"); printf("Vettore = ["); for (j=0; j<n; j++) { if (j<n-1) printf("%d,",v[j]); else printf("%d",v[j]); } printf("]\n"); printf("Inserisci l'elemento da ricercare: "); scanf("%d",&info); printf("\nElaborazione in corso....\n"); posiz=ricercaseq(v,n,info); system("Pause"); printf("\n- Risultati ricerca -\n"); if (posiz!=-1) printf("L'elemento %d e' presente in posizione %d\n",info,posiz); else printf("L'elemento % d non e’ presente nel vettore v\n",info); system("Pause"); return 0; } 6.7.7. Ricerca binaria Descrizione del problema: Scrivere una funzione per la ricerca dell’informazione info in un vettore non ordinato avente n elementi di un certo tipo T. In particolare la funzione deve produrre una indicazione sull’esistenza dell’informazione nel vettore e, nel caso esista, la posizione. Si fa l’ipotesi che nel caso in cui l’elemento non sia presente nel vettore la sua posizione è -1. Esempio: input: v= [9 5 6 8 7], info=8 output: msg=elemento presente, posiz=4 Descrizione dell’algoritmo: La ricerca di una informazione in un elenco si può effettuare come visto nel caso precedente confrontando uno dopo l’altro i valori dell’elenco con quello cercato fino a quando non si trova un elemento uguale o non si è analizzato l’intero elenco. Il metodo sequenziale richiede nel caso peggiore n confronti, dove n è il riempimento del vettore. Se l’elenco è ordinato, si ricorre allora ad una ricerca che ad ogni passo riduce l’insieme in cui cercare mediante un’opportuna tecnica di dimezzamento. In questo modo il caso peggiore richiede al Il linguaggio C 231 più log2(n) confronti. Il metodo noto col nome di ricrca binaria si basa sui seguenti passi: - determinazione del punto medio dell’insieme in cui cercare; - confronto dell’elemento in questa posizione con quello da cercare; - individuazione dell’insieme in cui continuare la ricerca se il passo 2 non ha successo; esso risulta per vettore ordinato in senso crescente: il sottoinsieme degli elementi in posizioni successive al punto medio se il valore da cercare è maggiore di quello del punto medio, altrimenti quello caratterizzato da posizioni inferiori; - ripetizioni dei passi precedenti finché il passo 2 non risulti verificato o non sia possibile fissare un sottoinsieme in cui continuare la ricerca. Facciamo ora un esempio. Sia assegnato il vettore: v=[15 22 29 36 50 55] (1 2 3 4 5 6) (tra le parentesi tonde sono indicate le posizioni degli elementi nel vettore) e sia 21 il valore da ricercare. Allora l’algoritmo procede iterativamente nel seguente modo: passo punto medio confronto corrente 1 2 3 29 15 22 21<29 21>15 21<22 prossimo sottoinsieme di ricerca [15, 22] [22] [22, 15] fine trovato no no si no no no A questo punto la ricerca termina perché non esiste un sottoinsieme (l’estremo inferiore è maggiore del superiore) in cui continuare la ricerca, con l’indicazione che 21 non è presente nel vettore. Cerchiamo quindi il valore 50: passo punto medio confronto corrente 1 2 3 29 50 22 50>29 50==50 21<22 prossimo sottoinsieme di ricerca [36, 50, 55] Vuoto [22, 15] fine trovato no si si no si no e la ricerca termina con l’indicazione che il valore 50 è presente all’interno del vettore nella posizione 5. Utilizzando la sintassi del C, l’algoritmo assume la seguente forma: posiz=-1; trovato=0; ei=0; es=n-1; while (!trovato && ei<es) { medio=(ei+es)/2; if (info==v[medio]) { trovato=1; posiz=medio; 232 Capitolo sesto } if (info<v[medio]) es=medio-1; else ei=medio+1; } Descrizione delle funzioni: Per la realizzazione dell’algoritmo si utilizza una sola funzione ricercabin con le seguenti caratteristiche: - Parametri di Input: [v,n,info] vettore d’ingresso, suo riempimento ed informazione da ricercare - Parametri di Output: [posiz] posizione dell’elemento all’interno del vettore - Variabili locali: [trovato,posiz,medio,ei,es] variabile binaria che indica se il confronto ha avuto successo, posizione dell’elemento all’interno del vettore, punto, medio, estremo inferiore ed estemo superiore dell’intervallo di ricerca Implementazione // FUNZIONE PER LA RICERCA BINARIA DI UN ELEMENTO IN UN VETTORE // La funzione int ricercaseq(v,n,info) // permette di ricerca la posizione dell’elemento info nel vettore v // Vettore è un alias di un tipo array monodimensionale di int int ricercabin(Vettore v, int n, int info) { // punto medio, etremi di ricerca, posizione dell'elemento e variabile d'arresto int ei,es,medio; int posiz=-1; int trovato=0; // inzializza l'intervallo di ricerca ei=0; es=n-1; // effettua la ricerca binaria aggiornando gli intervalli di ricerca while (!trovato && ei<es) { medio=(ei+es)/2; if (info==v[medio]) { trovato=1; posiz=medio; } if (info<v[medio]) es=medio; else ei=medio; } // ritorna la posizione dell'elemento return posiz; } Esempio d’uso: Per testare la funzione sviluppata può essere utilizzato il seguente main in cui è inserita anche la gestione dell’I/O. // Funzione main int main () { Vettore v; register int j; int posiz,info,n; printf("Benvenuti al corso di Fondamenti di Informatica\n"); printf("Esempio di programma per la ricerca di un elemento da un vettore\n"); Il linguaggio C 233 n=-1; while ((n<0) || (n>NMAX-1)) { printf("Inserisci il numero delle componenti del tuo vettore(>=0 <=50): \n"); scanf("%d",&n); }; printf("Inserisci gli elementi del vettore (dopo ogni numero premere INVIO)\n"); for (j=0; j<n; j++) { printf("elemento[%d]=",j); scanf("%d",&v[j]); } printf("\nVettore v prima della ricerca\n"); printf("Vettore = ["); for (j=0; j<n; j++) { if (j<n-1) printf("%d,",v[j]); else printf("%d",v[j]); } printf("]\n"); printf("Inserisci l'elemento da ricercare: "); scanf("%d",&info); printf("\nElaborazione in corso....\n"); posiz=ricercabin(v,n,info); system("Pause"); printf("\n- Risultati ricerca -\n"); if (posiz!=-1) printf("L'elemento %d e' presente in posizione %d\n",info,posiz); else printf("L'elemento % d non e' presente nel vettore v\n",info); system("Pause"); return 0; } 6.7.8. La ricerca del valore massimo in un vettore Descrizione del problema: Scrivere una funzione che determini il massimo di un vettore v formato da n elementi di un certo tipo T. Esempio: input: v=[10 55 20 11 30] output: max=55 Descrizione dell’algoritmo: L’individuazione del massimo in un insieme si effettua osservando uno dopo l’altro gli elementi che lo compongono e memorizzando di volta in volta il valore più grande. In particolare, si inizia facendo l’ipotesi che il massimo sia il primo elemento del vettore e successivamente lo si confronta con i restanti valori del vettore. Ogni volta che si incontra un valore più grande, si effettua l’aggiornamento del massimo. Facciamo un esempio. Sia assegnato il vettore: v=[3 5 6 8 7] (1 2 3 4 5) (tra le parentesi tonde sono indicate le posizioni degli elementi nel vettore). Allora l’algoritmo procede iterativamente nel seguente modo: massimo posizione confronto esito aggiornamento 234 Capitolo sesto vettore max=3 max=5 max=6 max=8 2 3 4 5 (max<elemento corrente del vettore) 3<5 5<6 6<8 8<7 confronto massimo vero vero vero falso si si si no Utilizzando la sintassi del C, l’algoritmo assume la seguente forma : max=v[0]; for (i=1;i<n;i++) { if (v([i]>max) max=v[i]; } Descrizione delle funzioni: Per la realizzazione dell’algoritmo si utilizza una sola funzione max con le seguenti caratteristiche: - Parametri di Input: [v,n] vettore d’ingresso e suo riempimento - Parametri di Output: [max] variabile contenete il massimo del vettore - Variabili locali: [i,max] contatore di ciclo e massimo corrente Implementazione // FUNZIONE PER LA RICERCA DEL MASSIMO DI UN VETTORE // La funzione Elem max(v,n) // permette di ricercare il massimo tra gli elementi di un vettore v // Vettore è un alias di un tipo array monodimensionale di int // Elem è un alias di un tipo intero rappresentante il tipo degli elemento del vettore Elem max(Vettore v, int n) { // contatore di ciclo e massimo corrente register int i; Elem max; // inzializza il massimo max=v[0]; // effettua la ricerca del massimo scorrendo gli elementi del vettore for (i=1;i<n;i++) { if (v[i]>max) max=v[i]; } // ritorna il massimo return max; } Esempio d’uso: Per testare la funzione sviluppata può essere utilizzato il seguente main in cui è inserita anche la gestione dell’I/O. // Funzione main int main () { Vettore v; register int j; int n; Elem massimo; printf("Benvenuti al corso di Fondamenti di Informatica\n"); printf("Esempio di programma per la ricerca del massimo vettore\n"); n=-1; Il linguaggio C 235 while ((n<0) || (n>NMAX-1)) { printf("Inserisci il numero delle componenti del tuo vettore(>=0 <=50): \n"); scanf("%d",&n); }; printf("Inserisci gli elementi del vettore (dopo ogni numero premere INVIO)\n"); for (j=0; j<n; j++) { printf("elemento[%d]=",j); scanf("%d",&v[j]); } printf("\nVettore v prima della ricerca\n"); printf("Vettore = ["); for (j=0; j<n; j++) { if (j<n-1) printf("%d,",v[j]); else printf("%d",v[j]); } printf("]\n"); printf("\nElaborazione in corso....\n"); massimo=max(v,n); system("Pause"); printf("\nIl massimo del vettore e' %d\n",massimo); system("Pause"); return 0; } 6.7.9. La posizione del valore minimo in un vettore Descrizione del problema: Scrivere una funzione che determini la posizione del valore minimo di un vettore formato da n elementi di un certo tipo T. Esempio: input: v=[10 55 20 11 30] output: posiz=1 Descrizione dell’algoritmo: L’individuazione della posizione del minimo in un insieme si effettua osservando uno dopo l’altro gli elementi che lo compongono e memorizzando di volta in volta la posizione dell’elemento avente il valore più piccolo. In particolare, si inizia facendo l’ipotesi che la posizione del minimo sia quella del primo elemento del vettore. Successivamente si confronta l’elemento nella presunta posizione di minimo con i restanti valori del vettore. Ogni volta che si incontra un valore più piccolo, si effettua l’aggiornamento della posizione in modo che alla fine dei confronti si abbia la posizione del minimo. Si fa l’ipotesi che nel vettore non esistano elementi uguali. Facciamo ora un esempio. Sia assegnato il vettore: v=[9 5 6 3 7] (1 2 3 4 5) (tra le parentesi tonde sono indicate le posizioni degli elementi nel vettore). Allora l’algoritmo procede iterativamente nel seguente modo: esito aggiornamento posiz. posizione confronto posizione minimo vettore (min>elemento confronto minimo corrente del vettore) posiz=1 2 9>5 Vero si 236 Capitolo sesto posiz=2 posiz=2 posiz=4 3 4 5 5>6 5>3 3>7 Falso Vero Falso no si no Utilizzando la sintassi del C, l’algoritmo assume la seguente forma: posiz=0; for (i=1;i<n;i++) { if (v([i]<v[posiz]) posiz=i; } Descrizione delle funzioni: Per la realizzazione dell’algoritmo si utilizza una sola funzione posmin con le seguenti caratteristiche: - Parametri di Input: [v,n] vettore d’ingresso e suo riempimento - Parametri di Output: [posiz] variabile contenete la posizione del minimo del vettore - Variabili locali: [i,posiz] contatore di ciclo e posizione del minimo corrente Implementazione // FUNZIONE PER LA RICERCA DEL MASSIMO DI UN VETTORE // La funzione int posmin(v,n) // permette di ricercare la posizione del minimo tra gli elementi di un vettore v // Vettore è un alias di un tipo array monodimensionale di int int posmin(Vettore v, int n) { // contatore di ciclo e posizione del minimo correnye register int i; int posiz; // inzializza la posizione del minimo posiz=0; // effettua la ricerca del massimo scorrendo gli elementi del vettore for (i=1;i<n;i++) { if (v[i]<v[posiz]) posiz=i; } // ritorna la posizione del minimo return posiz; } Esempio d’uso: Per testare la funzione sviluppata può essere utilizzato il seguente main in cui è inserita anche la gestione dell’I/O. // Funzione main int main () { Vettore v; register int j; int n, posiz; printf("Benvenuti al corso di Fondamenti di Informatica\n"); printf("Esempio di programma per la ricerca della posizione del minimo\n"); n=-1; while ((n<0) || (n>NMAX-1)) { printf("Inserisci il numero delle componenti del tuo vettore(>=0 <=50): \n"); scanf("%d",&n); Il linguaggio C 237 }; printf("Inserisci gli elementi del vettore (dopo ogni numero premere INVIO)\n"); for (j=0; j<n; j++) { printf("elemento[%d]=",j); scanf("%d",&v[j]); } printf("\nVettore v prima della ricerca\n"); printf("Vettore = ["); for (j=0; j<n; j++) { if (j<n-1) printf("%d,",v[j]); else printf("%d",v[j]); } printf("]\n"); printf("\nElaborazione in corso....\n"); posiz=posmin(v,n); system("Pause"); printf("\nLa posizione del minimo del vettore e' %d\n",posiz); system("Pause"); return 0; } 6.7.10. Ordinamento di un vettore col metodo della selezione Descrizione del problema: Scrivere una funzione per l’ordinamento in senso crescente di un vettore di n elementi di un certo tipo. Esempio: input: v=[30 10 50 12 22] output: v=[10 12 22 30 50] Descrizione dell’algoritmo: Ordinare un vettore in senso crescente significa imporre che scelti due qualsiasi indici i e j, tali che i<j risulti v(i)<v(j). Un meccanismo di ordinamento consiste nel dividere l’insieme da ordinare in due parti: una ordinata ed una disordinata. Si procede allora estraendo un elemento alla volta dall’insieme disordinato e accodandolo a quello ordinato in modo che l’ordinamento non venga alterato. All’inizio l’insieme ordinato è vuoto e l’algoritmo termina quando contiene tutti gli elementi del vettore. Vi sono vari modi di estrarre l’elemento dall’insieme disordinato. Quello che di seguito presentiamo seleziona ad ogni passo l’elemento da accodare. In particolare seleziona il minimo dall’insieme disordinato e lo accoda all’insieme ordinato. In tal modo si viene ad ogni passo a scegliere il valore più grandi di quelli che lo precedono e contemporaneamente più piccolo di quelli che lo seguono. Facciamo ora un esempio. Sia assegnato il vettore: v=[9 2 1 4 7] (1 2 3 4 5) (tra le parentesi tonde sono indicate le posizioni degli elementi nel vettore). Allora l’algoritmo procede iterativamente nel seguente modo: Parte Parte posizione accodamento nuovo 238 Capitolo sesto Ordinata Disordinata Vuota (1) (1 2) (1 2 3) (1 2 3 4) (1 2 3 4 5) (2 3 4 5) (3 4 5) (4 5) (5) minimo (valore minimo) 3(1) 2(2) 4(4) 5(7) 5(9) [1] [1 2] [1 2 4] [1 2 4 7] [1 2 4 7 9] intervallo di ricerca del minimo [9 2 4 7] [9 4 7] [9 7] [9] vuoto Si noti che l’algoritmo termina quando l’insieme disordinato si riduce ad un unico elemento. Inoltre poiché si usa lo stesso vettore per la parte ordinata e disordinata, l’accodamento viene effettuato scambiando di posto il minimo ed il valore che occupa la posizione di accodamento. Utilizzando la sintassi del C, l’algoritmo assume la seguente forma : for (i=0;i<n;i++) { imin=i; for (k=i+1;k<n;k++) { if (v[k]<v[imin]) imin=k; } temp=v[i]; v[i]=v[imin]; v[imin]=temp; } Descrizione delle funzioni: Per la realizzazione dell’algoritmo si utilizza una sola funzione ordina con le seguenti caratteristiche: - Parametri di Input: [v,n] vettore d’ingresso e suo riempimento - Parametri di Output: [v] vettore ordinato - Variabili locali: [i,k,temp,imin] contatori di ciclo, variabile d’appoggio per gli scambi e posizione del minimo corrente della parte disordinata Implementazione // FUNZIONE PER L'ORDINAMENTO DI UN VETTORE // La funzione void ordina(v,n) permette di ordinare in maniera crescente gli elementi del // vettore v // Vettore è un alias di un tipo array monodimensionale di int void ordina(Vettore v, int n) { // contatori di ciclo register int i,k; // indice contenente la posizione del minimo della parte disordinata // e variabile d'appoggio per gli scambi int imin, temp; // effettua l'ordinamento col metodo della selezione for (i=0;i<n;i++) { imin=i; for (k=i+1;k<n;k++) { if (v[k]<v[imin]) imin=k; } // effettua lo scambio tra l'elemento corrente ed il minimo della parte disord temp=v[i]; Il linguaggio C 239 v[i]=v[imin]; v[imin]=temp; } } Esempio d’uso: Per testare la funzione sviluppata può essere utilizzato il seguente main in cui è inserita anche la gestione dell’I/O. // Funzione main int main () { Vettore v; register int j; int n=-1; printf("Benvenuti al corso di Fondamenti di Informatica\n"); printf("Esempio di programma per l’ordinamento di un vettore\n"); while ((n<0) || (n>NMAX-1)) { printf("Inserisci il numero delle componenti del tuo vettore(>=0 <=50): \n"); scanf("%d",&n); }; printf("Inserisci gli elementi del vettore (dopo ogni numero premere INVIO)\n"); for (j=0; j<n; j++) { printf("elemento[%d]=",j); scanf("%d",&v[j]); } printf("\nVettore v prima dell'ordinamento\n"); printf("Vettore = ["); for (j=0; j<n; j++) { if (j<n-1) printf("%d,",v[j]); else printf("%d",v[j]); } printf("]\n"); printf("\nElaborazione in corso....\n"); ordina(v,n); system("Pause"); printf("\nVettore v dopo l'ordinamento\n"); printf("Vettore = ["); for (j=0; j<n; j++) { if (j<n-1) printf("%d,",v[j]); else printf("%d",v[j]); } printf("]\n"); system("Pause"); return 0; } 6.8. Esempi di programmi completi in C Si vogliono ora di seguito mostrare degli esempi di programmi completi realizzati con l’ausilio del linguaggio C. 240 Capitolo sesto 6.8.1. Gestione di un array Descrizione del problema: Scrivere una libreria di funzioni che permettano la gestione di un array di elementi di un dato tipo T. In particolare si vogliono realizzare delle funzioni per: - l’ordinamento dell’array - la ricerca del massimo - la ricerca sequenziale di un dato elemento nell’array - l’input da tastiera degli elementi dell’array - l’output su video degli elementi di un array - la gestione di un menù per l’attivazione delle funzioni Descrizione dell’algoritmo: Per la realizzazione delle funzioni si sfruttano gli algoritmi visti nella sezione precedente. Descrizione delle funzioni: Per la realizzazione delle funzioni si utilizzano le seguenti funzioni, di cui riportiamo per semplicità solo i parametri di ingresso/uscita: - ordina o paramentri di input: [v,n] vettore e riempimento o parametri di output: [v] vettore ordinato - max o paramentri di input: [v,n] vettore e riempimento o parametri di output: [max] elemento massimo del vettore - ricercaseq o paramentri di input: [v,n,info] vettore, riempimento e elemento da ricercare o parametri di output: [posiz] posizione dell’elemento ricercato nell’array - input_array o paramentri di input: nessuno o parametri di output [v] vettore e suo riempimento - output_array o paramentri di input: [v,n] vettore e riempimento o parametri di output: nessuno - menu o parametri di input: nessuno o parametri di output: nessuno Implementazione ed esempio d’uso: Di seguito è riportato l’implementazione dell’intero programma con il codice relativo alle varie funzioni ed al main utilizzato per testare il programma. // Gestione array // Preambolo del programma // Direttive di compilazione #include <stdio.h> #include <stdlib.h> Il linguaggio C 241 # define NMAX 50 // Alias di tipi typedef float Vettore [NMAX]; typedef float Elem; // prototipi funzioni void ordina (Vettore v, int n); int ricercaseq (const Vettore v, int n, Elem info); Elem max (const Vettore v, int n); void input_array (Vettore v, int* n); void output_array (const Vettore v, int n); void menu(); // FUNZIONE PER L'ORDINAMENTO DI UN VETTORE // La funzione void ordina(v,n) // permette di ordinare in maniera crescente gli elementi del vettore v void ordina(Vettore v, int n) { // contatori di ciclo register int i,k; // indice contenente la posizione del minimo della parte disordinata // e variabile d'appoggio per gli scambi int imin; Elem temp; // effettua l'ordinamento col metodo della selezione for (i=0;i<n;i++) { imin=i; for (k=i+1;k<n;k++) { if (v[k]<v[imin]) imin=k; } // effettua lo scambio tra l'elemento corrente ed il minimo della parte disordinata temp=v[i]; v[i]=v[imin]; v[imin]=temp; } } // FUNZIONE PER LA RICERCA DI UN ELEMENTO IN UN VETTORE // La funzione int ricercaseq(v,n,info) // ritona la posizione dell'elemento info nel vettore v // se l'elemento non è presente viene ritornato -1 int ricercaseq(const Vettore v, int n, Elem info) { // contatore di ciclo, posizione dell'elemento e variabile d'arresto register int i=0; int posiz=-1; int trovato=0; // effettua la ricerca sequenziale scorrendo gli elementi del vettore while (!trovato && i<n) { if (v[i]==info) { trovato=1; posiz=i; 242 Capitolo sesto } i++; } // ritorna la posizione dell'elemento return posiz; } // FUNZIONE PER LA RICERCA DEL MASSIMO DI UN VETTORE // La funzione Elem max(v,n) // ritorna il massimo tra gli elementi di un vettore v Elem max(const Vettore v, int n) { // contatore di ciclo e massimo corrente register int i; Elem max; // inzializza il massimo max=v[0]; // effettua la ricerca del massimo scorrendo gli elementi del vettore for (i=1;i<n;i++) { if (v[i]>max) max=v[i]; } // ritorna il massimo return max; } // FUNZIONE PER L'INPUT DI UN VETTORE // La funzione input_array(v,n) // gestisce l'input da tastiera degli elementi di un vettore v void input_array(Vettore v, int* n) { register int j; *n=-1; while ((*n<0) || (*n>NMAX-1)) { printf("Inserisci il numero delle componenti del tuo vettore(>=0 <=50): \n"); scanf("%d",n); }; printf("Inserisci gli elementi del vettore (dopo ogni numero premere INVIO)\n"); for (j=0; j<*n; j++) { printf("elemento[%d]=",j); scanf("%g",&v[j]); } } // FUNZIONE PER L'OUTPUT DI UN VETTORE // La funzione output_array(v,n) // gestisce l'output su monitor degli elementi di un vettore v void output_array(const Vettore v, int n) { register int j; printf("Vettore = ["); for (j=0; j<n; j++) { if (j<n-1) printf("%g,",v[j]); Il linguaggio C else printf("%g",v[j]); } } printf("]\n"); // FUNZIONE PER LA GESTIONE DELL'ATTIVAZIONE DELLE FUNZIONI // La funzione menu() // gestisce l'attivazione delle funzioni mediante un menù interattivo void menu () { Vettore v; int n, posiz; Elem massimo, info; char opzione; n=0; while (opzione!='6') { printf ("\n---PROGRAMMA PER LA GESTIONE DI UN ARRAY---\n"); printf ("[1] Inserimento elementi del Vettore\n"); printf ("[2] Visualizzione elementi del Vettore\n"); printf ("[3] Ricerca elemento nel Vettore\n"); printf ("[4] Visualizza elemento massimo nel Vettore\n"); printf ("[5] Ordina elementi del Vettore\n"); printf ("[6] Esci\n"); scanf("%s",&opzione); switch (opzione) { case '1': input_array(v,&n); break; case '2': if (n!=0) output_array (v,n); else printf("\nVettore vuoto\n"); break; case '3': printf("\nInserisci l'elemento da ricercare: "); scanf("%g",&info); posiz=ricercaseq(v,n,info); if (posiz!=-1) printf("\nL'elemento %g e' in posizione %d\n",info,posiz); else printf("\nL'elemento % g non e' presente nel vettore\n",info); break; case '4': massimo=max(v,n); printf("\nIl massimo del vettore e' %g\n",massimo); break; case '5': ordina(v,n); break; case '6': printf("Uscita programma...\n"); system("PAUSE"); default: printf("\nOpzione non supportata\n"); } 243 244 Capitolo sesto } } // MAIN int main() { menu(); return 0; } 6.8.2. Gestione di un archivio Descrizione del problema: Scrivere una libreria di funzioni che permettano la gestione di un archivio di di impiegati. Dove un impiegato può essere visto come un’istanza di un tipo strutturato caratterizzato dalle seguenti informazioni: - matricola - nome - cognome - dipartimento di afferenza In particolare si vogliono realizzare delle funzioni per: - il caricamento da file dell’archivio - il salvataggio sul file dell’archivio - l’inserimento di un nuovo impiegato in archivio la visualizzazione del contenuto dell’archivio - la ricerca di un impiegato per cognome - la modifica del dipartimento di afferenza di un dato impiegato Descrizione dell’algoritmo: Gli algoritmi necessari alla realizzazione delle funzioni si riconducono a quelle visti nella sezione precedente. Descrizione delle funzioni: Si utilizzano le seguenti funzioni, di cui riportiamo per semplicità solo i parametri di ingresso/uscita: - carica_archivio o paramentri di input [f]: nome del file su cui risiede l’archivio o parametri di output: [imp,n] archivio con impiegati - salva_archivio o paramentri di input [imp,f]: archivio corrente e nome del file su cui deve risiedere l’archivio o parametri di output: nessuno - inserisci_impiegato o paramentri di input: [imp] archivio corrente o parametri di output: [imp] archivio aggiornato - visualizza_archivio o paramentri di input [imp]: archivio corrente o parametri di output: nessuno - ricerca_impiegato o paramentri di input: [imp,cogn] archivio corrente e cognome dell’impiegato da ricercare o parametri di output [trovato]: variabile binaria che indica la presenza dell’impiegato in archivio - modifica_impiegato Il linguaggio C 245 o paramentri di input: [imp,cogn,dip] archivio corrente, cognome dell’impiegato da ricercare e nome del nuovo dipartimento o parametri di output [imp]: nuovo archivio Implementazione ed esempio d’uso: Di seguito è riportato l’implementazione dell’intero programma con il codice relativo alle varie funzioni ed al main utilizzato per testare il programma. // Gestione Archivio // Preambolo // direttive di compilazione #include <stdio.h> #include <string.h> #include <stdlib.h> #define DIM_MAX 100 #define STR_MAX 100 //Alias di tipi typedef char Stringa [STR_MAX]; typedef struct Impiegato { int matricola; Stringa Nome; Stringa Cognome; Stringa Dipartimento; }; typedef Impiegato Impiegati [DIM_MAX]; // variabile globale contenente il riempimento dell'archivio int num_impiegati=0; // inserisce un nuovo impiegato in archivio void inserisci_impiegato(Impiegati imp) { int n=0; register int i; if (num_impiegati<100) { do { printf("Numero Impiegati presenti in archivio: %d\n",num_impiegati); printf("Inserisci il numero di impiegati (>0 <=100): \n"); scanf("%d",&n); } while ((n<1) || (n+num_impiegati>DIM_MAX)); printf("Inserisci gli Impiegati (premere INVIO)\n"); for (i=num_impiegati; i<num_impiegati+n; i++) { printf("Inserire matricola dell'impiegato %d: ",i+1); scanf("%d",&imp[i].matricola); printf("Inserire nome dell'impiegato %d: ",i+1); scanf("%s",imp[i].Nome); printf("Inserire cognome dell'impiegato %d: ",i+1); scanf("%s",imp[i].Cognome); printf("Inserire dipartimento dell'impiegato %d: \n",i+1); scanf("%s",imp[i].Dipartimento); } num_impiegati=n+num_impiegati; } else printf("\nArchivio pieno!!!\n"); } //stampa a video il contenuto dell’archivio void visualizza_archivio(const Impiegati imp) { register int i; for (i=0; i<num_impiegati; i++) { printf("Matricola dell'impiegato %d: %d\n",i+1, imp[i].matricola); 246 Capitolo sesto } } printf("Nome dell'impiegato %d: %s\n",i+1,imp[i].Nome); printf("Cognome dell'impiegato %d: %s\n",i+1,imp[i].Cognome); printf("Dipartimento dell'impiegato %d: %s\n",i+1,imp[i].Dipartimento); //ricerca se un impiegato è presente in archivio void ricerca_impiegato(Impiegati imp, Stringa cogn){ register int i,j; int trovato=0; for (i=0; i<num_impiegati; i++) { if (strcmp(cogn,imp[i].Cognome)==0) { trovato=1; printf("Nome dell'impiegato %d: %s\n",i+1,imp[i].Nome); printf("Cognome dell'impiegato %d: %s\n",i+1,imp[i].Cognome); printf("Dipartimento dell'impiegato %d: %s\n",i+1,imp[i].Dipartimento); } } if (!trovato) printf("\n Impiegato non presente"); } //modifica il dipartimento di un impiegato void modifica_impiegato(Impiegati imp, Stringa cogn){ register int i,j; int trovato=0; i=0; while (!trovato && i<num_impiegati) { if (strcmp(cogn,imp[i].Cognome)==0) trovato=true; i++; } if (!trovato) printf("\n Impiegato non presente"); else { printf("Inserire nuovo Dipartimento dell'impiegato %d: \n",i); scanf("%s",imp[i-1].Dipartimento); } } // carica archivio da file void carica_archivio (Impiegati imp, Stringa f) { FILE * fp; int ret; register int i,j; if (!(fp=fopen(f,"r"))) { printf("\n L'archivio non puo' essere caricato\n"); } else { ret=fscanf(fp,"%d",&num_impiegati); if (ret!=EOF) { printf("\nNumero di impiegati presenti in archivio: %d\n",num_impiegati); if (num_impiegati>0) { for (i=0; i<num_impiegati; i++) { fscanf(fp,"%d",&imp[i].matricola); fscanf(fp,"%s",imp[i].Nome); fscanf(fp,"%s",imp[i].Cognome); Il linguaggio C } 247 fscanf(fp,"%s",imp[i].Dipartimento); } } else { printf("\nArchivio vuoto!!!\n"); num_impiegati=0; } } } // salva archivio su file void salva_archivio(const Impiegati imp, Stringa f) { register int i,j; FILE*fp; if (!(fp=fopen(f,"w"))) { printf("\n L'archivio non puo' essere salvato\n"); } else { fprintf(fp,"%d\n",num_impiegati); for (i=0; i<num_impiegati; i++) { fprintf(fp,"%d\n",imp[i].matricola); fprintf(fp,"%s\n",imp[i].Nome); fprintf(fp,"%s\n",imp[i].Cognome); fprintf(fp,"%s\n",imp[i].Dipartimento); } } } // main int main () { Impiegati i; char opzione; Stringa cogn, file; while (opzione!='7') { printf ("\n-PROGRAMMA DI GESTIONE DI UN ARCHIVIO DI IMPIEGATI-\n"); printf ("[1] Inserisci Impiegati\n"); printf ("[2] Ricerca Impiegato\n"); printf ("[3] Modifica Impiegato\n"); printf ("[4] Visualizza Archivio\n"); printf ("[5] Carica Archivio\n"); printf ("[6] Salva Archivio\n"); printf ("[7] Esci\n"); scanf("%s",&opzione); switch (opzione) { case '1': inserisci_impiegato(i); break; case '2': printf("\nInserisci il cognome dell'impiegato che vuoi ricercare: "); scanf("%s",cogn); ricerca_impiegato(i,cogn); break; case '3': printf("\nInserisci il cognome dell'impiegato che vuoi ricercare: "); scanf("%s",cogn); modifica_impiegato(i,cogn); break; 248 Capitolo sesto case '4': visualizza_archivio(i); break; case '5': printf("\nInserisci il nome del file-archivio: "); scanf("%s",file); carica_archivio(i,file); break; case '6': printf("\nInserisci il nome del file su cui salvare l'archivio: "); scanf("%s",file); salva_archivio(i,file); break; case '7': printf("Uscita programma...\n"); system("PAUSE"); return 0; default: printf("Opzione non supportata\n"); } } return 1; } Capitolo settimo Il linguaggio dell’ambiente MATLAB 7.1. Caratteristiche del linguaggio MATLAB è un vero e proprio ambiente di progetto rivolto principalmente allo sviluppo di programmi per l’analisi numerica. Tale ambiente utilizza un linguaggio di programmazione con una sintassi simile a quella del C, e, così come i linguaggi di programmazione più comuni, esso mette a disposizione del programmatore tutti i più noti “tipi elementari” di dato: - dati di natura numerico: o tipo intero, o tipo reale, - dati di natura alfanumerica: o caratteri, o stringhe. In particolare l’instanziazione di una variabile numerica avviene attraverso la sintassi: var=val dove var e val rappresentano rispettivamente il nome ed il valore della variabile numerica. Mentre l’instanziazione di una variabile alfanumerica avviene attraverso la sintassi: var=’val’ dove var e val rappresentano rispettivamente il nome ed il valore della variabile alfanumerica. Una variabile alfanumerica è contraddistinta dalla presenza degli apici, all’inizio e alla fine del valore della variabile stessa. Altra caratteristica fondamentale di MATLAB è il concetto di funzione. Ogni programma, sottoprogramma, procedura o funzione del linguaggio è sempre trattata come una funzione costituita o meno da un insieme di parametri di ingresso-uscita. Ogni dichiarazione di funzione MATLAB è preceduta dalla parola chiave function. Per quanto riguarda le istruzioni, il linguaggio MATLAB fornisce sia enunciati o statement semplici che di controllo. Quelli semplici sono: - l’assegnazione, che fornisce ad una variabile il valore che si ottiene calcolando il risultato di un’espressione composta in generale di costanti, variabili, operatori e funzioni; essa si indica (se var è la variabile che riceve il valore ed E è l’espressione che lo fornisce) con un sintassi del tipo: var=E 250 Capitolo settimo - l’attivazione di funzioni, eventualmente con il passaggio dei dati da elaborare e con la ricezione di uno più risultati. Si noti che MATLAB, come del resto altri linguaggi, mette a disposizione un insieme molto ampio di funzioni predefinite quali ad esempio quelle per il calcolo numerico oppure quelle per la lettura e la scrittura di dati sui supporti di ingresso e uscita. Gli enunciati di controllo si dividono al loro volta in: - enunciati composti, che si ottengono disponendo in sequenza enunciati semplici, di selezione e di iterazione; - enunciati di selezione; - enunciati di iterazione. Come anticipato il linguaggio MATLAB prevede la costruzione di programmi con la struttura di funzioni composte da un unico blocco di istruzioni, costituente la sezione esecutiva. A differenza di altri linguaggi, manca nel corpo di una funzione MATLAB la sezione dichiarativa in quanto tutte le variabili, come vedremo, sono automaticamente trattate e riconosciute o come set di reali o di caratteri. Una funzione può richiamare o essere richiamata da altre funzioni e può essere o meno caratterizzata dalla presenza di parametri di ingresso e di uscita. In figura 1 è riportato un esempio di programma MATLAB che richiama due funzioni. La funzione A è un esempio di funzione avente solo parametri di ingresso, mentre la funzione B è un esempio di funzione avente parametri sia di ingresso che di uscita. Il programma principale è esso stesso una funzione senza parametri. Tutte le variabili usate all’interno di una funzione MATLAB sono ad essa locali, ossia hanno una visibilità limitata alla sola funzione in cui sono usate. In altri termini una variabile definita all’interno della funzione A non può essere usata nella funzione B e viceversa. function nome_programma enunciato 1 del programma ……………………. funzioneA (lista_parametri_ingresso) ……………………. [lista_paramteri_ucita] = funzioneB(lista_parametri_ingresso) ……………………. enunciato N del programma function funzioneA(lista_parametri_ingresso) enunciato 1 della funzioneA ………………….. enunciato N1 della funzioneA function [lista_paramteri_ucita] = funzioneB(lista_parametri_ingresso) enunciato 1 della funzioneB ………………….. enunciato N2 della funzioneB Figura 1 – Esempio di programma Matlab 7.1.1. Il vocabolario del linguaggio Il vocabolario del linguaggio è costituito da sequenze di lunghezza finita di caratteri trattate come singole entità logiche (è quindi l'insieme delle parole Il linguaggio dell’ambiente MATLAB 251 costruite secondo le regole lessicali). Tali entità sono raggruppate in cinque classi differenti per finalità e caratteristiche. Le 5 classi sono: - separatori; - identificatori e parole riservate; - simboli speciali (operatori, delimitatori, frasi di commento); - numeri interi e decimali; - sequenze di caratteri racchiuse tra apici; Si noti che le terza e la quarta classe contengono le costanti (numeriche e alfanumeriche) gestite dal linguaggio. 7.1.2. I separatori Ciascuna entità lessicale è separata dalla successiva mediante opportuni separatori. I caratteri spazio ed ENTER (fine linea) sono considerati separatori espliciti, mentre gli operatori (aritmetici e di relazione), il punto e virgola e l'operatore dell'assegnazione (=) vengono implicitamente identificati come separatori. Così le frasi che seguono presentano due entità lessicali distinte: alfa = 10 if contatore mentre le frasi seguenti ne presentano soltanto una: ifcontatore whilecondizione 7.1.3. Gli identificatori e le parole chiavi Gli identificatori permettono di indicare i nomi di programmi, costanti, variabili e funzioni. Un identificatore è una stringa di caratteri che deve soddisfare i seguenti vincoli: - deve essere composta da lettere, cifre e dal carattere ‘_’; - il primo carattere deve essere una lettera; - deve essere costituito da un numero limitato di caratteri; - non deve ovviamente contenere al suo interno lo spazio. In figura 2 è riportata la carta sintattica per la costruzione di un identificatore. Figura 2 – Carta sintattica per identificatori 252 Capitolo settimo Esempi di identificatori sono: A a ALFA Alfa SOL1 PI_greco equaz_2_grado L’esempio sottolinea il fatto che MATLAB è “case sensitive” ovvero fa differenza tra lettere maiuscole e minuscole per cui ALFA e Alfa sono considerati distinti. Alcuni identificatori hanno un ben preciso e congelato significato nell'ambito del linguaggio e pertanto prendono il nome di parole riservate o chiavi. Tali parole sono riservate per usi particolari e non sono ridefinibili in altro modo dal programmatore in quanto sono “le chiavi” che guidano nella costruzione delle frasi del linguaggio. Esempi di parole chiavi del linguaggio sono: if, function, while, else, end. Accanto agli identificatori riservati, esiste un insieme di identificatori con significato ormai standard, che costituiscono l'ambiente di lavoro del linguaggio. In MATLAB ne esistono tantissimi, la maggior parte di questi fa riferimento a funzioni matematiche. Alcuni esempi sono: abs, max, min, sin, cos, sqrt, mean, etc… 7.1.4. I simboli speciali I simboli speciali sono o semplici caratteri o coppie adiacenti di essi ed indicano le operazioni predefinite presenti nel linguaggio e tutta la punteggiatura richiesta dalla sintassi. Essi sono i seguenti: + * / (operatori aritmetici) < > == <= (operatori di confronto) , ; . ‘ (delimitatori e punteggiatura) ( ) [ ] (parentesi) & | ~ () operatori logici) % (commenti) >= ~= : .. { } … Di seguito è descritto l’uso dei vari simboli speciali. Operatori Aritmetici Gli operatori aritmetici sono i classici operatori binari +,- ,*,/ e l’operatore di modulo %. La divisione tra interi tronca la parte frazionaria, mentre l’espressione x%y fornisce il resto della divisione di x per y. L’operatore di modulo può essere applicato solo a tipi integrali. Gli operatori + e – hanno la stessa priorità, priorità inferiore a * e / (che invece hanno identica priorità). Il linguaggio dell’ambiente MATLAB 253 Operatori Relazionali Gli operatori relazionali sono: > >= < <= mentre gli operatori di eguaglianza (e disuguaglianza) sono: == ~= Gli operatori di relazione hanno priorità maggiore rispetto a quelli di eguaglianza /disuguaglianza. Gli operatori relazionali hanno invece priorità minore rispetto agli operatori aritmetici. Operatori Logici Gli operatori logici sono: & (and), | (or) e ~ (not) & ha priorità maggiore di | ed entrambi hanno priorità inferiore agli operatori relazionali e di uguaglianza. Si noti che non esistendo un tipo logico predefinito, l’operatore ~ converte un operando non zero in un operando zero. I delimitatori Il linguaggio MATLAB non ha dei delimitatori canonici per delimitare le frasi del programma. L’unico carattere che ha una funzione di questo tipo è il carattere ‘;’ (punto e virgola). Tale carattere viene utilizzato come termine di ogni frase del linguaggio. Se non lo si inserisce al termine della frase, allora viene prodotta in automatico la stampa del valore delle variabili calcolate nella istruzione; se invece si chiude la frase con il punto e virgola tale stampa viene disabilitata. 7.1.5. Le frasi di commento Come tutti gli altri linguaggi di programmazione anche MATLAB consente di introdurre nel programma frasi prive di ogni valore esecutivo o dichiarativo che consentono di migliorare la leggibilità e la chiarezza del programma. Esse servono unicamente ad uno scambio di messaggi tra le persone. Tali frasi prendono il nome di frasi di commento. Nel linguaggio una frase che inizia con il simbolo speciale ‘%’ è un commento come mostrato in figura 3. % la variabile alfa deve avere valore negativo % i>=10 implica una condizione di errore Figura 3 – Carta sintattica per frasi di commento 254 Capitolo settimo 7.1.6. Le costanti numeriche I numeri trattati dal linguaggio sono di due tipi, interi e reali. Un numero intero è una sequenza di cifre eventualmente preceduta da un segno. Un numero reale ha una parte decimale ed eventualmente un fattore di scala. Sono costanti intere: 10 300 -1000 Sono costanti reali: 3.400 10.20 +1.99E+30 -30.008 -0.7E-10 9.02E3 In figura 4 è riportata la carta sintattica per la generazione di costanti numeriche. Figura 4 – Carta sintattica per costanti numeriche 7.1.7. Le costanti stringhe di caratteri Le costanti stringa di caratteri sono sequenze di caratteri racchiuse da una coppia di caratteri ' (apici). Il valore della stringa è dato dalla sequenza di caratteri esclusi gli apici che fungono da parentesi (vedi carta sintattica in figura 5). Una costante senza caratteri (una coppia di apici) rappresenta la stringa a lunghezza nulla. Sono costanti stringhe di caratteri: 'ALFABETO' 'ciao' 'Minuscolo' Il linguaggio dell’ambiente MATLAB 255 Figura 5 – Carta sintattica per stringhe 7.2. La struttura del programma Da un punto di vista testuale, il programma è visto come un insieme di frasi a cui viene costituito da una intestazione che ne riassume molto brevemente il significato ed un blocco che può essere visto come una entità sintattica che contiene la parte elaborativa del programma. Ogni intestazione è sempre preceduta dalla parola chiave function. Figura 6 – Programma in MATLAB 7.2.1. La dichiarazione e gestione dei tipi Come già anticipato in MATLAB non esiste il concetto di dichiarazione “esplicita” di tipo. Ogni variabile appartiene, in maniera implicita, ad uno dei tipi elementari messi a disposizione dal linguaggio. A differenza dei classici linguaggi di programmazione, il linguaggio MATLAB utilizza due soli tipi elementari di dato, o meglio, due sole classi elementari di oggetti: - double array (array bidimensionale di numeri reali); - char array (array bidimensionale di caratteri). La classe double array permette la gestione di tutti dati di tipo numerico come interi, reali a singola precisione e reali a doppia precisione. Di contro la classe char array permette la gestione di tutti i dati di tipo alfanumerico come caratteri e stringhe. Un oggetto di tali classi ha una struttura simile a quella di una matrice. Risulterà evidente che attraverso tali classi base di oggetti è possibile simulare tutti i tipi classici di un linguaggio di programmazione. variabili numeriche variabili alfanumeriche double array char array Figura 7 – Carta sintattica per i tipi di MATLAB Tipi Semplici: il tipo intero Il tipo intero contiene il sottoinsieme dei numeri interi compresi nell'intervallo [MAXINT, MAXINT] con MAXINT valore costante predefinito. Il valore di MAXINT 256 Capitolo settimo dipende dalla rappresentazione interna dei numeri interi ed è quindi dipendente dal particolare sistema di calcolo. Sulle variabili di tipo intero sono definite le operazioni di somma, sottrazione, divisione e moltiplicazione più moltissime altre funzioni tra cui ricordiamo: la funzione resto in modulo MOD, la funzione valore assoluto ABS, la funzione segno SIGN e così via. Tipi Semplici: il tipo reale Il tipo reale contiene un intervallo di estremi predefiniti dell'insieme dei numeri reali. Sui valori del tipo reale sono definite le operazioni di somma, sottrazione, divisione e di moltiplicazione, più un insieme ampio di funzioni più avanzate. Sono inoltre definite le due funzioni di conversione da reale a intero floor(x), ceil(x) e round(x). La prima restituisce la parte intera di x; la seconda restituisce la parte intera di x+1; la terza calcola l'intero mediante un arrotondamento. Esempio: floor (0.6) = 0 ceil (0.6) = 1 round(0.6) = 1 Tipi Semplici: il tipo char Le costanti che compongono il tipo char formano un insieme finito e ordinato di caratteri. L'ordinamento può variare a seconda del sistema di calcolo e dipende dal codice usato per la rappresentazione in memoria. Solitamente viene impiegato il codice ASCII a sette bit, per cui l'ordinamento viene stabilito dalla posizione del carattere all'interno della tabella introdotta dall'ASCII. Un carattere è denotato da uno dei simboli di tale tabella racchiuso tra apici (ad esempio 'm'). Tipi Semplici: il tipo booleano In MATLAB non esiste il tipo booleano, ma questo può essere simulato con una variabile intera che può assumere due soli valori (0 e 1): uno corrispondente a FALSE, l’altro a TRUE. Sui valori logici sono definite le operazioni di NOT (~), AND (&) ed OR (!). Tipi Strutturati: il tipo array La struttura array è composta da un insieme di elementi tutti dello stesso tipo e con un unico nome collettivo. Può avere una o più dimensioni fino ad un massimo prefissato. Ad ogni elemento dell'array è possibile accedere mediante un indice (o un numero di indici uguale alle dimensioni stabilite) che ne individua la posizione all'interno della struttura. In MATLAB il numero di elementi dell'array non è definito a priori e può cambiare nel corso del programma. Un array monodimensionale di interi o reali coincide con un oggetto della classe double array di dimensione 1xN, con N pari al numero di elementi contenuti nell’array. Un array monodimensionale di caratteri coincide con un oggetto della classe char array di dimensione 1xN, con N pari al numero di elementi contenuti nell’array. Gli elementi del vettore vanno racchiusi tra parentesi quadre e separati da uno spazio bianco o da una virgola. a=[1 2 3]; b=[7,2,8,6]; Il linguaggio dell’ambiente MATLAB 257 Nel primo caso si è creato il vettore a di tre elementi con valori 1, 2 e 3; nel secondo caso si è creato il vettore b con quattro elementi 7, 2, 8 e 6. Un array bidimensionale (matrice) di interi o reali coincide con un oggetto della classe double array di dimensione MxN, con M pari al numero di righe della matrice e N pari al numero di colonne. Un matrice di caratteri coincide con un oggetto della classe char array di dimensione MxN, con M pari al numero di righe della matrice e N pari al numero di colonne. Gli elementi della matrice vanno racchiusi tra parentesi quadre, gli elementi delle righe sono separati da uno spazio bianco o da una virgola, mentre ogni riga è separata dalla successiva mediante il carattere punto e virgola ‘;’. Con: a=[1 2 3 4; 5 6 7 8; 9 10 11 12]; si crea una matrice di tre righe e quattro colonne i cui elementi hanno i valori indicati. 1 a 2 3 4 5 6 7 8 9 10 11 12 La selezione di uno dei componenti dell'array si effettua facendo seguire al nome dell'array il valore dell'indice, o degli indici separati da virgola nel caso di array a più dimensioni, racchiusi tra una coppia di parentesi tonde. Sono esempi di funzioni di accesso: a(3,(I*J+2)) a(I,J) a(3) Nel caso di array con informazioni strutturate, si devono comporre le varie funzioni di accesso. Le operazioni consentite sugli array sono quelle che si possono fare sui componenti l'array stesso. In MATLAB anche i tipi semplici sono visti come casi particolari di quelli strutturati. In MATLAB le variabili intere, reali e caratteri sono gestite mediante un oggetto della classe double array di dimensione 1x1, mentre i caratteri mediante un oggetto della classe char array di dimensione 1x1, come si può vedere di seguito. a=1; Name Size a 1x1 Class double array a=1.5; Name Size a 1x1 Class double array a=’a’; Name Size a 1x1 Class char array 258 Capitolo settimo Le carte sintattiche di seguito mostrano come costruire array in MATLAB e come accedere agli elementi in essi contenuti. identificatore = reale [ ] carattere , identificatore = [ ‘ carattere ‘ ] , Figura 8 – Carta sintattica per la costruzione di array monodimensionali Figura 9 – Carta sintattica per per la costruzione di array bidimensionali identificatore indice ( indice ) ; Figura 10 – Carta sintattica l’accesso agli elementi di array Tipi Strutturati: il tipo record La struttura record è composta da un numero prefissato di componenti, anche di tipo differente. Ogni componente, detto anche campo del record, ha un suo nome e tipo. La definizione di un record prevede, allora, l'elencazione delle variabili, sia semplici che a loro volta strutturate, costituenti i campi componenti. Le operazioni consentite sul record sono quelle che si possono fare sui campi. In MATLAB la definizione di un record avviene definendo il nome del record e ponendo tra parentesi graffe le variabili costituenti i campi del record separate da Il linguaggio dell’ambiente MATLAB 259 virgole. Il valore delle variabili deve essere noto all’atto della definizione del record. MATLAB tratta i record come oggetti della classe non elementare cell array (array di celle). Una cella (campo del record) può essere a sua volta o di tipo char arry o di tipo double array. Figura 11 – Carta sintattica per la costruzione di record Nell’esempio successivo è mostrata la definizione del record ‘agenda’. Nominativo='Angelo Chianese'; Indirizzo='via Claudio 121'; citta='Napoli'; tel=’39081777777’; agenda={Nominativo,Indirizzo,citta,tel} L'accesso ad un campo si effettua specificando il nome del record e l’indice del campo compreso tra parentesi graffe. Se un campo è un'informazione strutturata, allora si deve per esso applicare la funzione di accesso caratteristica del tipo di appartenenza. agenda{1} agenda{3} Tipi Strutturati: il tipo intervallo Il tipo intervallo si definisce come sottoinsieme di un tipo predefinito. MATLAB tratta solo intervalli di natura numerica. Per definire un intervallo devono essere in generale specificati l’estremo iniziale (ei), quello finale (ef) ed un passo (p): in tale caso si generano tutti i valori che si ottengono calcolando x = x + p con valore iniziale di x uguale a ei e valore finale uguale a ef. Non è obbligatorio specificare il passo: nel caso venga omesso assume automaticamente il valore unitario. La sintassi per la generazione di un intervallo è la seguente: t=[ei:passo:ef] Con il tipo intervallo si introduce di fatto un array (vedi carta sintattica in figura 12) con tanti valori quanti sono quelli determinati dalla dichiarazione di intervallo. Esempi sono in tabella 1. Istruzione t=[1:10] t=[0:0.1:1] Intervallo generato 1 2 3 4 5 6 7 8 9 10 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1 Tabella 1 – Esempi di intervalli 260 Capitolo settimo identificatore intero o reale = : [ intero o reale array subrange : ] intero o reale Figura 12 – Carta sintattica per la costruzione di un intervallo Tipi Strutturati: il tipo file Un file è una sequenza di componenti tutti dello stesso tipo che risiede in modo permanente sui supporti di massa.Un file può essere visto come un dato strutturato utilizzabile per la registrazione di un qualsiasi numero di elementi. In MATLAB non esiste un tipo file, come vedremo in seguito, le operazioni di I/O da e verso file sono ottenute aprendo dei veri e propri canali di comunicazione tra il programma e la memoria di massa. 7.2.2. La dichiarazione di funzione MATLAB, come già anticipato, utilizza il concetto di funzione per la creazione di programmi e sottoprogrammi. La dichiarazione di funzione si compone di due parti, l'intestazione e il blocco di enunciati. L'intestazione è composta dalla parola chiave function, l'identificatore della funzione e due liste: la prima contenente gli eventuali parametri di uscita separati da virgola e racchiusi in parentesi quadre, la seconda contenente gli eventuali parametri di ingresso separati da virgola e racchiusi in parentesi tonde. Tali liste possono essere omesse nel caso di mancanza dei parametri di ingresso o uscita. Di seguito è riportato un esempio di intestazione di funzione a n uscite e a m ingressi. function [out1,out2,..outn]=nome_funzione(in1,in2,in3,…,inm) Figura 13 – Carta sintattica per l’intestazione di una funzione Una funzione, come qualsiasi elaborazione, comanda l'esecuzione di operazioni che, a partire da un insieme di dati iniziali, fornisce un insieme di risultati. Dati iniziali e risultati sono i parametri della funzione e sono anche detti argomenti della funzione. Si conviene che: Il linguaggio dell’ambiente MATLAB 261 - i parametri attuali sono forniti all'atto del richiamo e rappresentano i dati effettivamente elaborati; - i parametri formali sono quelli usati all'interno della procedura per poterla scrivere. Al momento del richiamo i parametri formali vengono sostituiti da quelli attuali. Per tale motivo il tipo dei parametri attuali deve corrispondere a quello dichiarato dei parametri formali. Le due liste di parametri della dichiarazione di funzione comprendono l'elenco dei parametri formali. Il linguaggio prevede due meccanismi di sostituzione, per valore e per riferimento o variabile. La sostituzione per valore prevede la valutazione del parametro attuale e l'assegnazione di tale valore a quello formale. La sostituzione per variabile o riferimento fa sì che i parametri formali coincidano con quelli attuali. Difatti, all'atto della chiamata, viene fornito al parametro formale l'indirizzo di memoria del parametro attuale in modo che tutte le operazioni sui parametri formali avvengono di fatto su quelli attuali. Per questo motivo i parametri attuali risentono delle elaborazioni fatte dalla procedura o funzione a differenza di quanto avviene per i parametri valore. In MATLAB i parametri di ingresso sono passati per valore quelli di uscita per riferimento. Il blocco contiene invece la specifica dell’algoritmo. 7.2.3. La specifica dell’algoritmo La specificazione delle azioni elaborative del programma si traduce in un enunciato o statement composto. Lo statement composto è formato da una sequenza di statement. Gli statement possono essere o meno separati dal carattere punto e virgola. Gli statement semplici che compongono la specificazione dell'algoritmo denotano azioni elaborative che devono essere svolte dall'esecutore macchina. Gli statement sono: - assegnazione, - if … else, - while, - switch … case, - for, - richiamo funzione. 7.2.4. Assegnazione L'enunciato di assegnazione è la più elementare forma di enunciato. Esso prescrive che venga dapprima calcolato il valore dell'espressione che si trova a destra del ‘=’, e che tale valore venga poi assegnato alla variabile che si trova a sinistra dello stesso simbolo. La variabile e il valore dell'espressione devono essere dello stesso tipo con l'eccezione che, se la variabile è reale, allora il tipo dell'espressione può anche essere intero. Una espressione è una formula o regola di calcolo che specifica sempre un valore detto risultato dell'espressione. È composta da operandi ed operatori. Gli operandi possono essere variabili, costanti o valori restituiti da una funzione. Gli operatori sono classificati in monadici o unari e in diadici o binari a seconda che prevedano uno o due operandi rispettivamente. Se in una espressione sono presenti più operandi, occorre specificare la priorità della loro esecuzione nel caso in cui 262 Capitolo settimo non sia esplicitata mediante l'introduzione nell'espressione delle parentesi. In particolare, il linguaggio prevede quattro classi di operatori a ciascuna delle quali è assegnata una precedenza nel senso che, in mancanza di parentesi, deve essere svolta prima l'operazione con operatore con precedenza maggiore. Gli operatori in ordine di precedenza decrescente sono: - l'operatore di negazione logica; - gli operatori detti moltiplicativi; - gli operatori detti additivi; - gli operatori di relazione. Ciascun operatore è applicabile solo a particolari tipi di operandi e il risultato della operazione risulterà del tipo stabilito dal linguaggio. Sono assegnazioni: alfa = 3.569 circonferenza = 2 * PI_GRECO * raggio area_rettangolo = base * altezza i=i-1 Figura 14 – Carta sintattica per l’enunciato di assegnazione 7.2.5. Richiamo di funzione Il richiamo della funzione determina l'esecuzione della funzione indicata. Affinché il richiamo sia corretto, si deve fornire una lista di parametri uguale, in numero e tipo, a quella specificata nella dichiarazione della funzione. La corrispondenza si stabilisce per ordine, nel senso che al primo parametro attuale è associato il primo formale, e così via. [parametri di uscita attuali]=nome_funzione(parametri di ingresso attuali) I parametri attuali devono essere forniti con rispetto del meccanismo di sostituzione e del tipo dei parametri indicati nella dichiarazione della procedura. In particolare, se si è fissato una sostituzione per valore, si possono usare come parametri attuali variabili, costanti ed espressioni di un tipo compatibile, secondo le regole viste a proposito dell'assegnazione di valore, con quello del parametro formale (si può ad esempio usare un parametro attaule di tipo sia intero che reale nel caso di un parametro formale di tipo reale, mentre deve essere assolutamente di tipo intero nel caso di parametro formale dichiarato intero). Nel caso di sostituzione per riferimento, si devono impiegare solo variabili e i tipi dei due parametri devono coincidere. Il linguaggio dell’ambiente MATLAB 263 Figura 15 – Carta sintattica per l’enunciato di richiamo di funzione Funzioni predefinite Il linguaggio mette a disposizione un ampio set di funzioni predefinite applicabili a tipi semplici e strutturati, per lo più di natura matematica. Tali funzioni possono essere introdotte all'interno di un programma, alcuni esempi sono: cos(x) sqrt(x) mean(v) max(v) det(A) Di particolare interesse sono le funzioni di conversione tra caratteri e reali e tra reali e caratteri: char(126) = ~ double(‘a’) = 97 Funzioni per l’I/O Il linguaggio fornisce alcune funzioni per le operazioni di lettura e scrittura dei valori delle variabili dai file INPUT e OUTPUT. In genere il file di INPUT rappresenta la tastiera del sistema di calcolo; quello di OUTPUT il monitor. Essi possono essere visti come organizzati in un testo costituito da sequenze di RIGHI separati tra loro dal carattere di fine rigo CR (Carriage Return). La sequenza di righi è terminata dal carattere di fine file. All'interno di ogni rigo sono distribuite le rappresentazioni delle informazioni opportunamente separate usando il carattere spazio. L'introduzione degli spazi può avvenire sia in maniera esplicita che implicita. In maniera esplicita il numero di caratteri di spazio viene specificato direttamente nelle istruzioni di lettura/scrittura. Nell'altro modo si assegna alla rappresentazione del valore delle variabili, mediante opportune dichiarazioni di formato, un numero di caratteri maggiore di quello necessario. Per tali motivi le procedure di lettura devono provvedere al riconoscimento, interpretazione e trasformazione delle sequenze di caratteri nei formati interni degli interi e reali; quelle di scrittura, alla trasformazione inversa di questi tipi in sequenze di caratteri 264 Capitolo settimo durante le operazioni di scrittura. Invece non sono richieste trasformazioni per la lettura e scrittura delle variabili di tipo carattere. Lettura da file di INPUT In MATLAB la lettura da un file avviene aprendo un canale di comunicazione con la memoria di massa attraverso il concetto di ‘apertura’ di un file. Un file deve essere aperto prima di essere utilizzato. L’apertura di un file avviene attraverso la funzione: [fid,message]=fopen (filename, permission) dove fid è l’identificativo del file, message è il messagio associato all’apertura del file, filename è il nome del file (path su disco del file) e permission è la modalità con cui si apre il file (in questo caso lettura). Dopo l’apertura, con l’identificatore del file è possibile eseguire le operazioni di lettura che avvengono con l’utilizzo della funzione: [A] = fscanf(fid, format) dove fid è l’identificativo del file, format indica il tipo degli elementi da leggere ed A è la matrice in cui si inseriscono gli elementi letti. La variabile format può assumere i seguenti valori a seconda del tipo di dato che si vuole leggere: %d %f, %g %c %s interi reali caratteri stringhe Di seguito è riportato un esempio di lettura da file: [fid,message]=fopen ('c:\file.txt','r); [v_input]=fscanf(fid,'%s') v_input = ciao La lettura di informazioni da standard input (tastiera) avviene attraverso la funzione: var_input=input(msg,[‘s’]) dove var_input rappresenta la variabile in cui verrà memorizzato il valore immesso da tastiera e msg è un messaggio visualizzato a video. Il parametro ‘s’ va specificato solo per le variabili di tipo stringa. var_input=input(‘Inserisci un numero:’) var_input=input(‘Inserisci una stringa:’,’s’) Scrittura su file di OUTPUT Anche la scrittura su file avviene aprendo un canale di comunicazione con la memoria di massa Un file deve essere aperto prima di essere utilizzato. Il linguaggio dell’ambiente MATLAB 265 Analogamente all’operazione di lettura l’apertura di un file avviene attraverso la funzione: [fid,message]=fopen (filename, permission) dove fid è l’identificativo del file, message è il messagio associato all’apertura del file, filename è il nome del file (path su disco del file) e permission è la modalità con cui si apre il file (in questo caso scrittura). A questo punto attraverso l’identificatore del file è possibile eseguire le operazioni di scrittura che avvengono con l’utilizzo della funzione: [count] = fprintf (fid, format,A) dove fid è l’identificativo del file, format indica il tipo degli elementi da scrivere, A è la matrice in cui sono presenti gli elementi da scrivere e count conta il numero di elementi effettivamente scritti su file. La variabile format assume gli stessi valori previsti nella lettura. Di seguito è riportato un esempio di scrittura su file: [fid,message]=fopen ('c:\file.txt','w'); stringa=’ciao’; [count]=fprintf(fid,stringa,'%s') count = 4 [fid,message]=fopen ('c:\file.txt','r'); [v_input]=fscanf(fid,'%s') v_input = ciao’ La scrittura di informazioni su standard output (monitor) avviene attraverso la funzione stessa fprintf senza specificare l’identificativo del file: fprintf (‘%s’,’Messaggio visualizzato a video’) 7.2.6. Gli enunciati di selezione Gli enunciati selettivi permettono la selezione di azioni elaborative in funzione del valore di una espressione booleana e non: ossia si valuta l'espressione e si sceglie l'istruzione successiva in funzione del valore trovato. Il costrutto IF-ELSE L'enunciato IF-ELSE (se - altrimenti) permette di effettuare la scelta tra due alternative in funzione del valore di una espressione booleana. Se il valore dell'espressione è TRUE, si sceglie l'alternativa introdotta dopo l’IF (l’insieme di enunciati compresi tra l’if e l’else), in caso contrario quella aperta dall'ELSE (l’insieme di enunciati compresi tra l’else e l’end). In entrambi i casi, dopo l'esecuzione del blocco selezionato, l'esecuzione stessa continua con l'enunciato successivo all'IF. È anche possibile usare una notazione abbreviata nel caso in cui non esista una delle due alternative, non specificando l'ELSE dell'enunciato. Nel liguagio MATLAB ogni costrutto if-else va terminato con la parola chiave end. Sono esempi di IF: 266 Capitolo settimo if (n>10) n=n+1; else n=n-1; end if (n>10) n=0; end; n=n-1; Si noti, infine, come l'uso appropriato dell'incolonnamento (o anche indentazione) delle strutture innestate l'una dentro l'altra migliori notevolmente la chiarezza del programma in quanto evidenzia l'ordine di esecuzione dei vari blocchi. Figura 16 – Carta sintattica per l’uso del costrutto if-else Il costrutto switch-case L'enunciato switch-case permette di scegliere nel caso in cui le alternative siano più di due. Esso consiste di un'espressione (di tipo numerico o carattere), detta selettore, e di una lista di enunciati, ciascuno dei quali identificato da uno o più valori costanti appartenenti al tipo del selettore preceduti dalla parola case. L'enunciato scelto è quello identificato dalla costante che è uguale al valore calcolato del selettore. Una volta eseguito l’enunciato scelto si esce dalla struttura switch. La scansione dei valori costanti, per cercare quello con valore uguale al selettore, avviene in modo sequenziale per cui vanno disposti per ultimi gli enunciati che hanno la minore probabilità di essere scelti. Qualora il valore calcolato dal selettore Il linguaggio dell’ambiente MATLAB 267 non è uguale ai valori costanti indicati viene eseguito l’enunciato preceduto dalla parola chiave otherwise se presente, altrimenti si esce dallo struttura. Sono esempi di switch-case: SWITCH(colore_semaforo) CASE ‘rosso’ msg=’stop’; CASE ‘verde’ msg=’vai’; CASE ‘giallo’ msg=’attenzione’; OTHERWISE msg=’semaforo rotto’; END SWITCH(numero mese) CASE {1,3,5,7,8,10,12} msg=’mese di 31 giorni’; CASE {4,6,9,11} msg=’mese di 30 giorni’; CASE 2 msg=’mese di 28 o 29 giorni’; OTHERWISE msg=’mese non valido’; END Infine, la struttura switch si presta in maniera brillante per far corrispondere insiemi di valori di tipo diverso. L'esempio che segue illustra l'utilità dello switch per creare la corrispondenza tra i valori numerici delle cifre (variabile valcifra) e il relativo carattere ASCII (variabile carcifra). SWITCH(valcifra) CASE 0 CASE 1 CASE 2 CASE 3 CASE 4 CASE 5 CASE 6 CASE 7 CASE 8 CASE 9 END carcifra='0'; carcifra='1'; carcifra='2'; carcifra='3'; carcifra='4'; carcifra='5'; carcifra='6'; carcifra='7'; carcifra='8'; carcifra='9'; 268 Capitolo settimo Figura 17 – Carta sintattica per l’uso del costrutto switch-case 7.2.7. Le strutture iterativa Il while Gli enunciati iterativi permettono l'esecuzione di un blocco di istruzioni un certo numero di volte. La terminazione della ripetizione avviene quando sono verificate certe condizioni che sono calcolate internamente al blocco. Se il numero di ripetizioni è noto a priori, la struttura viene anche detta ciclica o enumerativa. L'enunciato while è composto da una espressione logica e da uno o più enunciati da ripetere in funzione del valore di tale espressione. L'esecuzione del while comporta la valutazione dell'espressione e l'esecuzione degli enunciati compresi tra le parole while ed end nel caso in cui il valore calcolato dell'espressione sia TRUE. Il ciclo ha così termine quando l'espressione assume il valore FALSE per cui, se l'espressione risulta falsa alla prima esecuzione del while, tali enunciati non vengono mai eseguiti. Si osservi che una volta iniziato, il ciclo può terminare solo se all'interno di esso vi sono degli enunciati che modificano il valore di verità dell'espressione: cioè operano sulle variabili che ne fanno parte. È un esempio di while: somma=0; while (n<=20) somma=somma+vettore(n); n=n+1; end Il linguaggio dell’ambiente MATLAB 269 È possibile uscire da un ciclo WHILE in maniera incondizionata, cioè, in maniera indipendente dal valore di verità dell’espressione, attraverso la parola chiave BREAK. while ( statement composto espressione booleana ) end statement Figura 18 – Carta sintattica per l’uso del costrutto while Il for Il for è un enunciato iterativo enumerativo, detto anche ciclico, che deve essere usato ogni qualvolta il numero di ripetizioni è noto a priori. Si presenta nel seguente modo: for t = Tin : Tfin S end dove t viene detta contatore di ciclo e Tin e Tfin rappresentano il campo di variabilità di t e quindi il numero di volte che S viene ripetuto. La variabile contatore di ciclo e le variabili Tin ed Tfin possono appartenere ad uno dei tipi di cui è nota l'enumerazione. I valori di Tin ed Tfin sono calcolati all'inizio del ciclo e si ha che il blocco S è eseguito se e solo se, Tin risulta minore o uguale di Tfin, altrimenti si procede in sequenza con l'istruzione successiva al for. Il numero di volte che il blocco S viene ripetuto viene calcolato prima del ciclo ed è uguale a: (double(Tin)-double(Tfin)+1) Il valore di t non deve essere mai alterato dagli enunciati che compongono S (se ciò fosse consentito, si verrebbe a contraddire il presupposto che il numero di ripetizioni è noto a priori). Inoltre, eventuali modifiche di Tin ed Tfin interne ad S non alterano il numero di iterazioni del blocco S stesso. In MATLAB è possibile, così come avviene per la generazione degli, intervalli specificare anche il passo di iterazione, in tale caso il costrutto assume la seguente forma: for t = Tin :p: Tfin S end 270 Capitolo settimo dove p rapprenda il passo di iterazione (se non viene specificato è di default posto al valore 1). Quando il passo assume valore negativo e Tin > Tfin è possibile generare iterazioni in modo decrescente. Ad esempio attraverso l’istruzione: for t = 10 :-1:1 S end si esegue l’istruzione S comunque 10 volte con il contatore del ciclo che parte da 10 ed arriva ad 1 in modalità decrescente. Figura 19 – Carta sintattica per l’uso del costrutto for 7.3. Gli algoritmi di base in MATLAB Si vogliono ora di seguito mostrare degli algoritmi base dell’informatica realizzati con l’ausilio del linguaggio MATLAB. 7.3.1. Lo scambio di valore Descrizione del problema Scrivere una funzione che effettui lo scambio di valore tra due informazioni di un tipo T. Esempio: input: x=3, y=5 output: x=5, y=3 Descrizione dell’algoritmo Se si associano alle due informazioni due contenitori di liquidi, il problema diventa quello del loro trasferimento da un contenitore all’altro senza che i liquidi si mischiano. Risulta allora evidente che non c’è modo di effettuare tale trasferimento se non introducendo un terzo contenitore che permette di svuotare uno degli altri due contenitori. Si può così travasare il liquido il liquido dell’altro in quello che si è svuotato e infine ritrasferire in esso il liquido che è stato spostato per primo nel terzo contenitore. Si comprende allora che per scambiare due informazioni dello stesso tipo è necessario introdurre una terza variabile, ovviamente dello stesso tipo. Ad esempio date le tre informazioni: x [3] y [5] z[] si copia dapprima x in z: x [3] y [5] z[3] Il linguaggio dell’ambiente MATLAB 271 poi y in x: x [5] y [5] z[3] ed infine z in y: x [5] y [3] z[3] l’algoritmo assume la seguente forma : z=x; x=y; y=z; Descrizione delle funzioni Per la realizzazione dell’algoritmo si utilizza una sola funzione scambia con le seguenti caratteristiche: - Paramentri di input: [x,y] informazioni prima dello scambio - Parametri di output: [x,y] informazioni dopo lo scambio - Variabili locali: [z] informazione necessaria allo scambio Implementazione % FUNZIONE PER LO SCAMBIO DI VALORE % La funzione [x,y]=scambia(x,y) % permette di scambiare il valore delle variabili x ed y. % Esempio: % x=3, y=5 % [x,y]=scambia(x,y) % x=5, y=3 function [x,y]=scambia(x,y) % Intestazione funzione % effettua scambia per mezzo di una terza variabile z=x; x=y; y=z; Esempio d’uso Si riporta un esempio d’uso del programma mediante la shell (interprete dei comandi) dell’ambiente MATLAB. » help scambia FUNZIONE PER LO SCAMBIO DI VALORE La funzione [x,y]=scambia(x,y) permette di scambiare il valore delle variabili x ed y. Esempio: x=3, y=5 [x,y]=scambia(x,y) x=5, y=3 » x=9; » y=19; » [x,y]=scambia(x,y) x = 19 y=9 Si nota come tutta la documentazione che precede l’intestazione nel codice può essere utilizzata come help della funzione. 272 Capitolo settimo 7.3.2. L’inserimento in un vettore Descrizione del problema Scrivere una funzione per l’inserimento dell’informazione info nella posizione posiz di un vettore di n elementi di un certo tipo. Esempio: input: n=5, v= [10 50 20 40 35], info=66, posiz=3 output: n=6, v=[10 50 66 20 40 35] Descrizione dell’algoritmo Si può pensare al vettore come ad uno scaffale di libri allineati verso sinistra. Per far posto ad un nuovo libro in una posizione assegnata, bisogna spostare tutti i libri che si trovano in posizione successiva a quella data di una posizione verso destra. Lo spostamento può essere fatto un libro alla volta, cominciando da quello in ultima posizione. Dopo l’inserimento, il numero di libri o riempimento dello scaffale è aumentato di una unità. Si fa l’ipotesi che lo scaffale non sia mai pieno. Nel caso in cui si debba inserire la lettera a nella seconda posizione di un vettore di 4 elementi la procedura per l’inserimento è riportata di seguito: [x, y, z, f] si effettua lo spostamento (shift) di tutti gli elementi del vettore,a partire dall’ultimo fino a quello in seconda posizione, di un posto a destra: [x, , y, z, f] si copia il nuovo elemento nella seconda posizione e si incrementa di una unità il numero di elementi all’interno del vettore: [x, a, y, z, f] utilizzando la sintassi di Matlab, l’algoritmo assume la seguente forma : for i=n:-1:posiz v(i+1)=v(i); end; v(posiz)=info; n=n+1; Descrizione delle funzioni Per la realizzazione dell’algoritmo si utilizza una sola funzione inserimento con le seguenti caratteristiche: - Parametri di Input: [v, n, info, posiz] rispettivamente vettore, riempimento del vettore, elemento da inserire e rispettiva posizione, - Parametri di Output: [v, n] vettore e riempimento dopo l’inserimento - Variabili locali: [i] contatore di ciclo Implementazione % FUNZIONE PER L'INSERIMENTO IN UN VETTORE % La funzione [v,n]=inserimento(v,n,info,posiz) % permette di inserire nel vettore v di n elementi l'elemento info % alla posizione posiz. % Esempio: % v=[1 2 4 5], n=4, info=3, posiz=3 % [v,n]=inserimento(v,n,info,posiz) % v=[1 2 3 4 5], n=5 function [v,n]=inserimento(v,n,info,posiz) Il linguaggio dell’ambiente MATLAB 273 % sposta gli elementi del vettore di un posto a destra a partire dall’ultimo fino % a quello in posizione posiz for i=n:-1:posiz v(i+1)=v(i); end; % inserisci il nuovo elemento nella posizione specificata v(posiz)=info; % aumenta di una unità il numero di elementi nel vettore n=n+1; Esempio d’uso Si riporta un esempio d’uso del programma mediante la shell (interprete dei comandi) dell’ambiente MATLAB. » help inserimento FUNZIONE PER L'INSERIMENTO IN UN VETTORE La funzione [v,n]=inserimento(v,n,info,posiz) permette di inserire nel vettore v di n elementi l'elemento info alla posizione posiz. Esempio: v=[1 2 4 5], n=4, info=3, posiz=3 [v,n]=inserimento(v,n,info,posiz) v=[1 2 3 4 5], n=5 » v=[10 50 20 40 35]; » n=5; » info=66; » posiz=3; » [v,n]=inserimento(v,n,info,posiz) v =10 50 66 40 35 n=6 7.3.3. L’eliminazione in un vettore Descrizione del problema Scrivere una funzione per l’eliminazione dell’elemento in una posizione posiz data di un vettore di n elementi di un certo tipo. Esempio: input: n=5, v= [22 50 30 16 10], posiz=2 output: n=4, v=[22 30 16 10] Descrizione dell’algoritmo Si può pensare al vettore come ad uno scaffale di libri allineati verso sinistra. Dopo avere tolto il libro dalla posizione assegnata, bisogna spostare tutti quelli che si trovano alla sua destra di un posto verso sinistra per recuperare lo spazio resosi disponibile. Lo spostamento viene fatto un libro alla volta, cominciando da quello successivo al libro nella posizione assegnata e si termina quando si è arrivati all’ultimo libro. Dopo l’eliminazione, il numero di libri o riempimento dello scaffale è diminuito di una unità. Si fa l’ipotesi che lo scaffale contenga almeno un elemento. Nel caso in cui si l’ elemento in seconda posizione di un vettore di 4 elementi la procedura per l’eliminazione è riportata di seguito: [x, y, z, f] si effettua lo spostamento (shift) di tutti gli elementi del vettore, a partire da quello in terza posizione fino all’ultimo, di un posto a sinistra: [x, z, f] 274 Capitolo settimo si diminuisce di una unità il numero di elementi all’interno del vettore. Utilizzando la sintassi di Matlab, l’algoritmo assume la seguente forma: for i=posiz+1:1:n v(i-1)=v(i); end; n=n-1; Descrizione delle funzioni Per la realizzazione dell’algoritmo si utilizza una sola funzione eliminazione con le seguenti caratteristiche: - Parametri di Input: [v, n, posiz] vettore di partenza, riempimento del vettore e posizione dell’elemento da eliminare - Parametri di Output: [v,n] vettore e suo riempimento dopo l’eliminazione - Variabili locali: i contatore di ciclo Implementazione % FUNZIONE PER L'ELIMINAZIONE IN UN VETTORE % La funzione [v,n]=eliminazione(v,n,posiz) % permette di eliminare dal vettore v di n elementi l'elemento % alla posizione posiz. % Esempio: % v=[1 2 4 3], n=4, posiz=3 % [v,n]=eliminazione(v,n,posiz) % v=[1 2 3], n=3 function [v,n]=eliminazione(v,n,posiz) % sposta gli elementi del vettore di un posto a sinistra a partire % da quello successivo alla posizione posiz fino all'ultimo for i=posiz+1:1:n v(i-1)=v(i); end; % diminuisce di una unità il numero di elementi nel vettore n=n-1; % aggiorna il vettore in maniera da considerare solo i primi n elementi v=v(1:n); Esempio d’uso Si riporta un esempio d’uso del programma mediante la shell (interprete dei comandi) dell’ambiente MATLAB. » help eliminazione FUNZIONE PER L'ELIMINAZIONE IN UN VETTORE La funzione [v1,n]=eliminazione(v,n,posiz) permette di eliminare dal vettore v di n elementi l'elemento alla posizione posiz. Esempio: v=[1 2 4 3], n=4, posiz=3 [v,n]=eliminazione(v,n,posiz) v=[1 2 3], n=3 » v=[22 50 30 16 10]; » n=5; » posiz=2; Il linguaggio dell’ambiente MATLAB 275 » [v,n]=eliminazione(v,n,posiz) v = 22 30 16 10 n=4 7.3.4. L’eliminazione di una colonna da una matrice Descrizione del problema Scrivere una funzione per l’eliminazione di un data colonna di una matrice di N righe ed M colonne di un certo tipo. Esempio: input: N=3, M=4, A= [10 22 33 50; 20 54 80 41; 30 10 23 31], col=2 output: N=3, M=3, A=[10 33 50; 20 80 41; 30 23 31] Descrizione dell’algoritmo L’eliminazione di una colonna in una matrice si effettua applicando l’algoritmo di eliminazione di un elemento da un vettore a tutte le righe della matrice. Utilizzando la sintassi di Matlab, l’algoritmo assume la seguente forma: for j=col+1:1:M for i=1:N A(i,j-1)=A(i,j); end; end; Descrizione delle funzioni Per la realizzazione dell’algoritmo si utilizza una sola funzione elimina_colonna con le seguenti caratteristiche: - Parametri di Input: [A,N,M,colonna], matrice, numero di righe, numero di colonne ed indice della colonna da eliminare - Parametri di Output: [A,m] matrice e numero di colonne dopo l’eliminazione - Variabili locali: i, j indici di riga e colonna Implementazione % FUNZIONE PER L'ELIMINAZIONE DI UNA COLONNA IN UNA MATRICE % La funzione [A]=elimina_colonna(A,N,M,col) % permette di eliminare la colonna col dalla matrice A di N righe e M colonne. % Esempio: % A=[1 2 3; 4 5 6], N=2, M=3, col=3 % [A]=elimina_riga(A,N,M,col) % A=[1 2; 4 5] function [A]=elimina_colonna(A,N,M,col) % applica a tutte le righe della matrice l'algoritmo per l'eliminazione % di un elemento nella posizione col assegnata for j=col+1:1:M for i=1:N A(i,j-1)=A(i,j); end; end; % diminuisce di una unità il numero di colonne M=M-1; % aggiorna la matrice A=A(1:N,1:M); 276 Capitolo settimo Esempio d’uso Si riporta un esempio d’uso del programma mediante la shell (interprete dei comandi) dell’ambiente MATLAB. » help elimina_colonna FUNZIONE PER L'ELIMINAZIONE DI UNA COLONNA IN UNA MATRICE La funzione [A]=elimina_colonna(A,N,M,col) permette di eliminare la colonna col dalla matrice A di N righe e M colonne. Esempio: A=[1 2 3; 4 5 6], N=2, M=3, col=3 [A]=elimina_riga(A,N,M,col) A=[1 2; 4 5] » A=[10 22 33 50; 20 54 80 41; 30 10 23 31]; » N=3; » M=4; » col=2; » [A]=elimina_colonna(A,N,M,col) A= 10 33 50 20 80 41 30 23 31 7.3.5. L’eliminazione di una riga da una matrice Descrizione del problema Scrivere una funzione per l’eliminazione di un data riga di una matrice di N righe ed M colonne di un certo tipo. Esempio: input: N=3, M=4, A= [10 22 33 50; 20 54 80 41; 30 10 23 31], riga=1 output: N=2, M=4, A=[20 80 54 41; 30 10 23 31] Descrizione dell’algoritmo L’eliminazione di una riga in una matrice si effettua applicando l’algoritmo di eliminazione di un elemento da un vettore a tutte le colonne della matrice. Utilizzando la sintassi di Matlab, l’algoritmo assume la seguente forma : for i=riga+1:1:N for j=1:M A(i-1,j)=A(i,j); end; end; Descrizione delle funzioni Per la realizzazione dell’algoritmo si utilizza una sola funzione elimina_riga con le seguenti caratteristiche: - Parametri di Input: [A, N, M, riga] matrice, numero di righe, numero di colonne ed indice della riga da eliminare - Parametri di Output: [A,n] matrice e numero di righe dopo l’eliminazione - Variabili locali: i, j indici di riga e colonna Implementazione % FUNZIONE PER L'ELIMINAZIONE DI UNA RIGA IN UNA MATRICE % La funzione [A]=elimina_riga(A,N,M,riga) % permette di eliminare la riga assegnata dalla matrice A di N righe e M colonne. % Esempio: Il linguaggio dell’ambiente MATLAB % % % 277 A=[1 2 3; 4 5 6], N=2, M=3, riga=1 [A]=elimina_riga(A,N,M,riga) A=[4 5 6] function [A]=elimina_riga(A,N,M,riga) % applica a tutte le colonne della matrice l'algoritmo per l'eliminazione % di un elemento nella posizione riga assegnata for i=riga+1:1:N for j=1:M A(i-1,j)=A(i,j); end; end; % diminuisce di una unità il numero di righe N=N-1; % aggiorna la matrice A=A(1:N,1:M); Esempio d’uso Si riporta un esempio d’uso del programma mediante la shell (interprete dei comandi) dell’ambiente MATLAB. » help elimina_riga FUNZIONE PER L'ELIMINAZIONE DI UNA RIGA IN UNA MATRICE La funzione [A]=elimina_riga(A,N,M,riga) permette di eliminare la riga assegnata dalla matrice A di N righe e M colonne. Esempio: A=[1 2 3; 4 5 6], N=2, M=3, riga=1 [A]=elimina_riga(A,N,M,riga) A=[4 5 6] » A=[10 22 33 50; 20 54 80 41; 30 10 23 31]; » N=3; » M=4; » riga=1; » [A]=elimina_riga(A,N,M,riga) A= 20 54 80 41 30 10 23 31 7.3.6. La ricerca sequenziale Descrizione del problema Scrivere una funzione per la ricerca dell’informazione info in un vettore non ordinato avente n elementi di un certo tipo. In particolare la funzione deve produrre una indicazione sull’esistenza dell’informazione nel vettore e, nel caso esista, la posizione. Si fa l’ipotesi che nel caso in cui l’elemento non sia presente nel vettore la sua posizione è 0. Esempio: input: v= [9 5 6 8 7], info=8 output: msg=elemento presente, posiz=4 Descrizione dell’algoritmo La ricerca di un oggetto in un insieme si effettua fissando la strategia con la quale si possono effettuare i confronti dell’oggetto cercato con quelli dell’insieme. Se non si ha una conoscenza dell’insieme, l’unico modo di procedere è quello di 278 Capitolo settimo prendere un oggetto alla volta e confrontarlo con quello dato fino a quando non se ne trova uno uguale ad esso o è stato visionato l’intero insieme. Tale tipo di ricerca è detto sequenziale. La ricerca di un’informazione in un vettore che contiene valori tra loro non ordinati procede in maniera sequenziale. Si comincia a confrontare il primo elemento con quello ricercato. Poi il secondo, poi il terzo e così via fin quando il confronto non risulta verificato. In questo modo si verifica l’assenza dell’elemento dopo averlo confrontato con tutti gli elementi del vettore. Nel caso contrario, quando lo s incontra, si ferma l’indagine facendo l’ipotesi che nel vettore non esistano valori tra di loro uguali. Facciamo ora un esempio. Sia assegnato il vettore: v=[9 5 6 8 7] (1 2 3 4 5) (tra le parentesi tonde sono indicate le posizioni degli elementi nel vettore) e sia 4 il valore da ricercare. Allora l’algoritmo procede iterativamente nel seguente modo: passo confronto corrente trovato fine vettore (posizione nel vettore) 1 4==9 no no 2 4==5 no no 3 4==6 no no 4 4==8 no no 5 4==9 no si e la ricerca termina con l’indicazione che 4 non è presente nel vettore. Cerchiamo quindi il valore 8: passo (posizione nel vettore) 1 2 3 4 confronto corrente trovato fine vettore 8==9 8==5 8==6 8==8 no no no si no no no no e la ricerca termina con l’indicazione che il valore 8 è presente all’interno del vettore nella posizione 4. Utilizzando la sintassi di Matlab, l’algoritmo assume la seguente forma: trovato=0; i=0; posiz=0; while (!trovato & i<n) if (v[i]==info) trovato=1; posiz=i; msg='elemento presente'; end i++; end if trovato==0 msg='elemento non presente'; end Il linguaggio dell’ambiente MATLAB 279 Descrizione delle funzioni Per la realizzazione dell’algoritmo si utilizza una sola funzione ricercaseq con le seguenti caratteristiche: - Parametri di Input: [v, info] vettore d’ingresso ed informazione da ricercare - Parametri di Output: [msg, posiz] variabile contenete il messaggio sull’esito della ricerca e posizione dell’elemento all’interno del vettore - Variabili locali: [i, trovato, n] contatore di ciclo, variabile binaria che indica se il confronto ha avuto successo o meno e riempimento del vettore - Funzioni richiamate: length per il calcolo del riempimento del vettore. Implementazione % FUNZIONE PER LA RICERCA SEQUENZIALE DI UN ELEMENTO IN UN VETTORE % La funzione [msg,posiz]=ricercaseq(v,info) % permette di cercare l'elemento info nel vettore v. % Se la ricerca ha successo viene restituita la posizione posiz % dell'elemento nel vettore. % msg è una variabile che segnala l'esito della ricerca. % Esempio: % v=[1 2 8 9 5 6], info=8 % [msg,posiz]=ricercaseq(v,info) % msg= 'elemento presente', posiz=3 function [msg,posiz]=ricercaseq(v,info) % inizializzazione variabili locali % variabile binaria che indica il successo del confronto % (0 indica che l'elemento non è stato ancora trovato, % 1 il successo del confronto) trovato=0; % riempimento del vettore n=length(v); % cerca iterativamente l'elemento nel vettore confrontandolo con i suoi elementi % dal primo all'ultimo i=0; posiz=0; while (!trovato & i<n) if (v[i]==info) trovato=1; posiz=i; msg='elemento presente'; end i++; end if trovato==0 msg='elemento non presente'; end Esempio d’uso Si riporta un esempio d’uso del programma mediante la shell (interprete dei comandi) dell’ambiente MATLAB ed il confronto con la funzione find predefinita nel linguaggio. 280 Capitolo settimo » help ricercaseq FUNZIONE PER LA RICERCA SEQUENZIALE DI UN ELEMENTO IN UN VETTORE La funzione [msg,posiz]=ricercaseq(v,info) permette di cercare l'elemento info nel vettore v. Se la ricerca ha successo viene restituita la posizione posiz dell'elemento nel vettore. msg è una variabile che segnala l'esito della ricerca. Esempio: v=[1 2 8 9 5 6], info=8 [msg,posiz]=ricercaseq(v,info) msg= 'elemento presente', posiz=3 » v=[9 5 6 8 7]; » info=8; » [msg,posiz]=ricercaseq(v,info) msg = elemento presente posiz = 4 » posiz=find(v==8) posiz =4 7.3.7. La ricerca binaria Descrizione del problema Scrivere una funzione per la ricerca dell’informazione info in un vettore ordinato in senso crescente avente n elementi di un certo tipo. In particolare la funzione deve produrre una indicazione sull’esistenza dell’informazione nel vettore e, nel caso esista, la posizione. Si fa l’ipotesi che nel caso in cui l’elemento non sia presente nel vettore la sua posizione è 0. Esempio: input: v=[10 12 15 33 40 55], info=33 output: msg=elemento presente, posiz=4 Descrizione dell’algoritmo La ricerca di una informazione in un elenco si può effettuare come visto nel caso precedente confrontando uno dopo l’altro i valori dell’elenco con quello cercato fino a quando non si trova un elemento uguale o non si è analizzato l’intero elenco. Il metodo sequenziale richiede nel caso peggiore n confronti, dove n è il riempimento del vettore. Se l’elenco è ordinato, si ricorre allora ad una ricerca che ad ogni passo riduce l’insieme in cui cercare mediante un’opportuna tecnica di dimezzamento. In questo modo il caso peggiore richiede al più log2(n) confronti. Il metodo noto col nome di ricerca binaria si basa sui seguenti passi: 1. determinazione del punto medio dell’insieme in cui cercare; 2. confronto dell’elemento in questa posizione con quello da cercare; 3. individuazione dell’insieme in cui continuare la ricerca se il passo 2 non ha successo; esso risulta per vettore ordinato in senso crescente: il sottoinsieme degli elementi in posizioni successive al punto medio se il valore da cercare è maggiore di quello del punto medio, altrimenti quello caratterizzato da posizioni inferiori; 4. ripetizioni dei passi precedenti finché il passo 2 non risulti verificato o non sia possibile fissare un sottoinsieme in cui continuare la ricerca. Facciamo ora un esempio. Sia assegnato il vettore: Il linguaggio dell’ambiente MATLAB 281 v=[15 22 29 36 50 55] (1 2 3 4 5 6) (tra le parentesi tonde sono indicate le posizioni degli elementi nel vettore) e sia 21 il valore da ricercare. Allora l’algoritmo procede iterativamente nel seguente modo: fine trovato passo punto confronto prossimo medio corrente sottoinsieme di ricerca 1 29 21<29 [15, 22] no no 2 15 21>15 [22] no no 3 22 21<22 [22, 15] si no A questo punto la ricerca termina perché non esiste un sottoinsieme (l’estremo inferiore è maggiore del superiore) in cui continuare la ricerca, con l’indicazione che 21 non è presente nel vettore. Cerchiamo quindi il valore 50: fine trovato passo punto confronto prossimo medio corrente sottoinsieme di ricerca 1 29 50>29 [36, 50, 55] no no 2 50 50==50 Vuoto si si 3 22 21<22 [22, 15] si no e la ricerca termina con l’indicazione che il valore 50 è presente all’interno del vettore nella posizione 5. Utilizzando la sintassi di Matlab, l’algoritmo assume la seguente forma: posiz=0; trovato=0; ei=1; es=n; while (!trovato & ei<es+1) medio=floor((ei+es)/2); if (info==v(medio)) trovato=1; end; if (info<v(medio)) es=medio-1; else ei=medio+1; end; end; Descrizione delle funzioni: Per la realizzazione dell’algoritmo si utilizza una sola funzione ricercabin con le seguenti caratteristiche: - Parametri di Input: [v, info] vettore d’ingresso ed informazione da ricercare - Parametri di Output: [msg, posiz] variabile contenete il messaggio sull’esito della ricerca e posizione dell’elemento all’interno del vettore - Variabili locali: [i,trovato,n,ei,es,medio] contatore di ciclo, variabile binaria che indica se il confronto ha avuto successo o meno, riempimento 282 Capitolo settimo - del vettore, estremo inferiore, estremo superiore e punto medio del sottoinsieme di ricerca Funzioni richiamate: length per il calcolo del riempimento del vettore Implementazione % FUNZIONE PER LA RICERCA BINARIA DI UN ELEMENTO IN UN VETTORE % La funzione [msg,posiz]=ricercabin(v,info) % permette di cercare l'elemento info nel vettore v. % Se la ricerca ha successo viene restituita la posizione posiz % dell'elemento nel vettore. % msg è una variabile che segnala l'esito della ricerca. % Esempio: % v=[1 2 8 9 5 6], info=8 % [msg,posiz]=ricercabin(v,info) % msg= 'elemento presente', posiz=3 % Nota: la ricerca è applicabile solo se il vettore d'ingresso è ordinato function [msg,posiz]=ricercabin(v,info) % inizializza le varibili locali % posizione dell'elemento all'interno del vettore % (se l'elemento non viene trovato la sua posizione è nulla) posiz=0; % variabile binaria che indica il successo del confronto % (0 indica che l'elemento non è stato ancora trovato, % 1 il successo del confronto) trovato=0; % estremi superiore e inferiore dei sottoinsieme di ricerca % (inizialmente il sottoinsime di ricerca coincide con l'intero vettore) ei=1; es=length(v); % inizio ricerca % la condizione di continuazione della ricerca è che l'estremo inferiore del % sottoinsieme corrente di ricerca sia minore di quello superiore e che l’elemento % non sia stato ancora trovato while (!trovato & ei<es+1) % calcolo del punto medio medio=floor((ei+es)/2); % se l'elmento viene trovato pone ad 1 la variabile trovato % e arresta la ricerca if (info==v(medio)) trovato=1; end; % se l'elmento non viene trovato aggiorna gli estremi del sottoinsieme di ricerca % a seconda che l'info da ricercare sia minore o maggiore del punto medio if (info<v(medio)) es=medio-1; else ei=medio+1; end; end; % a secona dell'esito della ricerca aggiorna la variabile msg if (trovato==1) msg='elemento presente'; posiz=medio; else msg='elemento non presente'; end; Il linguaggio dell’ambiente MATLAB 283 Esempio d’uso Si riporta un esempio d’uso del programma mediante la shell (interprete dei comandi) dell’ambiente MATLAB. » help ricercabin FUNZIONE PER LA RICERCA BINARIA DI UN ELEMENTO IN UN VETTORE La funzione [msg,posiz]=ricercabin(v,info) permette di cercare l'elemento info nel vettore v. Se la ricerca ha successo viene restituita la posizione posiz dell'elemento nel vettore. msg è una variabile che segnala l'esito della ricerca. Esempio: v=[1 2 8 9 5 6], info=8 [msg,posiz]=ricercabin(v,info) msg= 'elemento presente', posiz=3 Nota: la ricerca è applicabile solo se il vettore d'ingresso è ordinato » v=[15 22 29 36 50 55]; » info=21; » [msg,posiz]=ricercabin(v,info) msg = elemento non presente posiz = 0 » info=50; » [msg,posiz]=ricercabin(v,info) msg = elemento presente posiz = 5 7.3.8. Il valore massimo in un vettore Descrizione del problema Scrivere una funzione che determini il massimo di un vettore formato da n elementi di un certo tipo. Esempio: input: v=[10 55 20 11 30] output: max=55 Descrizione dell’algoritmo L’individuazione del massimo in un insieme si effettua osservando uno dopo l’altro gli elementi che lo compongono e memorizzando di volta in volta il valore più grande. In particolare, si inizia facendo l’ipotesi che il massimo sia il primo elemento del vettore e successivamente lo si confronta con i restanti valori del vettore. Ogni volta che si incontra un valore più grande, si effettua l’aggiornamento del massimo. Facciamo ora un esempio. Sia assegnato il vettore: v=[3 5 6 8 7] (1 2 3 4 5) (tra le parentesi tonde sono indicate le posizioni degli elementi nel vettore). Allora l’algoritmo procede iterativamente nel seguente modo: massimo posizione vettore max=3 2 confronto (max<elemento corrente del vettore) 3<5 esito confronto aggiornamento massimo Vero si 284 Capitolo settimo max=5 max=6 max=8 3 4 5 5<6 6<8 8<7 Vero vero falso si si no Utilizzando la sintassi del nostro PDL, l’algoritmo assume la seguente forma: max=v(1); for i=2:n if (v(i)>max) max=v(i); end end Descrizione delle funzioni Per la realizzazione dell’algoritmo si utilizza una sola funzione massimo con le seguenti caratteristiche: - Parametri di Input: [v] vettore d’ingresso - Parametri di Output: [max] variabile contenete il massimo del vettore - Variabili locali: [i, n] contatore di ciclo e riempimento del vettore - Funzioni richiamate: length per il calcolo del riempimento del vettore Implementazione % FUNZIONE PER IL CALCOLO DEL MASSIMO DI UN VETTORE % La funzione [max]=massimo(v) % permette di calcolare l'elemento massimo max del vettore v. % Esempio: % v=[2 4 8 7 5] % [max]=massimo(v) % max=8 function [max]=massimo(v) % calcolo riempimento vettore n=length(v); % inizializza il massimo al primo elemento del vettore max=v(1); % iterativamente scorre il vettore e verifica se ci sia qualche % elemento maggiore del massimo for i=2:n % se l'elmento corrente è maggiore del massimo aggiorna il massimo if (v(i)>max) max=v(i); end end Esempio d’uso Si riporta un esempio d’uso del programma mediante la shell (interprete dei comandi) dell’ambiente MATLAB. » help massimo FUNZIONE PER IL CALCOLO DELA MASSIMO DI UN VETTORE La funzione [max]=massimo(v) permette di calcolare l'elemento massimo max del vettore v. Esempio: v=[2 4 8 7 5] [max]=massimo(v) Il linguaggio dell’ambiente MATLAB 285 max=8 » v=[3 5 6 8 7]; » [max]=massimo(v) max = 8 7.3.9. La posizione del valore minimo di un vettore Descrizione del problema Scrivere una funzione che determini la posizione del valore minimo di un vettore formato da n elementi di un certo tipo. Esempio: input: v=[10 55 20 11 30] output: posiz=1 Descrizione dell’algoritmo L’individuazione della posizione del minimo in un insieme si effettua osservando uno dopo l’altro gli elementi che lo compongono e memorizzando di volta in volta la posizione dell’elemento avente il valore più piccolo. In particolare, si inizia facendo l’ipotesi che la posizione del minimo sia quella del primo elemento del vettore. Successivamente si confronta l’elemento nella presunta posizione di minimo con i restanti valori del vettore. Ogni volta che si incontra un valore più piccolo, si effettua l’aggiornamento della posizione in modo che alla fine dei confronti si abbia la posizione del minimo. Si fa l’ipotesi che nel vettore non esistano elementi uguali. Facciamo ora un esempio. Sia assegnato il vettore: v=[9 5 6 3 7] (1 2 3 4 5) (tra le parentesi tonde sono indicate le posizioni degli elementi nel vettore). Allora l’algoritmo procede iterativamente nel seguente modo: posiz. minimo posizione vettore posiz=1 posiz=2 posiz=2 posiz=4 2 3 4 5 confronto (min>elemento corrente del vettore) 9>5 5>6 5>3 3>7 esito confronto aggiornamento posizione minimo vero falso vero falso si no si no Utilizzando la sintassi di Matlab, l’algoritmo assume la seguente forma : posiz=1; for i=2:n if (v(posiz)>v(i)) posiz=i; end end Descrizione delle funzioni Per la realizzazione dell’algoritmo si utilizza una sola funzione posmin con le seguenti caratteristiche: - Parametri di Input: [v] vettore d’ingresso 286 Capitolo settimo - Parametri di Output: [posiz] variabile contenete il massimo del vettore Variabili locali: [i, n] contatore di ciclo e riempimento del vettore Funzioni richiamate: length per il calcolo del riempimento del vettore Implementazione % FUNZIONE PER IL CALCOLO DELLA POSIZIONE DEL MINIMO DI UN VETTORE % La funzione [posiz]=posmin(v) % permette di calcolare la posizione dell'elemento minimo posiz del vettore v. % Esempio: % v=[2 4 8 7 5] % [posiz]=posmin(v) % posiz=1 function [posiz]=posmin(v) % calcolo riempimento vettore n=length(v); % inizializza la posizione del minimo alla prima nel vettore posiz=1; % iterativamente scorre il vettore e verifica se ci sia qualche elemento % minore di quello nella presunta posizione di minimo for i=2:n % se l'elmento corrente è minore aggiorna la posizione if (v(posiz)>v(i)) posiz=i; end end Esempio d’uso Si riporta un esempio d’uso del programma mediante la shell (interprete dei comandi) dell’ambiente MATLAB. » help posmin FUNZIONE PER IL CALCOLO DELLA POSIZIONE DEL MINIMO DI UN VETTORE La funzione [posiz]=posmin(v) permette di calcolare la posizione dell'elemento minimo posiz del vettore v. Esempio: v=[2 4 8 7 5] [posiz]=posmin(v) posiz=1 » v=[9 5 6 3 7]; » [posiz]=posmin(v) posiz = 4 7.3.10. Minimo e massimo in una matrice Descrizione del problema Scrivere una funzione che determini il valore minimo e massimo di una matrice formata da n x m elementi di un certo tipo. Esempio: input: A=[1 2 3 4; 8 7 6 5] output: max=8, min=1 Descrizione dell’algoritmo L’individuazione del minimo e del massimo in un insieme si effettua osservando uno dopo l’altro gli elementi che lo compongono e memorizzando di volta in volta la posizione i valori dell’elemento avente il valore più piccolo e più grande. In Il linguaggio dell’ambiente MATLAB 287 particolare, si inizia facendo l’ipotesi che il primo elemento della matrice sia contemporaneamente il massimo ed il minimo e successivamente lo si confronta con i restanti valori della matrice. Ogni volta che si incontra un valore più piccolo o più grande, si effettua l’aggiornamento del minimo e del massimo. Utilizzando la sintassi di Matlab, l’algoritmo assume la seguente forma : min=A(1,1); max=min; for i=1:n for j=1:m if (A(i,j)>max) max=A(i,j); else if (A(i,j)<min) min=A(i,j); end end end end Descrizione delle funzioni Per la realizzazione dell’algoritmo si utilizza una sola funzione minmax con le seguenti caratteristiche: - Parametri di Input: [A] matrice d’ingresso - Parametri di Output: [min, max] variabili contenete il minimo e massimo della matrice - Variabili locali: [i,j,n,m] indice di riga, indice di colonna, numero di righe e numero di colonne della matrice - Funzioni richiamate: size per il calcolo del numero di righe di colonne della matrice Implementazione % FUNZIONE PER IL CALCOLO DEL MINIMO e MASSIMO DI UNA MATRICE % La funzione [min,max]=minmax(A) % permette di calcolare il minimo e massimo (min e max) di una matrice A. % Esempio: A=[1 2;3 4] % [min,max]=minmax(A) % min=1, max=4 function [min,max]=minmax(A) % calcolo dimensioni matrice [n,m]=size(A); % inizializza il massimo e il minimo al primo elmento della matrice min=A(1,1); max=min; % iterativamente scorre la matrice e verifica se ci sia qualche % elemento minore o maggiore % di quello minimo o massimo presunto for i=1:n for j=1:m % se l'elmento corrente è maggiore del massimo aggiorna il massimo if (A(i,j)>max) max=A(i,j); else 288 Capitolo settimo % se l'elmento corrente è minore del minimo aggiorna il minimo if (A(i,j)<min) min=A(i,j); end end end end Esempio d’uso Si riporta un esempio d’uso del programma mediante la shell (interprete dei comandi) dell’ambiente MATLAB. » help minmax FUNZIONE PER IL CALCOLO DEL MINIMO e MASSIMO DI UNA MATRICE La funzione [min,max]=minmax(A) permette di calcolare il minimo e massimo (min e max) di una matrice A. Esempio: A=[1 2;3 4] [min,max]=minmax(A) min=1, max=4 » A=[1 2 3 4;8 7 6 5]; » [min,max]=minmax(A) min = 1, max = 8 7.3.11. Ordinamento di un vettore col metodo della selezione Descrizione del problema Scrivere una funzione per l’ordinamento in senso crescente di un vettore di n elementi di un certo tipo. Esempio: input: v=[30 10 50 12 22] output: v=[10 12 22 30 50] Descrizione dell’algoritmo Ordinare un vettore in senso crescente significa imporre che scelti due qualsiasi indici i e j, tali che i<j risulti v(i)<v(j). Un meccanismo di ordinamento consiste nel dividere l’insieme da ordinare in due parti: una ordinata ed una disordinata. Si procede allora estraendo un elemento alla volta dall’insieme disordinato e accodandolo a quello ordinato in modo che l’ordinamento non venga alterato. All’inizio l’insieme ordinato è vuoto e l’algoritmo termina quando contiene tutti gli elementi del vettore. Vi sono vari modi di estrarre l’elemento dall’insieme disordinato. Quello che di seguito presentiamo seleziona ad ogni passo l’elemento da accodare. In particolare seleziona il minimo dall’insieme disordinato e lo accoda all’insieme ordinato. In tal modo si viene ad ogni passo a scegliere il valore più grandi di quelli che lo precedono e contemporaneamente più piccolo di quelli che lo seguono. Facciamo ora un esempio. Sia assegnato il vettore: v=[9 2 1 4 7] (1 2 3 4 5) (tra le parentesi tonde sono indicate le posizioni degli elementi nel vettore). Allora l’algoritmo procede iterativamente nel seguente modo: Parte Parte posizione accodamento nuovo Il linguaggio dell’ambiente MATLAB Ordinata Disordinata Vuota (1) (1 2) (1 2 3) (1 2 3 4) (1 2 3 4 5) (2 3 4 5) (3 4 5) (4 5) (5) minimo (valore minimo) 3(1) 2(2) 4(4) 5(7) 5(9) 289 [1] [1 2] [1 2 4] [1 2 4 7] [1 2 4 7 9] intervallo di ricerca del minimo [9 2 4 7] [9 4 7] [9 7] [9] vuoto Si noti che l’algoritmo termina quando l’insieme disordinato si riduce ad un unico elemento. Inoltre poiché si usa lo stesso vettore per la parte ordinata e disordinata, l’accodamento viene effettuato scambiando di posto il minimo ed il valore che occupa la posizione di accodamento. Utilizzando la sintassi di Matlab, l’algoritmo assume la seguente forma : for i=1:n-1 imin=i; for j=i+1:n if (v(j)<v(imin)) imin=j; end end temp=v(i); v(i)=v(imin); v(imin)=temp; end Descrizione delle funzioni Per la realizzazione dell’algoritmo si utilizza una sola funzione ordina con le seguenti caratteristiche: - Parametri di Input: [v] vettore d’ingresso - Parametri di Output: [v] vettore ordinato - Variabili locali: [i,n,imin,temp] contatore di ciclo, riempimento del vettore, indice del minimo corrente dell’insieme disordinato e variabile d’appoggio per effettuare lo scambio ai fini dell’ accodamento - Funzioni richiamate: length per il calcolo del riempimento del vettore Implementazione % FUNZIONE PER L'ORDINAMENTO DI UN VETTORE % La funzione [v]=selectsort(v) % permette di ordinare in senso crescente il vettore v. % Esempio: v=[4 1 3 2 5] % [v]=selectsort(v) % v=[1 2 3 4 5] function [v]=selectsort(v) % calcolo riempimento vettore n=length(v); % doppio ciclo per l'ordinamento for i=1:n-1 290 Capitolo settimo imin=i; % determina il minimo per la parte disordinata for j=i+1:n if (v(j)<v(imin)) imin=j; end end % effettua l'accodamento scambiando di posto l'elemento minimo e il valore % che occupa la posizione di accodamento temp=v(i); v(i)=v(imin); v(imin)=temp; end Esempio d’uso Si riporta un esempio d’uso del programma mediante la shell (interprete dei comandi) dell’ambiente MATLAB. » help selectsort FUNZIONE PER L'ORDINAMENTO DI UN VETTORE La funzione [v]=selectsort(v) permette di ordinare in senso crescente il vettore v. Esempio: v=[4 1 3 2 5] [v]=selectsort(v) v=[1 2 3 4 5] » v=[9 2 1 4 7]; » [v]=selectsort(v) v=1 2 4 7 9 7.3.12. Ordinamento di un vettore col metodo del gorgogliamento Descrizione del problema Scrivere una funzione per l’ordinamento in senso crescente di un vettore di n elementi di un certo tipo. Esempio: input: v=[30 10 50 12 22] output: v=[10 12 22 30 50] Descrizione dell’algoritmo Ordinare un vettore in senso crescente significa imporre che scelti due qualsiasi indici i e j, tali che i<j risulti v(i)<v(j). Un meccanismo di ordinamento consiste nel dividere l’insieme da ordinare in due parti: una ordinata ed una disordinata. Si procede allora estraendo un elemento alla volta dall’insieme disordinato e accodandolo a quello ordinato in modo che l’ordinamento non venga alterato. All’inizio l’insieme ordinato è vuoto e l’algoritmo termina quando contiene tutti gli elementi del vettore. Vi sono vari modi di estrarre l’elemento dall’insieme disordinato. Quello che di seguito presentiamo fa in modo che il minimo dell’insieme disordinato gorgogli in prima posizione così come una bolla d’aria sale dal fondo alla superficie. In questo modo si viene ad ogni passo a scegliere il valore più grandi di quelli che lo precedono e contemporaneamente più piccolo di quelli che lo seguono. Il gorgogliamento viene eseguito confrontando a due a due gli elementi dell’insieme disordinato ed effettuando lo scambio di posizione Il linguaggio dell’ambiente MATLAB 291 quando le condizioni di ordinamento non sono rispettate. Se dopo uno scorrimento non si eseguono scambi si deduce che il vettore è ordinato e l’algoritmo può terminare. Facciamo ora un esempio. Sia assegnato il vettore: v=[9 2 1 4 7] (1 2 3 4 5) (tra le parentesi tonde sono indicate le posizioni degli elementi nel vettore). Allora l’algoritmo procede iterativamente nel seguente modo: Parte Parte Confronto gorgogliamento Ordinata Disordinata [9 2 1 4 7] Vuota (1 2 3 4 5) 4<7(4,5) [9 2 1 4 7] 1<4 (3,4) [9 1 2 4 7] 2>1(2,3) -> scambio [1] [9 2 4 7] 9>1(1,2) -> scambio [1] [9 2 4 7] (1) (2 3 4 5) 4<7(4,5) [1] [9 2 4 7] 2<4 (3,4) [1 2 ] [9 4 7] 9>2 (2,3) -> scambio [1 2] [9 4 7] (1 2) (3 4 5) 4<7(4,5) [1 2 4] [9 7] 9>4 (3,4) -> scambio (1 2 3) (4 5) 9>7 (4,5) -> [1 2 4 7] [9] scambio (1 2 3 4) (5) Nessuno [1 2 4 7 9] Si noti che l’algoritmo termina quando l’insieme disordinato si riduce ad un unico elemento. Inoltre se si applica l’algoritmo ad un vettore già ordinato si ha che non vengono effettuati scambi e quindi questa condizione può essere utile per arrestare l’algoritmo stesso. Utilizzando la sintassi di Matlab, l’algoritmo assume la seguente forma: scambio=1; i=1; while (i<n & scambio==1) scambio=0; for j=i+1:n if (v(j)<v(i)) temp=v(i); v(i)=v(j); v(j)=temp; scambio=1; end end i=i+1; end Descrizione delle funzioni Per la realizzazione dell’algoritmo si utilizza una sola funzione ordina con le seguenti caratteristiche: - Parametri di Input: [v] vettore d’ingresso - Parametri di Output: [v] vettore ordinato 292 Capitolo settimo - - Variabili locali: [i,n,scambio,temp] contatore di ciclo, riempimento del vettore, variabile binaria che indica se è avvenuto qualche scambio nell’iterazione corrente e variabile d’appoggio per effettuare lo scambio ai fini dell’accodamento nell’insieme ordinato Funzioni richiamate: length per il calcolo del riempimento del vettore Implementazione % FUNZIONE PER L'ORDINAMENTO DI UN VETTORE % La funzione [v]=bubblesort(v) % permette di ordinare in senso crescente il vettore v. % Esempio: % v=[4 1 3 2 5] % [v]=bubblesort(v) % v=[1 2 3 4 5] function [v]=bubblesort(v) % variabili locali % calcolo del riempimento n=length(v); % variabile locale che segnala se nell'iterazione corrente ci sono stati scambi scambio=1; % contatore di ciclo i=1; % ciclo di ordinamento while (i<n & scambio==1) scambio=0; for j=i+1:n % spostamento del valore minimo verso la parte ordinata if (v(j)<v(i)) temp=v(i); v(i)=v(j); v(j)=temp; scambio=1; end end i=i+1; end Esempio d’uso Si riporta un esempio d’uso del programma mediante la shell (interprete dei comandi) dell’ambiente MATLAB. » help bubblesort FUNZIONE PER L'ORDINAMENTO DI UN VETTORE La funzione [v]=bubblesort(v) permette di ordinare in senso crescente il vettore v. Esempio: v=[4 1 3 2 5] [v]=bubblesort(v) v=[1 2 3 4 5] » v=[9 2 1 4 7]; » [v]=bubblesort(v) v=1 2 4 7 9 Il linguaggio dell’ambiente MATLAB 7.4. 293 Esercizi completi Si vogliono ora di seguito mostrare ulteriori esempi di progetti di programmi realizzati in Matlab. Per semplicità si riporta per ogni esercizio la sola descrizione del problema e l’implementazione del programma risolutivo. Esercizio 1 Dato un vettore di numeri reali scrivere una funzione che: 1. Determini la somma degli elementi del vettore. 2. Determini la media degli elementi del vettore. 3. Determini il minimo degli elementi del vettore. 4. Determini il massimo degli elementi del vettore. function [somma,media,minimo,massimo]=esercizio1(v) n=length(v); somma=v(1); massimo=somma; minimo=somma; for i=2:n if (v(i)<minimo) minimo=v(i); end if (v(i)>massimo) massimo=v(i); end somma=somma+v(i); end media=somma/n; Esercizio 2 Data una matrice di numeri reali scrivere una funzione che: 1. Memorizzi, nel caso la matrice sia quadrata, in un vettore di uscita gli elementi della diagonale. 2. Calcoli, nel caso di matrice quadrata, la media degli elementi della diagonale. 3. Segnali errore nel caso in cui la matrice sia non quadrata. function [msg,media]=esercizio2(A) [n,m]=size(A); if (n==m) media=0; for i=1:n media=media+A(i,i); end media=media/n; msg=''; else msg='error: matrix must be square'; media='NaN'; end 294 Capitolo settimo Esercizio 3 Dato un vettore di numeri reali ordinato e due suoi elementi a e b, tali che a<=b, scrivere una funzione che memorizzi in un vettore di uscita tutti gli elementi del vettore di in ingresso compresi tra a e b. Esempio.: v_in=[1,3,6,8,70,555], a=6, b=70, v_out=[70]. function [vout]=esercizio3(vin,a,b) n=length(vin); if (a<b) indice1=1; indice2=1; for i=1:n if (vin(i)==a) indice1=i; break; end end for j=1:n if (vin(j)==b) indice2=j; break; end end vout=vin(indice1+1:1:indice2-1); else vout='NaN'; end Esercizio 4 Dato un vettore di caratteri (stringa) scrivere una funzione che: 1. Memorizzi in un vettore di uscita la stringa d’ingresso invertita. 2. Calcoli il tipo di lettera (a,b,c,d,e,f,…) e il numero di caratteri presenti nella stringa d’ingresso. Esempio: v_in=’hello’, v_out=’olleh’, ris=a 0, b 0,…,e 1, …, h 1,…, l 2, …, o 1, …, z 0 function [vout]=esercizio4(vin) n=length(vin); alfabeto=['a','b','c','d','e','f','g','h','i','l','m','n','o','p','q','r','s','t','u','v','z']; for i=1:n vout(n+1-i)=vin(i); end % la funzione zeros crea 1 array di interi della stessa dimensione del vettore % alfabeto ed inizializzati a 0 cont=zeros(1,length(alfabeto)); for i=1:n for j=1:length(alfabeto) if (vin(i)==alfabeto(j)) cont(j)=cont(j)+1; break; end end end for i=1:length(alfabeto) Il linguaggio dell’ambiente MATLAB 295 fprintf('%c ',alfabeto(i)); fprintf('%d\n',cont(i)); end Esercizio 5 Dato un vettore di numeri reali scrivere una funzione che: 1. Effettui lo shift (spostamento) a destra con l’inserimento di uno zero in testa degli elementi del vettore di ingresso (in questo caso si perde l’elemento di coda). Esempio.: v_in=[1,2,3,4], v_out=[0,1,2,3] 2. Effettui lo shift (spostamento) circolare a destra con l’inserimento dell’elemento di coda in testa. Esempio.: v_in=[1,2,3,4], v_out=[4,1,2,3] function [v_out1, v_out2]=esercizio5(v_in) n=length(v_in); for i=2:n v_out1(i)=v_in(i-1); v_out2(i)=v_in(i-1); end; v_out1(1)=0; v_out2(1)=v_in(n); Esercizio 6 Dato un vettore di numeri reali scrivere una funzione che: 1. Effettui lo shift (spostamento) a sinistra con l’inserimento di uno zero in coda degli elementi del vettore di ingresso (in questo caso si perde l’elemento di testa). Esempio.: v_in=[1,2,3,4], v_out=[2,3,4,0] 2. Effettui lo shift (spostamento) circolare a sinistra con l’inserimento dell’elemento di testa in coda. Esempio.: v_in=[1,2,3,4], v_out=[2,3,4,1] function [v_out1, v_out2]=esercizio6(v_in) n=length(v_in); for i=1:n-1 v_out1(i)=v_in(i+1); v_out2(i)=v_in(i+1); end; v_out1(n)=0; v_out2(n)=v_in(1); Esercizio 7 Dato un vettore di numeri interi scrivere una funzione che: 1. Memorizzi in primo vettore di uscita solo i numeri pari. 2. Memorizzi in un secondo vettore di uscita solo i numeri dispari. Esempio.: v_in=[1 2 33 11 8], v_out1=[2 8], vout2=[1 33 11] function [pari,dispari]=esercizio7(v) n=length(v); indice_pari=1; indice_dispari=1; for i=1:n % la funzione mod(x,y) restituisce il resto in modulo della divisione di x per y if mod(v(i),2)==0 296 Capitolo settimo pari(indice_pari)=v(i); indice_pari=indice_pari+1; else dispari(indice_dispari)=v(i); indice_dispari=indice_dispari+1; end end Esercizio 8 Dato una matrice di numeri reali scrivere una funzione che: 1. Memorizzi in un primo vettore di uscita, una dietro l’altra, le righe pari. 2. Memorizzi in un secondo vettore di uscita, una dietro l’altra, le righe dispari. Es.: A=[1 2 3;4 5 6;7 8 9], v1=[4 5 6], v2=[1 2 3 7 8 9] function [v1,v2]=esercizio8(A) [n,m]=size(A); i1=1; i2=1; for i=1:n if mod(i,2)==0 for j=1:m v1(i1)=A(i,j); i1=i1+1; end else for j=1:m v2(i2)=A(i,j); i2=i2+1; end end end Il linguaggio dell’ambiente MATLAB 297 Esercizio 9 Sia assegnato un vettore di numeri reali su un file di ingresso, scrivere una funzione che: 1. Legga e memorizzi in un vettore gli elementi dal file d’ingresso. 2. Effettui l’ordinamento del vettore utilizzando una funzione esterna. 3. Scriva su un file d’uscita il vettore ordinato. In questo caso si utilizzano due funzioni, una per l’ordinamento e una per l’input/output da file. function [v]=selectsort(v) n=length(v); for i=1:n-1 imin=i; for j=i+1:n if (v(j)<v(imin)) imin=j; end end temp=v(i); v(i)=v(imin); v(imin)=temp; end function esercizio9(pathin,pathout) fid1=fopen(pathin,'r'); v=fscanf(fid1,'%g'); v=selectsort(v); fclose(fid1); fid2=fopen(pathout,'w'); fprintf(fid2,'%g ',v); fclose(fid2); Nota: per l’ordinamento si è usata la funzione selectsort vista in precedenza. Esercizio 10 Sia assegnato un vettore “sparso” (contente molti elementi nulli) di numeri reali su un file di ingresso, scrivere una funzione che: 1. Legga e memorizzi su un vettore gli elementi da file d’ingresso. 2. A partire dal vettore ottenuto costruisca un vettore di uscita avente come elementi solo quelli non nulli (split). function [vout]=esercizio10(pathin) fid1=fopen(pathin,'r'); v=fscanf(fid1,'%g'); n=length(v); j=1; for i=1:n if v(i)~=0 vout(j)=v(i); j=j+1; end end 298 Capitolo settimo Esercizio 11 Scrivere un programma per il calcolo della media degli esami sostenuti e del voto maggiore.In questo caso si utilizzano tre funzioni, una per il calcolo del massimo di un vettore, una per il calcolo della media e una per la gestione dell’input/output. function [max]=massimo_esami(v_input) max=v_input(1); n=length (v_input); for i=2:n if (max<v_input(i)) max=v_input(i); end end function [media]=media_esami(v_input) n=length(v_input); media=0; for i=1:n media=media+v_input(i); end media=media/n; function esercizio11 fprintf('\nProgramma per la creazione di statistche sugli esami sostenuti\n'); fprintf('\nSezione per la raccolta dei dati anagrafici\n'); nome=input('Inserire nome:','s'); cognome=input('Inserire cognome:','s'); matr=input('Inserire matricola:','s'); fprintf('\nSezione per la raccolta dei dati relativi agli esami sostenuti\n'); num_esami=input('Inserire numero di esami sostenuti:'); for i=1:num_esami fprintf('Inserire votazione relativa esame '); fprintf('%d',i); voto_esami(i)=input(':'); end media=media_esami(voto_esami); voto_massimo=massimo_esami(voto_esami); fprintf('\nSezione relativa alle statistiche calcolate\n'); fprintf('\nLa media dei tuoi esami è: '); fprintf('%d',media); fprintf('\nLa tua votazione migliore è: '); fprintf('%d',voto_massimo); Capitolo ottavo La traduzione dei programmi 8.1. Introduzione La realizzazione di un programma è un'attività complessa che va dalla scelta di un algoritmo alla sua implementazione in un linguaggio di programmazione. Per il programmatore il linguaggio di programmazione si configura come una macchina virtuale che analizza il testo del programma e lo rende eseguibile secondo delle regole fissate dal linguaggio stesso. In realtà, la traduzione del testo del programma - detto anche testo sorgente o origine - in operazioni eseguibili sulla macchina reale, avviene secondo due modalità diverse: - mediante una preventiva traduzione del testo origine nel linguaggio della macchina reale e successiva esecuzione del programma tradotto in linguaggio macchina; - od attraverso una contestuale interpretazione del testo origine e sua esecuzione. In generale un traduttore trasforma un testo espresso in un certo linguaggio A in quello di un linguaggio B, detto oggetto o target: A è un linguaggio simbolico, B è un linguaggio prossimo a quello della macchina, ma non necessariamente coincidente con esso. Tradizionalmente, benché nei vari casi il processo di traduzione sia concettualmente lo stesso, i traduttori assumono nomi diversi secondo il tipo di linguaggio sorgente e del linguaggio oggetto. In particolare, si distinguono i seguenti traduttori. - i compilatori che sono i traduttori più diffusi e utilizzati. Il linguaggio sorgente è un linguaggio ad alto livello (ad esempio C, C++, Pascal, FORTRAN …), mentre il linguaggio oggetto è un linguaggio prossimo a quello macchina. - i pre-compilatori che traducono da linguaggio sorgente a linguaggio sorgente permettendo di estendere a basso costo le caratteristiche di un linguaggio esistente per il quale sia disponibile un traduttore. In tal caso, il processo di traduzione avviene in due fasi distinte: una prima traduzione, ad opera del pre-compilatore, in un linguaggio sorgente in cui i costrutti estesi sono ricondotti a sequenze elementari, e una seconda fase di traduzione, ad opera del compilatore, di questa forma espansa in linguaggio oggetto. 300 Capitolo ottavo - 8.2. gli assemblatori o assembler, che, invece, permettono di effettuare una traduzione da un linguaggio assemblativo simbolico al linguaggio macchina. Il processo di traduzione Un programma, dal momento in cui è ideato al momento in cui può essere eseguito, deve subire un insieme di trasformazioni, come mostrato in figura 1. Figura 1: Dalla scrittura del programma sorgente all’esecuzione A ciascuna trasformazione corrisponde una certa applicazione che la effettua. L’applicazione che permette di editare il testo di un programma, di visionarlo ed eventualmente di modificarlo è detto editor. Un editor offre un insieme di funzionalità per aiutare l’utente nelle scrittura del testo sorgente. In particolare, un editor deve in ogni modo garantire l’inserimento, la cancellazione, la visione, la ricerca e la modifica di parti del testo. Il testo sorgente prodotto dall’editor è memorizzato in forma permanente (ad esempio su un disco). Il compilatore è di contro, come già visto, l’applicazione che permette la traduzione da linguaggio sorgente a linguaggio oggetto. Il processo di compilazione produce un file oggetto. Il compilatore tuttavia non è in grado di mettere assieme le diverse componenti di un programma. Si osservi, ad esempio, il semplice programma C che stampa a video il messaggio ‘Salve mondo’. #include <stdio.h> main() { printf(“salve, mondo \n”); } Il prototipo della funzione printf è definita nel file header stdio.h, che è una delle librerie messe a disposizione dal linguaggio per svincolare il programmatore dalla conoscenza dell’insieme di operazioni, sicuramente non banali, per la scrittura di una stringa su video. Quando il compilatore incontra il nome della funzione printf, si limita a prevedere per essa un richiamo, senza però essere in grado di stabilire dove dovrà La traduzione dei programmi 301 essere trasferito il controllo durante l’esecuzione. Un compilatore non è quindi in grado di risolvere: - l’aggregazione di componenti software scritti in momenti diversi da persone diverse ed eventualmente in linguaggi diversi; - il collegamento tra la definizione di una procedura/funzione di libreria e il suo richiamo. Tali funzionalità vengono assolte da una applicazione da attivare successivamente alla fase di compilazione, detta linker o collegatore. Il linker riceve in ingresso un programma oggetto, un insieme di comandi che gli indicano come deve procedere e i riferimenti alle librerie da cui estrarre moduli software che devono essere collegati per produrre un unico programma eseguibile. Solo a valle dell’intervento del clinker il programma è finalmente pronto per l’esecuzione e viene detto eseguibile. Per essere eseguito deve però essere caricato in memoria centrale. Tale operazione è affidata ad un’altra applicazione detta caricatore o loader che legge da disco l’immagine di memoria del programma, decide dove posizionarlo in memoria e infine gli trasferisce il controllo. 8.2.1. La compilazione Il processo di traduzione è tanto più complesso quanto più il linguaggio da tradurre è vicino al linguaggio naturale. Peraltro, la possibilità di esprimere l’algoritmo in un linguaggio vicino al linguaggio naturale piuttosto che a quello della macchina, facilita il compito del programmatore: ed è questo il motivo per il quale i linguaggi di alto livello hanno raggiunto una grande diffusione. Ma quanto più il linguaggio ad alto livello si avvicina a quello naturale tanto più si allontana dal linguaggio macchina, facendo aumentare il gap semantico tra il programma sorgente e quello oggetto. Il compito di colmare tale gap è affidato al compilatore che nel tempo ha sempre più assunto un ruolo centrale nello sviluppo dei programmi. Il processo di compilazione evolve attraverso una sequenza di fasi di solito classificate in fasi di analisi e di sintesi. Le fasi di analisi sono rivolte ad analizzare il testo del programma in base alle regole del linguaggio definite dalla grammatica, mentre la fase di sintesi produce il codice oggetto sulla base delle informazioni raccolte durante la fase di analisi, nel caso in cui il programma è corretto grammaticalmente. Nel dettaglio, lo scopo della fase di analisi è: - riconoscere le frasi appartenenti al linguaggio e interpretarne il significato; e nel caso in cui il riconoscimento fallisca, rilevare gli errori ossia le violazioni delle regole grammaticali; - costruire un insieme di informazioni sulle variabili e sulle procedure usate, sui tipi definiti dall’utente e sullo spazio necessario per contenere i dati. Per comprendere il funzionamento della fase di analisi occorre richiamare gli aspetti costitutivi ed essenziali di un linguaggio. Elemento fondamentale di ogni linguaggio è l’alfabeto. L’alfabeto è definito come un insieme di simboli che chiameremo caratteri. L’aggregazione dei caratteri dell’alfabeto permette di costruire le parole. Le parole possono essere a loro volta aggregate in frasi del linguaggio. Naturalmente, non tutte le aggregazioni dei caratteri dell’alfabeto permettono di costruire parole valide, e non tutte le aggregazioni di parole 302 Capitolo ottavo permettono di costruire frasi corrette, ed infine non tutte le frasi corrette hanno un significato valido. Le regole di costruzione delle parole a partire dall’alfabeto sono dette regole lessicali. Il modo con cui le parole sono concatenate tra loro per costruire frasi corrette nel linguaggio costituisce la sintassi del linguaggio, mentre il significato da attribuire alla frase riguarda la semantica del linguaggio. L’insieme delle regole lessicali e sintattiche costituisce la grammatica del linguaggio. Le regole della grammatica di un linguaggio vengono illustrate attraverso opportuni metalinguaggi, come la BNF (Backus Naur Form) o le carte sintattiche. La figura 2 mostra come le regole della grammatica del linguaggio condizionano le fasi in cui si articola il processo di traduzione. Analisi Analisi Lessicale Costruzione di tabelle interne Analisi Sintattica Gestione degli errori Analisi Semantica Figura 2 - La fase di analisi del compilatore La prima fase è costituita dall’analisi lessicale o scanning. Lo scanning dapprima verifica le regole di costruzione delle parole, estraendo i cosiddetti token (parole chiave, identificatori, costanti, operatori, etc.). Ai token vengono anche associate delle informazioni. Successivamente trasforma il testo sorgente in una sequenza di token. Ad esempio per l’istruzione: x=100; vengono riconosciuti come validi i tre token (x, =, 100). Mentre ai token corrispondenti ai simboli chiave del linguaggio non viene aggiunta altro che la loro classificazione, per quanto attiene x e 100, si aggiungono informazioni quali il fatto che 100 è una costante intera e x è un identificatore associato ad un tipo, che tale identificatore ha una certa visibilità associata al luogo in cui la variabile è stata dichiarata. Lo scanning, infine, costruisce una rappresentazione dell’istruzione del tipo: identificatore, operatore di assegnamento, costante, punto e virgola. L’insieme delle informazioni ricavate dallo scanning, saranno utili per la fase di sintesi del codice e verranno perciò conservate in apposite tabelle gestite dal compilatore. Le tabelle sono generate durante la fase di scanning, ma vengono arricchite anche nel corso della fase di analisi. La fase successiva a quella di scanning è la fase di analisi sintattica o parsing. L’analizzatore sintattico, o parser, raggruppa i token in strutture sintattiche La traduzione dei programmi 303 rappresentate graficamente dagli alberi sintattici. Questi ultimi sono alberi i cui nodi sono gli operatori del linguaggio e le foglie gli operandi. Ad esempio l’istruzione: x = (a + b) * c rappresentabile dal seguente albero sintattico: = * x c + b a viene trasformata dal parser in una forma più generale indipendente dalla specifica frase utilizzando la descrizione dei token con le annesse informazioni estratte e collocate nelle tabelle interne, ottenendo l’albero di seguito. = * identificator + identificator identificator identificator La rappresentazione in alberi sintattici consente di avere la classificazione delle singole frasi che compongono il testo del programma nella radice dell’albero che le rappresenta. Lo scopo dell’analizzatore semantico è quello di stabilire se una frase ha qualche significato ed, in caso affermativo, capire quale. Questa affermazione nel caso dei linguaggi di programmazione ha implicazioni molto precise perché non tutte le violazioni delle regole semantiche possono essere gestite dal compilatore. A titolo di esempio, costituiscono violazioni semantiche a tempo di compilazione (o compilation time), le operazioni tra operandi di tipo non compatibile (esempio: a = b, con a di tipo carattere e b di tipo reale), operandi illegali per il tipo di oggetti a cui sono applicati, l’uso di oggetti non dichiarati, il richiamo di funzioni con un numero di parametri diverso da quello della dichiarazione. Molte altre violazioni semantiche si verificano solo durante l’esecuzione del programma tradotto. Ad esempio sono violazioni semantiche a tempo di esecuzione (o run time) i tentativi di accesso a file non aperti in precedenza, violazioni degli intervalli di definizione per gli indici dell’array, tentativi di accedere oltre la fine di un file, accessi a variabili dinamiche attraverso puntatori non validi e così via. Le informazioni estratte nella fase di analisi vengono conservate per poter poi procedere alla sintesi del codice. In particolare per quanto riguarda le variabili 304 Capitolo ottavo dichiarate nel testo di un programma, è necessario associare ad esse il nome e il tipo per riservare lo spazio destinato a contenerne il valore durante l’esecuzione, l’indirizzo di memoria in cui collocare il valore e le modalità di accesso (scope), che saranno diverse a seconda che si tratti di una variabile globale, locale, di un parametro o di una funzione. Informazioni che vengono strutturate in una tabella dei simboli a cui si accede mediante il nome dell’identificatore. Identificatore Classificazione Tipo Occupazione Scope Indirizzo 100 costante intero 1 byte locale X variabile reale 4 byte locale A variabile reale 4 byte globale B variabile intero 2 byte locale C variabile intero 2 byte locale Tabella 1 – Tabella dei simboli Solo dopo aver terminato la fase di analisi senza aver rilevato errori il compilatore può avviare la fase di sintesi con la generazione del codice in linguaggio macchina effettivamente eseguibile da una macchina reale. Sono disponibili gli alberi sintattici con la classificazione delle diverse componenti del programma e le informazioni riportate nella tabella dei simboli. L’approccio più semplice della fase di sintesi è quello di scandire i diversi alberi e le tabelle e di procedere con una generazione delle singole istruzioni in linguaggio macchina senza tener conto di possibili ottimizzazioni legate alla struttura del calcolatore . Ad esempio, dato il blocco: x = a+b ; y = a+b ; il codice prodotto nella macchina in tale situazione diventa: ADD STORE ADD STORE a,b x a,b y che è inaccettabile dal punto di vista dell’efficienza. Si noti infatti che il risultato della prima somma (uguale alla seconda) permane nell’accumulatore senza essere modificato dalla operazione di store in x. Situazione che il compilatore scopre anche analizzando i due alberi sintattici rappresentativi delle due istruzioni di assegnazione. In tale caso il compilatore procede alla ottimizzazione del codice eliminando la seconda ed inutile operazione di ADD come il codice che segue dimostra. ADD STOREA STOREA a,b x y che corrisponde all’assegnamento multiplo: x = y =a + b; La traduzione dei programmi 305 del tutto equivalente, dal punto di vista semantico, al testo originale. Il generatore di codice deve quindi in generale non solo rispettare la semantica del testo originale, ma anche gestire efficientemente le risorse della macchina reale per ottenere un programma con prestazioni ottimali. Ridurre la quantità di codice garantendo le stesse funzionalità comporta l’esecuzione di un numero minore di istruzioni con una riduzione dei tempi complessivi. Di solito, alla ottimizzazione del codice si accompagna anche una riduzione dell’occupazione in memoria, come l’esempio precedente dimostra. Si potrebbe operare anche l’ottimizzazione dell’occupazione di memoria da parte delle strutture dati. Ma a differenza del codice, l’ottimizzazione dell’area dati potrebbe provocare un aumento dei tempi di accesso alle strutture stesse, rendendo i due obiettivi (riduzione di spazio di memoria e velocità di esecuzione) conflittuali tra di loro. La complessità della fase terminale di un compilatore risiede pertanto nell’individuare il compromesso ottimale nel rapporto spazio/velocità. La qualità di un compilatore risiede pertanto nella sua capacità di produrre un codice ottimizzato. Per ottenere l’ottimizzazione del codice, i compilatori di solito operano in due passi: in una prima fase si genera un codice intermedio, non orientato ad alcuna macchina reale. Si genera, cioè, del codice di livello molto basso, ma ancora largamente indipendente dalla CPU per la quale verrà generato il codice finale. Tale tipo di codice, in genere, ignora l’esistenza dei registri di macchina, la loro natura, il loro numero e le reali modalità di indirizzamento della memoria. In una seconda fase, il codice intermedio viene tradotto nel codice macchina ottimizzato per un determinato processore. La forma intermedia prodotta, infatti, ha in genere delle caratteristiche che permettono di effettuare le ottimizzazioni necessarie. 8.2.2. Il collegamento Un programma può essere composto da un numero arbitrario di unità di compilazione, eventualmente provenienti da linguaggi sorgente diversi. Il collegamento si rende quindi necessario per aggregare tutte le componenti e costruire un’unica unità eseguibile. Il ruolo del linker è centrale in ambienti di sviluppo software di grandi dimensioni quale quello prodotto in ambiente industriale, dove i progetti raggiungono dimensioni di diversi anni uomo. In questi casi il progetto può essere completato in tempi più rapidi solo con la suddivisione delle attività da svolgere in un certo numero di gruppi di progetto, provvedendo in una fase successiva alla integrazione dei diversi moduli oggetto attraverso il loro collegamento. Il Linker o Collegatore fonde gli spazi di indirizzi separati dei moduli oggetto in una unica dimensione lineare. Per aggregare in un unico file eseguibile le diverse unità di compilazione prodotte, occorre che il compilatore generi delle informazioni opportune nel modulo oggetto, come, ad esempio, quelle mostrata in figura 3. Identificazione Tabella dei Simboli Esportati Tabella dei Simboli Importati Istruzioni macchina Dizionario di rilocazione Fine modulo Figura 3 - Un esempio di modulo oggetto 306 Capitolo ottavo La prima parte del modulo oggetto contiene il nome del modulo, più altre informazioni necessarie al linker come le lunghezze delle varie strutture dati del modulo ed eventualmente la data di compilazione. La seconda parte del modulo oggetto è una lista di simboli interni al modulo a cui possono far riferimento altri moduli (simboli esportati). La terza parte contiene una lista di simboli usati nel modulo, ma definiti in altri moduli (simboli importati). La quarta parte del modulo contiene il codice compilato e le costanti: cioè la parte che verrà caricata in memoria per essere eseguita. La quinta parte è il dizionario di rilocazione dove vengono forniti gli indirizzi base e le costanti di spiazzamento relative. La sesta ed ultima parte contiene una indicazione di fine modulo, a volte un controllo per rilevare eventuali errori di lettura del modulo e l’indirizzo da cui iniziare l’esecuzione. Molti linker richiedono due passi. Nel primo passo il linker legge tutti i moduli oggetto e costruisce una tabella dei nomi e delle lunghezze dei moduli oltre ad una tabella dei simboli globali costituita dai riferimenti esterni. Nel passo due i moduli oggetto vengono scanditi di nuovo, si calcolano i nuovi indirizzi di memoria (rilocazione) e si collegano. Più nel dettaglio, l’algoritmo del linker si compone dei seguenti passi. - Costruzione di una tabella con tutti i moduli oggetto con indicazione della loro dimensione. - Assegnazione di un indirizzo di caricamento ad ogni modulo riportato in tabella. - completamento delle istruzioni che contengono un indirizzo di memoria con la definizione dell’effettivo indirizzo di memoria. Per agevolare tale lavoro il compilatore non fissa all’interno dei singoli moduli degli indirizzi definitivi, ma assume che l’allocazione in memoria per ogni modulo compilato avvenga a partire dall’indirizzo zero. Così l’indirizzo fissato dal collegatore, detto anche costante di rilocazione, uguale al primo indirizzo occupato dal modulo, deve essere sommato a tutti gli indirizzi presenti nelle istruzioni perché si completi il piano effettivo di allocazione in memoria. - Risoluzione dei richiami di dati e procedure esterne ad un modulo. Il compilatore infatti, in presenza di richiami a moduli esterni non completa la traduzione perché non può conoscere dove tali moduli verranno allocati. Solo il collegatore, che provvede alla disposizione in memoria di tutti i moduli aggregare, può quindi il completare la definizione degli indirizzi nel caso di istruzioni che fanno riferimento ad identificatori non locali, quali ad esempio i richiami di sottoprogrammi e funzioni. In figura seguente viene mostrato il linking di tre moduli oggetto A,B,C (nella figura con JSR si indica il salto a sottoprogramma e con RTS il ritorno al programma chiamante). La traduzione dei programmi -… 110 111 …. 299 -- INIZIO MODULO A -… … 200 201 …. 400 -- INIZIO SUBROUTINE B -… … 200 -- JSR B END FINE MODULO A JSR C RTS FINE SUBROUTINE B 307 --110 111 …. 299 ---300 … … 500 501 …. 700 -- INIZIO PROGRAMMA ESEGUIBILE MAIN JSR B END FINE PROGRAMMA ESEGUIBILE MAIN INIZIO SUBROUTINE B JSR C RTS FINE SUBROUTINE B INIZIO SUBROUTINE C RTS FINE SUBROUTINE C -701 … … …. 901 -- INIZIO SUBROUTINE C RTS FINE SUBROUTINE C Figura 4 - Linking di tre moduli A, B, C. La tabella costruita nel passo 1 è mostrata nella tabella 2. Nome Etichetta A B C Lunghezza 300 401 201 Valore 0 300 701 Tabella 2 - Tabella dei moduli oggetto 8.2.3. Il caricamento Il caricatore ha il compito di disporre nei registri di memoria sia il codice che i dati del programma da eseguire e di provvedere alla sua attivazione. Quando i calcolatori erano utilizzati da un solo utente per volta, il problema del caricamento e dell’attivazione di un programma era di facile soluzione: i programmi erano tradotti in una forma oggetto detta binario assoluto, una forma che era l’immagine esatta del programma da eseguire. In essa tutti i riferimenti interni e gli indirizzi di caricamento erano definiti dai traduttori e dal linker. Per poter essere eseguito, il codice binario assoluto deve essere caricato nelle posizioni esatte per cui è stato definito: l’operazione di caricamento si riduce quindi alla 308 Capitolo ottavo scrittura in memoria, ad un indirizzo prefissato, del file eseguibile, mentre l’esecuzione ha inizio caricando il registro contatore di programma PC con l’indirizzo della prima istruzione eseguibile. Tuttavia, con l’avvento della multiprogrammazione, questo tipo di caricamento ha perso di significato: non è possibile, infatti, prevedere a priori da parte del linker l’esatta porzione di memoria libera per il caricamento. È necessario, quindi, ricorrere ad una forma di rappresentazione dei programmi più flessibile. Una via percorsa è quella del codice binario rilocabile. Anche in questo caso tutti i riferimenti a codice e dati sono fatti dai traduttori e dal linker; tuttavia non si tratta di indirizzi assoluti, cioè indirizzi che corrispondono uno a uno con gli indirizzi della memoria fisica, ma di indirizzi relativi ad un’origine con indirizzo zero. In fase di caricamento il caricatore può rilocare gli indirizzi spiazzandoli di una certa quantità rispetto all’origine: ad ogni indirizzo verrà sommato, a opera del caricatore, un certo valore iniziale, detto base di rilocazione. -… 100 101 102 ….. 300 -- INIZIO PROGRAMMA JSR 200 END FINE PROGAMMA base di rilocazione B=1000 -1000 … 1100 1101 1102 ….. 1300 -- INIZIO PROGRAMMA JSR 1200 END FINE PROGAMMA Figura 5: Rilocazione di un codice con base di rilocazione B=1000 Una tecnica interessante che in questa sede viene solamente accennata è quella che usata dai caricatori a classi di rilocazione. In questo caso, si fa uso di cosiddette marche di rilocazione - ovvero di informazioni ausiliarie prodotte dal linker attraverso le quali è possibile marcare esplicitamente le istruzioni che possono essere modificate – per potere allocare segmenti diversi di codice non contiguamente ma in zone di memoria differenti, accessibili mediante diverse basi di rilocazione. Così facendo s’impone una condizione meno restrittiva al caricamento del programma, potendo usare frammenti liberi non contigui di memoria e ottimizzando lo sfruttamento della stessa. 8.2.4. Gli interpreti Un linguaggio interpretato differisce in maniera sostanziale da uno compilato. Un interprete è essenzialmente un esecutore che definisce una macchina astratta La traduzione dei programmi 309 attraverso il linguaggio che riconosce: con il meccanismo dell’interpretazione il linguaggio sorgente di alto livello assume il ruolo di linguaggio macchina del sistema virtuale fatto dalla macchina effettiva che esegue l’interprete stesso. Figura 6- Un interprete esegue un programma scritto in un linguaggio ad alto livello I vantaggi degli interpreti rispetto ai traduttori sono la loro indipendenza dalla macchina reale e dal sistema operativo che li ospitano. Un interprete, infatti, costituisce un sistema chiuso: esso esegue un certo codice e non deve generare alcun eseguibile per una macchina reale. Non esistendo vincoli imposti dalla macchina reale, il modello di macchina astratta può così essere liberato da tutti i dettagli che caratterizzano un processore ed il sistema operativo che gira su di esso. Uno stesso programma, allora, può essere eseguito su macchine diverse: per questo quasi tutti i programmi che girano su reti di computer eterogenee - come internet sono scritti in Java, che è un linguaggio interpretato. Lo svantaggio di un programma scritto in un linguaggio interpretato è che esso è più lento rispetto ai programmi compilati, in quanto il calcolatore esegue sempre il programma interprete che a sua volte esegue il programma utente, con evidente spreco di memoria e di risorse. In altri termini, mentre nel caso della compilazione i tempi di compilazione e di esecuzione sono separati, nel caso dell’interprete gli stessi tempi si sommano. Ogni volta che un programma interpretato viene eseguito, esso viene anche tradotto. Nel caso della compilazione ciò non accade. 8.3. La verifica della correttezza dei programmi Lo sviluppo del software è una attività complessa, e come tale soggetta ad errori derivanti sia da una analisi inesatta da una implementare non corretta dei requisiti richiesti al programma. I primi sono estremamente dannosi, in quanto si ripercuotono su tutte le successive fasi (di progettazione e di implementazione) 310 Capitolo ottavo sino a quando non emergono dall’incapacità del sistema a fare quanto richiesto. I secondi vengono introdotti durante la progettazione o la codifica e diverse possono esserne la causa: scarsa conoscenza del linguaggio, assunzioni non verificate, complessità del progetto, e molte volte semplice distrazione. Le attività di testing rivestono un ruolo importante nello sviluppo del software in quanto provvedono a verificare la correttezza dei programmi. È importante sottolineare che la rilevazione degli errori deve avvenire quanto prima possibile per evitare che tutto quanto sviluppato in condizioni non corrette risulti inutile. Le tecniche di testing, intese come ricerca degli errori, includono metodi di prevenzione che portano a verificare i sistemi a partire dalla fase di progettazione. Inoltre è importante che durante la fase di analisi si provveda alla definizione di piani di test da attuare successivamente a programma terminato e in esecuzione. Nei modelli dei cicli di sviluppo del software, il testing viene spesso considerato come quella fase che segue lo sviluppo per sancire se il prodotto è pronto per andare in esercizio. In realtà il testing è un’attività complessa che accompagna un programma in tutta la sua vita e nei gradi progetti improntati alla qualità i suoi costi possono superare anche il 30% di quelli complessivi dell’intero progetto. Con il testing si può rilevare la presenza di malfunzionamenti, ma non può dimostrare che il prodotto è privo di errori soprattutto in progetti di grandi dimensioni e complessità. In generale non esiste un software privo di difetti e il testing è un’attività che non si può mai considerare definitivamente conclusa. Dal punto di vista pratico, a seconda della importanza dell’applicazione software, va individuata la soglia di convenienza tra la completezza dei test da effettuare e i costi da sostenere. È importante comunque che i casi di test siano definiti sin dalla fase di progettazione e durante lo sviluppo dei programmi al fine di evidenziare difetti, errori, e malfunzionamenti. Il difetto (detto anche Fault) è un elemento del software, oppure di una componente della documentazione relativa al prodotto software, che non risponde ai requisiti. Un solo difetto può generare diverse disfunzioni. L’errore è la causa che genera il difetto. Il malfunzionamento (o anche Failure) è invece l’effetto dell’errore, quindi la conseguenza di un difetto, che si manifesta durante l’utilizzo del prodotto software. L’obiettivo della fase di test è quello di eliminare la maggior parte possibile di difetti prima che il prodotto software venga utilizzato, in modo da evitare qualsiasi malfunzionamento prevedibile. Le attività di testing devono verificare se e in quali condizioni un programma non fa ciò che dovrebbe fare o fa quanto non dovrebbe fare. Un malfunzionamento può essere di tipo funzionale se non genera i risultati attesi; non funzionale se fa un cattivo uso di risorse e presenta problemi di prestazioni non adeguate, scarsa sicurezza; oppure ha una usabilità limitata in quanto la sua interfaccia utente non ne consente un facile uso. È anche possibile classificare i malfunzionamenti in base allo gravità delle conseguenze sull’utilizzo del prodotto software: - è grave se l’intero sistema non può essere utilizzato; - è rilevante se alcune funzionalità critiche del sistema sono indisponibili per gli utenti; - è media se solo alcune funzionalità non critiche del sistema non possono essere usate; La traduzione dei programmi - 311 è lieve se pur in presenza di funzionalità non usabili è comunque possibili l’utilizzo del prodotto. La verifica di un programma può essere di tipo statico o dinamico. è di tipo statico se viene effettuata senza eseguire il programma verificando le regole di codifica, la documentazione di progetto, la struttura del programma. è di tipo dinamico se è effettuata eseguendo il codice e osservandone il comportamento durante l’esecuzione dei casi previsti da un piano di test. Tra le più importanti verifiche dinamiche, si ricordano: - i test di singole unità (Unit Test) hanno l’obiettivo di individuare gli errori nel singolo modulo software; - i test di integrazione (Integration Test) con i quali si provano i collegamenti tra diverse unità hanno l’obiettivo di individuare gli errori nel software quando tutti i moduli che compongono un sottosistema o l’intero sistema vengono fatti lavorare insieme; - i test dell’intero sistema (System Test) hanno l’obiettivo di garantire che il prodotto software nel suo complesso soddisfi completamente i requisiti iniziali; - i test prestazionali (Performance Test) per verificare se il sistema produce i risultati utilizzando al meglio le risorse; una risorsa importante è, ad esempio, il tempo, per cui un sistema è efficiente se produce risultati in tempo utile al suo utilizzatore; - i test di stress che pongono il sistema in condizioni estreme di utilizzo per controllare se è capace di sostenere il carico che ne deriva e capire così i limiti di applicabilità; - i test di usabilità per misurare il grado di facilità con cui l’utente usa le diverse funzioni: l’usabilità tiene conto della progettazione delle interfacce con le quali l’applicazione interagisce con il mondo esterno. Un problema vivo nella progettazione delle interfacce è la loro rispondenza a requisiti che misurano la capacità di uso delle applicazioni da parte di tutti, anche di chi è portatore di alcuni handicap, e che vengono indicate con il termine di accessibilità. Le verifiche possono aver luogo in due condizioni contrapposte a seconda che il verificatore conosca o meno la logica e la struttura interna del software. Nel primo caso, detto anche white-box, il verificatore può predisporre i casi di prova in modo da poter provare il più possibile: - le istruzioni: in modo da eseguire almeno una volta tutte le parti del codice che è stato scritto; - le decisioni: in modo da eseguire almeno una volta tutti i cammini linearmente indipendenti che derivano dai nodi decisionali (if then else e case) presenti nel codice. Nel secondo caso, detto anche black-box, il verificatore conosce solo la logica del software che deve testare, ma ne ignora del tutto la struttura interna. Il testing si effettua confrontando l'analisi degli output generati dal sistema o da suoi componenti, con i risultati previsti come risposta ad input predefiniti. In tale situazione i casi di prova vanno costruiti prendendo in considerazione tutte le possibili combinazioni tra dati di ingresso e risultati in uscita, scegliendo in modo significativo i valori di prova. 312 Capitolo ottavo Con attività di debugging si indica invece l’insieme di tecniche da applicare per l’eliminazione degli errori. Vale la pena ribadire che una cattiva progettazione se all’inizio fa percepire una riduzione dei tempi di sviluppo implica solitamente dei tempi più lunghi di debugging. Nella stesura di un programma si possono commettere tre principali tipi di errori, comunemente chiamati bug: - Errori di sintassi: errori dovuti al non rispetto delle regole grammaticali imposte dal linguaggio di programmazione adottato. Sono gli errori più facili da individuare e da correggere poiché non è possibile eseguire il programma finché la loro presenza non è completamente rimossa. Un esempio comune consiste nella mancata chiusura di un costrutto if else o nell'uso errato dei costrutti di iterazione, o ancora nella scrittura di parole chiave in modo scorretto. - Errori logici: ossia gli errori dovuti ad una errata progettazione dell’algoritmo. Sono i più complessi da individuare in quanto l'esecuzione procede regolarmente senza però fornire il risultato desiderato. Il saper prevenire errori logici è una capacità che si acquisisce nel tempo e rappresenta una delle caratteristiche fondamentali di un buon programmatore. Solo una buona fase di analisi consente di evitare la costruzione di programmi sbagliati. - Errori di esecuzione: ossia quelli che intervengono quando il contesto in cui il programma opera è diverso da quello progettato. Sono errori di esecuzione la divisione di un numero per 0, l'accesso ad una posizione di un array non presente, il tentativo di connessione ad un file che non esiste. In presenza di un errore di esecuzione il programma viene terminato prematuramente con l’invio di un messaggio mostrato a video e corrispondente all’errore verificatosi. Nello specifico della scrittura di programmi il debugging consente la ricerca e la correzione di errori con particolare riferimento alla correzione degli errori logici presenti nella struttura dell’algoritmo: errori che si manifestano con un comportamento anomalo del programma rispetto a quello atteso. Gli errori di tipo logico, al contrario di quelli sintattici, non sono rilevabili in fase di compilazione del programma e ciò rende spesso più difficoltosa la loro localizzazione e correzione. Sebbene sia possibile svolgere l'attività di debugging senza alcun supporto da parte dell'ambiente di programmazione, osservando cioè il comportamento del programma ed esaminando manualmente il suo codice alla ricerca degli errori, molti ambienti di programmazione moderni offrono specifiche funzionalità volte a semplificare tale opera quali il tracciamento (Tracing), i punti di arresto (Breakpoint) e l’analisi dei valori delle variabili (Watchpoint). L’analisi dei valori delle variabili consente al programmatore di visionare lo stato dell’esecuzione. L’osservazione dei valori assunti per effetto delle istruzioni eseguite è importante per comprendere non solo la correttezza delle attività svolte ma soprattutto se ciò che deve essere ancora eseguito avverrà secondo precise e previste condizioni. Con il tracciamento il programma viene eseguito a scatti, una istruzione alla volta. L’ambiente al termine dell’esecuzione di una istruzione si ferma aspettando che il programmatore attivi l’esecuzione dell’istruzione successiva. Ad ogni arresto La traduzione dei programmi 313 il programmatore può osservare lo stato dell’esecuzione individuando così la genesi dell’errore. Eseguire passo passo il programma però non solo è faticoso ma in alcuni casi non utile in quanto l’effetto di alcune istruzioni non incide sulle cause degli errori. I punti di arresto consentono così di far interrompere l’esecuzione del programma in alcuni dei suoi punti più critici per controllare lo stato dell’esecuzione. La determinazione dei punti di arresto è uno degli aspetti più critici del debugging. Perché siano efficaci per la determinazione delle cause di errore, devono essere previsti sin dalla fase di progettazione del programma mediante l’adozione delle asserzioni con le quali il programmatore esegue continuamente assunzioni sullo stato dell’esecuzione: come ad esempio parametri che devono assumere valori precisi, precondizioni che devono essere verificate prima di svolgere un’azione, condizioni invarianti all’interno di un loop, post-condizioni che devono verificarsi dopo l’esecuzione di un processo di calcolo. L’uso delle asserzioni è uno dei cardini della programmazione professionale in consente infatti di scrivere codice che si verifica più facilmente. 8.4. Gli ambienti integrati L’esecuzione di un programma richiede la scrittura (editing) dei testi sorgenti delle diverse parti del programma e il completamento delle fasi di compilazione, collegamento e caricamento in memoria. La comparsa di un solo errore che si verifichi durante il ciclo di editing, compilazione, collegamento, caricamento ed esecuzione costringe il programmatore a ritornare nella fase di editing per immettere le correzioni del caso nel sorgente ed a rieffettuare un nuovo ciclo di compilazione, collegamento, caricamento ed esecuzione. Per ridurre i tempi di produzione dei programmi sono nati nel tempo ambienti di programmazione integrati con i quali che semplificano la generazione dei programmi eseguibili a partire dalla loro forma sorgente. L'editor, il compilatore, il collegatore ed il caricatore fanno parte di un unico ambiente operativo e sono strettamente interconnessi (ovvero integrati) tra loro. I vantaggi di tali ambienti sono elencati di seguito. - Una semplice interazione con l'utente: tutti i comandi sono selezionati mediante menù guidati. Non è più necessario ricordare sequenze di comandi spesso criptiche e foriere di errori. - La riduzione dei tempi di sviluppo: tutti gli strumenti sono integrati in un unico software e sono progettati in modo da lavorare sinergicamente e perciò con maggiore efficienza. - Una maggiore semplicità nelle operazioni di correzione degli errori (debugging): l'integrazione delle funzionalità di editing con quelle di compilazione, collegamento ed esecuzione consente di non abbandonare la fase di editing e quindi di rilevare e correggere l'errore quando si verifica. 8.4.1. L’ambiente DEV-C++ La creazione di programmi C viene semplificata con l’utilizzo di appositi ambienti di sviluppo o Integrated Development Environment (IDE), che forniscono al 314 Capitolo ottavo programmatore un’interfaccia di facile utilizzo per la rapida scrittura ed il successivo testing dei programmi. DEV-C++ è un esempio di IDE, ossia un ambiente di sviluppo per programmi scritti in linguaggio C o C++ con le seguenti caratteristiche: - usa Mingw, una versione del famoso compilatore C GCC (GNU Compiler Collection); - crea eseguibili Win32, ossia dei file .exe per macchine con sistemi operativi Windows; - è un free software scaricabile gratuitamente dalla rete; - è scritto in Delphi, che è un altro linguaggio di programmazione. Esso si presenta al programmatore attraverso una schermata simile a quella mostrata in figura 7. Figura 7 – Schermata di avvio di DEV-C++ In DEV-C++ si distinguono le seguenti componenti principali: - una finestra di editing per la scrittura dei programmi; - una barra dei menù dalla quale si accede ai comandi per la compilazione ed esecuzione dei programmi; - un finestra per l’organizzazione dei file contenenti i diversi programmi; tale finestra è utile per organizzare i file nel caso di sviluppo di progetti, ovvero di programmi composti da componenti distribuiti su più file; - un finestra contente gli output di compilazione nella quale compaino le segnalazioni degli errori rilevati durante la compilazione. Un programma C per poter essere eseguito richiede che vengano attivate le fasi di: - editing: il programma viene scritto nel linguaggio C attraverso un editor testuale; La traduzione dei programmi 315 - compilazione: il codice sorgente del programma viene tradotto in codice oggetto attraverso l’uso di un apposito compilatore, eventuali errori sintattici nella scrittura del programma sono risolti in questa fase; - linking ed esecuzione: eventuali funzioni di librerie esterne (già compilate) vengono collegate o linkate al codice oggetto all’atto del caricamento del programma in memoria, il quale successivamente può essere eseguito; - debugging e testing: il programma viene testato rispetto a dei casi di test ed in caso di comportamento anomalo si utilizzano meccanismi di debugging per correggere gli errori funzionali. La prima fase dello sviluppo di un programma è la scrittura del codice sorgente. Si utilizza la finestra di editing (dal menù File selzionare Nuovo->File sorgente). La successiva fase è quella di compilazione, atta a produrre il codice oggetto necessario all’esecuzione. Essa avviene utilizzando il comando di compilazione nella barra dei menù come mostrato in figura 8. Figura 8 – Compilazione attraverso DEV-C++ Gli errori rilevati durante la compilazione sono segnalati nella finestra degli output di compilazione. Ad ogni errore viene associata una diagnostica che il programmatore deve interpretare per comprendere la causa dell’errore stesso. Per rimuovere la causa dell’errore il programmatore deve modificare il testo sorgente prima di riavviare la compilazione (figura 9). Il compilatore DEV segnala tutti gli errori presenti nel programma legati ad una violazione delle regole grammaticali del linguaggio: alcuni di essi sono reali ma atri possono presentarsi a causa dei primi: se, ad esempio viene rilevato un errore in un if, anche le frasi successive che compongono l’intera struttura (l’else) vengono riportate come errate. Alcuni errori non sono gravi e il compilatore li segnala di tipo warning: per essi la compilazione procede comunque ed è il compilatore a introdurre una correzione in modo automatico. È importante che tutti gli errori rilevati vengano però rimossi, in quanto la correzione automatica introdotta dai warning potrebbe alterare la logica del programma. 316 Capitolo ottavo Le successive fasi di linking ed esecuzione avvengono attraverso il comando di esecuzione disponibile sempre nella barra dei menù (figura 10). Durante l’esecuzione si aprirà una finestra DOS che darà modo al programmatore di visualizzare i risultati dell’esecuzione del programma. Non è possibile eseguire un programma se prima non lo si è compilato. Perchè un programma sia compilato non devono essere segnalati errori dal compilatore ad eccezione di quelli di tipo warning. Gli errori logici non rilevabili a tempo di compilazione, che compaiono quindi solo durante l’esecuzione, sono individuabili e gestibili attraverso il meccanismo del debugging del codice sorgente. La verifica della correttezza funzionale del codice avviene inserendo all’interno del programma C dei punti di debug o breakpoints con un clic del mouse sulla barra nera del editor del DEV. Quando si inserisce un breakpoint e si esegue un programma con l’opzione di debug (attraverso il comando dalla barra dei menù), l’esecuzione si arresta al punto di debug prefissato; procedendo poi con l’opzione “Step Successivo” è possibile eseguire ed esaminare il codice linea per linea per l’individuazione di eventuali errori e della loro posizione all’interno del codice stesso. Figura 9 – Gestione errori di compilazione La traduzione dei programmi 317 Figura 10 – Esecuzione di programmi attraverso DEV-C++ La tecnica del “debugging” risulta molto utile quando una funzione non viene eseguita correttamente e, da una prima analisi visiva, non si riesce a determinate il punto in cui è presente l’errore. Inserendo i breakpoint risulta inoltre possibile stabilire quali sono le parti di codice corrette e quali non lo sono (figura 11 ed 12). Figura 11 – Programma con comportamento anomalo 318 Capitolo ottavo Figura 12 – Debug di un programma In DEV-C++ il debug permette inoltre di controllare l’aggiornamento delle variabili durante il ciclo di esecuzione. Tale controllo (attivabile mediante l’aggiunta di “osservazioni”, vedi figura 13) serve a stabilire se durante l’esecuzione del programma le variabili si aggiornano correttamente. Figura 13 – Controllo aggiornamento di variabili La traduzione dei programmi 319 Nell’esempio mostrato si può vedere che la variabile che non si aggiorna correttamente è la variabile b a causa di uno scorretto uso della funzione scanf. Un programma compilato con un compilatore C viene allocato in quattro aree di memoria distinte: - un’area codice contente il codice oggetto del programma; - un’area globale contente tutte le variabili globali del programma; - un’area heap contente le variabili allocate in maniera dinamica; - e un’area stack che viene usata durante l’esecuzione del programma per memorizzare l’indirizzo di rientro delle funzioni, gli argomenti passati alle funzioni, le variabili locali. 8.4.2. L’ambiente MATLAB MATLAB mette a disposizione una vasta gamma di funzioni per la risoluzione di problemi di natura matematica, ponendosi, agli occhi dell’utente, come un potente ambiente per il calcolo scientifico. MATLAB, nella cosiddetta modalità interattiva di calcolatrice scientifica, si presenta all’utente attraverso l’interfaccia grafica in figura 14 chiamata “Command Window”. Figura 14 – Command Window Tale interfaccia costituisce una vera e propria finestra di comandi in cui è possibile specificare, sottoforma di funzione, le azioni che MATLAB deve intraprendere. Un primo esempio banale, per capire la modalità di funzionamento della calcolatrice MATLAB, è la somma tra due numeri mostrata nella figura 15. Come si può notare la somma avviene, come in una normale calcolatrice, utilizzando il tasto (funzione) “+” e il tasto <enter>, che rappresenta l’invio del comando; il risultato della somma viene memorizzato in una particolare variabile “ans”, che MATLAB utilizza per conservare il risultato dell’operazione corrente. 320 Capitolo ottavo Figura 15 – Esempio d’uso della Command Window Per potere sfruttare tutte le potenzialità della calcolatrice MATLAB bisogna conoscere innanzitutto tutte le funzioni da essa messe a disposizione. Per avere un’idea della libreria di funzioni che si possono utilizzare, basta conoscere il comando di utilità help, il quale rende disponibile un vero e proprio manuale in linea per l’utilizzo di MATLAB. Per rendere più semplice l’utilizzo del manuale da parte dell’utente le funzioni sono classificate per argomento. Quando si digita nella finestra dei comandi help senza argomenti compare la serie di argomenti o topics a cui si può fare riferimento. Digitando poi help nome_argomento è possibile accedere alla lista di funzioni MATLAB relative all’argomento. Digitando infine help nome_funzione compare la descrizione della modalità di utilizzo della funzione. Un esempio di utilizzo dell’help MATLAB per risalire alla modalità d’uso della funzione abs (valore assoluto) potrebbe essere il seguente: help help elfun help abs ABS Absolute value. ABS(X) is the absolute value of the elements of X. When X is complex, ABS(X) is the complex modulus (magnitude) of the elements of X. See also SIGN, ANGLE, UNWRAP Come si può notare dall’esempio l’help relativo ad una funzione termina con un elenco di tutte le funzioni ad essa correlate, in modo da rendere semplice ed immediata la ricerca. Poiché l’elemento base dell’ambiente MATLAB è la matrice, tutti i dati vengono immessi sottoforma di matrici (reali o caratteri). Le matrici possono essere fornite in 3 modi diversi: 1. da tastiera come lista di elementi, 2. caricate da file-esterni di dati, 3. generate da funzioni . La traduzione dei programmi 321 È importante sottolineare che MATLAB distingue le lettere maiuscole da quelle minuscole. In particolare l’immissione di un dato da tastiera avviene utilizzando una sintassi del tipo: » nome_variabile=valore[;] <enter> e la presenza del carattere “;” (opzionale) consente di memorizzare l’informazione senza visualizzarla. Tutti i dati sono memorizzati sotto forma di matrice e gestiti nell’ambiente in una area che prende il nome di WORKSPACE (spazio di lavoro). Le variabili utilizzate rimangono in tale spazio di memoria per tutta una sessione di lavoro ed è possibile visualizzarne il valore sia durante l’esecuzione del programma che alla sua terminazione con i comandi who e whose. La funzione who mostra tutte la variabili allocate, mentre la funzione whose mostra oltre al nome delle variabili anche il tipo, la dimensione e la quantità di memoria occupata. » a=1; » v=[1 2 3]; » who Your variables are: a v » whos Name Size Bytes Class a 1x1 8 double array v 1x3 24 double array Grand total is 4 elements using 32 bytes Il comando di utilità per cancellare delle variabili dalla memoria è il clear; clear senza argomenti cancella tutte le variabili, clear nome_varaibile elimina la variabile con il nome specificato. Un’altra funzione utile quando si opera in modalità calcolatrice è la funzione clc che serve a pulire lo schermo. Vediamo ora degli esempi d’uso di MATLAB in modalità calcolatrice per la risoluzione si semplici problemi matematici. Esempio 1 - Trovare la trasposta di una data matrice A Per il calcolo della trasposta si utilizza il carattere (funzione) ‘ (apice). La sequenza di comandi usata per risolvere il problema potrebbe essere la seguente: » A=[1 2 3 4;5 6 7 8] A= 1 2 3 4 5 6 7 8 » A=A' A= 1 5 2 6 3 7 4 8 322 Capitolo ottavo Esempio 2 - Risolvere il sistema di equazioni lineari descritto da Ax=b con A matrice dei coefficienti (quadrata) e b vettore dei termini noti La soluzione del sistema è data da x=A-1b; per questo problema sono disponibili due funzioni: la prima inv permette il calcolo dell’inversa di una matrice, la seconda \ (funzione di divisione a sinistra) permette la risoluzione diretta del sistema suddetto. Delle sequenze di comandi utili per risolvere il problema potrebbero essere le seguenti: » A=[1 2;3 4] A= 1 2 3 4 » b=[1;1] b= 1 1 » x1=inv(A)*b x1 = -1 1 » x2=A\b x2 = -1 1 Esempio 3 - Riportare l’andamento della funzione sen(x) nell’intervallo [0,6.28]con passo 0.1. Per determinare l’andamento di una funzione bisogna prima rappresentare l’intervallo delle ascisse, poi calcolare il vettore delle ordinate, ed, infine tracciarne il grafico. La funzione utilizzata per graficare funzioni è la funzione plot. Essa ha come parametri di ingresso il vettore delle ascisse e quello delle ordinate della funzione da graficare. Infine per il calcolo del seno di un numero si utilizza la classica funzione sin. Una sequenza d comandi utile per risolvere il problema potrebbe essere la seguente: » x=[0:0.1:6.28]; » y=sin(x); » plot(x,y) La traduzione dei programmi 323 1 0.8 0.6 0.4 0.2 0 -0.2 -0.4 -0.6 -0.8 -1 0 1 2 3 4 5 6 7 Figura 16 – Plot di una funzione Esempio 4 - Trovare il determinante di una data matrice A. Per il calcolo del determinante si utilizza la funzione det. La sequenza di comandi usata per risolvere il problema potrebbe essere la seguente: » A=[1 2;3 4] A= 1 2 3 4 » det(A) ans = -2 Esempio 5 - Trovare le radici di un dato polinomio p. Per il calcolo delle radici di un polinomio si utilizza la funzione roots. Essa ha come parametro di ingresso un vettore contenente i coefficienti del polinomio.La sequenza di comandi usata per risolvere il problema con p(s)=2s2-4s+1 potrebbe essere la seguente: » p=[2 -4 1] p= 2 -4 1 » roots(p) ans = 1.7071 0.2929 Esempio 6 - Calcolare l’integrale definito tra a e b di una data funzione f(x). Per il calcolo dell’integrale di una funzione si utilizza la funzione quad. Essa ha come parametri di ingresso la funzione f e gli estremi di integrazione. La sequenza di comandi usata per risolvere il problema con f(x)=log(x), a=1 e b=10 potrebbe essere la seguente: » quad('log',1,10) ans = 14.0257 324 Capitolo ottavo MATLAB si presenta anche come ambiente per la produzione di programmi con il quale ampliare la già vasta libreria di funzioni disponibile. A tale scopo MATLAB offre all’utente un editor per lo sviluppo di programmi, chiamato M-file editor, che si presenta all’utente con l’interfaccia mostrata in figura. Figura 17 – M-file editor Con l’editor è possibile sviluppare programmi, o meglio funzioni, sfruttando i costrutti e le proprietà del linguaggio di MATLAB. Un primo semplice esempio di funzione MATLAB potrebbe essere quello del calcolo della somma tra tre numeri reali, il cui codice da scrivere è: function [d]=somma(a,b,c) d=a+b+c; Al termine della scrittura, la funzione deve essere salvata con il suo stesso nome in modo tale da poter da essere poi richiamata nella finestra dei comandi, o all’interno di altre funzioni. Nell’esempio la funzione presenta tre variabili d’ingresso a,b,c ed una di uscita d e i valori ai parametri di ingresso possono essere dalla Command Window assegnati nel modo seguente: » [ris]=somma(1,2,3) ris = 6 I valori dei parametri possono essere passati sia direttamente che per mezzo di altre variabili; inoltre, si osservi che, quando viene richiamata una funzione, non è necessario che il nome delle variabili di ingresso e uscita coincidano con quelli utilizzati per la scrittura della funzione, ciò che è importante è che siano per ordine e tipo coincidenti con esse. Facendo riferimento all’esempio seguente s1, s2 e s3 vengono associate ad a,b,c. » s1=1; » s2=2; » s3=3; La traduzione dei programmi 325 » [ris]=somma(s1,s2,s3) ris = 6 Quando non ci sono parametri di ingresso da passare mediante la finestra di comandi, anche l’opzione Run, presente nella voce del menù Tools dell’editor, consente di attivare una funzione. Gli errori relativi al codice di una funzione vengono riportati nella finestra dei comandi durante la sua esecuzione. Ad esempio se in una funzione di nome somma si inserisce: a=iput('Inserisci il secondo numero: '); si riceve il seguente messaggio durante la sua esecuzione: ??? Undefined function or variable 'iput'. Error in ==> C:\MATLABR11\work\somma.m On line 3 ==> a=iput('Inserisci il secondo numero: '); MATLAB dà informazione relativamente alla tipologia e posizione di tutti gli errori all’interno del codice e quindi rende possibile una loro rapida correzione. Nell’esempio viene segnalato che la funzione iput risulta non definita. La verifica della correttezza non solo sintattica, ma anche funzionale, del codice può invece avvenire inserendo all’interno della funzione dei punti di debug attraverso l’opzione Set/Clear Breakpoints presente nella voce del menù Debug dell’editor. Quando si inserisce un breakpoint e si esegue una funzione, l’esecuzione si arresta al punto di debug; procedendo poi con l’opzione Single step è possibile esaminare il codice linea per linea per l’individuazione di eventuali errori e della loro posizione. La tecnica del debugging risulta molto utile quando una funzione non viene eseguita correttamente e, da una prima analisi, non si riesce a determinate il punto in cui si genera l’errore. Inserendo i breakpoint risulta inoltre possibile stabilire quali sono le parti di codice corrette e quali non lo sono. Ad esempio si scriva la seguente funzione di somma: function somma a=input('Inserisci il primo numero: '); b=input('Inserisci il secondo numero: '); c=input('Inserisci il terzo numero: '); d=a+b; fprintf('La somma è %g',d) palesemente errata in quanto non calcola la somma delle tre variabili a, b, e c ma solo delle prime due. In tal caso la funzione, pur risultando sintatticamente corretta, non ritorna il giusto risultato: » somma Inserisci il primo numero: 1 Inserisci il secondo numero: 2 Inserisci il terzo numero: 3 La somma è 3 326 Capitolo ottavo Per capire il non corretto funzionamento della funzione si potrebbe inserire un breakpoint sull’istruzione di somma come mostrato in figura 18. Figura 18 – Debugging con Matlab Eseguendo la funzione si nota come la sua esecuzione si arresta prima dell’istruzione indicata. Spostando ora il cursore del mouse sulle variabili a, b e c, si può osservare che queste contengono effettivamente il valore digitato e quindi l’errore non è presente nella parte di codice relativa all’inserimento dati. Procedendo poi con l’opzione Single Step viene eseguita la sola istruzione alterata consentendo di comprendere la causa dell’errore. Un secondo semplice esempio di funzione potrebbe essere quella del calcolo della somma degli elementi di un vettore, funzione, per altro, già presente nella libreria MATLAB (sum). Essa si compone di un ciclo iterativo in cui ad ogni passo k viene calcolata la somma dei primi k elementi del vettore. Una prima versione di codice, in cui si prevede che il vettore d’ingresso sia passato dalla finestra dei comandi, risulta la seguente: function [somma]=sommav1(v) n=length(v); somma=0; for i=1:n somma=somma+v(i); end; In questo caso occorre prelevare la dimensione del vettore mediante la funzione length. L’esempio d’uso della funzione è mostrato in seguito: La traduzione dei programmi 327 » v=[1 2 3 4 5 6 7 8 9 10]; » s=sommav1(v) s= 55 In una seconda versione, in cui si prevede l’inserimento del riempimento e degli elementi del vettore durante l’esecuzione della funzione, il codice potrebbe essere il seguente: function sommav2 somma=0; n=input('Inserisci la dimensione del vettore: '); for i=1:n fprintf('Inserisci l''elemento %d-esimo: ',i); v(i)=input(''); somma=somma+v(i); end fprintf('\nLa somma è: %g',somma); In tal caso la lettura degli elementi è preceduta da quella del riempimento al fine di dimensionare il ciclo for di inserimento. Un esempio d’uso della funzione è mostrato in seguito: » sommav2 Inserisci la dimensione del vettore: 3 Inserisci l'elemento 1-esimo: 1 Inserisci l'elemento 2-esimo: 2 Inserisci l'elemento 3-esimo: 3 La somma è: 6 In una terza versione, in cui si prevede il solo inserimento degli elementi ma non il riempimento, il codice potrebbe essere il seguente: function sommav3 somma=0; k=1; risp='s'; while (risp=='s') fprintf('Inserisci l''elemento %d-esimo del vettore: ',k); v(k)=input(''); somma=somma+v(k); risp=input('Altro elemento? (s)i o premere un zltro tasto per uscire: ','s'); k=k+1; end fprintf('\nLa somma è: %g',somma); In tal caso dopo l’inserimento di un dato elemento viene chiesto, attraverso un ciclo while, all’utente se continuare o meno con ulteriori inserimenti. Un esempio d’uso delle funzione è mostrato in seguito: 328 Capitolo ottavo » sommav3 Inserisci l'elemento 1-esimo del vettore: 2 Altro elemento? (s)i o premere un qualsiasi tasto per uscire: s Inserisci l'elemento 2-esimo del vettore: 3 Altro elemento? (s)i o premere un qualsiasi tasto per uscire: s Inserisci l'elemento 3-esimo del vettore: 4 Altro elemento? (s)i o premere un qualsiasi tasto per uscire: n La somma è: 9 In una quarta ed ultima versione, in cui si prevede l’inserimento degli elementi ed un numero particolare (tappo) di fine riempimento, il codice potrebbe essere il seguente: function sommav4 somma=0; k=1; fine=0; while (fine~=1) fprintf('Inserisci l''elemento %d-esimo del vettore(-1 per terminare): ',k); v(k)=input(''); if (v(k)~=-1) somma=somma+v(k); k=k+1; else fine=1; end end fprintf('\nLa somma è: %g',somma); In tal caso la scelta del tappo a -1 limita il programma al funzionamento solo nel caso di numeri positivi. Un esempio d’uso delle funzione è mostrato in seguito: » sommav4 Inserisci l'elemento 1-esimo del vettore(-1 per terminare): 1 Inserisci l'elemento 2-esimo del vettore(-1 per terminare): 2 Inserisci l'elemento 3-esimo del vettore(-1 per terminare): 3 Inserisci l'elemento 4-esimo del vettore(-1 per terminare): -1 La somma è: 6 Un terzo semplice esempio di funzione MATLAB potrebbe essere quello del calcolo della somma degli elementi della diagonale principale di una matrice quadrata di numeri interi, funzione già presente nella libreria MATLAB (trace). L’estrazione degli elementi della diagonale può essere fatta attraverso un unico ciclo iterativo, essendo questi caratterizzati dal fatto di avere l’indice di riga uguale a quello di colonna. Una prima versione del codice da scrivere, in cui la matrice è passata dalla finestra dei comandi, potrebbe essere per il nostro esempio la seguente: La traduzione dei programmi 329 function [somma]=diagv1(A) [n,m]=size(A); somma=0; for i=1:n somma=somma+A(i,i); end Analogamente al caso monodimensionale è necessario conoscere la dimensioni di riga e di colonna della matrice, ottenibili attraverso la funzione size. Un esempio d’uso della funzione è mostrato di seguito: » A=[1 10;20 30] A= 1 10 20 30 » s=diagv1(A) s= 31 Una seconda versione del codice da scrivere, in cui le dimensioni di riga e colonna e gli elementi della matrice sono richiesti durante l’esecuzione della funzione, potrebbe essere la seguente: function diagv2 n=input('Inserire ordine Matrice: '); for i=1:n for j=1:n fprintf('Inserire elemento di posto(%d,%d): ',i,j); A(i,j)=input(''); end end fprintf('Matrice letta:'); A somma=0; for i=1:n somma=somma+A(i,i); end fprintf('La somma è %d',somma); In tal caso la lettura degli elementi è preceduta da quella delle dimensioni di riga e colonna al fine di dimensionare i cicli for di inserimento. Un esempio d’uso della funzione è mostrato in seguito: » diagv2 Inserire ordine Matrice: 3 Inserire elemento di posto(1,1): 1 Inserire elemento di posto(1,2): 2 Inserire elemento di posto(1,3): 3 Inserire elemento di posto(2,1): 4 Inserire elemento di posto(2,2): 5 Inserire elemento di posto(2,3): 6 Inserire elemento di posto(3,1): 7 Inserire elemento di posto(3,2): 8 330 Capitolo ottavo Inserire elemento di posto(3,3): 9 Matrice letta: A= 1 2 3 4 5 6 7 8 9 La somma è 15 Capitolo nono La programmazione orientata agli oggetti 9.1. I limiti del paradigma procedurale Gli ultimi anni hanno visto una chiara e netta evoluzione dei modelli di programmazione da un approccio orientato a procedure e funzioni, e, quindi alla programmazione dal punto di vista del calcolatore, a modelli denominati di Object Oriented Programming (OOP) che, partendo dal punto di vista dell’utente, suddividono l’applicazione in “concetti” rendendo il codice più comprensibile e semplice da mantenere. Il modello classico di programmazione conosciuto come paradigma procedurale si basa su una filosofia top-down di tipo “divide et impera”: in tale tipologia di approccio, un problema complesso viene suddiviso in problemi più semplici in modo che siano facilmente risolvibili mediante procedure e funzioni. Nella pratica l’applicazione di tecniche top-down permette di ottenere programmi non costituiti da un unico blocco monolitico di istruzioni, ma programmi composti da moduli funzionali, ciascuno dei quali realizza un singolo e ben preciso compito. Una tale impostazione, in cui l’attenzione del programmatore è rivolta al problema, risulta un valido aiuto per l’attività di programmazione, in quanto rispetta la limitazione degli esseri umani che, solitamente, sono in grado di esaminare un solo aspetto di un problema alla volta. Inoltre, il processo di raffinamento iterativo produce una gerarchia di sottoproblemi di complessità via, via decrescente. Tale approccio è stato formalizzato in molti modi ed è ben supportato da molti linguaggi che forniscono al programmatore un ambiente in cui sono facilmente definibili procedure e funzioni. All’interno di un’applicazione procedurale i dati vengono condivisi dalla varie procedure e funzioni mediante l’uso di variabili globali, passaggio di parametri e ritorno di valori. Un esempio di applicazione procedurale è riportata in figura 1. L’applicazione in questione consente la gestione di cartelle cliniche relative a pazienti di una struttura ospedaliera. Come si può osservare, il problema suddetto è stato risolto suddividendolo in più sottoproblemi, e utilizzando varie procedure, alcune, più di alto livello, si occupano della gestione dei dati del paziente (mediante variabile globale) ed l’interfacciamento con gli utenti dell’applicazione; altre, di più basso livello, si occupano della gestione dei file fisici su memoria di massa che contengono le informazioni dei pazienti in maniera persistente. Ogni volta che bisogna accedere al file system per gestire i file dei pazienti si utilizza una delle tre procedure: Salva su file(), Elimina file(), Leggi da file(). 332 Capitolo nono Figura 1 – Diagramma delle componenti di un’applicazione procedurale Dal momento che di solito le procedure non sono “auto-documentanti” (selfdocumenting), ossia non rappresentano entità ben definite, se un programmatore deve modificare l’applicazione aggiungendo la procedura di aggiornamento dei dati di un paziente, questi, non conoscendo a fondo il codice dell’applicazione, potrebbe utilizzare la routine Aggiorna Cartella Clinica(), senza chiamare la procedura di Ricerca Cartella Clinica(), dimenticando così l’aggiornamento dei dati globali su cui lavorano le procedure di gestione dei file, e, producendo di conseguenza nel programma effetti indesiderati, difficilmente gestibili. Per tale motivo, le applicazioni basate sul modello procedurale sono in generale difficili da aggiornare e manutenere, nonché da controllare con i classici meccanismi di debugging. I “bug” possono presentarsi in qualunque punto del codice causando una propagazione incontrollata dell’errore. 9.2. Introduzione al paradigma object oriented La programmazione Object-Oriented nasce storicamente dall’incapacità dei linguaggi procedurali (e.g, Pascal, C) di gestire in maniera efficiente programmi complessi, soprattutto in termini di difficoltà di manutenzione (e.g., correzione errori e apporto di modifiche), come visto nell’esempio del paragrafo precedente. A tale proposito, una possibile soluzione al problema della gestione delle cartelle cliniche potrebbe essere quella di raggruppare (in gergo incapsulare) in La programmazione orientata agli oggetti 333 appositi moduli software tutta la logica di gestione dei dati dei pazienti, ed in un altri la logica di gestione dei file, costringendo i programmatori ad usare tali moduli per lo sviluppo di nuove procedure che lavorano sui dati in questione. Su tale presupposto si basa il paradigma Object Oriented, che, a differenza del paradigma procedurale, accentra l’attenzione verso i dati. L’applicazione viene suddivisa in un insieme di oggetti in grado di interagire tra di loro e codificati in modo tale che la macchina sia in grado di comprenderli. La programmazione orientata agli oggetti rappresenta quindi un nuovo modo di pensare alla risoluzione dei problemi per mezzo di un calcolatore: “invece di modellare il problema adattandolo alla logica del calcolatore, l’approccio OO adatta il calcolatore al problema” . I problemi vengono dunque analizzati individuando delle entità indipendenti che sono in relazione con altre. Tali entità non sono scelte perché facilmente traducibili nella logica di programmazione, ma in quanto esistono dei vincoli fisici o concettuali che le separano dal resto del problema e sono rappresentate come oggetti di un programma. La scelta degli oggetti non va effettuata dunque adattando il problema alla logica della programmazione, ma guardando al problema in termini semplici. Il paradigma object oriented formalizza la tecnica di incapsulare e raggruppare parti di un programma in oggetti software che rappresentano concetti ben definiti sia a livello di utente che a livello applicativo. Tali oggetti sono poi riuniti per formare un’applicazione. Nell’esempio precedente, si può osservare come l’applicazione di gestione delle cartelle cliniche dei pazienti si accentri su tre concetti principali: Cartella Clinica, Gestore Cartelle e Gestore di file. I concetti suddetti rappresentano dell’entità, classi di oggetti, del mondo reale che hanno caratteristiche comuni: ad esempio tutte le cartelle cliniche avranno come informazioni il nome e cognome del paziente, la sua tessera sanitaria, il suo gruppo sanguigno, una descrizione di tutte le attività sanitarie svolte sul paziente, una descrizione del suo stato di salute, etc…Su tali oggetti sono poi definite una serie di “azioni” o “funzionalità” come quella dell’aggiornamento e recupero dei dati di un paziente, eseguite, ad esempio, dal personale medico ogni qualvolta il paziente si ricovera presso la struttura ospedaliera. Allo stesso modo è possibile pensare al gestore di cartella come un’entità (ad esempio un impiegato della reception della struttura) che si occupa di gestire (aggiornare, recuperare, creare, etc…) le cartelle cliniche, mentre un Gestore di File rappresenta un’entità (ad esempio un impiegato dell’ufficio archivi) preposta all’archiviazione delle cartelle. L’individuazione degli oggetti e delle azioni su di essi non è però sufficiente alla realizzazione dell’applicazione, il secondo passo è quello di definire l’interazione tra gli oggetti. È necessario, ad esempio, stabilire come un medico, un impiegato della reception ed uno dell’ufficio archivi devono comunicare qualora si deve aggiornare la cartella clinica di un paziente al momento del ricovero. La cosa più ovvia è che il gestore della reception, all’atto del ricovero del paziente, recuperi la cartella clinica con l’aiuto dell’addetto agli archivi e la consegni al chirurgo, il quale dopo l’operazione aggiorna lo stato di salute del paziente. All’atto della dimissione del paziente, l’impiegato della reception provvederà a conservare la cartella aggiornata consegnandola all’addetto all’archiviazione. 334 Capitolo nono In altri termini, il programmatore in un approccio di tipo object oriented inizia con l’analizzare tutti i singoli aspetti concettuali che compongono un programma. Questi concetti costituiscono gli oggetti dell’applicazione ed hanno nomi legati a ciò che rappresentano. Una volta che le classi di oggetti sono identificati, il programmatore decide di quali attributi (caratteristiche) e funzionalità dotare le entità. L’analisi infine dovrà includere le modalità di interazione tra gli oggetti. Proprio grazie a queste interazioni sarà possibile riunire gli oggetti per formare una applicazione. Programmare in un linguaggio orientato agli oggetti significa quindi creare nuovi tipi di dato chiamati classi e associare ad essi delle operazioni, dette metodi, che indicano alle classi come trattare i messaggi ad esse inviati. L’utente definisce poi delle variabili del tipo creato, dette oggetti o istanze, e interagisce con tali oggetti. “Il concetto di programmazione ad oggetti si può riassumere nel seguente modo: si invia un messaggio ad un oggetto e si lascia che esso lo gestisca. Essa nasconde i dati e la complessità di un programma e pone al centro dell’attenzione i tipi di dato e le operazioni: non sono i dati ma i messaggi a muoversi nel sistema, Non si invocano funzioni sui dati, ma si inviano messaggi agli oggetti”. A differenza di procedure e funzioni, gli oggetti sono “auto-documentanti”. Una applicazione può essere scritta a partire da poche informazioni ed in particolar modo il funzionamento interno delle funzionalità degli oggetti è completamente nascosto al programmatore. Riferendoci sempre all’esempio precedente, l’applicazione complessiva risulterà costituita da 3 classi: Cartella Clinica, Gestore Cartelle e Gestore file. La prima avrà come attributi o variabili membro i dati anagrafici del paziente, il codice di tessera sanitaria, una descrizione di tutte le attività sanitarie svolte sul paziente, e una descrizione dello stato attuale di salute; di contro, i metodi definiti su oggetti di tipo Cartella Clinica sono essenzialmente quelli che permettono l’aggiornamento delle variabili membro. Sulla seconda classe, Gestore Cartella, sono definiti tutti i metodi per l’inserimento, aggiornamento, ricerca e cancellazione di cartelle. Infine, sulla terza classe, Gestore file, sono definiti i metodi per l’archiviazione su file di cartelle. La figura 2 schematizza il diagramma delle classi per l’applicazione in questione. La comunicazione ed interazione tra gli oggetti avviene mediante l’invocazione dei metodi e scambi di messaggio. Ad esempio se il Gestore Cartelle vuole creare una nuova cartella e archiviarla, dovrà richiamare (istanziare) prima un oggetto di tipo Cartella Clinica e utilizzare i vari metodi di set della classe, e, successivamente richiamare un oggetto di tipo Gestore File con il metodo di salvataggio, esplicitando nel messaggio di richiamo del metodo tutte le informazioni relative alla cartella clinica da archiviare. È da notare, in questo caso, come l’aggiunta di nuove funzionalità di gestione delle cartelle (ad esempio la stampa dell’elenco di tutte le cartelle dei pazienti) non provochi problemi nel codice (tutta la logica di gestione dei dati delle cartelle è incapsulata nella classe Cartella Clinica ed è solo attraverso essa che è possibile accedere ai dati di un paziente), ma solo l’aggiunta di un metodo nella classe Gestore Cartella. La programmazione orientata agli oggetti 335 Figura 2 – Diagramma delle classi i di un’applicazione OO 9.2.1. La nascita della programmazione ad oggetti I primi concetti relativi al Object Oriented Programming (OOP) si ritrovano nel vecchio linguaggio Simula-67, creato in Scandinavia nel 1967. Simula fu creato per rendere semplice il compito della simulazione, e, dal mondo della simulazione, provengono le idee portanti del paradigma ad oggetti. La simulazione di sistemi reali, infatti, tratta una quantità di entità separate ed autonome chiamate oggetti, che comunicano tra di loro scambiandosi dei messaggi. Fu naturale rendersi conto che nello stesso modo si poteva impostare la soluzione di molti altri problemi di natura completamente diversa: nacquero così i primi linguaggi interamente ad oggetti, nei quali cominciano a comparire i concetti di ereditarietà e delle proprietà ad essa collegate. Smaltalk è sicuramente il più completo ed il più puro dei primi linguaggi object oriented, anche se la programmazione Object Oriented deve il suo successo a linguaggi più semplici ed efficienti come C++ (ottenuto per estensione del C) e Java, oramai largamente usati nel mondo universitario, ma anche nel mondo delle industrie del software. In figura 3 è mostrata l’evoluzione dei linguaggi di programmazione a partire da quelli imperativi e procedurali ai linguaggi orientati agli oggetti concorrenti. Figura 3 – Evoluzione dei linguaggi di programmazione Nel paradigma di programmazione imperativo un programma specifica l’insieme delle azioni che devono essere eseguite in sequenza per calcolare i 336 Capitolo nono risultati a partire dai dati, in quello procedurale un programma è visto come un insieme di procedure o funzioni, mentre nella nuova visione del paradigma ad oggetti, un programma è un insieme di oggetti software che cooperano per produrre risultati. 9.3. I fondamenti della programmazione ad oggetti Come già ampiamente discusso, la caratteristica fondamentale della OOP, rispetto ai paradigmi di programmazione precedenti, è che essa permette di focalizzare l’attenzione sui “concetti” invece di pensare al “codice” (alle procedure) che servono ad implementare quei concetti, come accade per i linguaggi convenzionali. Scrivere un programma significa allora concentrarsi anzitutto sulla individuazione dei concetti e sul modo in cui essi sono in relazione tra di loro. I concetti chiave dell’OOP sono tre: gli oggetti, le classi e l’ereditarietà. Da ciò discendono due caratteristiche molto potenti, quali il polimorfismo ed il binding dinamico, come vedremo dettagliatamente nel seguito. 9.3.1. Oggetti e classi La programmazione orientata agli oggetti si basa sul seguente punto di vista: il mondo è fatto di oggetti, e, dato che un programma è una rappresentazione (un modello) nel calcolatore di una certa realtà di interesse, allora anche il programma sarà composto di oggetti, detti oggetti software (o anche solo oggetti). Nella programmazione orientata agli oggetti, l’enfasi è posta non tanto sulle azioni che devono essere svolte quanto sugli oggetti che compiono/subiscono tali azioni. Gli oggetti cooperano e comunicano tra loro mediante messaggi. Più nel dettaglio, da un lato, abbiamo a che fare con gli oggetti della realtà fisica: ad esempio, nella modellazione di elementi geometrici, sono oggetti punti, linee, angoli, solidi; nella modellazione di un zoo, sono oggetti i differenti animali, le gabbie, il cibo, e così via. Dall’altro canto, un programma non ha a che fare direttamente con oggetti reali, ma con una loro appropriata rappresentazione che può a sua volta essere chiamata oggetto. La programmazione Object Oriented utilizza il concetto di oggetto in entrambi i sensi: al livello di analisi e progetto, un oggetto coincide con un oggetto reale, mentre a livello “implementativo” un oggetto è una rappresentazione appropriata di un oggetto reale. Si consideri ad esempio un oggetto reale quale una autoradio. Essa è contraddistinta da un nome (attraverso il quale è possibile referenziarla) e da una marca e modello. Un oggetto di tipo autoradio saprà inoltre compiere determinate azioni quali, accendersi, spegnersi, cambiare canale, regolare volume, sintonizzarsi un nuovo canale, etc…,e, si trova in ogni istante in un certo stato, specificato da un insieme di proprietà. Per esempio, può essere accesa e sintonizzata su un dato canale con il volume al massimo o al minimo (le proprietà hanno un nome, un insieme di valori ammissibili e un valore corrente). L’insieme delle operazioni che una radio è in grado di compiere costituisce il comportamento dell’oggetto. Si possono poi utilizzare alcuni comandi presenti sul volante della macchina per far eseguire all’autoradio delle operazioni, nella terminologia della programmazione ad oggetti, la richiesta dell’esecuzione di un’operazione viene La programmazione orientata agli oggetti 337 fatta mediante l’invio di un messaggio ad un oggetto. Un altro aspetto notevole di una radio è che per utilizzarla non è necessario conoscerne il funzionamento interno, ma è sufficiente avere una descrizione delle operazioni che è in grado di eseguire (come si trovano ad esempio nel manuale d’uso). La descrizione di ciascuna operazione è data specificando il formato del messaggio (e.g.,, premere un tasto) e il significato (e.g., la pressione del tasto provoca il cambiamento di canale). Le precedenti caratteristiche si ritrovano anche negli oggetti software. Un oggetto software: - ha un nome che permette di referenziarlo univocamente, - ha un comportamento in quanto è capace di eseguire determinate operazioni, - ha uno stato che è caratterizzato da un insieme di proprietà, - soddisfa un’interfaccia che è una descrizione delle operazioni che può eseguire, - ha un’implementazione: ossia un insieme di istruzioni che specificano nel dettaglio come le operazioni sono eseguite. Figura 4 – Oggetto di tipo “autoradio” Un oggetto è caratterizzato quindi da un nome, dei dati o proprietà e dei metodi od operazioni. L’oggetto software autoradio è nella pratica un modello di un’autoradio, che esiste solo, virtualmente, nel calcolatore. Solo gli aspetti di un’autoradio reale che sono ritenuti rilevanti per l’applicazione sono mantenuti (ad esempio, le diverse modalità di funzionamento dell’autoradio, come lettore CD o MP3 non sono considerate), inoltre, i suoi dati sono modificabili esclusivamente attraverso i metodi, la cui implementazione è celata all’utente (incapsulamento). In figura 4 è riportata una rappresentazione di un oggetto software di tipo autoradio. In generale, per usare un oggetto software è sufficiente conoscerne solo l’interfaccia (il manuale d’uso, o in altri termini, la descrizione delle operazioni). Nella terminologia della programmazione ad oggetti, lo stato di un oggetto è determinato dai suoi dati, e il comportamento dai suoi metodi. Una proprietà importante degli oggetti è, come già anticipato, l’incapsulamento dei dati, che permette di nascondere la rappresentazione dei dati e l’implementazione delle operazioni sui dati. Ciò offre diversi vantaggi: - modularità che implica che un oggetto può essere gestito indipendentemente dagli altri 338 Capitolo nono - scomponibilità in quanto è possibile costruire oggetti complessi a partire da oggetti semplici - riusabilità perchè gli oggetti possono essere riutilizzati - Il meccanismo di separazione tra l’interfaccia di un oggetto e la sua implementazione è anche noto con termine di information hiding. L’unica modalità a disposizione dei programmatori per l’attivazione dei metodi definito su un dato oggetto è lo scambio di messaggi. Ciò implica che lo stato di un oggetto è manipolabile solo dai metodi definiti sull’oggetto stesso (vedi figura 5) . Un messaggio è composto da tre elementi: - ricevitore: l’oggetto cui è indirizzato il messaggio - selettore: il metodo che si vuole eseguire - parametri: eventuali parametri del metodo. Figura 5 – Schematizzazione di oggetti In seguito alla ricezione di un messaggio, un oggetto verifica che vi sia un’operazione avente lo stesso profilo, ed esegue il metodo corrispondente. Ad esempio se si vuole aumentare di 1 il volume dell’’autoradio, allora il telecomando potrebbe inviare un messaggio caratterizzato dalla tripla: (Autoradio, Aumenta Volume(…), 1). Altri esempi di oggetti sono ancora un conto bancario, caratterizzato dagli attributi, numero conto corrente, intestatario, ABI, CAB, CIN, nome filiale, e, dai metodi “apri conto”, “chiudi conto”, “preleva”, “deposita”; oppure un magazzino caratterizzato dagli attributi, nome, descrizione, locazione e lista prodotti, e, dai metodi “inserisci prodotto” e “rimuovi prodotto”, “cerca prodotto”, “stampa inventario prodotti”. Ogni oggetto può essere quindi visto come un’entità dotata di caratteristiche (attributi) e funzionalità o servizi (metodi), il cui stato è modificabile solo mediante uno scambio di messaggi con altri oggetti del dominio applicativo. Il confinamento di informazioni e funzionalità in oggetti permette poi livelli maggiori di astrazione e semplifica la gestione di sistemi complessi. Un altro concetto fondamentale nella programmazione orientata agli oggetti è quello di classe. Una classe può essere vista come una “descrizione”, sia strutturale che comportamentale, di un insieme di oggetti simili. Una classe è una sorta di modello o prototipo che definisce un tipo di oggetto (attributi e metodi simili ad un insieme di oggetti), e, ancora, una classe è il progetto di una famiglia di oggetti software. Tutti gli oggetti appartenenti a una classe hanno: la stessa struttura (attributi), un comportamento simile (operazioni) ed una semantica comune. La programmazione orientata agli oggetti 339 In genere una classe identifica un tipo di dato astratto, ovvero un tipo non predefinito nel linguaggio, ma “costruito” ad hoc dal programmatore, e gestito come un tipo primitivo del linguaggio (la sua rappresentazione può essere modificata solo mediante gli operatori definiti sul tipo attraverso il meccanismo dell’incapsulamento). Esempi di classi sono: l’insieme delle autoradio prodotte da varie case costruttrici, l’insieme dei conto correnti di uno o più istituti bancari, l’insieme delle cartelle cliniche dei pazienti afferenti ad una data struttura ospedaliera, etc… Riferendoci all’esempio dell’autoradio, così come si possono costruire molte autoradio diverse sulla base di un unico progetto (e.g., modelli differenti di una stessa marca), analogamente, si possono costruire molti oggetti descritti da una stessa classe. Nella programmazione orientata agli oggetti è comune avere molti oggetti dello stesso tipo con caratteristiche simili. Il processo di creazione di un oggetto a partire da una classe è detto istanziazione, e, un oggetto descritto da una certa classe è detto un’istanza di quella classe; istanze diverse di una stessa classe sono da considerarsi distinte, in particolare, ciascuna istanza possiede e manipola un proprio stato. Le classi, invece, non hanno uno stato, così come non ha senso dire che il progetto di un autoradio è spento o acceso. Il lavoro del programmatore consiste nell’individuazione delle varie tipologie di oggetti software necessarie per un programma e nella definizioni della classi, per ciascuna tipologia di oggetto. La progettazione di una classe prevede la definizione degli attributi e delle operazioni comuni agli oggetti descritti dalla classe, la progettazione dell’interfaccia con la definizione dei metodi accessibili pubblicamente, e l’implementazione di ciascun metodo. Una classe deve rappresentare un unico concetto e soddisfare i seguenti requisiti: - coesione - gli attributi e i metodi devono essere strettamente correlati al concetto rappresentato dalla classe - coerenza - la descrizione delle operazioni deve seguire uno schema coerente. È opportuno che un oggetto rappresenti un concetto ben definito e che rimanga in uno stato consistente per tutto il tempo che viene utilizzato, dalla sua creazione alla sua distruzione. Inoltre in un insieme di classi è preferibile avere un basso livello di accoppiamento, cioè di dipendenza tra le classi. Un’eccezione è costituita, come vedremo, dal meccanismo dell’ereditarietà. 9.3.2. Oggetti software e classi come implementazioni di tipi di dato astratto Da un punto di vista implementativo un dato oggetto può essere visto come una sorta di dato strutturato, ad esempio un record Pascal o una struttura C, su cui agiscono un insieme di metodi che permettono di aggiornare i campi elementari del dato. Un oggetto è quindi assimilabile ad una struttura che occupa uno spazio di memoria durante l’esecuzione del programma, ed è composto da un certo numero di campi. In figura 6 sono mostrati degli oggetti che descrivono in modo parziale personaggi di film fantasy. 340 Capitolo nono Un oggetto spesso include riferimenti ad altri oggetti. Nel caso dei personaggi di un film, il riferimento è sicuramente al film stesso. Un riferimento può contenere un oggetto come suo valore (vedi figura 7). Programmare con il paradigma OO significa creare un gran numero di oggetti in maniera dinamica, seguendo un pattern che non è semplice predire a priori. Come già discusso nel paragrafo precedente, nel paradigma OO, invece che descrivere un unico oggetto, si descrive quanto vi è di comune ad una intera categoria di oggetti, attraverso il concetto di classe. Da un punto visto implementativo, quindi, una classe è simile ad un tipo record od ad un tipo struttura. Di seguito viene riportata una possibile definizione in un linguaggio simile al C, per la classe Personaggio, che descrive i personaggi di film fantasy (ssi suppone l’esistenza del tipo String per la gestione di stringhe di caratteri). Class Personaggio { String Nome; String Razza; String Classe; Film NomeFilm; } Nome, Razza, Classe e NomeFilm rappresentano gli attributi o variabili membro della classe; NomeFilm è inoltre il riferimento ad un oggetto appartenente alla classe Film (con attributi Nome, Regista, Nazione) che descrive invece i film. Class Film { String Nome; String Regista; String Nazione; } Figura 6 – Oggetti come record La programmazione orientata agli oggetti 341 È da notare che ogni classe descrive un insieme di oggetti potenzialmente infinito. Quindi un oggetto è l’istanza di una classe, la quale può essere vista come il tipo di un oggetto. Si noti inoltre che, mentre il concetto di oggetto esprime un concetto a tempo di “run-time” (un oggetto è creato durante l’esecuzione di un programma), quello di classe è invece un concetto statico a livello di compilazione: a tempo di esecuzione esistono solo oggetti; a tempo di compilazione solo classi. Figura 7 – Relazioni tra oggetti Oltre che dagli attributi, una classe è caratterizzata da operazioni o metodi che modificano il valore dei suoi attributi. Prendiamo ad esempio in considerazione la classe che descrive i numeri complessi: class Complex{ float Re; float Im; } e siano c1 ed c2 due oggetti della classe Complex Complex c1, c2; Per operare su di essi si possono definire delle procedure che permettono di accedere alla parte reale e alla parte immaginaria e di calcolare il modulo del numero complesso. Tali procedure caratterizzano il tipo Complex, e costituiscono a tutti gli effetti parte delle descrizione dell’oggetto complesso, quindi della sua 342 Capitolo nono classe. Tenendo conto dei metodi associati ad una classe, la definizione del tipo Complex si modificherà allora come nel seguito: class Complex{ float Re; float Im; public: float getReal(); float getImg(); void setReal(float reale); void setImg(float immaginario); float getModule(); } Dove l’identificatore public indica che i metodi di getReal, getImg, setReal, setImg e getModule, sono metodi “pubblici” che possono essere utilizzati da qualsiasi altro oggetto esterno. Gli attributi, al contrario, non sono modificabili da oggetti esterni. In realtà concetti di classe ed oggetto intuitivamente ricalcano quello che in informatica teorica viene chiamato Tipo di Dato Astratto (Abstract Data Type, ADT). Come già ricordato, un tipo di dato astratto è un tipo di dato definito dall’utente, e, quindi non predefinito nel linguaggio, che però viene gestito come un tipo primitivo, ovvero la sua rappresentazione può essere modificata solo mediante gli operatoti definiti sul tipo. Una classe è quindi l’implementazione di un ADT. Un ADT descrive una classe di strutture dati non attraverso la sua implementazione, ma attraverso un elenco di “servizi” disponibili sulla struttura dati, e attraverso alcune proprietà che formalmente descrivono e specificano questi servizi. Un ADT, in altre parole, è una descrizione precisa, non ambigua, e completa di una classe di dati, senza alcuna descrizione della sua possibile rappresentazione fisica. Possiamo dire allora che un ADT è una classe di strutture dati descritte attraverso una vista esterna: servizi disponibili e proprietà di questi servizi. Un esempio classico è quello dello Stack. Uno stack è caratterizzato da alcuni servizi verso l’esterno e da alcune proprietà che ne definiscono la struttura; la più importante delle quali è la strategia con cui si accede agli elementi dello stack che è la cosiddetta LIFO (Last In First Out). Il tipo Stack, definito su un insieme di oggetti T, deve poter offrire quindi all’esterno operazioni per la creazione dello stack (CreaUnoStack), per l’inserimento e prelievo di un elemento dallo stack (push e pop) e per verificare se lo stack è vuoto (Vuoto): CreaUnoStack(); push (T elem); T pop (); Vuoto(); operazioni che devono soddisfare le seguenti precondizioni o assiomi: La programmazione orientata agli oggetti - 343 è possibile effettuare un’operazione di pop se e solo se lo Stack non è vuoto; - appena creato lo Stack, quest’ultimo è vuoto; - effettuando un’operazione di push, lo Stack non è vuoto; - se si esegue un’operazione di pop dopo aver fatto una di push, lo Stack rimane nello stato di partenza; - un oggetto inserito, viene inserito sempre in testa. Nel caso più generale, un classe è dotata di tre tipi di metodi: - Costruttori - Metodi di Accesso - Metodi di trasformazione Di fatto, nel caso precedente dello stack, il metodo creaUnoStack istanzia un possibile stack, ed è dunque un costruttore di tipo; il metodo Vuoto è invece un metodo che restituisce una informazione relativa allo stack, dunque è un metodo di accesso alle stack. I metodi push e pop, invece, sono dei metodi che trasformano lo stato dello stack, e sono dunque dei metodi di trasformazione. Il metodo Vuoto, inoltre, potrebbe essere messo a disposizione dell’utente del tipo, oppure semplicemente usato per verificare la presenza di elementi nell’esecuzione delle operazioni di pop. In questo ultimo caso, diciamo che il metodo è privato alla classe, ovvero quando è usabile solo dai metodi della classe stessa e non dall’esterno. Si nota, dunque, che il tipo di dato astratto nasconde all’esterno l’informazione (information hiding): l’unico modo per manipolare i dati che essa gestisce, è l’uso dei metodi. Come è implementato lo stack, ovvero la struttura dati che ne permette la realizzazione, non interessa all’utente finale: lo stack può essere realizzato tramite array di elementi di tipo T o tramite liste a puntatori, ed il tutto deve essere trasparente all’utente finale. Ogni utente deve poi poter continuare ad usare le operazioni di push e pop in modo del tutto indifferente dalla loro implementazione: non interessa quindi il “come è fatto”, ma “cosa” si possa fare con un ADT. In un certo senso, è come se l’insieme dei servizi offerti dall’ADT ad un utente, ovvero la sua interfaccia, costituisca una sorta di “contratto” tra il programmatore e l’utilizzatore dell’ADT. Di seguito è riportata una possibile implementazione completa della classe Stack per la gestione di pile di caratteri. // Definizione della classe Stack Class Stack { // struttura dati utilizzata per l’implementazione dello stack char elem_array [DIMMAX] ; //riferimento alla testa dello stack ed elemento di testa dello stck int top; char top_elem; // Definizione dei metodi per la gestione dello stack public: // Metodo Costruttore void CreaUnoStack(); // Operazioni di pup e push 344 Capitolo nono void push (char elem); void pop(); private: // Metodo di verifica se lo stack è vuoto int Vuoto(); }; // Implementazione dei metodi // Metodo costruttore: produce l'inizializzazione dello stack void Stack::CreaUnoStack () { top=-1 ; // resetta il riferimento alla testa dello stack }; // Metodo push: inserisce un elemento in testa allo stack void Stack::push ( char elem ) { top=top+1 ; elem_array[top]=elem ; }; // Metodo pop: elimina l'elemento di testa dallo stack char Stack::pop () { if (!Vuoto()) { top_elem=elem_array[top] ; top=top-1 ; } else top_elem=’’; return top_elem; }; // Metodo Vuoto: controlla se lo stack è vuoto (ritornando il valore 1) int Stack::Vuoto () { if ( top==-1 ) return 0 ; else return 1 ; }; 9.3.3. Il meccanismo dell’ereditarietà L’ereditarietà è sicuramente una delle caratteristiche più importanti dei linguaggi orientati agli oggetti. Come visto, il concetto di classe e di oggetto ricalcano quanto teoricamente previsto negli ADT o in alcuni linguaggi basati sugli oggetti (object based). Un linguaggio segue il paradigma di programmazione orientato agli oggetti se e solo se è object based ed implementa un meccanismo linguistico noto con il termine di ereditarietà o inheritance . Il concetto di ereditarietà è alla base della riusabilità e dell’estensibilità dei sistemi software. Il mondo reale, d’altro canto, permette di definire tipi di oggetto a partire da tipi di oggetto già noti, per estensione, specializzazione o combinazione di questi. L’ereditarietà è il processo di creazione di una nuova classe con le caratteristiche di una classe esistente e con altre caratteristiche peculiari. Più nel dettaglio, essa rappresenta il meccanismo attraverso il quale è possibile “riusare” La programmazione orientata agli oggetti 345 (ereditare) le caratteristiche (attributi e metodi) di una classe, detta superclasse, da parte di altre classi dette sottoclassi. Ogni sottoclasse eredita la “struttura” ed il “comportamento” dalla superclasse e viene specializzata rispetto alle caratteristiche di quest’ultima attraverso la definizione delle proprie caratteristiche specifiche che consiste nell’aggiunta di nuovi attributi e metodi , e/o, ridefinizione dei metodi esistenti. Inoltre, la modifica delle caratteristiche della superclasse influenzano direttamente tutte le sottoclassi. In altri termini l’ereditarietà è un potente meccanismo di strutturazione dei programmi che consente di definire nuove classi a partire da quelli esistenti, ponendosi come obiettivi principali il riuso e l’estensibilità del codice. Si prenda, ad esempio, in considerazione il mondo geometrico, ed in particolare una classe ben definita detta Poligono. Un poligono, come mostrato di seguito, è definito dai suoi vertici e da alcuni metodi per il calcolo, ad esempio, del perimetro o dell’area. Class Poligono { Vertex v1,…,vn; public: float area(); float perimetro(); } Se si deve definire un rettangolo vi sono due possibili alternative: si definisce la classe Rettangolo da zero, rimplementandone struttura dati e metodi, oppure si prende in considerazione il fatto che un rettangolo è un tipo di poligono, per cui valgono tutte le proprietà già definite per il poligono più altre proprietà caratteristiche del rettangolo stesso. Il meccanismo che permette di ottenere tale gerarchia di classi è, per l’appunto, l’ereditarietà. Il Poligono verrà detto superclasse o genitore del rettangolo (parent oppure ancestor), mentre il Rettangolo è detto sottoclasse o figlio (descendant) del poligono. Si deve dunque esprimere che una classe Rettangolo “eredita” dalla classe Poligono i suoi dati ed i suoi metodi con una sintassi del tipo: class Rettangolo inherits from Poligono{ public: float diagonale( ); } Il che vuol dire che anche area e perimetro sono metodi della classe Rettangolo, ed anche i vertici v1,…,vn (eventualmente per n=4) sono dati della classe stessa. In aggiunta la classe Rettangolo prevede il metodo per il calcolo della diagonale. In altre parole, l’ereditarietà permette di riutilizzare i servizi offerti da una classe padre: se Y è ereditata da X, allora tutti i servizi di X sono automaticamente disponibili in Y, senza ulteriori definizioni, restando inteso che Y può aggiungere nuove caratteristiche (dati e servizi) per i prorpri scopi specifici. Si noti che l’ereditarietà è a volte vista come una estensione ed altre volte come una specializzazione. Dire che un rettangolo discende da poligono, equivale a 346 Capitolo nono dire che un rettangolo è un poligono. Nelle reti semantiche, questo tipo di relazione è anche detta relazione di tipo is_a. D’altro canto, considerando i servizi offerti dal rettangolo, possiamo dire che essi sono sicuramente i servizi offerti dal poligono con i più i metodi particolare del rettangolo: in questo senso un rettangolo è anche una estensione della classe poligono. Un ultima considerazione riguarda la possibilità di prevedere nei linguaggi orientati agli oggetti la cosiddetta ereditarietà multipla, che permette ad una classe di discendere da più classi contemporaneamente. L’ereditarietà favorisce la progettazione e lo sviluppo di un sistema software in modo completamente differente da quello tradizionale: invece di riprogettare ed implementare tutto da zero, l’idea è quella di costruire nuovi programmi su quanto già fatto, estendendone le caratteristiche. Per concludere il paragrafo, si vuole infine fare osservare come, nella progettazione OO, l’ereditarietà non è l’unica forma di relazione tra oggetti. In pratica esistono varie tipologie di relazioni, le più comuni sono: - generalizzazione/specializzazione ottenute attraverso il meccanismo di ereditarietà; - composizione/aggregazione che indicano che un dato oggetto di una data classe è costituito da uno o più oggetti di un’altra classe; - associazione che indica che ad un dato oggetto di una data classe è associato uno o più oggetti di un’altra classe. Infine un ulteriore potente meccanismo della programmazione OO è quello della redefinition o overloading: alcuni metodi offerti da X possono essere ridefiniti in Y in maniera più appropriata. La ridefinizione permette di cambiare l’implementazione di un metodo da parte della classe figlia, senza alterazione della semantica. 9.3.4. Polimorfismo e binding dinamico Con il termine polimorfismo si intende in generale la capacità di assumere forme differenti. Nella programmazione orientata agli oggetti, si intende la capacità di un oggetto di essere istanza di classi differenti. Siano ad esempio date le dichiarazioni: Poligono p; Rectangle r; Allora la seguente assegnazione è corretta: p=r; essendo ovviamente p ed r i riferimenti agli oggetti. Ciò discende dal fatto che r, essendo rettangolo, è un poligono. E’ invece scorretta l’assegnazione: r=p; in quanto un poligono non è un rettangolo. L’idea di oggetti che assumono “forma differente” risulterà chiaro dall’esempio seguente. Sia data la seguente gerarchia di classi ottenuta attraverso il meccanismo di ereditarietà: La programmazione orientata agli oggetti 347 // Esempio perpolimorfismo class Animale{ public: void RiproduciVerso(); }; class Mammifero inherits from Animale; class Quadrupede inherits from Mammifero; class Cane inherits from Quadrupede; class Gatto inherits from Quadrupete; Cane pluto; Gatto tom; L’oggetto tom (così come l’oggetto pluto) è a sua volta un oggetto di tipo quadrupede, un oggetto di tipo mammifero ed un di tipo oggetto animale. È come dire che i gatti (ed i cani) sono degli animali mammiferi. Consideriamo, tuttavia, il metodo pubblico della classe animale RiproduciVerso che evidentemente deve poter esprimere il verso di un animale (se emette suoni). Si supponga che la classe Cane e la classe Gatto ridefiniscano il metodo, nel seguente modo: Cane::RiproduciVerso(){ printf(“ Bau Bau Bau”); } Gatto::RiproduciVerso(){ printf(“ Miao Miao”); } Si consideri ora il seguente frammento di codice (si nota che con la sintassi nome_oggetto->nome_metodo si attiva il metodo selezionato della classe a cui appartiene l’oggetto): a=pluto; a->RiproduciVerso(); a=tom; a->RiproduciVerso(); La prima esecuzione del metodo RiproduciVerso stamperà a video “Bau Bau Bau”, mentre la seconda esecuzione farà stampare a video “Miao Miao”. L’oggetto animale è dapprima un cane, per cui verrà applicato il metodo Cane::RiproduciVerso(), quindi è un gatto (per effetto del polimorfismo), e dovrà essere logicamente applicato il metodo Gatto::RiproduciVerso(). Questa regola è nota anche con il termine di collegamento dinamico o dynamic binding ed afferma che è la forma dinamica dell’oggetto a determinare quale versione del metodo deve essere applicato ad un oggetto polimorfo. Polimorfismo e binding dinamico, entrambi direttamente discendenti dal 348 Capitolo nono meccanismo dell’ereditarietà, sono le proprietà più importanti dei sistemi Object Oriented. 9.3.5. Note di progetto Come si può intuire alla fine di questa breve trattazione, programmare ad oggetti non velocizza l’esecuzione dei programmi e non ottimizza l’uso della memoria. È allora lecito domandarsi: perché programmare a oggetti? La risposta è che programmare ad oggetti facilita, come visto e discusso ampiamente nei precedenti paragrafi, la progettazione, lo sviluppo e il mantenimento di sistemi software molto complessi, cosa che invece non accade con un approccio di tipo procedurale. Ricapitolando, si è visto come le caratteristiche principali del paradigma object oriented sono: - astrazione dei dati: è possibile creare nuovi tipi di dato per adattare l’ambiente di programmazione alle proprie esigenze; - ereditarietà: una volta definito un tipo, si possono specificare altri tipi basati su di esso; - polimorfismo: è possibile rendere i tipi derivati “estensibili” rispetto ai tipi base. Attraverso tali caratteristiche la programmazione ad oggetti consente di: - ridurre la dipendenza del codice di alto livello dalla rappresentazione dei dati (l’accesso ai dati è mediato da un’interfaccia); - riutilizzare del codice di alto livello; - sviluppare moduli indipendenti l’uno dall’altro. Vengono così a scomparire i difetti derivanti da un software non manutenibile (ad esempio ottenuto con un approccio procedurale) come: - la rigidità (i.e., non può essere cambiato con facilità e non può essere stimato l’impatto di una modifica), - la fragilità (i.e., una modifica singola causa una cascata di modifiche successive ed i bachi sorgono in aree concettualmente separate dalle aree dove sono avvenute le modifiche), - la non riusabilità (i.e, esistono molte interdipendenze, quindi non è possibile estrarre parti che potrebbero essere comuni). Capitolo decimo Introduzione ai Sistemi Operativi 10.1. Introduzione Un Sistema Operativo è una componente fondamentale di un sistema di calcolo e cercare di delineare brevemente le sue caratteristiche fondamentali è una attività complessa. Per questo, invece di iniziare con definizioni rigorose, cercheremo di far anzitutto comprendere il motivo per cui sono sorti i sistemi operativi sulla scena dell'informatica, mostrando di volta in volta le problematiche che essi sono chiamati a risolvere. Si consideri un sistema di calcolo secondo il modello di von Neumann: i processi elaborativi, intesi come programmi o parti di programma in esecuzione vengono eseguiti di fatto dalla CPU con velocità diverse. Ad esempio un processo di stampa dei risultati viene eseguito con una velocità differente rispetto ad un processo di calcolo per la lentezza dei dispositivi (alcuni meccanici) della stampante. Più precisamente, prendiamo in esame un sistema come quello in figura 1 ed un programma che effettui la somma ed il prodotto di due numeri: inizialmente i valori devono essere prelevati da tastiera e, dopo aver effettuato gli opportuni calcoli, i risultati devono essere visualizzati sul monitor. Figura 1 – Esempio di sistema con i due canali input standard ed output standard 350 Capitolo decimo La CPU non solo deve eseguire le istruzioni di calcolo ma, interfacciandosi con i dispostivi di I/O, anche quelle di ingresso e uscita dei dati con processi diversi che devono essere sincronizzati, come mostrato di seguito. input(dato1); input(dato2); somma:=dato1+dato2; output(somma); prodotto=dato1xdato2; output(prodotto); Individuiamo banalmente tre processi: input da tastiera, calcolo e stampa; se è plausibile che essi presentano tempi di elaborazione differenti, è altresì impensabile che la somma dei numeri sia eseguita prima dell’input dei dati, o che l’output avvenga dopo la produzione dei risultati. In altre parole, è necessario introdurre dei meccanismi di sincronizzazione tra i tre processi: è possibile fare le operazioni sono quando i dati di ingresso sono pronti, ed è possibile effettuare una stampa solo quando i risultati sono stati prodotti. Un sistema semplice per effettuare la sincronizzazione dei sistemi periferici consiste nell’attribuire ad ogni sistema di gestione della periferica (detto anche canale) un registro di stato, in cui un particolare bit, detto bit di flag, indica se la periferica ha completato l’ultima operazione richiesta. Figura 2 – Diagrammi di flusso per la sincronizzazione Nel caso del canale di gestione della tastiera, tale bit è 1 quando è stato premuto e rilasciato un carattere nel buffer di lettura, ed è 0 quando tale carattere è prelevato dalla CPU. Analogamente per il monitor, il bit uguale ad 1 indica che la periferica ha terminato l’operazione di stampa ed è pronta a ricevere un altro carattere nel buffer (si suppone che la periferica sia in grado di stampare un solo carattere per volta), il bit viene poi posto a 0 dalla CPU dopo l’invio del dato. In un sistema così gestito, la CPU per effettuare le istruzioni di input ed output interroga ciclicamente i registri di stato delle periferiche, aspettando che abbiano termine eventi esterni non dipendenti da essa, quali l’inserimento di un Introduzione ai sistemi operativi 351 dato da parte di un operatore: solo quando i bit di flag sono uguali ad uno, può dirsi completata l'operazione di lettura, ovvero: - nel caso della tastiera prelevando il dato dal buffer dati e ponendo il bit a 0; - nel caso del monitor inviando il dato nel buffer dati e ponendo il bit a 0. Quelle finora descritte prendono il nome operazioni di lettura e scrittura a controllo di programma. Figura 3 – Diagrammi di flusso per la sincronizzazione La strategia di sincronizzazione presentata, a causa delle differenti velocità di elaborazione tra le istruzioni di calcolo interno (dell’ordine dei nanosecondi) e le istruzioni di input/output (dell’ordine dei millisecondi perchè richiedono il colloquio con i dispositivi di I/O) portano ad un uso non intelligente della risorsa CPU che, nel caso di programmi con molte operazioni di I/O, passa la gran parte del proprio tempo ad interrogare lo stato delle periferiche. I tempi complessivi di elaborazione sono dati dalla somma dei tempi necessari alla terminazione dei singoli processi e comprendono quindi tempi che non dipendono dalle capacità della CPU. Nel caso delle operazioni di input, ad esempio, tali tempi dipendono dalle capacità dell’operatore o dalla tempestività con la quale inserisce i valori. Figura 4 – Tempi di elaborazione 352 Capitolo decimo Quanto precedentemente descritto mostra come, anche per un esempio molto semplice, esiste la necessità di utilizzare al meglio il tempo della CPU, evitando di introdurre inutili tempi di attesa a scapito della velocità e dell’efficienza che si richiede a sistemi complessi e, per certe applicazioni, con tempi di risposta stringenti e critici. Ciò ha fatto nascere l’esigenza di introdurre programmi capaci di amministrare in modo più razionale tutte le risorse di un calcolatore, CPU compresa. Tali programmi sono appunto i sistemi operativi. 10.2. Caratteristiche di un Sistema Operativo Esistono nella relativa letteratura specializzata molte definizioni di che cosa è un sistema operativo e di quale sono le sue funzionalità, anche per il fatto che i sistemi operativi hanno avuto un’evoluzione storica connessa al parallelo sviluppo dell’hardware dei calcolatori e alle crescenti esigenze degli utilizzatori. Seconda una definizione di tipo generale, un sistema operativo è un insieme di programmi (spesso di grande complessità) che implementano le funzioni essenziali per la gestione di un sistema di elaborazione e realizzano un’interfaccia “facile” da usare tra l’hardware della macchina e gli utilizzatori. Da un punto di vista di tipo top-down un sistema operativo può essere quindi visto come una macchina estesa che si pone tra l’hardware della macchina e gli utilizzatori finali (programmatori ed utenti), offrendo a questi ultimi un’interfaccia gradevole ed amichevole. Tale aspetto pone l’accento su tutti i problemi di interfaccia utente e di semplicità d’uso che sono molto attuali per chi progetta e realizza sistemi operativi. Basti pensare alle interfacce grafiche degli attuali sistemi operativi quali Windows, Linux e MacOS che permettono agli utenti di interagire con la macchina attraverso semplici “click” del mouse. Da un punto di vista bottom-up, invece un sistema operativo risulta un gestore delle risorse hardware e software di un sistema di elaborazione quali la CPU, la memoria, i dischi, i dispositivi di I/O, programmi, etc… In questo caso l’attenzione è posta sull’attività di “supervisore” che il sistema operativo deve esercitare nei confronti degli altri programmi eseguiti sulla macchina e di “gestore” delle risorse hardware di un calcolatore. Si può dire che un sistema operativo consente di creare un ambiente di lavoro virtuale su di una macchina, ponendosi tra la ‘macchina nuda” (l’hardware) ed il mondo delle applicazioni. La “virtualizzazione” delle risorse presenta ai suoi utenti una macchina astratta caratterizzata dalle sue capacità e non dalla sua struttura. E le funzionalità principali che devono essere offerte sono: - offrire un ambiente di lavoro amichevole; - assegnare a programmi ed utenti le risorse hardware e software disponibili ottimizzandone l’utilizzo; - controllare l’esecuzione dei programmi ed in particolare l’uso della CPU, della memoria e dei dispositivi di I/O. Le applicazioni che vengono solitamente usate dall’utente possono “prendere vita” soltanto all’interno dell’ambiente generato dal sistema operativo. Esso ha il compito fondamentale di controllare e coordinare l’utilizzo dell’hardware della macchina per conto degli utenti e delle applicazioni. In altri termini, un sistema operativo è un insieme di programmi che si interpone tra l’hardware di un calcolatore e chi lo deve utilizzare. Tali programmi Introduzione ai sistemi operativi 353 si pongono l’obiettivo di consentire al calcolatore di svolgere le proprie funzioni con la massima efficienza possibile. L’efficienza di un sistema di elaborazione delle informazioni, intesa come capacità del sistema di soddisfare al meglio le esigenze degli utilizzatori, può essere misurata in vari modi. Tra le necessità principali che hanno guidato la comparsa prima, e l’evoluzione poi dei sistemi operativi, c’è quella di rendere massimo il lavoro compiuto dal calcolatore in termini di programmi eseguiti nell’unità di tempo, indicato col termine throughput; nel contempo, si vuole limitare il tempo che intercorre per ogni programma tra l’introduzione dei dati di ingresso e la presentazione dei risultati dell’elaborazione, di solito indicata con il termine tourn around time. Si noti che tali caratteristiche sono in palese contrasto tra loro in quanto bassi tempi di tourn around time prevedono un uso esclusivo delle risorse di un sistema da parte dei singoli programmi, condizione che va in contraddizione con l’aumento del throughput. Affinché un programma presenti il più basso tourn around time, infatti, deve trovare sempre disponibili sia la CPU che le periferiche di input ed output: ciò costringerebbe la CPU a dedicarsi completamente ad un programma quando potrebbe, invece, eseguirne degli altri mentre il primo è fermo in attesa che operazioni di ingresso ed uscita arrivino a terminazione. Per consentire alla CPU di non preoccuparsi della gestione dell’input e dell’output il modello di Von Neumann è stato nel tempo modificato fornendo alle periferiche un’autonomia di funzionamento e la capacità di segnalare la terminazione delle proprie attività. Ogni periferica viene dotata di un processore dedicato il cui compito è quello di gestire le operazioni di ingresso ed uscita liberando la CPU da tale compito. Il processore dedicato opera in parallelo con la CPU e segnala che ha terminato il compito affidatogli inviando una richiesta di interruzione. La CPU, dal canto suo, dopo aver attivato il processore dedicato, può dedicarsi ad altro fino a che non viene interrotto. La figura seguente confronta il comportamento di un sistema senza processori dedicati (detti anche canali) con uno che li possiede. Figura 5 – Confronto tra tempi di elaborzione 354 Capitolo decimo Si nota un miglioramento complessivo del sistema perché vengono utilizzati i tempi morti dei canali, anche se il tourn around time dei singoli programmi peggiora. Inoltre per rendere il calcolatore facilmente usabile e condivisibile da più utenti, così come se si vogliono prevenire situazione di guasti, è necessario concedere tempo di esecuzione e risorse ai programmi del sistema operativo, sottraendoli ad altri programmi in esecuzione. Si introduce così una condizione di “sovraccarico gestionale” o overhead che è di norma tanto più grande quanto più complesso e sofisticato è il sistema operativo. Basti pensare ad esempio al sistema operativo Windows Vista che per un corretto funzionamento richiede quasi un Giga di memoria centrale. 10.2.1. L’evoluzione storica dei Sistemi Operativi I sistemi operativi, analogamente all’hardware dei calcolatori, hanno subito nel corso della storia una serie di cambiamenti che hanno portato alla nascita degli attuali sistemi. L’evoluzione dei sistemi operativi è classificabile in otto generazioni, dove ogni generazione si distingue dalla precedente per una precisa evoluzione tecnologica. 1. Anni ’40: Assenza dei Sistemi Operativi 2. Anni ’50: Monitor e Sistemi Operativi Batch 3. Anni ’60: Sistemi Operativi Multiprogrammati 4. Anni ’60-70: Sistemi Operativi General Purposes 5. Anni ’70: Sistemi Time Sharing 6. Anni ’80: Sistemi Operativi per Personal Computer e Sistemi Operativi Transazionali 7. Anni ’90-00: Sistemi Operativi Real-Time, Sistemi Operativi con supporto per multiprocessore e Sistemi Operativi Distribuiti 8. Anni ’00 ad oggi: Sistemi Operativi per dispositivi mobili (palmari, cellulari, etc..) Nel periodo che va dalla nascita dei calcolatori agli anni ’50 i sistemi operativi sono praticamente inesistenti: gli utilizzatori scrivono programmi direttamente in linguaggio macchina e si fanno carico anche del caricamento in memoria e dell’avvio dei programmi. Ogni programma viene caricato singolarmente, solitamente usando schede perforate, e quindi fatto eseguire fino al suo completamento o fino a che, per qualche motivo, non si ferma. A questo punto l’operatore può eseguire la stampa dei risultati oppure, in caso di terminazione anomala, il dump (stampa) dei registi della memoria per esaminare la causa dell’errore. La CPU rimane inattiva durante tutte i periodi relativi alla gestione delle operazioni di I/O e tra l’esecuzione di un programma ed un altro. Negli anni ’50 la General Motors produce il primo sistema operativo per il proprio calcolatore, l’IBM 701. Tale sistema operativo, così come altri della stessa generazione, era una sorta di monitor che si occupava di caricare i programmi in memoria e di controllare l’esecuzione dello stesso, azzerando poi la macchina tra un’esecuzione e l’altra di programmi. I programmi erano fatti in modo da restituire il controllo, alla fine della propria esecuzione, al sistema operativo che si occupava di azzerare (anche detto resettare) lo stato della macchina e di iniziare a leggere il Introduzione ai sistemi operativi 355 programma successivo. L’obiettivo era quello di limitare i tempi morti della CPU, riducendo l’azione dell’uomo nell’esecuzione dei programmi. Un’ulteriore ottimizzazione introdotta dai sistemi operativi di questa generazione è quella di raggruppare più programmi in un singolo gruppo o lotto (batch), caricandoli tutti, prima dell’esecuzione, su una periferica più veloce del lettore di schede, ad esempio su nastro magnetico. Il caricamento su nastro viene fatto fuori linea da un apposito dispositivo, mentre il controllo della esecuzione ed il passaggio da un programma all’altro avviene utilizzando un apposito linguaggio di controllo detto Job Control Language (JCL). Inoltre, i suddetti sistemi operativi mettono a disposizione dei programmatori delle primitive per l’input e output sui dispositivi standard (schede, nastro, stampante, etc…) in modo da svincolare questi ultimi dalla natura delle periferiche di input/ouput. Con la gestione a lotti si superano i tempi morti legati all’inizio e terminazione dei programmi, permane però ancora il problema della inattività della CPU durante le operazioni di I/O. La successiva generazione di sistemi operativi introduce il concetto di multiprogrammazione che consiste nel caricare in memoria centrale più programmi facendo in modo che condividano la CPU. Tale caratteristica è resa possibile grazie all’introduzione di due importanti innovazioni: - i processori di I/O, ovvero processori più semplici della CPU, dotati di un insieme proprio di istruzioni e registri, ed in grado di gestire la comunicazione con i dispositivi di I/O; - le interruzioni, ovvero segnali generati dai controllori dei dispositivi di I/O in grado, arrivando alla CPU, di interrompere il suo normale ciclo di esecuzione. Attraverso l’introduzione dei processori di I/O e del meccanismo delle interruzioni è possibile sovrapporre le operazioni di input ed output con quelle di calcolo, eliminando i tempi morti della CPU dovuti all’attesa del completamento delle operazioni stesse di I/O. Se la CPU sta eseguendo un programma ed esso richiede un’operazione di I/O, il programma corrente viene sospeso e l’operazioni di I/O è delegata al processore di I/O del dispositivo. Intanto, la CPU può eseguire un altro programma, mentre un’interruzione segnalerà il termine dell’operazione di I/O e la ripresa del programma precedentemente sospeso. Con una tale gestione il sistema operativo diventa l’amministratore delle risorse del calcolatore: i vari programmi che coesistono nella memoria centrale devono dividersi l’uso delle periferiche, della memoria e della CPU. La successiva generazione di sistemi operativi porta all’ottimizzazione della tecnica della multiprogrammazione, prevedendo sia il caricamento in memoria di quelli idonei all’esecuzione, mediante la tecnica dello swapping (ogni programma viene spostato dalla memoria di massa alla memoria centrale solo quando serve), che il caricamento in memoria non di un intero programma ma di sole specifiche parti (quelle che con più probabilità serviranno in un dato istante secondo il principio di località dei dati e delle istruzioni), aumentando così il numero di programmi che è possibile caricare contemporaneamente nella memoria centrale (grado di multiprogrammazione). Con la memoria virtuale viene inoltre estesa la capacità della memoria centrale con quella delle memorie di massa, ed un programma può avere una dimensione potenzialmente infinita. Infatti con la divisione del programma in parti vengono portate in memoria solo quelle che devono essere eseguite mentre le restanti restano a disposizione sulla memoria di 356 Capitolo decimo massa. Il sistema operativo trasferisce le porzioni in memoria centrale quando non servono (swap out) e preleva dalla memoria di massa quelle che devono essere eseguite (swap in). I sistemi operativi time-sharing della quinta generazione nascono con l’esigenza di consentire a più utilizzatori di avere accesso diretto e contemporaneo ai loro programmi e dati durante l’esecuzione, si parla infatti di sistemi multiutente o conversazional/interattivii. Il problema dei sistemi della precedente generazione risiedeva nel fatto che programmi con molto calcolo interno potevano monopolizzare l’uso della CPU, causando lunghi tempi di attesa per gli altri programmi. Affinché ciò non accada è possibile assegnare degli intervalli di tempo della CPU, detti anche quanti di tempo o time slice, ad ogni programma, dopodiché terminato tale tempo il programma in esecuzione deve abbandonare la CPU (anche se non ha richiesto alcuna operazione di I/O) e cedere il posto al programma successivo secondo prestabilite politiche di schedulazione. Poichè un programma potrebbe richiedere molte time slice prima di terminare, si ha che, rispetto ai sistemi multiprogrammati non time-sharing, il tourn around time peggiora ma migliora il throughput complessivo. Nella generazione successiva i sistemi operativi hanno un’architettura interna pressoché invariata e l’attenzione viene posta sulle interfacce d’utilizzo al fine di facilitare l’uso dei primi personal computer agli utenti inesperti. Nascono così i primi sistemi operativi user-fiendly che nelle successive generazioni porteranno alla comparsa degli attuali sistemi operativi Windows, Linux e MacOS con interfacce di utilizzo completamente grafiche. Si assiste quindi alla nascita di sistemi operativi le cui funzionalità sono rivolte alla tipologia di applicazioni per cui sono progettati. Nascono così i sistemi operativi transazionali (si pensi ai sistemi operativi per gli sportelli del bancomat), ovvero sistemi interattivi destinati ad eseguire transazioni ovvero sequenze di operazioni elementari logicamente legate, dove l’utente interroga ed aggiorna archivi di dati. Un altro esempio è costituito dai sistemi operativi real-time, ovvero sistemi di tipo che gestiscono programmi che interagiscono con l’ambiente esterno attraverso periferiche e che garantiscono tempi di risposta utili per le applicazioni e l’ambiente esterno stesso (si pensa ai sistemi per il controllo di un reattore nucleare o dei radar). Negli anni ‘90 l’evoluzione dell’hardware e la diffusione della rete internet porta alla replicazione di alcune componenti del modello di Von Neumann (es. schede madri multiprocessore) ed alla distribuzione delle risorse di calcolo (es. stampanti, file system) nella rete, e ciò comporta la nascita dei sistemi operativi con supporto multiprocessore e distribuiti, ovvero di sistemi in grado di gestire più processori e risorse multiple che non sono più localizzate su una macchina, ma si trovano distribuite sia fisicamente che logicamente. L’ultima generazione (2008) dei sistemi operativi vede infine un “adeguamento” delle loro caratteristiche ai dispositivi mobili, come palmari e cellulari. In tali sistemi si prediligono caratteristiche come interfacce grafiche evolute e risparmio energetico. 10.2.2. Alcuni esempi di Sistemi Operativi Nel seguito si riporta una breve descrizione dei sistemi operativi più conosciuti ad oggi (2008), per quanto riguarda il mondo dei personal computer, che Introduzione ai sistemi operativi 357 sono stati pietre miliari nel mondo dell’informativa e che, almeno per quanto riguarda i più recenti, probabilmente saranno ancora sulla scena nei prossimi anni. DOS Dal punto di vista storico, non si può non partire dal Disk Operating System, o DOS. Il DOS più famoso è certamente MS-DOS della Microsoft, ma è giusto ricordare anche il DR-DOS della Digital Research, (peraltro compatibile con MSDOS) e il PC-DOS della IBM (su licenza della Microsoft). MS-DOS è stato progettato e sviluppato per lavorare con i microprocessori (8088-8086) Intel montati dalla IBM sui primi personal computer agli inizi degli anni Ottanta. Di MS-DOS sono state commercializzate diverse versioni, l’ultima delle quali è la 7.0. MS-DOS è nato ed è rimasto un sistema operativo monoutente. UNIX e LINUX UNIX/Linux è un sistema operativo multiutente e multitasking (in grado quindi di gestire contemporaneamente diversi programmi che possono essere visualizzati sullo schermo). Esistono differenti versioni di UNIX, ognuna con diverse caratteristiche a seconda del produttore. UNIX è il sistema operativo più aperto e portatile tra quelli esistenti perché, semplicemente apportando, se necessarie, alcune modifiche, è in grado di gestire piattaforme diverse (dal mainframe al PC), nonché di essere utilizzato su computer basati su microprocessori con diverse tecnologie costruttive. LINUX è la versione freeware molto potente di UNIX, che è scaricabile gratuitamente da Internet o acquistabile, a prezzo simbolico, su CD-ROM. Linux è un prodotto realizzato a livello universitario. L’interfaccia grafica si chiama X Window. Il sistema è costantemente aggiornato da sviluppatori delle più rinomate università del mondo (il codice sorgente è infatti pubblico) e può costituire una valida soluzione che non ha niente da invidiare alle implementazioni commerciali di UNIX. OS/2 OS/2 è frutto di un’intesa tra Microsoft e IBM, per le piattaforme basate sui processori INTEL e compatibili. La prima versione risale al 1987. Fu il primo sistema operativo multitasking e multiutente. Due anni più tardi uscì la versione 1.3, l’ultima sviluppata congiuntamente da IBM e Microsoft. OS/2 forniva le prestazioni di un vero sistema operativo a 32 bit anche a piattaforme Intel. WINDOWS Windows nasce come un ambiente grafico per il DOS, al quale si appoggiava per svolgere molte delle sue funzioni interne: era a tutti gli effetti una specie di “guscio” intorno al DOS del quale ha bisogno per poter essere eseguito. Prodotto dalla Microsoft negli anni Ottanta, riesce ad imporsi sul mercato solo con la versione 3.1, dieci anni dopo. La versione successiva alla 3.1 è Windows 3.11 for Workgroup, particolarmente adatta alla connessione in rete. A partire da tale versione, Windows diventa uno standard mondiale per computer con microprocessori Intel compatibili. L'evoluzione a Windows 95 (dall’anno di commercializzazione) porta ad un radicale cambiamento e la versione successiva 358 Capitolo decimo (Windows 98) ha aggiunto multimedialità, grafica molto curata ed un modo di operare estremamente intuitivo. Nasce inoltre anche una versione multitasking e multitutente, detto Windows NT (New Technology), che funzionava su piattaforme Intel compatibili, Alpha e alcuni RISC. Il successore di Windows NT è Windows 2000, conosciuto anche con i nomi in codice Cairo e Windows NT 5. Al pari del suo predecessore, Windows 2000 è pensato per essere usato in ambito professionale e come server grazie alle elevate prestazioni, stabilità e sicurezza. Nel 2000, Microsoft presenta Windows ME (Millennium Edition), un progetto che serve come ripiego temporaneo tra Windows 98 e il nuovo Windows XP. L'unificazione delle linee Windows NT/2000 e Windows 3.1/95/98/ME viene raggiunto con Windows XP, rilasciato nel 2001. Windows XP usa il kernel di Windows NT. Nel 2003, Microsoft rilascia Windows Server 2003, un aggiornamento del sistema operativo per server che incorpora molte delle caratteristiche di Windows XP con migliorate componenti server e strumenti di amministrazione. Allo stato (2008), la versione più recente di Windows è Windows Vista, che contiene molte nuove funzioni rispetto a XP, e diverse migliorie, come la nuova interfaccia grafica (Graphical User Interface, GUI) chiamata Windows Aero. Windows Vista si presenta come un sistema operativo che garantisce meglio l'utilizzatore dal punto di vista della sicurezza e vulnerabilità rispetto ai virus informatici, malware ed ai problemi legati ad errori di sistema. Mac OS Parallelamente alla linea Microsoft, la Apple Computer ha proposto sul mercato il Mac OS, acronimo di Macintosh Operating System, a tutti gli effetti il primo sistema operativo ad utilizzare con successo un'interfaccia grafica. Il sistema funzionava essenzialmente sui processori CISC Motorola della serie 68000, utilizzati nei Macintosh per molti anni. Nel 1994 vennero lanciati i Power Macintosh basati sui processori RISC PowerPC, sviluppata da un consorzio comprendente Apple, IBM e Motorola, ed il sistema operativo venne gradualmente convertito in codice PowerPC. A partire dalla versione Mac OS 10, denominata Mac OS X, la Apple rivoluziona il suo sistema operativo, completamente riscritto e basato su piattaforma Unix. A fine giugno 2005 viene adottata la CPU Intel per le macchine Apple, appositamente modificati per far funzionare il Mac OS X. La versione attuale (2008) è la Mac OS X 10.5, o Leopard. 10.3. L’architettura dei Sistemi Operativi I moderni sistemi operativi presentano un’architettura a livelli, detta anche a buccia di cipolla. Introduzione ai sistemi operativi 359 hardware Kernel Gestore Memoria Gestore periferiche Gestore file system Programmi di utilità Shell e programmi utente Figura 6 – Architettura di un sistema operativo Ogni strato o livello del sistema operativo costituisce una macchina virtuale: cioè una macchina definita da ciò che è capace di fare e non da come è fatta. Ogni macchina virtuale si avvale delle macchine sottostanti per funzionare e una tale impostazione modulare favorisce lo sviluppo del sistema operativo consentendo di modificare solo alcuni livelli senza toccare tutti gli altri. Solo lo strato hardware costituisce una macchina reale (Modello di Von Neumann). I livelli, da quello più vicino all’hardware a quello più a contatto con gli utenti e le applicazioni, sono: 1) il nucleo o kernel che gestisce i processi e la sincronizzazione tra i processi. Alloca la CPU ai processi tramite un opportuno schedulatore (scheduler) e gestisce le interruzioni; 2) il gestore della memoria che effettua la corrispondenza tra indirizzi logici e fisici dei programmi e alloca la memoria ai programmi che ne fanno richiesta. Si occupa infine del caricamento e scaricamento delle informazioni dalla e verso la memoria di massa (swapping) e della protezione delle aree di memoria; 3) il gestore delle periferiche che fornisce le primitive che implementano le operazioni di I/O sui dispositivi e gestisce parzialmente i malfunzionamenti; 4) il file system che gestisce l’organizzazione logica e fisica delle informazioni nella memoria di massa (i file), e ne controlla gli accessi (per leggere, scrivere, o modificare i file); 360 Capitolo decimo 5) l’interprete dei comandi che forma un guscio (shell) attraverso cui l’utente e le applicazioni interagiscono con il sistema operativo; 6) i programmi di utilità, ossia programmi di sistema per il supporto allo sviluppo dei programmi, quali editor di testi, fogli elettronici, database, etc Al primo livello, la macchina kernel presuppone che ogni processo disponga virtualmente di un proprio processore. Al secondo livello la macchina gestore della memoria può essere schematizzata invece come una macchina in cui ogni processo possiede virtualmente una propria CPU e una propria memoria. Analogamente al terzo livello la macchina gestore delle periferiche assegna a ciascun processo la sua CPU, la sua memoria ed un insieme di periferiche. La macchina del quarto livello permette ad ogni processo di vedere ed utilizzare un proprio file system. Infine, ai livelli successivi la macchina rimane sostanzialmente la stessa e ci sono solo servizi come i programmi di utilità, la shell ed i programmi utenti. 10.3.1. La gestione dei processi Un processo non è altro che un programma, o una sua parte, eseguita dalla CPU: si può pensare ad un processo come ad una sequenza di istruzioni, con un inizio ed una fine determinate, che fa parte di un programma più grande. In un sistema multiprogrammato più processi risiedono in memoria e procedono nella esecuzione con tecniche di gestione differenti. In un tale ambiente i diversi processi vengono detti concorrenti perché si contendono il possesso delle stesse risorse (CPU, memoria, input ed output) e il sistema operativo deve provvedere a risolvere le problematiche di cooperazione, competizione e interferenza tra essi esistenti. La competizione è una forma di interazione tra i processi prevedibile, ma non desiderata, perché ne rallenta l’esecuzione. Viceversa la cooperazione è una forma di interazione prevista e desiderata, fra processi tra loro logicamente ‘imparentati’ nel senso che hanno obiettivi comuni. Un esempio di cooperazione è quello tra processi concorrenti che si scambiano messaggi di sincronizzazione per proseguire nella esecuzione. Un esempio di competizione è, invece, quello tra più processi che vogliono usare una stessa risorsa comune (come una stampante). Il sistema operativo deve poter gestire la possibilità (non desiderata) che vi possano essere altri processi concorrenti che, richiedendo l’uso della stessa stampante, scatenino conflitti, non certi, ma possibili, capaci di bloccare il sistema. Quanto all’interferenza, essa si verifica in presenza di interazioni tra processi che la natura del problema non mostra in modo esplicito come ad esempio accade quando un processo P ha bisogno di effettuare una stampa, ma prima che ciò avvenga, un secondo processo Q altera senza alcuna necessità lo stato della stampante bloccandola. L’interferenza può essere anche provocata da erronee soluzioni a problemi di cooperazione e competizione. Il sistema operativo può occuparsi solo delle interferenze macroscopiche, mentre la maggioranza delle interferenze deve essere gestite a monte nel progetto dei diversi programmi. Affinché un sistema operativo possa gestire i processi, deve esistere per ogni processo un’area dati non accessibile al processo ma associata ad esso, contenente un insieme di informazioni indispensabili dette descrittore del processo. Un processo non può esistere se non esiste il suo descrittore. Un processo nasce nel momento in cui viene allocato il suo descrittore, e quindi un attimo prima che Introduzione ai sistemi operativi 361 venga eseguita la sua prima istruzione. Per tale ragione si può affermare che un processo “prende vita” ancor prima di cominciare ad essere eseguito. Il descrittore è un record, ovvero un insieme di campi contenenti tutte le informazioni necessarie all’esecuzione di un dato processo. Tra i principali campi si trovano il nome o identificatore del processo ed il suo stato. Un ulteriore campo contiene l’insieme dei valori dei registri del processore nel momento in cui quest’ultimo perde temporaneamente l’usufrutto della CPU, ossia nel momento in cui passa dallo stato running allo stato waiting o di ready (come vedremo di seguito). Altri campi sono utilizzati per memorizzazione informazioni quali la priorità, i dispositivi di I/O utilizzati, il tipo di relazione (“parantela”) con altri processi, e così via. La componente del sistema operativo dedicata a svolgere la gestione dei processi è il cosiddetto nucleo o kernel. Il kernel deve presentare le seguenti funzionalità: - Creazione di processi: che permette la generazione di nuovi processi all’interno di un sistema operativo. Un esempio sono le primitive di fork che consentono ad un dato processo in esecuzione di creare un altro processo, detto figlio, a cui delegare determinate operazioni (ad esempio stampa dei risultati elaborativi), e, quelle di join, che consentono al processo padre di sincronizzarsi con la terminazione dei figli (ovvero attesa della stampa prima di proseguire con le successive elaborazioni). - Sincronizzazione di processi: che consente di risolvere i citati problemi di cooperazione, competizione ed interferenza. A tale proposito i sistemi operativi seguono due modelli fondamentali, i modelli a memoria comune ed a scambio di messaggi. Nel primo modello si hanno più processi che accedono a risorse comuni (hardware/software), le quali si trovano in un’unica memoria condivisa. I problemi più diffusi in tale modello sono quelli relativi alla competizione, mentre gli stratagemmi utilizzati per risolverli si basano sul concetto di semaforo, ovvero sull’utilizzo di apposite strutture dati gestite da procedure che regolano l’accesso alle risorse (e.g. un processo può interagire con una risorsa, se e solo se, interrogando il semaforo con le sue procedure si accorge che questo è “verde” ovvero che la risorsa è libera e non impegnata da altri processi). Nel secondo modello, invece, esistono più processi dotati di risorse locali (e quindi visibili ad un singolo processo) e l’unica forma di comunicazione consentita è quella basata sullo scambio di messaggi. A tale proposito apposite primitive di send (“manda”) e receive (“ricevi”) vengono messe a disposizione per realizzare la cooperazione tra i processi. - Scheduling dei processi: che consente di scegliere (“schedulare”) quali processi devono essere assegnati alla CPU. Lo “schedulatore” dei processi interviene ogni qual volta un processo deve essere assegnato alla CPU ed ha lo scopo di ottimizzare uno o più dei parametri caratteristici di un sistema operativo: throughput, tourn around time, tempo di attesa e tempo di risposta dei vari processi. Classici algoritmi di scheduling sono quelli che si basano su una strategia di accesso a code di tipo FIFO (FirstIn-First-Out), dove il primo processo ad arrivare in coda è il primo ad essere servito, oppure quelli basati su code a priorità dove esistono più 362 Capitolo decimo code di processi che vengono servite sulla base della priorità o importanza dei processi in esse contenuti, o infine, algoritmi di tipo SJF (Short Job First), in cui ad essere serviti sono i processi che durano di meno. A secondo poi dell’utilizzo o meno del time-sharing, un processo verrà eseguito solo per un time-slice di tempo o per intero. In un sistema così strutturato, i processi (programmi in esecuzione nel sistema) possono trovarsi negli stati di: c) running - in quanto sono in esecuzione avendo ottenuto il possesso della CPU; d) ready - nel senso che sono pronti per l’esecuzione ed attendono solo che gli venga concessa la CPU; e) waiting – perché sono in attesa di qualche evento, come la fine di un’operazione di I/O, che li farà diventare prima ready e poi, quando sarà possibile, running. Figura 7 – Diagramma degli stati di un processo Lo schedulatore ha il compito di selezionare tra i processi che si trovano in uno specifico stato, quello che deve essere gestito per primo. A tal fine ad ogni processo è associato oltre allo stato anche una priorità che ne determina l’ordine di schedulazione. Un processo nello stato di running passa in un altro stato se: è arrivato alla sua naturale terminazione o se un errore grave ne ha decretato una terminazione prematura; ha avviato una operazione di input ed output e deve pertanto aspettare che il canale attivato segnali il completamento dell’operazione richiesta con una richiesta di interruzione; in tale caso il processo passa nello stato di waiting; ha terminato il quanto di tempo assegnato se il sistema è gestito secondo la modalità del time-sharing; in tale caso il processo passa nello stato di ready. Un processo nello stato di waiting può solo passare nello stato di ready quando l’operazione di input/output richiesta è terminata. Infine un processo nello stato di ready passa nello stato di running quando ha acquisito il diritto per farlo. La tecnica del time-sharing garantisce che tutti i processi procedano con le stesse garanzie evitando che la CPU possa essere monopolizzata da uno solo di Introduzione ai sistemi operativi 363 essi. Si pensi ad un processo costituto da lunghi calcoli iterativi seguiti da operazioni di input/output: un tale processo lascerebbe il possesso della CPU solo dopo molto tempo costringendo tutti gli altri ad aspettane la terminazione. Con i quanti di tempo tutti i processi vengono eseguiti poco alla volta ma procedono tutti assieme. Tutte le risorse del sistema vengono equamente distribuite tra tutti i processi. Si comprende che quanto più sono i processi da eseguire tanto più l’utente è costretto ad aspettare che il sistema gli fornisca i risultati desiderati. Una tecnica diversa è quella del tempo reale o real time. In tali sistemi le risposte devono essere fornite in un tempo utile all’ambiente circostante. Tipici sistemi real-time sono i sistemi di controllo nei quali le risposte devono regolare il funzionamento di dispositivo (una caldaia, un motore) secondo dei tempi prestabiliti: si comprende che se il segnale di controllo non arriva in tempo utile il dispositivo si porta in una condizione di errore (lo scoppio della caldaia, la fusione del motore). Nei sistemi real time i processi devono mantenere le risorse fino a quando non producono i risultati desiderati e quindi terminano. Nei sistemi moderni le due tecniche convivono. Ai processi di tipo real time vengono assegnate priorità molto alte per far sì che lo schedulatore li selezioni prima di tutti gli altri processi. 10.3.2. La gestione della memoria Il gestore della memoria è il componente del sistema operativo che si occupa dell’allocazione e deallocazione dei processi in memoria centrale. Si trova ad un livello superiore al kernel in quanto un processo che non è allocato in memoria non può essere eseguito. La memoria e la CPU sono pertanto le due risorse fondamentali che un processo deve possedere per poter essere eseguito. Un sistema è monoprogrammato se un solo processo alla volta viene allocato in memoria; è invece multiprogrammato se più processi vengono disposti in memoria anche se uno solo di essi viene eseguito. Poiché può capitare che lo spazio di memoria arrivi a saturazione, il gestore di memoria deve attuare delle tecniche per assegnarla solo a quei processi che possono essere eseguiti. Con il meccanismo dello swapping si attua il trasferimento di processi dalla memoria centrale a quella di massa (swapout) quando essi sono in uno stato di waiting, e, viceversa, il trasferimento di processi da memoria di massa in memoria centrale (swap-in) di quelli che sono tornati ready. Lo scopo dello swapping è di lavorare con un numero di processi attivi maggiore di quello che potrebbe essere consentito con le dimensioni effettive della memoria, aumentando il grado complessivo di multiprogrammazione. Le tecniche che sono state studiate fino ad oggi per la gestione della memoria possono essere raggruppate in due grandi classi: 5) le tecniche basate su paginazione e segmentazione che richiedono che il codice e i dati di un processo siano integralmente contenuti nella memoria centrale; 6) le tecniche basate su memoria virtuale, che richiedono che il codice e i dati di un processo siano solo in parte contenuti in memoria centrale. La paginazione consiste nel suddividere la memoria fisica in blocchi di dimensioni fissate, detti frame, e la memoria logica dei processi in blocchi di uguale dimensione chiamati appunto pagine. Quando un processo deve essere eseguito vengono caricate nei frame le pagine. Il mapping tra gli indirizzi logici del processo e quelli fisici di memoria avviene a tempo di esecuzione (dinamicamente) 364 Capitolo decimo mediante un hardware dedicato detto Memory Management Unit (MMU), che utilizzando un’apposita tabella converte gli indirizzi logici di pagina del processo in indirizzi fisici di frame. Nella segmentazione, invece, la memoria è suddivisa in partizioni a lunghezza variabili dette segmenti. Analogamente allo spazio fisico di memoria, anche lo spazio logico dei processi viene suddiviso in segmenti ed inoltre la corrispondenza tra indirizzi logici e fisici avviene come nel caso della paginazione attraverso una MMU. Infine la memoria virtuale può essere applicata sia nell’ambito di una memoria paginata, che in quello di una memoria segmentata e si basa sull’assunto che un dato processo per essere eseguito non deve risiedere per forza tutto in memoria centrale. La memoria (segmentata o paginata che sia), necessaria alla esecuzione di tutti i processi, non coincide più con la sola memoria centrale ma risulta essere fisicamente allocata sulla memoria di massa, e solo una sua parte rimane in memoria centrale. Se durante l’esecuzione di un processo accade che lo stesso abbia bisogno di accedere ad una particolare sua pagina non residente nella memoria centrale ma sul disco (condizione detta di page fault), il sistema operativo deve prevedere un meccanismo automatico che effettui l'arresto del processo, l'ingresso della pagina mancante in memoria e la ripresa del processo, il tutto in maniera completamente trasparente rispetto all'utente. Se nella memoria centrale esiste una pagina libera, la nuova pagina potrà occuparla, altrimenti bisognerà effettuare lo swap-out di alcune delle pagine preesistenti. La gestione del page fault è realizzata tramite un apposito algoritmo di sostituzione che seleziona la pagina “vittima” da swappare su disco e carica la pagina richiesta in memoria: tale algoritmo deve garantire buone prestazioni individuando nel modo più opportuno la pagina vittima sulla base di opportuni criteri (ad esempio, viene scelta la pagina meno usata di recente). 10.3.3. Il file system Il file system gestisce essenzialmente la collocazione dei file sulla memoria di massa e tutte le informazioni necessarie per consentire l’esecuzione delle operazioni su di essi. Il file system permette di ottenere un’astrazione della memoria di massa organizzandola in insiemi di directory e file: mentre le directory contengono informazioni riguardanti i file, i file contengono i dati veri e propri (contenuti in appositi blocchi fisici del disco). In realtà un file system vede sia le directory che i file come un insieme di file. In particolare le directory sono file che contengono un array di record in cui ogni record è il descrittore di un file. Le informazioni più comunemente memorizzate in un campo del descrittore di file sono: - nome del file (nome simbolico del file); - tipo di file (solo in S.O. che gestiscono il tipo, cioè essenzialmente file eseguibili e non); - locazione (dispositivo e allocazione sul dispositivo); - posizione corrente (puntatore alla posizione corrente di lettura/scrittura del blocco) - protezione (informazioni per il controllo dell’accesso); - contatore di utilizzo (numero di utenti che allo stato condividono il file); - ora e data (ora, data e identificatore di processo); Introduzione ai sistemi operativi 365 - identificatore di processo (relativamente ad operazioni quali: creazione, ultima modifica e ultimo utilizzo del file). Per ciò che concerne le operazioni, le operazioni fondamentali che vengono fatte a livello di directory sono la creazione e la cancellazione di un file, oppure l’apertura o la chiusura o visualizzazione dei dati relative ad un file. Creare o aggiornare un file significa aggiungere un descrittore di file all’interno di un file directory; cancellare un file significa eliminarlo, visualizzare significa accedere o ricercare dei dati. Tali operazioni presuppongono una visita dei descrittori di file, ed in particolare la creazione deve fare una operazione di visita totale, mentre la visualizzazione e la cancellazione presuppongono una visita parziale. Se si vuole lavorare su di un file, le operazioni fondamentali diventano: apertura di un file, chiusura di un file, creazione di un file(scrittura), lettura di un file, reset di un file. Mentre i metodi di accesso ai file supportati da un sistema operativo sono: accesso sequenziale, accesso diretto ed accesso mediante un indice (cioè mediante un tabella di corrispondenza chiave e posizione). Per ciò che concerne infine l’allocazione dei file su disco, tre possono essere le modalità: - allocazione contigua che comporta che un file sia memorizzato in blocchi contigui del disco e nel descrittore del file si trova l’indirizzo del blocco iniziale ed il numeri di blocchi occupati; - allocazione linkata che porta un file ad essere memorizzato in blocchi non contigui di memoria e nel descrittore di file si ha ancora il blocco di inizio del file così come nella allocazione contigua, però invece di avere il numero di blocchi consecutivi, i vari blocchi sono linkati mediante puntatori che sono contenuti nei blocchi stessi (si tratta quindi di una lista a puntatori i cui elementi sono i blocchi del disco). Per percorrere il file basta quindi entrare nel primo blocco e poi utilizzare i vari puntatori che si trovano in posizione standard nei blocchi. - allocazione indicizzata con la quale un file è memorizzato in blocchi non contigui di memoria e nel descrittore di file si ha l’indirizzo di un blocco particolare, detto indice, che contiene la posizione sul disco (puntatori) di tutti i blocchi costituenti il file. 10.3.4. L’interprete dei comandi: la shell In un sistema operativo, la shell rappresenta la componente che permette agli utenti di comunicare con il sistema. Essa crea un “ambiente di lavoro” attraverso il quale è possibile impartire comandi al sistema. La shell è un vero e proprio interprete di linguaggio che prende anche il nome di Job Control Langauge (JCL) in quanto serve a controllare il lavoro del sistema operativo. I comandi più noti sono quelli che richiedono che i programmi vengano prima caricati in memoria e poi eseguiti. Esistono molti tipi di shell, che si dividono principalmente in shell testuali e grafiche. Una shell testuale è un programma con una interfaccia a linea di comando, che viene eseguito da un terminale testuale. L'utente digita un comando, ovvero richiede l'esecuzione di un programma, e il programma eseguito può interagire con l'utente e/o stampare dati sul terminale. Una delle più note shell testuali è quella dei sistemi operativi DOS caratterizzata dal suo prompt dei comandi “C:>”, ben noto a 366 Capitolo decimo quanti hanno familiarità con i personal computer della prima generazione. In ambiente Unix, esistono diverse shell, una tra le più famose è sicuramente la Bash (Bourne-Again Shell), ma ne esistono altre come la Korn Shell e la C Shell, con un insieme di funzionalità e caratteristiche di base in comune. L'evoluzione grafica delle shell è rappresentata dalle Graphic User Interface, che attraverso l’uso di icone e finestre permettono di svolgete le varie operazioni possibili con un clic del mouse, rendendo il sistema user-friendly. Sebbene le shell grafiche siano un passo avanti per l'interazione uomo-macchina, il loro punto di debolezza è rappresentato dall’alto consumo delle risorse di calcolo del computer che si contrappone alla potenza e velocità d'uso di quelle testuali. Le Shell grafiche più conosciute sono GNOME e KDE (sotto ambiente UNIX) ed EXPLORER (sotto ambiente Windows). Capitolo undicesimo Le reti di comunicazione 11.1. I sistemi di comunicazione Alla base della trasmissione dell’informazione è il concetto di messaggio inteso, informalmente, come una informazione che viene trasferita da un entità detta sorgente ad una o più entità dette destinatario. Un sistema di comunicazione, come mostrato in figura 1, è in generale formato da cinque elementi fondamentali: - una sorgente che genera messaggi; - un sistema di codifica che trasforma un messaggio della sorgente in una sequenza di segnali; - un canale trasmissivo (ad esempio un doppino telefonico, fibre ottiche, l’etere) usato per trasferire segnali dalla sorgente alla destinazione; - un decodificatore che esegue di solito le operazioni inverse di un codificatore, ossia trasforma i segnali in messaggi; - un destinatario che riceve i messaggi. Si noti che l’insieme costituito da codificatore, canale e decodificatore è anche detto sistema di trasmissione dell’informazione. Inoltre codificatore e decodificatore sono a volte chiamati trasmettitore e ricevitore rispettivamente. Si consideri ad esempio una tipica comunicazione telefonica su telefono fisso: un essere umano parla emettendo un sequenza di suoni (i simboli in formato acustico che costituiscono il messaggio da trasmettere), quindi un opportuno codificatore trasforma il segnale acustico in un segnale elettrico; tale segnale viene trasmesso su un canale, costituito da cavi telefonici e da un insieme di interruttori che trasportano il segnale al decodificatore del destinatario; a questo punto il segnale elettrico viene trasformato nuovamente in segnale acustico consegnando in questo modo il messaggio al destinatario. Si noti che i canali di comunicazione hanno una capacità limitata, misurata in bit al secondo. Tale misura rappresenta il numero di bit che il canale di comunicazione è in grado di trasmettere nell’unità di tempo. Lo studio sistematico dei sistemi di comunicazione è una attività vasta e complessa che esula dagli scopi di questo testo. Nel seguito mostreremo i concetti fondamentali ed introduttivi della problematica, rimandando a testi specializzati di networking, di telecomunicazioni e telematica per una trattazione esaustiva e sistematica. 368 Capitolo undicesimo Figura 1 - Sistema di Comunicazione 11.1.1. Codici e codifica Un messaggio prodotto da una sorgente può essere modellato come una sequenza di lunghezza finita di simboli appartenenti ad un insieme prefissato, detto alfabeto della sorgente. In altre parole, stiamo pensando ad una applicazione (matematica) detta codifica che, data una informazione i, la associa ad una opportuna parola codice, wc composta da simboli dell’alfabeto sorgente, ovvero: i I wc C (1) con I e C insiemi di cardinalità finita. Si noti che le caratteristiche principali di un messaggio sono il formalismo scelto per rappresentarlo ed il significato del messaggio, caratteristiche indipendenti l’una dall’altra (il formalismo di rappresentazione non dipende dal significato del messaggio). Una codifica si dice non ambigua se l’applicazione (1) è iniettiva, ovvero ogni coppia di informazioni distinte di I sono trasformate in coppie di parole distinte wc. Detto card(I) la cardinalità, ovvero il numero di elementi dell’insieme I, e card(C) la cardinalità di C, una codifica si dice: - non ridondante se card(I) = card(C); - ridondante se card(C) > card(I) . Si noti che se card(C) < card(I), allora si ha una codifica ambigua. Nel nostro caso, distinguiamo tra: - codice sorgente: ovvero il codice con il quale sono rappresentati i messaggi emessi dalla sorgente; - codice canale: ovvero il codice col quale sono rappresentati i messaggi trasmessi lungo il canale; - codice destinatario: ovvero il codice del destinatario, che a volte puòessere diverso da quello della sorgente. Le reti di comunicazione 369 Si prenda ad esempio il codice Morse. Si tratta del primo esempio di codice esplicitamente pensato per trasmissioni su canali di comunicazione. Il codice fu ideato nel 1844 da Samuel F.B. Morse per l’uso del telegrafo elettrico, anch’esso frutto della sua inventiva. La prima linea telegrafica sperimentale fu la tratta Washington - Baltimora, inaugurata con l’invio in tempo reale degli impulsi elettrici che corrispondevano alla frase biblica “Così ha permesso Dio”, passata alla storia come il primo messaggio telegrafico in alfabeto Morse. L’alfabeto Morse è una combinazione di punti e linee che rappresentano numeri e lettere dell’alfabeto, dove la durata di una linea equivale a quella di tre punti: per trasmettere i messaggi codificati ci si può servire di segnalazioni luminose, acustiche o elettriche, che permettono di codificare le due “durate” (lunga e breve) dei segnali. Tuttora rappresenta un codice internazionale di segnali usato nei sistemi di radiotelegrafia di tutti i paesi, eccetto Stati Uniti e Canada, e ovunque nel mondo per le comunicazioni in navigazione marittima. Il codice base è formato da 36 simboli (26 caratteri e 10 cifre), come in figura 2, codificati in sequenze di punti e linee. Si noti che la codifica è ambigua, in quanto non consente sempre al decodificatore di associare ad un messaggio un unico significato se non si dispone di una sincronizzazione tra un carattere ed il successivo (ad esempio una pausa). Figura 2 - Esempio di Codifica: il codice Morse 11.1.2. Il problema degli errori I canali di trasmissione sono di solito costituiti da circuiti elettronici che possono, durante la trasmissione, dare luogo ad una serie di disturbi, cui viene dato il nome di rumore. Di solito si distinguono due tipi fondamentali di rumore: - rumore bianco: si tratta di un disturbo sempre presente nei dispositivi elettronici ( è in gran parte generato dal moto degli elettroni) e può essere facilmente mitigato attraverso opportuni dispositivi elettronici detti filtri; - rumore impulsivo: si tratta di un disturbo dovuto a cause imprevedibili che possono verificarsi durante la trasmissione (caduta di tensione, effetti parassiti delle linee elettriche e così via). Il rumore impulsivo genera errori casuali e altera in modo imprevedibile i bit trasmessi. Solitamente, per ridurre gli effetti dei disturbi (soprattutto quelli imprevedibili), sono state definite tecniche di trasmissione dei messaggi che permettano nel contempo di rilevare la presenza di errori e, sotto opportune ipotesi, di correggerli. 370 Capitolo undicesimo La considerazione intuitiva alla base delle tecniche di correzione di errore è la seguente: se il dato ricevuto dal destinatario attraverso il canale è diverso da quello che vi era stato immesso, allora si è verificato un errore di trasmissione. In particolare, un errore è singolo se è stato alterato un solo bit del messaggio (si ipotizza una codifica in binario delle informazioni); è doppio se sono stati alterati due bit, etc. Come si fa a riconoscere che un bit ricevuto è differente da quello inviato, e quindi ad individuare la presenza di un errore? A tal fine si utilizzano tecniche di codifica dell’informazione basate sull’uso di codici ridondanti. In linea di principio, ad una “parola dati” di m bit si aggiungono r bit di controllo e si ottiene una “parola codice” di n = m + r bit: quando si legge una parola, vengono controllati gli r bit in eccesso al fine di rilevare l’errore (cioè ad invalidare il dato) o correggerlo (cioè a sostituire il dato errato con quello esatto). 11.1.3. La trasmissione dell’informazione Per trasferire un messaggio binario lungo una linea di trasmissione, i bit che costituiscono gli elementi da trasmettere devono essere convertiti in segnali elettrici: una soluzione può essere quella di associare al valore logico 1 un dato valore di tensione, ad esempio +V , e al valore logico 0 un valore di tensione ben distinto, ad esempio V . Il ricevitore sarà in grado di interpretare i valori di tensione ricevuti, secondo la convenzione assunta. Rappresentando il valore nel tempo del fenomeno fisico come una funzione s(t), si può studiare matematicamente il segnale risultante. Il segnale fondamentale che prendiamo in considerazione è una funzione sinusoidale del tipo: s(t) = A sin(2 f t). Esso, come è noto, è caratterizzato dai seguenti parametri: ampiezza A (il massimo valore in modulo del segnale); periodo T (il tempo entro cui la funzione si ripete); frequenza: l’inverso del periodo f = 1/T , misurata in cicli al secondo (Hz). In realtà una qualsiasi funzione s(t), definita in un intervallo T, può essere espressa come una somma di un numero infinito di funzioni sinusoidali (analisi di Fourier): s(t) a0 an cosn2 f 0 t n 1 bn sinn2 f 0 t n 1 dove f0 = 1/T è la frequenza fondamentale ed an e bn sono le ampiezze dell’ennesima armonica (o termine), che ha una frequenza n volte più grande della frequenza. Un segnale variabile nel tempo è di fatto equivalente ad una somma di funzioni sinusoidali aventi ciascuna una propria ampiezza e frequenza. Si può quindi rappresentare un segnale s(t) di durata T in un modo diverso, e cioè attraverso il suo spettro di frequenze, ossia attraverso la sua scomposizione in sinusoidi. Qualunque segnale è dunque caratterizzato da un intervallo di frequenze nel quale sono comprese le frequenze delle sinusoidi che lo descrivono. Tale intervallo va sotto il nome di banda di frequenza del segnale. Anche i mezzi fisici sono caratterizzati da una banda di frequenze, detta banda passante del mezzo fisico. Essa rappresenta l’intervallo di frequenze che il mezzo fisico è in grado di trasmettere (senza alterarle oltre certi limiti). Le alterazioni principali sono la attenuazione e l’introduzione di ritardo, che di solito variano al variare delle Le reti di comunicazione 371 frequenze trasmesse. A volte la dimensione della banda passante dipende solo dalle caratteristiche fisiche del mezzo trasmissivo, a volte deriva anche dalla presenza di opportuni filtri che tagliano le frequenze oltre una certa soglia (detta frequenza di taglio, fc ). Ad esempio, nelle linee telefoniche la banda passante è dell’ordine dei kHz. E’ opportuno notare che i mezzi trasmissivi attenuano i segnali in proporzione alla distanza percorsa e alla frequenza del segnale e che propagano i segnali a velocità proporzionali alle loro frequenze. Da queste considerazioni discende che la banda passante si riduce all’aumentare della lunghezza del mezzo stesso. Perché un segnale sia ricevuto come è stato trasmesso, è necessario che la banda passante sia almeno uguale o più ampia della banda di frequenza del segnale stesso, altrimenti, il segnale viene privato di alcune delle sue armoniche (quelle di frequenza più elevata) e viene quindi distorto. Se un numero sufficiente di armoniche arriva a destinazione, il segnale è comunque utilizzabile. Nyquist ha dimostrato (teorema del campionamento o teorema di Nyquist) che un segnale analogico di banda B può essere completamente ricostruito mediante una campionatura effettuata 2 B volte al secondo. 11.1.4. I mezzi trasmissivi Trasmettere un segnale s(t) elettrico esige un mezzo trasmissivo che generalmente è una linea di trasmissione. Nel caso storicamente più semplice, la linea è costituita da una coppia di conduttori (fili), oppure si possono avere onde elettromagnetiche che si propagano nello spazio o fasci di luce che vengono convogliati e guidati in una fibra ottica. Avere conoscenza del tipo di mezzo trasmissivo a cui si fa riferimento è importante in quanto determina il numero massimo di bit che possono essere trasmessi in un secondo, o bps. Di seguito sono descritti i mezzi trasmessivi più diffusi. La linea bifilare è formata da due fili (di rame), isolati l’uno dall’altro. Viene generalmente usato per collegare dispositivi che non distano più di 50 m, ad una velocità di circa 19.2 bps. In uno dei due fili viaggia di solito una tensione (o corrente), nell’altro una tensione di riferimento. Un cavo multiplo è formato da n fili su cui viaggia l’informazione ed un filo per il riferimento. Pur essendo molto semplice, questo tipo di linea è affetta da fenomeni di interferenza dei segnali elettrici dei fili adiacenti nel cavo (diafonia). Per rendere più robusta la trasmissione rispetto al rumore, si può utilizzare una linea bifilare con fili intrecciati (binatura), detta coppia simmetrica o doppino. In questo modo si riducono i fenomeni di diafonia. Il doppino può essere singolo (una sola coppia) oppure in una treccia di una serie più meno numerosa di coppie. Queste linee, dette anche in inglese Unshielded Twisted Pair (UTP) non prevedono alcun tipo di schermatura. La versione schermata (Shielded Twisted Pair, STP), riduce ulteriormente l’interferenza. I cavi UTP sono costituiti solitamente da quattro coppie di fili, isolati singolarmente ed avvolti in spire a due a due senza schermatura aggiuntiva. I cavi sono caratterizzati da categorie (level) e le differenze consistono nella realizzazione degli avvolgimenti; i livelli sono definiti in base a capacità di banda entro distanze definite (100 m), come nel seguito riportato: - UTP level 3: garantisce fino a 16 MHz di banda; - UTP level 4: garantisce fino a 20 MHz; 372 Capitolo undicesimo - UTP level 5: garantisce fino a 100 MHz. - doppini performanti, level 5 e, 6 e 7. I doppini UTP level 3 sono detti anche di qualità fonica, e sono utilizzati sia per la telefonia che per la trasmissione dati fino a 10 Mbps. I doppini UTP level 5 sono utilizzati nelle reti locali a velocità superiore (fino a 1 Gbps). Il doppino per le sue caratteristiche di maneggevolezza e di basso costo è molto diffuso sia per la telefonia (quasi tutte le connessioni del sistema telefonico nell’ultimo tratto sono costituite da doppini), che per le reti locali (generalmente realizzato tramite UTP o STP). Il cavo coassiale è costituito da un conduttore interno in rame, avvolto in un isolante di materiale plastico (dielettrico) attorno al quale è posto il conduttore esterno (una “calza” metallica), il tutto ricoperto da un isolante. Per la sua struttura, il cavo coassiale mostra una minore sensibilità alle interferenze nonchè una minore attenuazione del segnale in funzione della distanza, rispetto al doppino. Il cavo coassiale ha una larghezza di banda fino a 500 MHz: per questo è molto diffuso per le connessioni a lunga distanza e per le trasmissioni che richiedono una banda larga. Come il doppino telefonico, sulle distanze superiori al Km necessita di amplificatori o ripetitori. Il cavo in fibra ottica è costituito da un sottile filo di sostanza vetrosa, generalmente silicio, molto fragile, detto core (nucleo), attraverso il quale si propaga la luce; il nucleo è avvolto da una sostanza (mantello) con proprietà ottiche differenti dal nucleo stesso; a sua volta il mantello è avvolto in una guaina che protegge il cavo da umidità deformazioni. Le proprietà ottiche del nucleo e del mantello sono tali che la luce introdotta nel nucleo non ve ne esce più, e viene riflessa in modo da viaggiare lungo il nucleo fino a destinazione. Il segnale luminoso può essere generato in due modi differenti: - tramite LED (Light Emitting Diode): più economico, adatto per trasmissioni a tratta corta su fibre multimodali ed a basso tasso trasmissivo; - tramite diodi ad emissione laser: molto più costoso, adatto per trasmissioni ad alto tasso trasmissivo per lunghe distanze, più sensibile al calore. La banda trasmissiva della fibra si aggira intorno ai 30 THz (30000 GHz); la tecnologia attuale permette tassi trasmissivi fino a 10 Gbps, ma in laboratorio si raggiungono tassi maggiori a breve distanza. Tipicamente, i cavi a fibra ottica contengono centinaia di fibre distinte. Le fibre sono molto più sottili e leggere dei cavi in rame (e ciò può essere “fisicamente” un problema quando si vuole stendere un cavo sulle lunghe distanza, ad esempio uno transoceanico); di contro, la fibra garantisce una attenuazione significativamente inferiore al rame; i continui miglioramenti tecnologici del mezzo trasmissivo hanno ad oggi consentito di effettuare una trasmissione, senza ripetitori, fino a 25/30 km di distanza. Figura 3 - Doppino Telefonico, Cavo coassiale, Fibra Ottica Le reti di comunicazione 373 In figura 3 sono mostrati schematicamente i tre diversi mezzi trasmissivi guidati. La trasmissione di dati viene spesso realizzata utilizzando la trasmissione di onde elettromagnetiche nell’aria o nello spazio (wireless), caratterizzate dallo spettro di trasmissione. Se le onde sono trasmesse in una banda compresa tra il KHz ed il GHz, si parla di radiodiffusione. Si utilizza di solito in modalità unidirezionale per le trasmissioni radio o televisive. Nella regione compresa tra 1 e 40 GHz (microonde), la propagazione delle onde elettromagnetiche è abbastanza direzionale (o direzionabile con antenne paraboliche), e viene utilizzata per trasmissioni punto-punto in ponte radio, o trasmissioni satellitari punto-punto o broadcast. La banda di frequenza delle microonde (1-40 GHz) ha la caratteristiche di poter utilizzare antenne paraboliche di dimensioni maneggevoli (fino a qualche metro di diametro). Si può quindi realizzare una comunicazione punto-punto tra sorgente e destinazione con allineamento ottico delle antenne. Molto usate sono anche le trasmissioni satellitari, in cui microonde vengono trasmesse da una stazione di terra al satellite. Di solito nelle telecomunicazioni le trasmissioni satellitari si avvalgono di satelliti GEO (Geostationary Earth Orbit), a 36000 Km di quota in orbita equatoriale, che appaiono in posizione fissa nel cielo. 11.2. Le reti di calcolatori La storia dell’umanità ha registrato negli ultimi trecento anni l’introduzione di nuove tecnologie che hanno modificato profondmente la vita dell’uomo. D’altra parte, come dice il filosofo Hannah Arendt, l’essenza di tutte le rivoluzioni è legata all’apparire di una “novità” nell’esperienza umana. Ciò è vero anche e soprattutto per le innovazioni tecnologiche, quali la rivoluzione industriale, favorita dalla nascita dei primi sistemi meccanici e dall’invenzione dei motori a vapore; senza dubbio gli ultimi cinquant’anni del secolo scorso sono stati invece caratterizzati dalle tecnologie dell’informazione e delle comunicazione. Nel secolo appena passato si sono via via diffusi a livello mondiale il sistema telefonico, la radio e la televisione, i computer ed i satelliti per telecomunicazioni. Queste tecnologie stanno rapidamente convergendo: la combinazione di elaboratori e sistemi di telecomunicazione, in particolare, ha avuto una profonda influenza sull’organizzazione dei sistemi di calcolo, passando, infatti, dal vecchio modello mainframe – terminali, in cui la potenza di calcolo è concentrata in un unico grande elaboratore a cui si accede per mezzo di un certo numero di terminali, a quello attuale in cui vi è un grande numero di elaboratori autonomi connessi in rete. Le reti di computer o computer network rappresentano quindi l’evoluzione tecnologica, sia software sia hardware, dei primi sistemi informatici e nascono dall’esigenza, sempre più impellente, di permettere agli utenti che operano su diversi elaboratori di “comunicare” fra di loro. Nell’accezione più propria del termine, rete di computer sta ad indicare un insieme di computer autonomi interconnessi. Il termine interconnesso, quando applicato ad un insieme di elaboratori, sta ad indicare che questi offrono la possibilità di scambiarsi informazioni in un formato comune. L’autonomia si riferisce, invece, sia alla capacità intrinseca di elaborazione locale, cioè alla 374 Capitolo undicesimo possibilità di ogni singola macchina di lavorare con il proprio software senza far riferimento alla connessione in rete, sia alla possibilità di ogni utente di amministrare il proprio computer seguendo criteri personali. Tramite una rete, gli utenti condividono: programmi, dati, risorse di calcolo, dispositivi hardware indipendentemente dalla loro allocazione fisica. Lo scopo primario di una rete è quello di migliorare la comunicazione fra i vari utenti che vi afferiscono. Questa affermazione, così come enunciata, potrebbe sembrare del tutto generica e superficiale. Le potenzialità di quanto detto si esplicano, invece, nelle modalità in cui una rete consente agli utenti la reciproca comunicazione e soprattutto, negli effetti pratici che l’introduzione di questi nuovi metodi di comunicazione comportano su diversi aspetti dell’organizzazione del lavoro. Citeremo, alcuni dei motivi, che possono motivare, in alternativa ad un sistema centralizzato, l’ adozione di un sistema di rete. - Riduzione dei costi. Sono diversi i settori in cui, l’adozione di un’adeguata struttura di rete, permette una consistente riduzione dei costi sia come effetto diretto, sia come conseguenza dell’aumento di efficienza e della diminuzione dei tempi necessari per l’espletamento di alcune procedure. Si pensi, ad esempio, al sistema di posta elettronica (e-mail): che permette, da un lato, la drastica riduzione dei costi telefonici e parallelamente la riduzione dei tempi di trasferimento dei documenti tra gruppi di utenti dislocati geograficamente. Inoltre una rete di personal computer costa molto meno di un main-frame. - Condivisione delle risorse. È probabilmente la funzionalità più utilizzata nelle reti: la condivisione delle risorse rende i programmi, i dati e le apparecchiature, disponibili a qualsiasi utente della rete, indipendentemente dalla dislocazione sia di queste che dell’utente. Si può, ad esempio, investire in una stampante di alta qualità condivisa tra più utenti piuttosto che, a parità di costo, acquistare un certo numero di stampanti più piccole, guadagnando così, sia in prestazioni che in efficienza. La condivisione di un dispositivo di trasmissione fax permette, come si può facilmente immaginare, l’ottimizzazione della sua utilizzazione e nel contempo la riduzioni di costi e di tempi di attesa. Ancora, un utente potrebbe vedere il disco di un altro utente come se fosse una risorsa locale utilizzando quindi i dati ed i programmi disponibili su quel disco, esattamente come se fossero presenti sul proprio computer. - Aumento dell’affidabilità. La possibilità di replicare i dati, ed in generale le risorse e programmi più importanti, ai quali hanno necessità di accedere più di un utente su diverse macchine, rende una rete molto più affidabile di un sistema ad elaborazione concentrata. In quest’ultimo caso, infatti, un blocco del sistema centrale renderebbe inservibile l’intera struttura mentre, nel caso di una rete, esistono - grazie appunto alla possibilità di replicazione - tecniche automatiche o semiautomatiche di recovering che permettono agli utenti di continuare a lavorare tranquillamente mentre i tecnici ripristinano la configurazione ordinaria. ). Tale caratteristica è fondamentale sistemi che devono funzionare a tutti i costi (traffico aereo, centrali nucleari, sistemi militari, ecc.) - Decentralizzazione del calcolo e distribuzione delle attività al fine di migliorare la produttività ed efficienza del sistema complessivo. - Cooperazione tra utenti/programmi nello svolgimento di operazioni. Le reti di comunicazione - 375 Scalabilità. Si possono aumentare le prestazioni del sistema aumentando il numero di elaboratori (entro certi limiti). 11.2.1. Tipologie di reti di calcolatori La Prima di introdurre le principali caratteristiche hardware e software delle reti, è necessario premettere una serie di concetti che in buona parte provengono dalla teoria più generale delle reti di comunicazione. Iniziamo anzitutto a distinguere tra le comunicazioni di tipo broadcast e quelle punto punto. Le reti broadcast sono dotate di un unico canale di comunicazione (bus) che è condiviso da tutti gli elaboratori (vedi figura 4). In particolare, dei messaggi di breve lunghezza (spesso chiamati pacchetti) vengono inviati da un elaboratore e sono quindi ricevuti da tutti gli altri elaboratori. Per specificare chi è l’effettivo destinatario del messaggio, il trasmettitore specifica nel messaggio un “indirizzo” del destinatario, inteso, per ora, come un riferimento unico nel sistema del computer destinatario. Quando un elaboratore riceve un pacchetto, esamina l’indirizzo di destinazione: se questo coincide col proprio indirizzo, allora il pacchetto viene elaborato altrimenti viene ignorato. Le reti broadcast, in genere, consentono anche di inviare un pacchetto a tutti gli altri elaboratori usando un opportuno indirizzo (broadcasting): in tal caso tutti prendono in considerazione il pacchetto. Figura 4 - Rete broadcast (a bus) Le reti punto a punto consistono invece di un insieme di connessioni fra coppie di elaboratori (vedi figura 5). Per arrivare dalla sorgente alla destinazione, un pacchetto può dunque dover passare per uno o più elaboratori intermedi, e spesso esistono più cammini alternativi per arrivare da un sorgente ad un destinatario: si comprende, allora, intuitivamente il ruolo importante degli algoritmi di “instradamento” o “routing” in questo contesto. In generale, anche se è non sempre vero, le reti localizzate geograficamente tendono ad essere di tipo broadcast, mentre le reti molto estese tendono ad essere punto a punto. Ulteriore differenza è spesso fatta sulla base del modello di elaborazione effettuata, ovvero il modello client-server e quello peer to peer. Una rete client-server è costituita da entità che richiedono servizi, o client, e da entità che forniscono tali servizi, o server. 376 Capitolo undicesimo Figura 5 - Rete punto a punto Il server di solito è un calcolatore più potente che gestisce risorse condivise, come ad esempio stampanti, basi di dati, file, programmi. Un client è di solito un elaboratore “leggero”, poco potente (al limite - ma ciò accadeva soprattutto in passato - un terminale con tastiera e schermo). Nelle reti peer to peer la filosofia è completamente differente: tutti gli elementi della rete sono allo stesso livello ed i calcolatori della rete comunicano direttamente gli uni con gli altri senza far riferimento ad un server. In altri termini un computer può fungere in un’architettura P2P sia da client che da server a seconda delle necessità. Una ulteriore ed importante classificazione delle reti è quella basata sulle loro dimensioni. Come si può osservare in tabella 1, la principale distinzione è tra reti locali, reti metropolitane e reti geografiche Distanza 10 m 100 m 1 Km 10 Km 100 Km 1000 km 10.000 km Ambito Stanza Edificio Campus Città Nazione Continente Pianeta Tipo di rete Rete locale Rete locale Rete locale Rete Metropolitana Rete geografica Rete geografica Internet (Interconnessione di reti) Tabella 1 - Tipi di Rete Le reti locali, o Local Area Network, LAN, sono reti in genere possedute da una singola organizzazione (reti private) ed hanno un’estensione che arriva fino a qualche km, all’interno di un singolo edificio o di un campus (si ricordi che allo stato non si può di norma, posare cavi sul suolo pubblico). Le LAN sono molto usate per connettere tra di loro Personal Computer e/o workstation. Esse si distinguono dagli altri tipi di rete per tre caratteristiche: (i) hanno una dimensione ridotta; (ii) usano una tecnologia trasmissiva di tipo broadcast; (iii) hanno una ”topologia” classicamente a stella, bus o ad anello Le reti di comunicazione 377 (ring). Si ricorda che con il termine topologia si fa riferimento alle strutture architettoniche che rappresentano la disposizione dei cavi della rete e il conseguente interfacciamento delle postazioni. Nella topologia a stella, il ruolo centrale della trasmissione è svolto da un dispositivo o insieme di dispositivi chiamati centri stella (vedi figura 6) o hub, ripetitori che semplicemente inviano le informazioni che ricevono a tutte le porte collegate. L’hub permette la comunicazione tra i vari computer fornendo un sistema di connessione comune e localizzato (“concentrato”). Gli hub possono essere disposti a cascata in modo da espandere la rete in modo progressivo. Nelle reti con topologia a stella i pacchetti che vengono inviati da un elaboratore ad un altro sono ripetuti su tutte le porte del centro stella: questo permette a tutti i PC di vedere qualsiasi pacchetto inviato sulla rete. La topologia a stella consente un controllo centralizzato (grazie agli hub) delle comunicazioni con prestazioni elevate per le connessioni; come rovescio della medaglia, si possono verificare dei punti critici nella linea, proprio in corrispondenza degli hub. Figura 6 - Topologia a stella Nella topologia a bus (vedi figura 4), in ogni istante un elaboratore solo può trasmettere, mentre gli altri devono astenersi: è dunque necessario un meccanismo di arbitraggio, centralizzato o distribuito, per risolvere i conflitti quando due o più elaboratori vogliono trasmettere contemporaneamente. In un arbitraggio centralizzato, una apposita apparecchiatura accetta richieste di trasmissione e decide chi abilitare, mentre nell’ arbitraggio distribuito, ognuno decide per conto proprio. Lo standard IEEE 802.3 (chiamato impropriamente Ethernet) è lo standard di riferimento per la trasmissione su reti broadcast, basate su un bus, con arbitraggio distribuito, operante a 10 oppure 100 Mbps (oggi anche a 1 Gbit/s e a 10 Gbit/s). Più nel dettaglio, per risolvere una contesa si utilizza uno schema di accesso a canale condiviso detto CSMA/CD: Carrier Sense Multiple Access Collision Detection. Il principio di funzionamento di tale tecnica è estremamente semplice: quando una stazione presente sul canale deve trasmettere lo fa senza attendere alcuna autorizzazione. Se la trasmissione ha successo, cioè i dati trasmessi non “collidono” con quelli trasmessi da altre stazioni, bene; altrimenti la stazione è in grado di rilevare la collisione e provvede, in un secondo momento, a ritentare la trasmissione dei dati, fino a quando questa non ha successo In altri termini, ogni stazione della rete è in ascolto sul canale, e, non appena il canale è libero (lo si 378 Capitolo undicesimo scopre individuando la presenza o meno di un segnale, onde il nome ”carrier sense”), essa, qualora abbia informazioni da inviare, può iniziare a trasmettere. In generale, più stazioni sono in attesa di trasmettere (onde il nome “multiple access”), e le diverse informazioni si possono propagare da diverse zone fino a collidere. Non appena le trasmittenti rilevano queste collisioni, si arresta il tentativo di trasmissione, si aspetta un tempo casuale e poi si riprova a trasmettere, nel caso di mancata collisione, invece, la trasmittente può completare l’invio del messaggio La topologia a bus è una topologia passiva nel senso che, le stazioni non concorrono in alcun modo alla trasmissione dei messaggi lungo la linea: esse rimangono solo in ascolto, nell’ attesa o di inviare un messaggio o di ricevere un messaggio a loro destinato. Ciò conduce a diverse implicazioni, di cui due piuttosto importanti: se una stazione va in errore (o viene a mancare) il funzionamento del resto della rete non verrà alterato; se, però, il cavo venisse interrotto in un punto qualsiasi, la trasmissione e quindi la comunicazione, risulterebbe bloccata per l’intera rete. A suo vantaggio va inoltre citato anche il costo estremamente basso sia dei cavi che della connessione. Nella topologia a ring, le macchine sono disposte lungo un anello (vedi figura 7) ed ogni bit-informazione “circumnaviga” l’anello in un tempo tipicamente inferiore a quello di trasmissione di un messaggio. Anche qui è necessario un meccanismo di arbitraggio spesso basato sul possesso di un gettone (token) che abilita alla trasmissione. Più nel dettaglio, il diritto di trasmettere si basa sul continuo passaggio del token circolante fra le stazioni della rete: la stazione che ha il token può trasmettere (le altre si trovando in modalità di ascolto) fino al completamento dell’invio del messaggio; il gettone viene quindi passato alla stazione successiva che passa allo stato di trasmissione se ha qualcosa da trasmettere, altrimenti passa, a sua volta, il token alla stazione che la segue nell’anello. Si noti che il token altro non è che un insieme di bit con particolare configurazione che circola continuamente sull’anello. Figura 7 - Topologia ad anello Lo standard IEEE 802.5 (derivante dalla rete IBM Token Ring) è lo standard di riferimento per reti broadcast basate su ring, con arbitraggio distribuito. A titolo di esempio, si mostrano di seguito le classiche componenti di una rete locale, come anche mostrato dalla figura 8. - Sistema di cablaggio: è il sistema fisico di interconnessione costituito dai cavi (UTP o STP, coassiali, fibre ottiche) o connessioni wireless (onde radio) e dai dispositivi Le reti di comunicazione - - 379 (hub, switch, etc…) che permette il collegamento fisico tra i vari computer che compongono la rete. Il cablaggio segue norme ben precise – cablaggio strutturato – (ad esempio, TIA/EIA 568B, ISO/IEC 11801 ed EN 50173), a cui si fa in genere riferimento per quanto riguarda l’installazione dei cavi, la topologia della rete, i mezzi trasmissivi, le tecniche di identificazione dei cavi, la documentazione e le caratteristiche tecniche dei prodotti impiegati. Il sistema di cablaggio comprende di solito un cosiddetto cablaggio di dorsale, che collega diversi edifici (dorsale di campus) e piani di differenti edifici (dorsale di edificio), di solito in fibra ottica, e cablaggio di piano che partendo da un armadio tecnologico contenente concentratori raggiunge tutti i vari posti di lavoro, realizzando una topologia a stella. Il sistema di cablaggio condiziona, in modo a volte determinante, la velocità e le prestazioni di una rete. Server e Workstation: i servizi utente di una rete sono offerti da macchine che ricoprono ruoli specifici e che sono denominati, come già accennato, server. In particolare, i servizi relativi a programmi, dati e file in genere, sono offerti dai file server. Su queste macchine risiedono, di norma, le risorse che devono essere condivise da tutti gli utenti. Altri servizi comunemente disponibili sono i servizi di stampa (print server) e quelli di comunicazione (fax server, communication server, etc.). Una singola macchina può offrire più di un servizio per cui, ad esempio, una macchina può essere contemporaneamente file-server e print-server. L’alta specializzazione dei server, ossia uno per ogni servizio, è giustificabile solo in quelle installazioni in cui, il particolare servizio presenta un volume di operazioni talmente elevato, nell’unità di tempo, da portare rapidamente alla saturazione un’unica macchina nel caso in cui coesistessero anche altri servizi. Gli utenti accedono ai servizi offerti dai server dalle proprie stazioni di lavoro (denominati anche: workstation, stazioni client o semplicemente client). In linea di principio, non c’è nessun motivo per cui le stazioni di lavoro siano concettualmente diverse dai server di una rete: entrambi sono computer che possono essere in tutto e per tutto uguali, salvo che per il loro utilizzo: la differenza tra server e stazioni di lavoro, in linea di principio, è nel software di base delle macchine (il sistema operativo). Nella realtà si preferisce - per motivi di efficienza, prestazioni e sicurezza - che i server siano macchine mediamente più potenti delle relative stazioni di lavoro il cui carico di elaborazione sia inferiore, nelle normali applicazioni, a quello dei server. Schede di rete: per creare una rete, occorre istallare su ogni elaboratore una scheda di rete che permetta al computer di inviare e ricevere messaggi, via cavo o via onde radio. Dispositivi condivisi: stampanti, scanner, dispositivi di memorizzazione di massa e altre periferiche a seconda delle esigenze degli utenti. 380 Capitolo undicesimo Figura 8 - Componenti di una rete locale Le reti geografiche o Wide Area Network, WAN, invece, si estendono a livello di una nazione, di un continente o dell’intero pianeta. Per introdurre i principali aspetti tecnologici legati a tale tipologia di rete, prendiamo consideriamo una rete di comunicazione planetaria di uso comune, la rete telefonica. La rete è disegnata in questo modo: esiste un distretto telefonico che contiene una centrale di smistamento, che comunica con le centrali degli altri distretti. Quando facciamo una telefonata, la chiamata viene fatta passare attraverso una o più centrali, fino a raggiungere il numero chiamato: le centrali costruiscono una connessione diretta fra i due telefoni, che dura per tutto il tempo della telefonata. In altre parole, viene creato un circuito virtuale che unisce i due telefoni all’atto della chiamata: per questo, tale tipologia di comunicazione viene anche detta a commutazione di circuito, in quanto il canale viene “costruito” per ogni nuova sessione di comunicazione, collegando singoli tratti di linee dedicate. E’ chiaro che la soluzione adottata per le reti telefoniche non si adatta bene alle comunicazioni tra elaboratori: si utilizza in questo caso, infatti, un diverso tipo di commutazione detta ”a pacchetto”. Nella commutazione a pacchetto (packet switching), ogni messaggio è diviso in tanti elementi di dimensione fissa detti pacchetti e opportunamente numerati. Ogni pacchetto contiene altresì l’informazione relativa all’indirizzo del computer destinatario e a quello del mittente e viene trasmesso separatamente, facendo virtualmente una strada diversa dagli altri pacchetti del messaggio per arrivare al destinatario. Si noti, allora, che i pacchetti non arrivano necessariamente nello stesso ordine con cui sono stati inviati: per questo il destinatario deve aspettare la ricezione di tutti i pacchetti per poterli poi ricomporre e ricostruire il messaggio (vedi figura 9). Una rete di comunicazione che è basata su questo principio viene anche detta punto a punto, o store and forward. Le reti di comunicazione 381 Figura 9 - Commutazione di pacchetto Una WAN, dunque, è costituita di due componenti distinte: un insieme di elaboratori detti host oppure end system ed una communication subnet (o subnet). Figura 10 - LAN collegate da una WAN A sua volta, la subnet consiste di due componenti: linee di trasmissione (dette anche circuiti, canali, trunk) ed elementi di commutazione (switching element), detti anche “sistemi intermedi”, oppure ”nodi di commutazione” o anche router. Come mostrato in figura 10, una WAN è usata per connettere più LAN tra di loro. In generale una WAN contiene numerose linee (spesso telefoniche) che congiungono coppie di router. Ogni router allora deve: - ricevere un pacchetto da una linea in ingresso; - memorizzarlo in un buffer interno; - instradare il pacchetto appena la necessaria linea in uscita è libera: ogni nodo, infatti, mantiene una tabella che indica a quali vicini ritrasmettere un pacchetto non destinato a lui, in base all’indirizzo di destinazione del pacchetto. Per concludere, citiamo le reti metropolitane (Metropolitan Area Network, MAN), che hanno un’estensione tipicamente urbana. Per le reti metropolitane è stato introdotto uno standard apposito, IEEE 802.6 o DQDB, che prevede l’esistenza di un mezzo trasmissivo di tipo broadcast (due bus 802.6) a cui tutti i computer sono attaccati. 382 Capitolo undicesimo 11.2.2. Cenni all’Internetworking Con il termine internetworking ci si riferisce all’insieme di norme tecniche, protocolli e dispositivi che consentono la connessione di due o più reti “eterogenee”. Una internetwork è quindi formata quando reti diverse (sia LAN che MAN o WAN) vengono collegate fra loro. Alcuni problemi però sorgono inevitabilmente quando si vogliono connettere fra di loro reti progettualmente diverse (spesso incompatibili fra loro). In questo caso si deve ricorrere a speciali attrezzature, dette gateway (o router multiprotocollo), che oltre ad instradare i pacchetti da una rete all’altra, effettuano le operazioni necessarie per rendere possibili tali trasferimenti. Oltre al router, per l’interconnessione delle reti si utilizzano altri dispositivi che citeremo brevemente: - repeater: e un dispositivo che collega reti LAN identiche e fornisce la possibilità di “rigenerare” i segnali in transito tra una rete e l’altra, amplificandoli (in tal modo è possibile estendere l’area della rete); - bridge: è un dispositivo che collega reti diverse aventi però uno stesso schema di indirizzamento; di solito il bridge rimane in ascolto sulle due reti che connette e, quando riconosce un pacchetto proveniente da una rete e destinato a una stazione appartenente all’altra rete, lo preleva, lo memorizza e lo ritrasmette con il metodo di accesso proprio della rete di destinazione (viene di solito usato per connettere reti LAN con tecnologia diversa, e.g. reti token-ring con reti ethernet); - gateway: è un dipositivo che crea collegamenti tra reti con ambienti applicativi differenti. 11.2.3. Aspetti software delle reti di calcolatori Dopo aver mostrato quelle che sono le connessioni fisiche tra calcolatori, dobbiamo cercare di capire come farli comunicare dal punto di vista del ”software”. Gli elaboratori, infatti, devono seguire alcune regole dette protocolli di comunicazione. Un protocollo stabilisce le regole di comunicazione che debbono essere seguite da due interlocutori. Consideriamo il classico esempio del colloquio tra due persone che si parlano al telefono: le prime frasi scambiate (“pronto? chi parla? Sono Carlo, con hi parlo?”) costituiscono un esempio di regola in cui gli interlocutori stabiliscono la loro identità prima di iniziare un colloquio vero e proprio. Per protocollo di comunicazione tra due elaboratori si intende, più precisamente, un insieme di regole che permette la corretta instaurazione, mantenimento e terminazione di una comunicazione di qualsiasi tipo tra due o più entità. Per questo motivo, il protocollo deve poter definire la sintassi da seguire (il formato del messaggio), e l’ordine dello scambio di messaggi. Data la complessità del problema derivante dal problema della comunicazione tra gli host di una rete, più che un protocollo, in realtà, si definiscono famiglie di protocolli, in cui ogni protocollo gestisce univocamente una componente ben definita della comunicazione condividendo con gli altri protocolli i dati di cui essi necessitano. L’architettura dei protocolli è organizzata a livelli. Lo scopo di ogni livello – come consuetudine di ogni sistema fatto a strati – è di fornire servizi alle entità del livello immediatamente superiore, mascherando il modo in cui questi sono implementati e Le reti di comunicazione 383 sfruttando opportunamente i servizi che gli vengono a sua volta forniti dal livello immediatamente inferiore, come mostrato in figura 11. Figura 11 - Architettura a livelli Per quanto attiene la comunicazioni tra computer, cerchiamo anzitutto di comprendere quali sono gli ”strati” fondamentali da considerare, in quanto una visione a livelli permette di semplificare notevolmente il problema della comunicazione. A tal fine, pensiamo alla seguente analogia con la comunicazione fra esseri umani (vedi figura 12), nella quale un filosofo indiano vuole conversare con uno stregone africano, sapendo che il filosofo indiano parla solo l’hindi, mentre lo stregone africano solo lo swahili. C’è allora bisogno innanzitutto di due traduttori, che parlino una lingua in comune tra di loro, ad esempio l’inglese: un traduttore traduce da hindi in inglese ed un traduttore da swahili in inglese. Dopo aver fatto la traduzione, si inviano quanto tradotto su un mezzo fisico, ad esempio via fax. Dal punto di vista logico, il filosofo dialoga con lo stregone, il traduttore indiano con il traduttore africano, e il fax indiano con il fax africano. Figura 12 – Problema della comunicazione 384 Capitolo undicesimo Dall’esempio fatto notiamo anzitutto che per comunicare è necessaria l’esistenza di un canale fisico di comunicazione, ovvero di una infrastruttura che permetta di scambiare segnali tra le due entità della comunicazione. A ciò occorre aggiungere la conoscenza da parte del trasmittente e del ricevente dei protocolli di trasmissione, ovvero di regole che permettono di interpretare i segnali scambiati. Infine, è necessario concordare delle regole che permettono di dare una interpretazione ai segnali scambiati (protocollo applicativo). Si noti che dal punto di vista dell’utente finale, le soluzioni adottate per i diversi “livelli” della comunicazione devono essere completamente trasparenti. Le entità (processi) che effettuano tale conversazione si chiamano peer entitiy (entità di pari livello). Il dialogo fra due peer entity di livello n viene materialmente realizzato tramite i servizi offerti dal livello precedente e virtualmente mediante il procollo applicativo definito allo steso livello, mentre il dialogo tra peer entity dello stesso host avviene sfruttando apposite interfacce di comunicazione(vedi figura 13). Figura 13 - Comunicazione tra host Nell’esempio mostrato in figura i protocolli applicativi potrebbero essere quelli di livello N e livello N + 1, mentre il protocollo di trasmissione coincide con quello di livello N 1. Dato che i protocolli devono essere usati da tutti gli elaboratori, essi sono definiti da organismi internazionali di standardizzazione. Il più famoso è l’ISO (International Standard Organization) che ha proposto lo standard OSI (Open System Interconnection) per la schematizzazione a livelli di un’architettura di rete con l’obiettivo di garantire l’interoperabilità tra vari sistemi. I livelli del protocollo ISO-OSI, mostrati in figura 14 , sono i seguenti: (1) Livello Fisico (Physical Interface Layer): è preposto alla gestione degli aspetti fisici della comunicazione, dei meccanismi di Le reti di comunicazione 385 collegamento ed offre ai livelli superiori funzionalità indipendenti dal mezzo trasmissivo utilizzato. (2) Livello dei collegamenti dei dati (Data Link Control): si occupa della correttezza del trasferimento dei dati (suddivisione dei pacchetti in frame, rilevazione e correzione di errori, controllo del flusso dei pacchetti). (3) Livello di rete (Network Layer): si occupa del controllo della congestione della rete e gestisce e controlla il routing. (4) Livello di trasporto (Transport Layer): ha lo scopo di segmentare una unità di trasmissione in messaggi fisici di uguale grandezza (pacchetti) e di garantire l’affidabilità della comunicazione (controllo flusso dei messaggi). (5) Livello sessione (Session Layer): si occupa di aprire e chiudere il dialogo tra gli elaboratori. (6) Livello di presentazione (Presentation Layer): si occupa di convertire codici e formati tra il mittente ed il destinatario del messaggio. (7) Livello applicazione (Application Layer): è responsabile di offrire servizi telematici generalizzati tra cui, ad esempio, la posta elettronica ed il trasferimento dei file. A partire dal livello 4 le comunicazioni trai vari peer entity diventano di tipo “host to host (end to end)”, in altri termini a questo livello è completamente nascosta l’infrastruttura di comunicazione fisica sottostante. Figura 14 - Modello ISO/OSI Qualche parola infine, sul sistema operativo o sui sistemi operativi presenti sugli host di una rete, indispensabili per la comunicazione software tra sistemi. 386 Capitolo undicesimo Iniziamo col dire che le definizioni fornite finora non implicano, in alcun modo, la presenza del medesimo sistema operativo, su tutte le macchine della rete. Anzi, un condizionamento del genere sarebbe estremamente riduttivo agli scopi di una rete. Addirittura ogni macchina potrebbe, in linea teorica, avere un proprio sistema operativo diverso da quello presente sulle altre macchine o sui server. Tra i due eccessi appena esposti, esistono diverse soluzioni intermedie che dipendono dal tipo di applicazione della rete: alcune situazioni - ad esempio programmi preesistenti, formazione del personale, etc. - potrebbero vincolare i sistemi operativi client (cioè quelli installati sulle stazioni di lavoro), mentre in generale il sistema operativo server (quello che gestisce e controlla l’attività di uno o più server) è generalmente vincolato da considerazioni di efficienza, tipologia delle prestazioni, sicurezza e costi. Il sistema operativo server (spesso chiamato impropriamente sistema operativo di rete) è in generale un s.o. piuttosto complesso che richiede, il più delle volte, buone capacità tecniche da parte di chi è preposto alla sua amministrazione. Sovrintende tutte le attività di rete, controlla che le operazioni richieste non invalidino l’integrità dei dati, verifica l’accesso da parte degli utenti a dati riservati e altro ancora. Il s.o. client interagisce con il s.o. server “rigirando” le richieste utente a quest’ultimo ogni qualvolta coinvolgano un server della rete (una richiesta di apertura di file o di inoltro di messaggi, etc...). 11.2.4. Il modello “Internet” Differentemente dal modello teorico OSI, il modello “Internet”, alla base dell’attuale rete Internet, è impostato su un’architettura a 5 livelli: (1) Livello fisico (Physical Interface Layer): funge da interfaccia fisica tra le stazioni e il mezzo fisico di comunicazione per la trasmissione dei dati. (2) Livello di accesso alla rete (Medium Access Control Layer): regola lo scambio dati fra un sistema finale e la rete a cui è collegato, specificando come organizzare i dati in frame e come trasmetterli sulla rete. (3) Livello internet (Internet Layer): si occupa dello scambio di dati tra sistemi che non appartengono alla stessa rete. In particolare, a tale livello si trova l’Internet Protocol (IP) che definisce le procedure per l’attraversamento di reti multiple interconnesse. Più nel dettaglio, esso specifica il formato dei pacchetti inviati attraverso la rete e i meccanismi utilizzati per farli transitare dal calcolatore sorgente attraverso uno o più router e verso il destinatario (instradamento o routing). Si nota che l’IP non gestisce l’affidabilità della comunicazione: i pacchetti vengono instradati sulla rete senza alcun controllo con il solo obiettivo di farli giungere a destinazione nel tempo minore possibile. L’affidabilità è invece demandata ai protocolli di livello superiore (4) Livello di trasporto (Transport Layer) (host to host): a tale livello i messaggi provenienti dai livelli superiori sono segmentati (e riassemblati quando giungono dai livelli inferiori) in pacchetti. Il protocollo base di tale livello, il Transmission Control Protocol (TCP), garantisce inoltre che i pacchetti siano trasmessi in modo affidabile, con la garanzia che tutti giungano a destinazione nello stesso ordine di partenza (controllo del flusso). Le reti di comunicazione 387 (5) Livello di applicazione (Application Layer): (host to host) specifica come un’applicazione può utilizzare l’insieme dei protocolli TCP/IP comunicando cooperativamente con calcolatori differenti. Il successo di questa architettura si deve al fatto che è stata ed è un’eccellente piattaforma per la realizzazione di applicazioni di rete affidabili ed efficienti ed ha permesso da subito di condividere informazioni tra organizzazioni diverse. Il TCP/IP è ormai implementato in tutti i sistemi operativi di rete ed è stato supportato dalle grandi casi costruttrici di apparati attivi di rete. 11.2.5. La struttura di una rete TCP/IP Come descritto, l’architettura di una rete TCP/IP si basa su uno schema di riferimento costituito dai seguenti elementi fondamentali: - l’infrastruttura di comunicazione; - il protocollo di comunicazione o di base; - il protocollo applicativo, ossia l’insieme di regole e il possesso di competenze comuni per il trattamento di informazioni in diversi contesti applicativi (il web, la posta elettronica, lo scambio di file, etc…); - un insieme di computer o host. Il protocollo di base è, come già descritto, il protocollo TCP/IP (Transmission Control Protocol/ Internet Protocol). Tale protocollo consente l’invio affidabile di dati sulla rete. In particolare, ogni dato spedito sulla rete viene suddivisa in sotto unità denominate pacchetti, che vengono poi ricostruite all’atto della ricezione. Quando si richiede, ad esempio, un’immagine ad un computer remoto, la spedizione avverrà nel modo seguente: prima l’immagine è suddivisa in pacchetti (ad esempio 1000), tutti di uguale dimensione e poi ogni pacchetto è inserito in una busta su cui saranno indicati: l'indirizzo del mittente, quello del destinatario e il numero d'ordine. Le 1000 buste viaggeranno in piena autonomia e, nel caso in cui esistessero 1000 tragitti diversi percorribili tra mittente e destinatario, ogni busta potrebbe viaggiare transitando su un diverso percorso. L’ordine di arrivo di ogni pacchetto può essere indifferente e, qualunque esso sia, le varie parti saranno ricomposte dal destinatario al fine di ottenere l’immagine di partenza. I pacchetti possono trasportare informazioni di qualsiasi tipo: parti di una immagine, di un messaggio, di una pagina di testo. I protocolli applicativi rendono possibile, a due sistemi capaci di comunicare con il TCP/IP, la comprensione delle informazioni scambiate. In una rete TCP/IP, come in qualsiasi altra architettura di rete, ogni computer deve essere identificato in maniera univoca. La tecnica usata, in questo caso, attribuisce ad ogni host un numero tipicamente rappresentato in una notazione simile alla seguente: 192.20.48.125 Si tratta, per la precisione, di un numero a 32 bit rappresentato come quattro numeri di 8 bit (un byte) separati da un punto. Tale numero è detto indirizzo IP dell'host in questione. Una parte dell'indirizzo IP, quella più a sinistra, è usata per identificare una rete mentre la rimanente parte, per identificare i singoli host della rete. Le dimensioni delle due parti, in termini di numero di bit, sono variabili come illustrato schematicamente nella figura 15. 388 Capitolo undicesimo Net 10. Host 31.112.32 Net 192.168.4. Host 114 Figura 15 - Schema della struttura di un indirizzi IP Poiché esistono reti di dimensioni notevolmente diverse, da quelle costituite da pochi computer ad altre composte da migliaia di host, la parte dell'indirizzo IP che identifica la rete varia in funzione della dimensione della stessa. È stata quindi necessaria la suddivisione convenzionale degli indirizzi IP in cinque classi, in funzione della loro suddivisione tra indirizzo di rete e indirizzo di host. Per poter indicare quale parte dell’indirizzo si usa per identificare la rete, è necessario specificare la cosiddetta maschera di sottorete o subnet mask. Si tratta di un numero, simile a un indirizzo IP, i cui elementi assumono solo i valori 0 o 255. I byte posti a zero identificano l’host e quelli posti a 255 la rete, il tutto facendo riferimento all’ indirizzo IP cui si riferisce la maschera. Ad esempio con la scrittura 192.168.1.24 / 255.255.255.0 – nella quale il primo è l’indirizzo IP e il secondo la relativa maschera di sottorete – si intende specificare che per l’indirizzo in questione la terna 192.168.1 (corrispondente a 255.255.255) identifica l’indirizzo di rete, mentre il numero 24 identifica un particolare host all’interno della suddetta rete. È chiaro che una comunicazione basata unicamente sugli indirizzi IP sarebbe improponibile nel mondo reale. Si ricorre, pertanto, a indirizzi simbolici cioè a nomi che possono essere immediatamente significativi per l'utente. Ogni host della rete è, secondo questa convenzione, identificabile da un indirizzo che ha un formato di questo tipo: host_name.subdomain.top_level_domain nel quale l’ host_name è il nome della singola macchina, mentre top_level_domain, unito ad un subdomain, identificano quella che possiamo definire la posizione logica dell'host all'interno della rete. Un nome così costruito è tecnicamente definito: Fully Qualified Domain Name (FQDN) o anche semplicemente "indirizzo simbolico". Si può facilmente intuire che, l'uso degli indirizzi simbolici, rende più agevole l' utilizzo della rete. In questo modo gli utenti si trovano a lavorare con indirizzi significativi, piuttosto che con numeri, il più delle volte indecifrabili. Ad esempio l'indirizzo “www.osys.com” identifica l'host www del subdomain osys appartenente al dominio com (organizzazioni commerciali). Analogamente, l’indirizzo “garbo.uwasa.fi” identifica l'host garbo della rete uwasa (University of Waasa) a sua volta appartenente al top level domain fi (Finlandia). Ovviamente, deve essere sempre prevista la traduzione da indirizzo simbolico a indirizzo IP, e viceversa, ogni qualvolta sia necessario e, possibilmente, in modo trasparente per Le reti di comunicazione 389 l'utente. Tale traduzione inizialmente era effettuata consultando un'unica tabella mentre oggi, per le titaniche dimensioni raggiunte da Internet, l’originaria tabella è divenuta un database distribuito in tutta la rete. Pertanto, non è necessario che ogni macchina conosca tutte le corrispondenze IP-FQDN, è necessario semplicemente conoscere dove andare a reperire le corrispondenze di cui non si è a conoscenza. Questo sistema, che funziona a cascata, è chiamato Domain Name System (DNS). Gli host che effettuano le ricerche delle corrispondenze prendono il nome di Domain Name Server (DNS). Siffatto sistema di ricerca, a dispetto della intrinseca complessità, è comunque molto veloce e la risoluzione di una corrispondenza è pressoché immediata. Come già indicato, una macchina connessa in rete, definita host, è dotata di un indirizzo IP e di un corrispondente indirizzo simbolico. Un host fa parte di una rete (o di una sottorete) e questa, a sua volta è parte di un dominio. Un dominio è identificato dal nome più a destra di un indirizzo simbolico che, in prima approssimazione, corrispondono ai numeri più a sinistra di un indirizzo IP. Generalmente c'è una stretta corrispondenza tra un dominio e la corrispondente parte dell'indirizzo IP. I domini in Internet sono regolamentati da specifiche istituzioni. Negli indirizzi citati in precedenza ad esempio, le estensioni: “.com” e “.it” rappresentano il nome dei domini e forniscono indicazioni significative sull’ indirizzo stesso. Le estensioni i più comuni sono: - .com: indica un organizzazione commerciale (microsoft.com, ad esempio, è il nome della rete dell'organizzazione Microsoft); - .edu: indica una organizzazione educativa (generalmente riferito ad università o a centri di istruzione statunitensi. mit.edu denota il noto Massachusset Institute of Technology, mentre ucla.edu è l'indirizzo dell'università di Los Angeles in California); - .gov: indica gli enti governativi americani (il più celebre è whitehouse.gov, che è l'indirizzo della Casa Bianca, ma notissimo è anche nasa.gov; - .mil: indica organizzazioni militari americane; - .net: Indica enti organizzativi o amministrativi di qualunque rete (come internic.net); - .org: indica organizzazioni di tipo privato, generalmente non a scopo di lucro (Fidonet.org è, infatti, il dominio della rete amatoriale Fidonet). In tale elenco di domini le estensioni .edu, .gov e .mil si applicano soltanto ad organizzazioni americane, mentre gli altri domini possono identificare anche organizzazioni di altri paesi. Le istituzioni di “governo” di Internet, hanno predisposto domini identificativi del paese di residenza di una certa rete. Così una rete italiana è, di norma, etichettata con il suffisso it, una francese con fr, una tedesca con de e così via. Come già descritto, un indirizzo simbolico viene decodificato a partire proprio dal dominio e procedendo verso sinistra: ad esempio l'indirizzo na.infn.it identifica la sottorete napoletana (na) della rete che fa capo all'Istituto nazionale di Fisica Nucleare (infn) del dominio relativo all'Italia (it). A ogni dominio è associato un calcolatore responsabile del dominio: per esempio, l’indirizzo mac1. labadam. unina. it ) prevede un computer responsabile 390 Capitolo undicesimo per il dominio it, un computer per il dominio unina.it, un terzo computer per il dominio labadam.unina.it ed un ulteriore computer per mac1.labadam.unina.it. Il calcolatore responsabile di un dominio mantiene un elenco dei calcolatori responsabili dei suoi sottodomini (e ne conosce i relativi indirizzi IP): il calcolatore responsabile del dominio it deve sapere chi sono i calcolatori responsabili di tutti i suoi sottodomini (tra cui c’è unina.it, ma anche unisa.it, miur.it); il calcolatore responsabile del dominio unina.it deve a sua volta sapere chi sono i calcolatori responsabili di tutti i suoi sottodomini (tra cui c’ è labadam.unina.it, ma anche mobilab.unina.it) e così via. Per tradurre, allora, l’indirizzo DNS di un calcolatore nel suo indirizzo IP si deve interrogare il responsabile di ciascuno dei domini cui quel calcolatore appartiene. Infine va ricordato che nella pratica, in TCP/IP l’indirizzamento è su due livelli, l’indirizzo IP e la porta TCP. L’indirizzo IP è come visto un indirizzo globale unico associato a ogni calcolatore collegato ad una sottorete e viene utilizzato da IP per l’instradamento e la consegna dei pacchetti; la porta TCP è invece un indirizzo unico all’interno dell’host e viene usato da TCP per consegnare i dati, magari aggiungendovi altre informazioni di controllo, alla particolare applicazione interessata. 11.2.6. Le applicazioni di una rete TCP/IP In questo paragrafo andremo ad illustrare i servizi applicativi più comuni offerti da una tipica rete TCP/IP. Molti dei servizi utente, e soprattutto il modo con cui sono presentati all’utente, dipendono non solo dall’insieme di protocolli usati per implementare la rete ma anche dal sistema operativo usato. Le applicazioni che richiedono i servizi di rete, e che li rendono visibili ed utilizzabili all’utente, sono costituiti da processi che girano nel contesto del sistema operativo della macchina sulla quale si utilizzano. In generale i sistemi operativi “server” implementano questi servizi in maniera indipendente dal modello di client: l’utilizzo più o meno efficiente di questi servizi dipende quasi in maniera esclusiva dai sistemi operativi client. Nondimeno esiste un insieme minimo di servizi che devono essere assicurati e che, salvo eccezioni, sono disponibili su quasi tutti i sistemi operativi utilizzati sui client di rete: è a questo insieme minimo che faremo riferimento nell’esposizione che segue. - HTTP - il servizio HTTP, acronimo di HyperText Transfer Protocol, è un protocollo che regola lo scambio di ipertesti (pagine web) tra gli host presenti in Internet. E’ il protocollo alla base del cosidetto World Wide Web (WWW) che è un sistema di rappresentazione di informazioni basate sulle tecnologie ipertestuali. WWW ha portato il concetto di ipertesto, generalizzandolo sotto vari aspetti, su Internet dando origine all’idea del villaggio globale dell’informazione su rete. WWW presenta le informazioni sotto forma di “pagine”, dove una pagina può contenere del testo ordinario, immagini, filmati, suoni ed una serie di link ad altre pagine. Il software che permette di usufruire del servizio WWW e di visualizzare le pagine web viene, di norma, chiamato browser WWW o semplicemente browser. In sintesi, attraverso l’http, è possibile esplorare l’insieme delle pagine web (residenti su apposite macchine, denominate ”HTTP Server”), da cui è costituito il WWW. Le reti di comunicazione - - - 391 FTP - il servizio FTP, acronimo di File Transfer Protocol, è un protocollo che si utilizza per trasferire file tra computer collegati ad internet. Con l’ FTP ci si può connettere a distanza ad un sistema remoto (“FTP server”), visualizzare i suoi archivi di file e trasferire file dal computer personale a quello remoto e viceversa. Per trasferire files con FTP c’ è bisogno di installare sul computer un programma ad hoc che viene quasi sempre fornito dai sistemi operativi che supportano il protocollo TCP/IP. Windows, ad esempio, include un client FTP nella sua installazione, ciononostante, anche in rete sono disponibili molti client FTP, molti dei quali openware o shareware. Telnet - Il servizio di ”terminale remoto virtuale” è forse il più tecnico tra i tools storici di Internet e pertanto successivamente superato dalle evoluzioni delle tecnologie informatiche che hanno portato allo sviluppo di tools di controllo remoto con un’interfaccia sempre più amichevole (servizi di desktop remoto). Telnet è un protocollo di comunicazione che consentiva all’ utente generico di un computer, nodo della rete, di utilizzare la propria postazione di lavoro come terminale di un altro qualsiasi nodo (con un servizio di telnet abilitato) e permetteva così di controllare un computer a distanza.In pratica, tramite Telnet si “entra” in un computer remoto (mediante un proprio login e una propria password), e nel caso si dispone di un’area personale (una parte del suo hard disk a propria disposizione), si potevano svolgere a distanza tutte le normali operazioni (eseguire programmi, leggere e spedire posta elettronica, copiare, cancellare, rinominare file...) E-mail - : La posta elettronica o e-mail realizza la versione elettronica del tradizionale sistema postale. Come sempre, per poter utilizzare un servizio di Internet, occorre disporre di due cose: un calcolatore connesso alla rete e che parli in TCP/IP e un prodotto software che fornisce l’interfaccia utente per utilizzare lo specifico servizio. Nella fattispecie della posta elettronica il prodotto software di e-mail (client di posta elettronica o mailer) deve consentire, dal punto di vista del mittente, di: (i) scrivere una lettera in formato elettronico (dunque un banale word processor), (ii) specificare un mittente ed un destinatario su una sorta di busta elettronica, (iii) consegnare il messaggio alla rete per la trasmissione , e , naturalmente, dal punto di vista del destinatario di: (i) ricevere dalla rete il messaggio, (ii) leggere la lettera. Il tutto, come nel tradizionale servizio postale, deve avvenire in maniera asincrona tra i due attori della comunicazione (ovvero, il mittente deve poter scrivere e spedire la lettera senza preoccuparsi di cosa sta facendo il destinatario in quel momento e viceversa). In atri termini, la posta elettronica (e-mail) è un servizio di messaggistica personale, di norma protetto, che permette di facilitare le comunicazioni tra gli utenti. Per implementare un servizio simile è necessario che una macchina, detta mail server, agisca da gestore dell’ufficio postale, cui pervengono tutti i messaggi e dal quale sono inoltrati ai rispettivi destinatari. In questo tipo di servizio il sistema operativo si limita a fornire il supporto per un mail server e, nelle stazioni, per i mail client assicurando, quando possibile, la massima segretezza per i messaggi. La comunicazione trai client e i server di posta elettronica 392 Capitolo undicesimo avviene attraverso due appositi protocolli applicativi denominato SMTP (Simple Mail Transfer Protocol) e POP (Post Office Protocol) che governano, rispettivamente, l’inoltro e la ricezione dei messaggi di posta. La carrellata si conclude ricordando che l’introduzione di Internet ha dato grande impulso ai sistemi informativi aziendali, proponendo soluzioni a basso costo ed efficaci simili al paradigma di internet. Una rete intranet è una rete privata interna ad una azienda che utilizza l’infrastruttura classica di una rete TCP/IP. Attraverso l’intranet, l’azienda consente ai propri dipendenti di accedere con rapidità alle informazioni interne aziendali e di condividere risorse utili per il proprio lavoro. A ciò si aggiunge che viste di quelle informazioni possono essere messe anche a disposizione dei clienti o di altri soggetti interessati. Sviluppando la tecnologia intranet, si può creare una rete extranet che offre un accesso esterno (ad esempio a fornitori o collaboratori aziendali) sicuro e controllato. Le reti extranet si sono molto diffuse per particolari applicazioni del buisiness to buisness (b2b), come gli acquisti e le vendite. Sia per le reti internet che per quelle intranet, la sicurezza è un requisito fondamentale. Una delle tecniche oggigiorno più usate per prevenire accessi non autorizzati nelle reti è quella che si basa su firewall. Un firewall è un sistema hardware e software che blocca gli utenti non autorizzati impedendogli l’accesso alla intranet: ciò si ottiene costringendo tutti i pacchetti a passare attraverso una porta che regola i flussi di ingresso tra le reti, identificando gli utenti, filtrando eventuali virus e mettendo in essere altre sofisticate misure di controllo e sicurezza. Capitolo dodicesimo Il mondo di Internet 12.1. Introduzione Il passaggio da un’economia basata quasi esclusivamente sulla produzione e lo scambio di beni materiali ad una sempre più incentrata su servizi, informazioni e idee, comporta la trasformazione della società e delle sottostanti regole del gioco. La conoscenza, da sempre chiave dell’innovazione e supporto allo sviluppo economico, diventa oggi l’elemento chiave nel processo di creazione del valore. Negli ultimi venti anni, si sono verificate trasformazioni di portata epocale che hanno determinato profondi mutamenti in tutti gli aspetti sociali, economici e culturali del nostro modo di vivere. Nella nuova economia che sta prendendo forma, i mercati stanno cedendo il passo alle reti, le risorse fisiche stanno perdendo parte del loro valore e del loro significato e la proprietà tende ad essere sostituita da forme di fruizione basate sul concetto di accesso. In un’economia basata sull’informazione e sulla comunicazione, le capacità innovative diventano più importanti di quelle produttive. L’incertezza e la rapidità del cambiamento diventano la norma e cresce lo spirito competitivo, poiché le idee sono molto più semplici da copiare e da replicare rispetto ai beni materiali. Di conseguenza, per svilupparsi le imprese hanno bisogno di rinnovarsi e di adottare nuovi modelli organizzativi in cui l’interazione risulterà sempre più mediata tecnologicamente. Internet diventa così il centro di una rete di relazioni complesse in cui, il possesso dell’informazione crea la differenza. La conoscenza, è sempre stata l’ingrediente imprescindibile dell’innovazione e dello sviluppo economico, tuttavia, essa ha smesso oggi di essere un semplice supporto alla crescita, diventando invece l’elemento determinante del processo stesso di creazione della ricchezza. Nella società postmoderna l’informazione è diventata, quindi, il nuovo bene economico attorno a cui si concentrano gli interessi produttivi primari. Più del 60% della forza lavoro, non a caso, è impiegata proprio in questo settore, che è divenuto rapidamente la principale fonte d’occupazione causando la profonda trasformazione del rapporto dell’uomo con i propri strumenti di lavoro. Oggi il lavoro è, in tutte le sue manifestazioni, mediato tecnologicamente da programmi, sistemi, elaboratori, supporti e memorie, ma nella “knowledge society” non mutano unicamente i mezzi di produzione, ma anche gli strumenti della comunicazione. 394 Capitolo dodicesimo Attualmente le telecomunicazioni, Internet in primis, svolgono un ruolo fondamentale, rimpiccioliscono la dimensione del mondo e lo riducono a villaggio globale. Gli strumenti di divulgazione del sapere: multimedialità, realtà virtuale, reti telematiche e satelliti, modificano quindi ontologicamente i processi di comunicazione e di acquisizione delle conoscenze. La nostra epoca vive un sovvertimento culturale assimilabile a quello del secolo scorso, siamo nella stessa condizione vissuta dai nostri avi alle soglie della Rivoluzione Industriale. Allora, si richiedeva al cittadino una nuova preparazione, diversa da quella necessaria ad una società basata esclusivamente sulla produzione agricola ed anche oggi, si richiede un aggiornamento e una preparazione rispondente ai bisogni del nuovo millennio. All’epoca della manifattura, e successivamente durante il lungo apogeo della fabbrica fordista, l’attività lavorativa era muta, chi lavorava taceva, la produzione era una catena silenziosa in cui era ammesso solo un rapporto meccanico ed esteriore. Nella metropoli postfordista invece, il processo lavorativo è descrivibile come complesso di atti linguistici e di interazioni simboliche. Poiché il processo produttivo ha per materia prima l’informazione e le relazioni sociali, ne consegue che chi lavora deve saper agire comunicativamente. Lo sviluppo di Internet e le applicazioni delle tecnologie dell’informazione e della comunicazione si presentano oggi come il fattore condizionante per la competitività e il progresso dell’intero sistema, poiché la cultura dell’innovazione coincide con la cultura tecnologica e digitale. Internet, garantisce quindi la simultanea disponibilità di conoscenze tecniche, sociali e umane grazie all’accesso a banche dati da parte di persone, imprese e istituzioni. In passato si riteneva che la semplice adozione delle nuove tecnologie potesse risolvere tutti i problemi. La risposta però si è rivelata parziale, a volte persino controproducente, poiché le nuove tecnologie possono addirittura aumentare la congestione informativa a livello operativo, senza produrre sostanziali miglioramenti nella fruizione dell’informazione necessaria per scegliere, decidere e governare. Questi inconvenienti sono la diretta conseguenza della negligenza mostrata verso l’impatto ambientale della tecnologia nel contesto sociale di applicazione e verso la trasformazione che essa comporta nella dimensione cognitiva dell’utente. Molte applicazioni tecnologiche non hanno soddisfatto gli obiettivi prefissati proprio perché nella progettazione si è considerato solamente il livello uomo-macchina, tralasciando la valutazione del contesto organizzativo più ampio nelle quali le tecnologie si andavano ad impattare. Le tecnologie di rete non sono quindi neutre. Internet cambia il modo di pensare e di interagire degli individui, modifica gli ambienti aziendali, di apprendimento e di divertimento sia nel contenuto sia nel tipo di operazioni mentali richieste. Le tecnologie di rete introducono attraverso la velocità, l’accessibilità e la malleabilità dell’informazione nuove dinamiche nei flussi di informazione, producono l’accelerazione dei processi comunicativi e nuovi “modus operandi” in cui esser connessi, essere on line diventa sempre più una sistema che accomuna ogni espressione del nostro vivere quotidiano. La rete valorizza e ottimizza al massimo le risorse in campo, sia il medium, che la stessa informazione trasportata. L’oggetto informatico che si sta creando grazie ai nuovi modelli di rete e di distribuzione dell’informazione è il computer globale, che ha generato a livello Il mondo di Internet 395 sociale, militare e economico delle forti accelerazioni e questo perché la rete è sempre meno un media, e sempre più un “locus”, un ambiente virtuale che ha un suo linguaggio, un suo alfabeto e suoi codici di comportamento. I cambiamenti epocali in corso hanno stimolato le disquisizioni di molti filosofi, sia dello schieramento tecnofobo che tecnofilo, tra cui ad esempio il filosofo francese Pierre Lévy che ha definito Internet un momento rivoluzionario per la società umana. Ciò che per lui è in atto sarebbe infatti la produzione di un “Intelligenza collettiva” cioè di un’ intelligenza omogeneamente distribuita, continuamente rivalutata che, posta in sinergia in tempo reale condurrà, nel cyberspazio, ad un processo di reciproco riconoscimento degli individui e dei gruppi, proprio tramite la rinnovata interazione comunicativa e la faciltà di fruizione dei flussi informativi. 12.1.1. Una ragnatela di connessioni Lo scambio di informazioni, la cui importanza vitale è stata sottolineata nel capitolo precedente, si è evoluto nel tempo dal trasferimento di beni materiali, come le lettere tradizionali, alla trasmissione di segnali fisici, come i suoni sulla linea telefonica o telegrafica e, infine, alla trasmissione di bit, attraverso computer e servizi elettronici. Oggi è difficile che un elaboratore elettronico lavori in isolamento, poiché regolarmente si utilizzano i servizi messi a disposizione da altri elaboratori elettronici sparsi nel mondo. Tutto ciò è reso possibile alla presenza di una fitta rete di connessioni tra computers, nota con il nome di Internet. Se si desidera, ad esempio, effettuare una ricerca su Dante Alighieri e non si hanno a disposizione fonti tradizionali cartacee (come i libri ad esempio), è possibile, tramite specifiche applicazioni pensate per Internet, reperire facilmente tutte le informazioni a cui si è interessati, anche se dislocate su computer che sono geograficamente distanti. Un ulteriore esempio è costituito dai contesti aziendali, in cui gli elaboratori sono localmente interconnessi, per velocizzare lo scambio interno di documenti condivisi, attraverso reti locali di piccole dimensioni (LAN) , o a livello mondiale attraverso Internet (WAN). Internet, anche nota come la “Rete delle reti” deve essere vista, innanzitutto, come uno strumento potentissimo che consente la comunicazione tra computer convenzionalmente denominati host - di tutto il mondo. Per comprendere l’entità di un fenomeno che vanta oggi una crescita esponenziale, si possono osservare i dati riportati in molte pubblicazioni. Tuttavia, è importante sottolineare che la rilevazione della dimensione del fenomeno e le previsioni del suo sviluppo futuro, si basano su metodi che non forniscono dati certi ed assoluti, sia per l’assenza di un sistema di registrazione unico degli utenti ma, soprattutto, per il tumultuoso ed incontrollato sviluppo della rete. Alcune pubblicazioni riportano che, agli inizi del 2000, gli utenti attivi nel mondo risultavano essere circa 260 milioni. Gli utenti italiani erano circa 9,5 milioni, di cui l’85% dotati di una connessione da casa, mentre il 37% dal posto di lavoro. Nel sito dell’ Internet Software Consortium (http://www.isc.org) si trovano informazioni precise sul numero di host presenti nella rete e sulla modalità di correlazione tra tale numero a quello degli utenti di Internet. Recenti rilevazioni statistiche, riportano che i sistemi host presenti in Internet sono passati da 1,7 milioni del gennaio 1993 ai 170 milioni dello stesso mese del 2003. Una 396 Capitolo dodicesimo crescita che ha visto raddoppiare, di anno in anno, il numero dei sistemi erogatori di servizi in rete. Con un margine di approssimazione accettabile, il numero di utenti può essere calcolato moltiplicando il numero di host per un valore compreso tra tre e sei in base alla diffusione di Internet nel paese considerato. In passato, anche le previsioni più ottimistiche si sono dimostrate eccessivamente prudenti ed è un dato di fatto che, la crescita degli utenti di Internet, registri un aumento esponenziale. Internet è, quindi, la “Rete delle reti” per antonomasia, la più estesa ed articolata connessione di elaboratori elettronici. Connettendosi ad essa, è possibile usufruire dei servizi e delle informazioni messi a disposizione da Università, laboratori di ricerca, banche dati, società private, strutture scientifiche, etc. Tutto questo è possibile su scala mondiale, superando le barriere spazio-temporali e i confini fisici e culturali. Internet può essere vista come un insieme di collegamenti (e di apparecchiature dedicate alla loro gestione) specificamente progettati per creare un efficiente sistema di comunicazione tra elaboratori elettronici. Questa rete, concepita originariamente con finalità militari, non possiede alcun centro nevralgico ed un messaggio (in termini tecnici: un pacchetto), spedito da un computer ad un altro, in caso di interruzione di un percorso, ha la possibilità di seguirne un altro aggirando così l'ostacolo. Figura 1 - Architettura di riferimento di un’infrastruttura di rete La comunicazione fisica tra i vari host della rete avviene mediante i mezzi più disparati: reti telefoniche, reti locali, cavi sottomarini, collegamenti via satellite, trasmissioni via radio, etc. Per spiegare l’infrastruttura tecnologica che sorregge Internet, possiamo usare la metafora delle autostrade: i cavi sono le autostrade e le informazioni che viaggiano sono le auto. Come accade sulla rete viaria, anche in Internet si possono verificare rallentamenti causati dal traffico se, in un dato istante, sono molte le informazioni che transitano per un tratto della rete. Come la rete stradale, quindi, anche l’infrastruttura su cui si basa Internet è una maglia di connessioni pertanto, tra due nodi non esiste un unico cammino percorribile. Come per i caselli autostradali, anche l’accesso alla rete è regolata da Il mondo di Internet 397 sistemi di accesso simili e indispensabili a stabilire il sistema di pagamento per l’uso della rete. L’infrastruttura di base della rete Internet è pertanto simile a quelle di cui è dotata attualmente la nostra società: dalla rete idrica a quelle elettrica, per finire a quella telefonica. Tutte le summenzionate infrastrutture sono caratterizzate da: - sistemi che erogano un servizio sull’infrastruttura (come le centrali elettriche: nucleari, a carbone, eoliche, idriche, etc., che producono l’energia elettrica; i bacini idrici che alimentano gli acquedotti; le centrali telefoniche pubbliche che provvedono alla distribuzione delle telefonate); - sistemi che utilizzano quanto prodotto dai primi (come le macchine elettriche: ad es. lavatrici, televisori, rubinetti dell’acqua; telefoni); - l’infrastruttura (costituita dai fili e dai tubi, che servono a collegare i primi sistemi ai secondi, per il trasporto della corrente elettrica, dell’acqua o del segnale telefonico). I primi sistemi, cioè quelli che erogano il servizio, sono definiti server, mentre quelli che sono fruitori del servizio, client. La figura 1 mostra l’architettura di riferimento per tutte le infrastrutture di cui si è parlato. In generale, la stessa architettura deriva da un modello molto utilizzato in Informatica, in cui si distinguono due entità: quella del produttore e quella del consumatore di un servizio. Poiché in Internet, le informazioni viaggiano in formato digitale, il consumatore può ricevere il servizio prodotto solo nel caso in cui sussistano tre precise condizioni, cioè: - le due entità devono essere collegate da una infrastruttura per il trasporto di quanto prodotto; - affinché la comunicazione abbia luogo, è indispensabile che le due entità condividano lo stesso linguaggio; - le due entità, oltre a condividere lo stesso linguaggio, devono necessariamente comprendersi. 12.1.2. Internet ed il modello di comunicazione L’utilizzo di Internet consiste, generalmente, nella fruizione delle funzionalità messe a disposizione da opportuni server che l’utente percepisce come servizi erogati da questi ultimi. Un server, così inteso, è una macchina che mette a disposizione servizi. In realtà, il termine server è, a volte, ambiguo poichè identifica sia la macchina (l’host), sia il software che consente l’erogazione del servizio stesso. L’ ambiguità summenzionata non è considerata un problema, poiché una macchina può essere contemporaneamente server di più servizi. Gli utenti accedono alle funzionalità offerte dai server, mediante specifici programmi, denominati client, installati sul proprio elaboratore. I client trasmettono le richieste ai server che a loro volta inviano le risposte seguendo delle regole prestabilite. Il modello di erogazione di servizi descritto, é un modello di tipo client/server. Il motivo del suo successo è da ricercarsi soprattutto nell’ essere riuscito a mettere contemporaneamente a disposizione di più utenti, un sistema efficiente per l’erogazione di servizi in rete. La connessione di rete è effettuata solo per un arco di tempo molto breve: giusto l’intervallo necessario per trasmettere la richiesta e recuperare la risposta dal server. 398 Capitolo dodicesimo Il vero motivo dell’efficienza della rete è da individuare proprio nell’ assenza di una connessione “continua”: in altri termini le linee di trasmissione non sono impegnate costantemente ma solo quando c’è effettiva necessità di trasmissione. Più in dettaglio, nel modello client/server, i processi di elaborazione sono suddivisi tra il client e il server e la comunicazione si basa su una sequenza di richieste e risposte tra i due soggetti: - il client, che richiede servizi o informazioni a un altro computer (il server); - il server, che risponde alla richiesta (del client) inviandogli i “risultati” della richiesta. La figura 16 schematizza il meccanismo di comunicazione client/server: Figura 16 - Modello Client/Server Il client, sulla base di quanto specificato dall’utente, prepara una richiesta in un formato appropriato (che dipenderà, tra i tanti fattori, anche dal tipo di servizio) che successivamente trasmetterà al server. Il server, a sua volta, analizzerà la richiesta del client e la elaborerà al fine di soddisfarla. Qualora l’elaborazione non presentasse problemi, sarà confezionata la risposta che verrà inviata al client. Il client, infine, riceverà la riposta e la “formatterà” affinché essa sia disponibile, o anche leggibile se si tratta di un servizio come il web, all’utente. Si può facilmente intuire che le richieste e le risposte devono essere “confezionate” in modo tale da essere comprensibili ai vari server e client disponibili in commercio, in maniera tale che – per quanto possibile – le funzionalità siano indipendenti dai particolari software. Ciò presuppone la presenza di norme che disciplinino quegli aspetti della comunicazione che, nel caso specifico, prendono il nome di “protocolli”. Per la rete Internet essi sono quelli di una rete TCP/IP descritti nel precedente capitolo. 12.1.3. La storia di Internet L’idea di una rete Internet fu proposta per la prima volta da J.C.R. Licklider del Massachusetts Institute of Technology (MIT) nel 1962. Licklider immaginava un insieme di computer interconnessi, attraverso cui, da ogni località si potesse facilmente e velocemente avere accesso a dati e programmi. In quegli stessi anni, fu pubblicato il primo lavoro sul metodo, battezzato packet switching, utilizzato Il mondo di Internet 399 per trasportare dati sulla rete, attraverso la separazione in pacchetti indipendenti ricomposti una volta giunti a destinazione. Intorno alla fine degli anni sessanta, l’ARPA (Advanced Research Projects Agency) del Ministero della Difesa Statunitense avviò un programma di ricerca alla cui direzione pose Licklider. Vista l’importanza del progetto e la sua utilità a fini bellici, ARPA passò presto nelle mani del Ministero della Difesa statunitense: la sua missione divenne quella di applicare le conoscenze tecnologiche acquisite alla difesa statunitense per rispondere efficacemente, in caso di attacco, ai progressi tecnologici dei nemici. Nel 1969 nacque Internet, allora chiamata ARPANET, che si basava sul collegamento di quattro computer che, all’epoca, utilizzavano un protocollo innovativo chiamato Network Control Protocol (NCP). Il concetto base era la segmentazione dell’oggetto della comunicazione in unità di informazione più piccole (pacchetti) al fine di poter essere efficientemente trasmesse anche nel caso in cui le linee di comunicazione fossero state inefficienti. Tra la fine degli anni sessanta e gli inizi degli anni settanta, anche altri centri di elaborazione iniziarono ad utilizzare, per il collegamento dei propri sistemi, l'innovativa tecnologia del packet switching di ARPANET. In quegli stessi anni, un numero sempre crescente di computer, iniziò rapidamente a collegarsi alla rete ARPANET. Nel 1971, si contavano già 23 host in rete, mentre nel 1980, con i primi collegamenti internazionali, ce n’ erano gia più di 200. Nel 1972 si ebbe la prima vera dimostrazione pubblica di ARPANET all’International Computer Communication Conference (ICCC). Sempre in quegli anni, furono sviluppati i primi software applicativi per la rete ARPANET, tra cui l’electronic mail, o oggi più nota semplicemente come email. A causa dei limiti di cui soffrivano alcuni dei neonati programmi, Robert Kahn e Vint Cerf incominciarono a sviluppare una nuova versione del protocollo NPC, affinché fosse consentito ai packet switching di soddisfare le esigenze di un ambiente ad architettura aperta, nel quale le informazioni potessero essere inviate da un computer ad un altro fino al raggiungimento della destinazione finale. Questo protocollo venne in seguito definito Transmission Control Protocol/Internet Protocol (TCP/IP) e, per tale motivo, Vint Cerf e Bob Khan furono universalmente riconosciuti come i “padri fondatori” di Internet proprio perché spetta a loro il merito dell’ invenzione della definizione “Internet Protocol”. In quegli stessi anni, il Defense Advanced Research Projects Agency (DARPA) degli Stati Uniti, avviò un programma di ricerca sulle tecnologie per collegare reti di differente tipologia. L’obiettivo era lo sviluppo di un protocollo di comunicazione in grado di far interagire in modo trasparente e immediato i computer in rete. Tale programma venne definito Internetting Project e il sistema di reti che nacque dalla ricerca “Internet”. Il sistema di protocolli sviluppato dalla ricerca in corso diede vita al TCP/IP Protocol Suite, in omaggio ai due primi protocolli sviluppati: Transmission Control Protocol (TCP) e Internet Protocol (IP). Nel 1979, finalmente la rete ARPANET fu trasferita, come rete operativa, dal DARPA alla Defense Communications Agency. I vantaggi dell'interconnessione tra reti divennero sempre più popolari grazie alla geniale invenzione di efficaci 400 Capitolo dodicesimo servizi di rete che resero possibile lo sviluppo di modalità comunicative sincrone ed asincrone quali la Posta elettronica, i Newsgroup, l’ Irc, etc. La conseguenza di questo successo fu la nascita, durante gli anni ottanta, di tre reti principali: - BITNET (Because It's Time Network); - CSNET (Computer Science Network); - NSFNET (National Science Foundation Network). NSFNET divenne la rete portante di Internet (definita backbone o spina dorsale) grazie al collegamento a 56 Kbps. Nel 1983 avvenne la transizione, del protocollo ospite di ARPANET, da NCP a TCP/IP. ARPANET da quel momento, incominciò ad essere utilizzato, oltre che dagli organismi per la difesa militare, da un numero sempre più significativo di dipartimenti di ricerca e da organizzazioni operative. Nel frattempo, nel 1984, al fine di facilitare la ricerca dei sempre più numerosi host connessi alla rete (il loro numero continuava a crescere in modo esponenziale), fu introdotto il Domain Name System (DNS). Nella metà degli anni ottanta, Internet era considerata la tecnologia in grado di mettere in contatto una larga comunità di ricercatori e sviluppatori. Anche altri tipi di comunità cominciarono ad adoperarlo come strumento quotidiano di comunicazione telematica. La posta elettronica cominciava ad essere utilizzata in vari paesi, anche se spesso i sistemi su cui ci si basava erano differenti. Solo alla fine degli anni ottanta, Internet ebbe un momento di grande attenzione da parte dei mass-media poiché il primo mitico virus (verme o "worm") inventato da Robert J. Morris causò il blocco di alcune migliaia di calcolatori presenti nel network. Alla fine del 1986, si potevano già contare alcune migliaia di hosts connessi (computer che fungono da nodi della rete). Alla fine degli anni ottanta: Australia, Germania, Israele, Italia, Giappone, Messico, Olanda, Nuova Zelanda, Porto Rico e Regno Unito, erano connessi al NSFNET. Nel 1990 ARPANET venne sostituito da NSFNET, tutto il mondo era “online” e world.std.com divenne il primo provider commerciale per l’accesso telefonico ad Internet. Tra la fine degli anni ottanta e l'inizio degli anni novanta, Internet subì alcune trasformazioni radicali. Nel 1989, il canale di comunicazione principale (backbone) venne potenziato con il passaggio ad una linea ad alta velocità (1,544 Mbps) poiché il numero di hosts connessi alla rete aveva superato le 100.000 unità. Inoltre, per superare il divieto imposto dal backbone NFSNET di un uso a fini di lucro della rete, ARPANET venne eliminato e fu creato il Commercial Internet Exchange (CIX) dando così il via definitivo ad un uso commerciale della rete stessa. La conseguenza di tale scelta, fu la crescita esponenziale degli host collegati ad Internet che, in un arco di tempo brevissimo, arrivarono ad essere più di 700.000. Nel frattempo, la veloce diffusione della rete impose l’esigenza di standardizzare sia i protocolli, sia le modalità di connessione al network, con la conseguente istituzione dell’ Internet Society (1992). Nello stesso anno, in Svizzera, il CERN di Ginevra, introdusse la prima realizzazione di uno dei programmi più usati sulla rete: un sistema multimediale ipertestuale con tecnologia client/server chiamato “World-Wide-Web”. Poco dopo, nel 1993, negli Stati Uniti, il National Center Il mondo di Internet 401 for Supercomputing Application (NCSA) presso la University of Illinois a UrbanaChampaign, diffuse Mosaic, un programma client (browser) con interfaccia utente per navigare nel World-Wide Web. Lo sviluppo dei browser significò lo sviluppo di un accesso diffuso e democratico alla rete per cui, non solo chi lavorava all’interno delle università o dei dipartimenti di ricerca, ebbe il diritto di accesso ad Internet, ma da qualsiasi posto e chiunque, grazie alla semplicità della navigazione permessa dai browser web, avrebbe potuto connettersi, avendo a disposizione un computer ed un modem. Le semplici operazioni da effettuare con un browser Web resero, inoltre, possibile l’accesso ai documenti presenti sul Web anche all’utenza priva di conoscenze tecniche approfondite. Il World Wide Web divenne così uno strumento di informazione e di comunicazione di massa. Nella prima metà degli anni novanta, anche i media incominciarono ad affacciarsi sulla rete; la Casa Bianca e le Nazioni Unite erano online. Nel dicembre del 1993 si contavano, nel mondo, 623 siti Web. Mentre nel 1994 usciva sul mercato l’innovativo browser con interfaccia grafica Netscape Navigator per la navigazione in rete, ARPANET/Internet celebrava il suo venticinquesimo anno di vita. Nella seconda metà degli anni 90, nacquero i primi fornitori di accesso ad Internet (Provider) quali CompuServe, AOL e Prodigy. Dalla fine degli anni novanta ai giorni nostri, Internet incominciò ad avere una crescita esponenziale, sia nelle dimensioni, che nella diversificazione dei servizi offerti, diventando un fenomeno rivoluzionario di respiro mondiale con effetti molto profondi sul vivere quotidiano. 12.2. Il World Wide Web Come già accennato, alle soglie degli anni '90, Internet si afferma come fenomeno di dimensioni mondiali. Nel 1989 presso i laboratori del CERN di Ginevra, un ricercatore di nome Tim Berners Lee sviluppò il primo esempio di un’ applicazione che avrebbe rivoluzionato il modo di concepire Internet. Tale applicazione venne chiamata World Wide Web. Il World Wide Web costituisce attualmente l'uso più diffuso e popolare di Internet, tuttavia non l’unico. E’ spesso abbreviato in WWW o W3, e viene comunemente designato come l'insieme di iperoggetti, cioè ipertesti o ipermedia, navigabili attraverso i collegamenti ipertestuali. E’ importante a questo punto definire cosa si intenda per ipertesto e ipermedia. Gli ipertesti possono essere definiti come documenti elettronici in cui le singole sotto-unità non sono disposte secondo un ordine sequenziale (come le pagine, i paragrafi, i capitoli, all’interno di un libro), bensì secondo un ordinamento reticolare o anche definito “rizomatico”. Da ogni sotto-unità di un ipertesto, chiamato nodo, si può accedere direttamente a qualsiasi altra sotto-unità ad essa collegata. I collegamenti tra i nodi sono gestiti mediante link che rappresentano legami significativi stabiliti dall’autore. Quando si legge un ipertesto, sullo schermo del computer appare un documento al cui interno appaiono delle indicazioni, che possono essere testuali o frecce, rimandi, parole in neretto, sottolineate, o altro, che stanno ad indicare che da quel punto si può “saltare” verso un altro nodo dell’ipertesto, cioè verso un’altra unità 402 Capitolo dodicesimo testuale. Scegliendo una di queste opzioni con il sistema di puntamento, ci si troverà automaticamente in un altro elemento testuale, che a sua volta può presentare un’altra serie di link verso altri nodi, tra i quali è possibile scegliere di nuovo e così via. L’ipermedia, invece, estende semplicemente l’idea di ipertesto, includendovi anche informazioni sonore, visive, animazioni e altre forme di dati, quindi denota un “locus” che collega informazioni verbali a informazioni non verbali. I collegamenti ipertestuali collegano sia informazioni interne allo stesso file, che contenute in file diversi, che a nodi “esterni” al sito considerato, cioè ad altre pagine web. Il testo così generato sarà un testo aperto, fluido e multilineare. Il vantaggio degli iperoggetti è quello di fornire all'utente una funzione in più: non solo quella “statica” di contenere in sé informazioni finite (come ad esempio un libro, un quadro, un programma televisivo), ma anche quella “dinamica” di richiamare su richiesta dell'utente altre informazioni correlate. Esiste una differenza tra Internet e il World Wide Web, nonostante oggi i 2 termini siano speso confusi. Fisicamente, Internet è semplicemente una grande rete di elaboratori interconnessi. Il World Wide Web, invece, è un insieme di oggetti virtuali che è stato realizzato sfruttando la possibilità offerta da Internet di collegare questi oggetti tra loro: mentre Internet è, tutto sommato, qualcosa di essenzialmente fisico, il World Wide Web è invece estremamente virtuale, cioè un insieme di informazioni multimediali codificate. Internet è l’ insieme di vie di comunicazione che permette il passaggio di veicoli di qualsiasi tipo, mentre il www utilizza una parte di questo insieme, e alcuni veicoli ben specifici, per collegare tra loro alcuni punti della rete e trasportare un determinato tipo di oggetti. Grazie alla possibilità di richiamare in modo molto semplice qualsiasi “risorsa” in qualunque parte del mondo, identificata univocamente attraverso l’ indirizzo (URL), l’autore di ipertesti può facilmente creare collegamenti tra le proprie pagine e altre pagine del web, permettendo quindi all'utente di spostarsi non per vicinanza geografica, ma per correlazione tematica. Non ha più importanza se una pagina web sia a New York piuttosto che a Parigi o a Mosca, poiché la rete rende nulle le distanze geografiche; interessa piuttosto che gli argomenti siano correlati. Ciò permette di ritrovare una quantità enorme di informazioni, in tutto il mondo, con un semplice click del mouse. L'idea geniale del WWW risiede, in breve, nella organizzazione delle informazioni sulla rete in formato ipertestuale e nella sua presentazione multimediale. Le pagine web sono individuate attraverso il loro specifico indirizzo web, mentre ogni computer dal relativo indirizzo IP. Tempi di risposta della rete permettendo, si potrà avere accesso all’informazione globale in modo semplice ed istantaneo. Nel mondo del Web è ben definita la distinzione del modello client-server, che definisce chi svolge il ruolo di fornitore dell’ informazioni e di chi ne fa richiesta risultando cliente dei servizi offerti dai primi. La parte client dell'applicazione Web costituisce l'interfaccia verso l'utente finale e viene anche detta browser. Il suo scopo consiste nel presentare le pagine web, interpretando opportunamente la descrizione del loro contenuto che è stato scritto mediante uno specifico linguaggio chiamato HTML (HyperText Markup Language), che utilizza i cosiddetti tag per l’organizzazione delle informazioni in una pagina web. I link, Il mondo di Internet 403 quindi, contengono l' indirizzo di una pagina Web o, più in generale, di una risorsa Web. L’ indirizzo web, ancora una volta derivazione ed estensione degli indirizzi IP, è come già visto, detto URL (Uniform Resource Locator). Un URL contiene una porzione che specifica il linguaggio (protocollo) che si sta usando per reperire la risorsa, l'indirizzo IP dell'host e, l'indicazione (il percorso nell'albero delle directory) della collocazione della risorsa sull'host. Un esempio di questo tipo di indirizzo è il seguente: http://www.dol.unina.it/index.html in cui: http:// identifica il protocollo www.dol.unina.it il nome dell' host index.html il file che contiene la pagina cercata. Il protocollo indica come il documento viene “servito” dal computer che fornisce l'informazione. Per quanto riguarda il nome dell’ host, ogni computer come già descritto collegato a Internet può essere identificato con un nome (oltre ad un numero) e talvolta anche più di uno. Questo nome (come il numero) viene in parte assegnato seguendo alcuni criteri e in parte deciso arbitrariamente dal possessore del computer. Le altre informazioni che si hanno nell’URL, se presenti, servono a localizzare il documento sul disco del computer che contiene altri documenti, indicando il nome del file ed il percorso per arrivare allo stesso. Quando, dunque, l'utente seleziona un determinato link, normalmente attraverso l’azione del mouse, viene attivata una connessione TCP/IP con il nodo Internet che ospita la pagina richiesta e, si richiede che il lato server dell'applicazione web localizzi e fornisca al browser richiedente la pagina in questione. Naturalmente, la pagina appena ottenuta può contenere link ad altri nodi localizzati in qualunque parte del mondo. Questo processo di visitazione, denominato “navigazione” può essere ripetuto progressivamente fino a coinvolgere centinaia o migliaia di pagine e relativi server. Il protocollo che stabilisce le regole con le quali i browser ed i server web dialogano tra loro è definito HTTP (HyperText Transfer Protocol). Naturalmente, la capacità di trattare informazioni e link multimediali (l'ipermedialità) nasce proprio dal fatto che sia l'HTML che l'HTTP sono rispettivamente un linguaggio ed un protocollo specificamente progettati per consentire un esplicito ed efficiente trattamento di questa tipologia di informazione. Un ulteriore punto di forza del WWW consiste nell’aver inglobato, in una forma molto attraente ed accessibile all'utente medio, poco “alfabetizzato tecnologicamente”, anche altri servizi fondamentali di Internet quali la posta elettronica e l'FTP. Questi servizi ultimamente sono stati arricchiti della dimensione multimediale. Infatti è possibile, ad esempio, inviare un’ e-mail che contiene messaggi vocali o un videoclip, e visionarli in un ambiente unico; 404 Capitolo dodicesimo effettuare uno scambio di file mediante FTP, associando ai file degli iperlink e ottenendo quindi il loro trasferimento con un semplice "click" sul nome del file. 12.2.1. Nuovi standard per il Web L'evoluzione di Internet procede incessantemente. La crescente richiesta di nuove potenzialità e applicazioni trasforma la rete in un continuo work in progress, un laboratorio dove si sperimentano tecnologie e soluzioni innovative. Se, da una parte, questo processo produce sviluppi disordinati spesso determinati da singole aziende che cercano di trarre il massimo profitto dal boom di Internet, dall'altro lato, le organizzazioni indipendenti, che gestiscono l'evoluzione della rete, svolgono una continua attività di ricerca per la definizione di nuovi standard. L'organizzazione ufficiale deputata allo sviluppo degli standard per la rete è il World Wide Web Consortium o W3C (http://www.w3.org), che raccoglie centinaia di aziende, organizzazioni e centri di ricerca interessati allo sviluppo delle tecnologie di rete. In questi ultimi anni il W3C ha prodotto una serie di specifiche divenute standard ufficiali. Tutti i materiali prodotti dal W3C, pubblicati sul sito Web del consorzio sono di pubblico dominio. La maggiore parte delle innovazioni, riguardano il funzionamento del World Wide Web. I settori di maggior interesse per il W3C sono sostanzialmente: - il potenziamento delle funzionalità di gestione editoriale e grafica dei documenti su Web; - la certificazione ed il controllo del contenuto dei siti; - l'individuazione ed il reperimento di documenti e risorse su Internet. In principio, il World Wide Web era concepito come un sistema per la distribuzione di semplici documenti testuali in rete. La comunità di utenti a cui era destinato era molto circoscritta e poco preoccupata degli aspetti stilistici nella presentazione dell'informazione. Tuttavia, con il passar del tempo il Web è diventato un vero e proprio sistema di editoria elettronica on-line. Ovviamente, l'espansione della rete discostandosi dal progetto originale, ha stimolato una serie di revisioni e di innovazioni degli standard tecnologici originari. Un grande interesse è stato mostrato verso il potenziamento della capacità di gestione e controllo dei documenti multimediali e dei testi elettronici pubblicati su Web. Il formato usato nella realizzazione di pagine Web è il linguaggio HTML che ha subito, nel corso degli anni, una notevole evoluzione. Un ruolo propulsivo in questo processo è stato svolto dalle grandi aziende produttrici di browser. Nel corso degli anni, sia Microsoft che Netscape, hanno introdotto innovazioni ed estensioni al linguaggio originario, al fine di conquistare fette di mercato sempre più grandi. La corsa all' innovazione, ha migliorato l'aspetto e la fruibilità delle pagine pubblicate su Web tuttavia, ha avuto effetti deleteri dal punto di vista della portabilità dei documenti. Per ovviare a questa situazione il W3 Consortium ha accelerato il processo di aggiornamento dello standard ufficiale, che di volta in volta ha accolto (o rigettato) parte delle modifiche introdotte. Parallelamente all'aggiornamento delle attuali tecnologie, l'organizzazione ha iniziato a sperimentare nuove soluzioni per la creazione e gestione dei documenti su Web come ad esempio l’XML. Il mondo di Internet 405 12.2.2. I browser Poiché la dimensione e la struttura del Web richiamano alla mente l'immagine di un oceano di informazione, la consultazione delle sue pagine è comunemente definita “navigazione”. La navigazione tradizionale richiede una barca per navigare, nel caso della navigazione telematica la “barca” è uno specifico software, la cui funzione è di richiamare dalla rete le pagine che l'utente desidera consultare e mostrarne il loro contenuto sullo schermo. Nel gergo telematico i programmi di questo tipo vengono chiamati browser, dall'inglese to browse (scorrere), poiché permettono, appunto, di scorrere i documenti. Il browser visualizza le pagine Web in modalità grafica. Un browser, è un programma che permette di leggere, ma non di modificare i file e rende possibile l’accesso a qualsiasi oggetto si desideri recuperare inserendo un “indirizzo”, come gìà accennato, definito URL, che racchiude in sé tutte le informazioni necessarie per l'operazione richiesta. La parte client dell'applicazione Web, detta appunto browser, costituisce l'interfaccia verso l'utente finale. Il suo scopo consiste nel presentare le pagine web, interpretando l’HTLP. I due browser più diffusi tra gli utenti della rete sono Netscape Navigator dall'inventore dell'interfaccia Mosaic, Marc Andreessen, Microsoft Internet Explorer oggi fornito come corredo standard a Windows, ed ultimamente, Mozilla Firefox. Tutte le applicazioni, accanto al modulo di navigazione su Web vero e proprio, offrono dei programmi per l'accesso alla posta elettronica, ai newsgroup e persino degli editor per realizzare le pagine da pubblicare sul Web. I browser sono stati, fin dall'inizio, progettati al fine di esaltarne semplicità e funzionalità d’uso, (facendo ricorso a concetti come quelli di “bottone”, “menù a tendina”, ecc.). Quando, dunque, l'utente seleziona un determinato link, cliccandoci su con il mouse, viene attivata una connessione TCP/IP con il nodo Internet che ospita la pagina richiesta, e si richiede che il lato server dell'applicazione web localizzi e fornisca al browser richiedente la pagina in questione. 12.2.3. La ricerca dell’informazione ed i motori di ricerca L’intera storia dell’uomo è caratterizzata dal desiderio di organizzare le informazioni per renderle fruibili ed accessibili in modo semplice ed immediato. Le biblioteche sono la testimonianza più evidente del grande lavoro di raccolta e classificazione delle informazioni necessarie ad alimentare la trasmissione del sapere. Il web è diventato, negli ultimi anni, lo strumento principale per pubblicare e rendere disponibile una quantità enorme di informazione e i motori di ricerca sono, oggi, lo strumento utilizzato per reperire velocemente e con facilità il dato pertinente. Può sembrare paradossale, ma disporre di tantissima informazione, equivale a non possederne alcuna, se non si è in possesso di metodi o strumenti efficaci ed efficienti di ricerca. Il grosso problema di un qualsiasi sistema che accoglie troppa informazione è sempre stato quello del reperimento dell’informazione utile. Su Internet, questo problema è già esploso: probabilmente esistono in rete informazioni su qualsiasi argomento, ma non è sempre facile riuscire a sapere dove esse siano esattamente collocate. 406 Capitolo dodicesimo Esistono alcuni siti che offrono la possibilità di ritrovare gli indirizzi web di pagine utili alla nostra ricerca. Questi strumenti sono sostanzialmente di due tipi: Indici di rete e Motori di ricerca. Essi si basano su due diversi metodi di catalogazione dell’informazione. Sebbene concettualmente siano abbastanza diversi, di fatto, i due sistemi tendono verso l’'unificazione, per cui i motori di ricerca offrono anche una divisione in categorie e gli indici di rete permettono anche ricerche per parole chiave. Indici di rete Negli indici di rete sono memorizzati gli indirizzi di un immenso numero di pagine, raggruppati per categoria. È quindi necessario scegliere la categoria a cui si è interessati. Cliccando sullo specifico link-categoria, si riceverà in risposta un elenco di siti con i relativi titoli. I siti sono, solitamente, inseriti su segnalazione degli autori o dei singoli utenti. In molti indici di rete l'inserimento è gratuito, mentre altri richiedono il pagamento di una quota. L’ indice di rete più diffuso è sicuramente Yahoo! In questi ambienti, la catalogazione è manuale, cioè prevede che l’autore della pagina sottometta la propria opera mediante il form di inserimento disponibile. Motori di ricerca In inglese Search Engine, sono programmi che “percorrono” il www leggendo e catalogando le pagine che incontrano; l'utente per effettuare la sua ricerca deve inserire una o più parole chiave e in risposta riceverà, dal programma, un elenco con gli indirizzi delle pagine che contengono la/le parole inserite nella query, solitamente ordinati in funzione del numero di occorrenze e quindi dell'interesse probabile. Anche in questo caso, gli utenti possono segnalare gli indirizzi. Qui le pagine, diversamente dagli indici di ricerca, non sono suddivise in categorie ma, di ogni indirizzo web è memorizzato un breve sommario (in genere il titolo e le prime righe), la cui lettura guiderà l’utente nella selezione delle pagine più rispondenti ai personali obiettivi di ricerca. Nei motori di ricerca, la catalogazione degli indirizzi web è effettuata automaticamente dai gestori dei motori, attraverso gli spider (ragni), così chiamati per il loro crawling (zampettare) in giro per la Rete, al fine di trovare e catalogare le pagine Web, monitorandone poi le eventuali variazioni. Gli spider, possono essere tenuti “alla larga” dal proprio sito inserendo determinati codici all’ interno della pagina, al contrario, altri codici possono rendere il nostro sito rintracciabile con più facilità. I motori di ricerca catalogano non solo le pagine Web, ma documenti di ogni tipo (come immagini, newsgroups, etc.) Ogni motore di ricerca, riesce ad aggiornare la propria base di dati tenendo conto di link non più attivi, aggiungendo nuovi documenti o aggiornando gli indirizzi già esistenti. Esistono molti motori di ricerca, il consiglio è di provarne un certo numero e di trovare quello che si adatta meglio alle specifiche esigenze di ricerca. Il motore attualmente più popolare è Google, che usa un sofisticatissimo meccanismo di analisi delle pagine (detto PageRank). In breve tempo è divenuto uno dei più raffinati strumenti di ricerca presenti sul web. Il mondo di Internet 407 Per concludere, tutti i motori di ricerca presentano un’architettura che deve provvedere: - al reperimento delle pagine web; - alla indicizzazione dei contenuti in esse presenti; - alla creazione delle liste in risposta alle interrogazioni. Devono pertanto essere in grado di: - conoscere gli indirizzi degli host che ospitano le informazioni; - fornire un elenco delle informazioni che è possibile reperire; - fornire una risposta veloce ed esauriente alle più svariate richieste. Il segreto per una ricerca efficace (trovare cioè informazioni pertinenti e valide) ed efficiente (la ricerca non deve durare molto e non si deve perdere il senso dell'orientamento) consiste nel saper: - scegliere il motore adeguato - definire una stringa d'interrogazione mirata - utilizzare, quando occorre, gli operatori booleani Nella ricerca dell’ informazione, il problema più complesso è legato proprio alla sua efficacia, ossia alla rilevanza, alla pertinenza e alla coerenza dei risultati restituiti dalla ricerca operata. La bontà di un motore di ricerca è legata, pertanto, alla rispondenza dei risultati ottenuti e alla capacità di semplificare la selezione di un numero elevatissimo di pagine, i cui contenuti spesso non sono veramente attinenti agli obiettivi personali della ricerca. Nonostante l'uso dei motori sia molto intuitivo, per ottimizzare la ricerca è opportuno leggere le “istruzioni per l’uso” contenute in ognuno di essi. Quando viene effettuata una ricerca per parole chiave è importante capire come sia possibile formulare la richiesta al fine di ottenere il risultato migliore. Molti siti permettono l’uso degli operatori booleani. Si tratta di quattro operatori principali: AND, OR, AND, NOT, NEAR, i quali sono stati standardizzati dai più celebri motori e consentono di restringere e affinare la nostra ricerca. Non sono “case sensitive”, cioè non tengono conto delle lettere maiuscole o minuscole e ultimamente sono stati sostituiti da interfacce amichevoli per “la ricerca avanzata” (v. Google) per facilitarne ancora di più il loro uso. Segue un elenco degli operatori più comuni. - AND. Se uniamo due o più parole chiave con l’operatore AND, in alternativa al quale può usarsi il segno “+”, significa che vogliamo solo i documenti che contengono tutte le parole indicate (questo AND quello) e non soltanto una di esse considerata singolarmente. Le parole vanno inserite in ordine di importanza, le più importanti devono essere al prino posto. Se cerchiamo gli accordi delle canzoni di Battisti, è meglio inserire “Battisti AND accordi” piuttosto che “accordi AND Battisti”, visto che prima di tutto ci interessano le pagine dedicate al cantante e, tra queste, quelle che riportano gli accordi. In questo modo il motore restituirà in risposta tutti i documenti indicizzati che contengono le parole inserite. - OR. Questo operatore è utilizzato quando la ricerca si concentra su diversi termini e non è necessario che li comprenda tutti, ma anche solo uno di essi. L’uso di OR, quindi, allarga il campo della ricerca; “ostelli 408 Capitolo dodicesimo OR campeggi”, ad esempio, cerca i documenti che parlano sia di ostelli che di campeggi. - NOT. L’uso di NOT è importante quando vogliamo escludere delle sottocategorie di documenti. Esclude i documenti contenenti il termine o la frase specificata. Ad esempio, “alberghi NOT pensioni” troverà i documenti contenenti il termine alberghi ma non il termine pensioni. Nelle interfacce di ricerca semplice l’operatore NOT è in genere sostituito dal segno - (meno). Ha un funzionamento opposto all’operatore AND. - NEAR. Funziona in modo simile ad AND ma, oltre a ricercare i documenti che contengono le parole chiave inserite, restringe il campo di ricerca stabilendo che i termini debbano trovarsi ad una certa distanza l’uno dall’altro. Tale distanza è diversa a seconda del motore che si sta usando. AltaVista, per esempio, imposta l’operatore NEAR su 10 parole, mentre Lycos su 25. Altri servizi (quali Excite), al contrario, non lo riconoscono. - Asterisco (*). Alcuni motori di ricerca accettano l’asterisco come sostituto di una o più lettere. L’asterisco può quindi essere usato per cercare tutte le declinazioni di una parola, come il genere e il numero dei sostantivi o il tempo e il modo dei verbi. Ad esempio, “gatt*” sta per “gatto, gatta, gatti, gatte”, ma anche per “gattopardo, gattabuia” e per tutte le altre parole che cominciano per “gatt”. L’asterisco può essere usato anche all’interno di una parola, ad esempio quando non si è sicuri della sua ortografia. - Virgolette (“ ”). Le virgolette alte “___” indicano al motore di ricerca che il loro contenuto deve essere trattato come una frase, cioè come una sequenza di parole che devono comparire nel testo come un blocco unico (ad esempio “bed & breakfast”, ma anche “Giuseppe Verdi”). Se inseriamo una frase senza racchiuderla tra virgolette il motore di ricerca interpreta le parole come legate da OR e quindi, invece di restringere l’ambito della ricerca, lo allarga, pertanto non troverà solo i documenti che contengono la frase “bed & breakfast”, ma tutti quelli che contengono la parola “bed” e tutti quelli che contendono la parola “breakfast”. Motori di ricerca e indici di rete sono quindi tra le applicazioni più complesse del mondo dell’informatica se si pensa alle risorse hardware e software necessarie per far funzionare correttamente uno strumento che deve indicizzare, archiviare e mantenere aggiornate decine (a volte centinaia) di milioni di pagine Web. 12.2.4. I Portali Molto utilizzati dagli utenti meno esperti, ma poco adatti ai professionisti della ricerca in Internet, i portali si candidano a costituire il sito di riferimento dei navigatori non solo per la ricerca di informazioni ma anche per ogni altra attività Il mondo di Internet 409 effettuabile in Rete (comunicazione, giochi, acquisti in linea, prenotazione di servizi, etc). Includono quasi sempre una directory per argomento molto orientata alle necessità della vita quotidiana, un motore di ricerca, sviluppato in proprio, e un offerta dei più svariati servizi: notiziari, quotazioni di borsa, indirizzi e-mail e spazio Web gratuiti, oroscopi, stradari, chat e forum, invio di Sms, previsioni del tempo e così via. 12.3. Internet ed il mondo del business La diffusione a macchia d’olio di Internet negli ultimi anni ha, per così dire, contagiato anche il mondo del “business”. Con lo sviluppo delle reti di trasmissione del web e con l’emergere di concetti quali connettività e convergenza, le imprese hanno la possibilità di modificare le proprie strategie ed attività commerciali per sfruttare appieno le opportunità offerte dalle tecnologie ICT ed in particolare, dalla rete Internet. Le sfide imposte dalla globalizzazione richiedono alle aziende di operare in un mercato dove velocità e efficienza sono condizioni irrinunciabili: si fa strada quindi il concetto di “connessione continua” con clienti, fornitori, partner e si impone il modello di un’impresa “estesa”. In un tale contesto, la rete Internet diventa l’unica e più importante piattaforma su cui progettare l’impresa, poiché rappresenta l’elemento in grado di mettere in comunicazione facilmente tutti gli attori in gioco. Oggi si parla di e-business o “Net Economy”, proprio a testimonianza del fatto che parte dei volumi di affari di un gran numero di aziende, si svolgono oramai su Internet che diventa così, sia una infrastruttura primaria di comunicazione, sia uno strumento di business utile a migliorare l’efficienza di alcuni processi aziendali (commercializzazione e approvvigionamento) e, in prospettiva capace di sviluppare nuove opportunità di business (incremento del numero di clienti). Grazie alle ultime innovazione tecnologiche nel campo dell’informatica, l’ ebusiness ha superato la fase di scetticismo iniziale, legata alla “scarsità di banda” e alla “scarsa sicurezza e affidabilità della rete”, imponendosi come soluzione globale e investendo anche in settori e in aziende “tradizionali”, legate alla “Old Economy”. Negli ultimi tempi è stato intenzionalmente coniato il termine “B2B” (business to business) per indicare e caratterizzare il modello su cui si basano le transazioni economiche tra imprese svolte mediante l’utilizzo della rete Internet. Non sono più dunque gli ambienti fisici ma quelli virtuali ad essere i nuovi depositari delle relazioni tra clienti e fornitori, delle attività di commercializzazione e dell'approvvigionamento e dell'incrocio tra la domanda e l'offerta. Il tutto nella prospettiva di accedere ad informazioni generaliste e/o di settore, ricercare nuovi partners commerciali, condividere tutta una serie di servizi comuni da quelli finanziari a quelli logisitici, per arrivare alla consulenza. Quindi nell’attuale mercato, caratterizzato da una continua innovazione e da una concorrenza sempre maggiore (dovuto all’allargamento geografico dei confini di operatività delle aziende), l’ adozione di soluzioni e-business diviene un fattore necessario per le aziende che intendono aprirsi al Web e ad Internet. In un così ampio e complesso scenario, le applicazioni di e-business più diffuse in ambito aziendale e all’interno delle Pubbliche Amministrazioni, che si sono fatte 410 Capitolo dodicesimo strada nel caotico universo della rete, sono l’e-commerce, l’e-banking, l’e-trading e l’e-procurement. 12.3.1. L’e-commerce Per commercio elettronico (e-commerce) si intende la vendita tramite sito Web (dell’azienda o di terzi) di prodotti e servizi sia all’utenza finale (individui, aziende) che a rivenditori e distributori. Perché sia possibile parlare di commercio elettronico è necessario che almeno l’ordine di acquisto debba essere compilato on line, il pagamento può essere invece effettuato tramite canali tradizionali (bonifico,assegni, contante ecc.) o via Web mediante l’utilizzo di carta di credito. I progressi compiuti soprattutto nel campo della sicurezza su rete (crittografia, firma digitale, etc…) e, la contemporanea nascita di enti che “certificano” la sicurezza di un portale di e-commerce, hanno abbattuto l’iniziale diffidenza nei confronti di questa tecnologia (soprattutto per quel che riguardava la privacy e la sicurezza in rete del cliente, che doveva digitare i propri dati online e effettuare pagamenti a rischio con carta di credito) e hanno fatto sì che essa diventasse una realtà per tutti: per le aziende che possono oggi ordinare merci in tempo utile (“on time”), acquistare materiali da fornitori, o vendere beni/servizi, e, per i privati che, comodamente da casa, possono acquistare libri, biglietti, musica, etc.. I vantaggi legati all’adozione delle moderne soluzione di e-commerce sono duplici. Da un lato esse offrono alle aziende una sempre più variegata gamma di prodotti e strategie di web marketing attraverso cui definire proficuamente la propria presenza in rete: le aziende che attivano portali di e-commerce possono vendere i loro prodotti in un negozio virtuale accessibile a tutti, facile da gestire, sicuro e, portare il proprio marchio on-line. Dall’altro lato i clienti possono acquistare merci (riempiendo il proprio “carrello elettronico”) in negozi sempre aperti, con offerte e prezzi convenienti, indipendentemente dalla loro collocazione geografica. 12.3.2. L’e-banking L'imponente sviluppo delle tecnologie dell'informazione e della comunicazione e i cambiamenti economici e normativi hanno modificato in modo sostanziale il settore bancario. La possibilità di offrire servizi attraverso la rete Internet ha intensificato la competizione interna del settore bancario prescindendo, almeno in parte, dalla necessità della presenza di filiali sul territorio. Tale situazione ha comportato il rapido sviluppo, in termini qualitativi e quantitativi, dei servizi on line offerti dalle banche, cioè dell’e-banking. L’e-banking permette ai correntisti di una banca di operare per mezzo di applicazioni gestite direttamente on line e relative all’ interrogazione di conti correnti, alla visualizzazione dei movimenti sul conto corrente, alla possibilità di eseguire bonifici e pagamenti, etc. L’uso personale dell’e-banking consente all’utente di effettuare on line, oltre alle normali operazioni bancarie, anche il pagamento di tasse e bollettini postali, di accedere e operare sui principali mercati finanziari e di fare shopping on line. Inizialmente le iniziative di e-banking sono sorte per soddisfare la domanda dei privati, successivamente, le banche italiane hanno incominciato a proporre servizi di e-banking anche alle imprese. Attualmente l'offerta di servizi bancari on line Il mondo di Internet 411 per le imprese presenta già delle interessanti proposte. Le imprese, ad esempio, possono già sfruttare i servizi di eBanking per gestire, almeno parzialmente, pagamenti e riscossioni via Internet. 12.3.3. L’e-trading Internet è divenuto un valido ausilio tecnico che accompagna l’offerta di erogazione di servizi d'investimento. L’e-trading o “on line trading” rappresenta quell’insieme di procedure telematiche che consente la negoziazione di titoli attraverso la rete. L'impiego della rete ha rappresentato una straordinaria opportunità di crescita per i mercati finanziari e costituisce l'area principale dell'internet banking poiché, l'immaterialità del denaro e del concetto stesso di possesso, rendono il mondo della rete il “locus” ideale per questo tipo di transazioni. Il mercato telematico rappresenta un mercato altamente concorrenziale grazie all’offerta di informazioni virtualmente illimitate e accessibili a costo quasi nullo. Ciò ha portato ad una drastica riduzione delle commissioni di negoziazione e alla possibilità di operare su molti mercati, monitorando in tempo reale le quotazioni, di inserire il proprio ordine al livello desiderato (in acquisto o vendita) e di visualizzare tempestivamente le operazioni effettuate. Sorto a metà degli anni '90 negli Stati Uniti , solo dal 1998 l’e-trading ha iniziato a suscitare anche in Italia l'interesse di alcune banche pioniere. Oggi lo sviluppo delle telecomunicazioni, in termini di sicurezza, capacità trasmissiva ed economicità, ha favorito l'affermarsi di mercati finanziari altamente informatizzati: la diffusione delle informazioni di mercato, l'incontro tra domanda e offerta di titoli, il collegamento automatico con i sistemi di regolamento delle transazioni sono affidati in misura crescente a procedure telematiche. Gli altri fattori che hanno contribuito maggiormente allo sviluppo del trading on line sono stati la sicurezza, intesa come creazione di chiavi di crittografia più affidabili, l'evoluzione della tipologia dell’utenza, la tecnologia che ha reso disponibile l’abassamento dei costi di collegamento alla rete, la diffusione della cultura informatica e finanziaria e infine, il miglior grado di trasparenza dovuto dall'aumento dell'informazione. 12.3.4. L’e-procurement Il termine e-procurement indica quell’insieme di tecnologie, procedure, operazioni, e modalità organizzative che consentono l’acquisizione di beni e servizi on line, grazie alle possibilità offerte dallo sviluppo della rete Internet e del commercio elettronico. L’e-procurement permette di fatto alle imprese di gestire completamente il rapporto con i fornitori dalla conduzione della trattativa all'approvvigionamento, dall'ordine al fornitore, fino alle consegne. Vengono inoltre soddisfatte tutte le necessità relative alla gestione dei rapporti con i terzi. Una migliore gestione delle scorte, la diminuzione del numero di errori che avvengono nella fase di gestione degli ordini, il risparmio sul costo dei beni acquisiti, la possibilità di pianificare in maniera ottimale la gestione della propria “supply chain” (flussi di materiali, delle informazioni e dei mezzi di pagamento), di effettuare il “tracking” degli ordini in tempo reale, la diminuzione dei tempi di 412 Capitolo dodicesimo gestione, sono i principali vantaggi dell'e-procurement. Tra le modalità di attuazione dell’e-procurement rientrano, infine, anche le cosiddette “Aste on line”, che rappresentano delle procedure telematiche per acquisti di beni e servizi secondo le norme che regolano le normali gare e licitazioni pubbliche o private. 12.3.5. Internet e la Pubblica Amministrazione: l’e-governement Anche il mondo della Pubblica Amministrazione, negli ultimi anni, ha risentito della diffusione di Internet ed ha avvertito come l’utilizzo della rete potesse portare ad un miglioramento qualitativo dei propri servizi, soprattutto per quanto riguarda la comunicazione tra amministrazioni nello scambio di documenti o informazioni, e, per quanto concerne la fruizione dei servizi da parte del cittadino/impresa. A tale proposito è stato introdotto il termine di e-government per indicare l’insieme di tutte quelle iniziative atte a favorire il cambiamento della Pubblica Amministrazione, attraverso l’utilizzo delle moderne tecnologie ICT e l’apertura verso la rete Internet. Tra le azioni previste nel piano di e-government ricordiamo: - azioni dirette ad informatizzare la erogazione dei servizi ai cittadini e alle imprese che spesso implicano una integrazione tra i servizi di diverse amministrazioni; - azioni dirette a consentire l’accesso telematico degli utilizzatori finali ai servizi della pubblica amministrazione e alle sue informazioni; - azioni di informatizzazione dirette a migliorare la efficienza operativa interna delle singole amministrazioni. In un’ottica di rete, in cui tutte le amministrazioni centrali e locali sono abilitate per una cooperazione informatica paritetica, viene assegnato un ruolo particolare alle amministrazioni locali, le quali dovranno assumere, in un modello distribuito, sempre più il ruolo operativo di front-office del servizio pubblico, mentre le amministrazioni centrali sono destinate a svolgere un ruolo di back-office e coordinamento. In questo disegno, il cittadino/impresa potrà ottenere il servizio pubblico, cui ha diritto, rivolgendosi ad una qualsiasi amministrazione di frontoffice abilitata al servizio, indipendentemente da ogni vincolo di competenza territoriale o di residenza. Per l’attuazione delle azioni e dei progetti previsti, si identificano tre strumenti fondamentali: - la rete nazionale, cioè la rete che connette tra loro tutte le reti centrali, regionali, locali, di categoria e di settore amministrativo, quelle già esistenti e quelle in via di attivazione; - la carta d’identità elettronica, come sistema unificato di accesso alle rete e che darà al cittadino il diritto all’accesso a tutti i servizi della Pubblica amministrazione erogati on-line; - la firma digitale, che servirà per dare validità giuridica a tutti quei rapporti tra le pubbliche amministrazioni e i privati che la richiedono . In tale contesto i portali web per l’erogazione di servizi consentiranno, oltre all’accesso alle informazioni, anche la formulazione interattiva di richieste di servizio o la esecuzione di transazioni. L’accesso ai portali di servizi richiederà anche l’uso della firma digitale come meccanismo di convalida legale delle Il mondo di Internet 413 dichiarazioni trasmesse e la carta di identità elettronica, come mezzo di identificazione per l’accesso ai servizi dei portali. 12.3.6. Internet ed il mondo della formazione: l’e-learning Internet e il World Wide Web hanno spinto il mondo della formazione verso radicali cambiamenti metodologici, tecnologici ed organizzativi. In particolare i progressi compiuti nell’area delle nuove tecnologie della comunicazione; delle reti telematiche, della multimedialità, della simulazione e della realtà virtuale, costituiscono un arricchimento dei mezzi messi a disposizione delle strategie formative e possono offrire potenzialità rinnovate alle modalità di apprendimento. La conoscenza non è più un privilegio di una elité ristretta, poichè ormai, un’enorme quantità di informazioni e risorse è accessibile in maniera semplice e rapida tramite il web. In particolare, gli strumenti forniti dalle nuove tecnologie hanno permesso, da un lato, la creazione e divulgazione di materiale didattico innovativo e fortemente interattivo, dall’altro hanno reso più pervasivo l’utilizzo delle nuove metodologie per la formazione a distanza di terza generazione , meglio nota con il nome di e-learning o “on line education”. L’elearning è fortemente basato sull’uso di tecnologie informatiche e telematiche che rendono possibile la creazione di nuove metodologie di apprendimento nelle quali l'interazione tra i partecipanti, attiva la condivisione delle esperienze per l’acquisizione di nuove conoscenze e la realizzazione degli obiettivi prefissati. La “on line education” si rivela adatta, in particolare, a progetti di formazione in cui, la condivisione del vissuto personale e la possibilità di sviluppare una situazione di apprendimento non rigidamente eterodiretto, ma orientato e collaborativo, diventano fattori determinanti di successo. Inoltre, diventa necessaria quando i tempi a disposizione sono brevi e l’utenza è numerosa. Oggetto dell’ e-learning è, dunque, accrescere l’impegno nella formazione ad ogni livello, in particolare promuovendo una “cultura digitale” per tutti e generalizzando modelli formativi fondati sulla gestione dei mutamenti. L’attenzione si concentra sempre più su “colui che impara” superando la compartimentazione tra i diversi settori e livelli di istruzione. Si è quindi giunti ad un modello educativo centrato sulla figura del discente che può ora interagire in maniera autonoma e personale con i contenuti didattici reperibili sulla rete. La particolare flessibilità del mezzo adoperato, che rende possibile, non solo l’annullamento delle distanze geografiche tra i partecipanti al corso, ma anche la possibilità di comunicare con tempi e in momenti diversi, fa della comunicazione in rete uno strumento particolarmente funzionale ai progetti di formazione e aggiornamento professionale in azienda. L’e-learning si è diffuso, dapprima, in ambito aziendale come strumento per l’aggiornamento degli “skills” professionali: a tale scopo sono nate le prime piattaforme di e-learning, attraverso le quali era possibile erogare corsi digitali per il personale aziendale, che poteva seguire comodamente le lezioni dall’ufficio o da casa, tramite il proprio PC. Tali piattaforme si sono poi evolute nel tempo per soddisfare esigenze di realtà differenti da quella aziendale a quella universitaria, in cui necessità e caratteristiche dei processi formativi sono molto differenti da quello che è un semplice aggiornamento professionale. 414 Capitolo dodicesimo Attualmente, sul mercato, esiste una vasta gamma di piattaforme, sia commerciali che “open-source”, ognuna con proprie peculiarità: tutte le piattaforme più recenti, spinte anche da ragioni di mercato, tentano, in realtà, a loro modo, di venire incontro alle esigenze del mondo universitario, fornendo innovativi tools di supporto alle attività didattiche e avanzati strumenti collaborativi; chat, lavagne virtuali, bacheche elettroniche, test di valutazione, forum, web-conference, sono tutti oramai servizi consolidati delle moderne piattaforme di e-learning. Oggi, circa il 70% della aziende europee e buona parte delle Università ha attivato al proprio interno esperienze di e-learning, che vanno da una semplice erogazione di corsi web-based alle più avanzate modalità di apprendimento a distanza. Il diffondersi dell’e-learning è la testimonianza di come, l’esplosione della rete Internet, abbia reso possibile la strutturazione di ambienti in cui è possibile apprendere senza vincoli di tempo, di spazio e soprattutto, senza vincoli di appartenenza sociale. L’offerta formativa in rete non è, come si è visto, monolitica. L’utilizzo della rete nella formazione spazia dai percorsi di autoaggiornamento, basati sull’uso dei materiali didattici strutturati per essere fruiti in autoistruzione, alla gestione di percorsi interattivi centrati sull’apprendimento collaborativo, fino all’uso di strategie di apprendimento reciproco, tipico delle comunità di pratica. Ne consegue che, le diverse modalità di utilizzo delle reti a supporto della formazione, determinano altrettante tipologie di apprendimento. Ciononostante l’e-learning, idealmente, dovrebbe presentare alcune specificità trasversali. Tra le caratteristiche principali dell'e-learning ricordiamo innanzitutto la dinamicità. e-learning è sinonimo di aggiornamento continuo, di specializzazione ed approfondimento. Non vi è spazio nei corsi online per informazioni obsolete né per conoscenze vaghe e superficiali, in questo caso l'efficacia ne risulterebbe compromessa. Pertanto, offrire e-learning comporta la presenza di un esperto in materia, di fonti sicure e rilevanti, di aggiornamento continuo delle informazioni offerte e di un immediato intervento tecnico in caso di emergenza e malfunzionamenti. Nella formazione tradizionale il momento di studio personale e il momento di fruizione della lezione in aula (spesso unicamente frontale), sono rigidamente separati e il materiale di riferimento viene letto e studiato, in un secondo momento, specialmente nella formazione universitaria. Un corso online è caratterizzato invece, oltre che dall’erogazione di contenuti, anche dalla costante presenza di figure di riferimento, cioè il docente e il tutor on line, con cui il discente può costantemente interagire in modalità sincrona o asincrona all’interno della classe virtuale. Tutti i partecipanti sono coinvolti in un percorso formativo che non esisterebbe senza una intensa collaborazione tra pari (collaborative learning); grazie alla cooperazione è possibile far emergere le conoscenze "tacite", cioè il “patrimonio culturale silenzioso” degli utenti che, attraverso il coinvolgimento in attività e discussioni di gruppo spontaneamente daranno vita al processo di costruzione della conoscenza. L’obiettivo principale dell' e-learning è, da questo punto di vista, mettere insieme risorse, sia all'interno sia all'esterno di una stessa organizzazione: la condivisione di esperienze e la collaborazione nel raggiungimento di determinati obiettivi favoriscono l'interscambio di Il mondo di Internet 415 informazioni rilevanti, e nell’ottica costruzionista, sono più rilevanti dei contenuti stessi del corso. Altra specificità dell'e-learning è la possibilità che offre alla personalizzazione del servizio erogato centrato sulle esigenze del singolo. Non si tratta più di portare gli utenti verso la formazione, ma la formazione verso gli utenti. All’utente è offerta la possibilità di scegliere le attività formative più rispondenti alle proprie esigenze personali, al proprio background, al proprio lavoro e alle prospettive di carriera. L'e-learning, si differenzia metodologicamente dalla formazione tradizionale, poiché eroga un’ offerta formativa multimodale, permettendo al discente di scegliere quella più consona al proprio stile individuale di apprendimento. L'atteggiamento di fondo è di libertà di scelta all'interno di un ambiente che offre svariate modalità di apprendimento degli stessi contenuti. E’ evidente il cambiamento della prospettiva dalla formazione tradizionale, in cui era unicamente il docente, il “magister”, a programmare l’unico percorso espletabile. Nell'e-learning non vi sono percorsi obbligati, ma "consigli" e "suggerimenti" che si limitano a dare un'idea di ciò che è possibile fare, senza limitare i movimenti dell'utente. Tuttavia, la libertà di scelta può essere un'arma a doppio taglio: non sempre avere molte possibilità porta a scegliere le migliori, o le più adatte al proprio stile di apprendimento e alle proprie reali esigenze formative. Non si deve dimenticare che ogni discente ha propri ritmi, tempi ed uno stile personale di apprendimento; per tale motivo esistono percorsi che prevedono attività formative sincrone o asincrone e, nei migliori dei casi, la formazione in rete è progettata proponendo un mix di entrambe. La sincronicità permette di essere più vicini allo stile dell'aula, in cui, docente e discenti, sono virtualmente compresenti; l'asincronicità permette invece, a chi ha problemi di tempo, di apprendere in totale autonomia, perdendo la possibilità di condivisione sincronica della propria esperienza con gli altri discenti ma non escludendo la comunicazione a livello asincrono. In breve, ad esigenze diverse l'e-learning, offre soluzioni diverse. La fruizione di un corso on line potrebbe mettere in difficoltà l'utente scarsamente “alfabetizzato tecnologicamente”, tuttavia a volte anche i discenti più esperti si sentono disorientati dinanzi alla varietà dei percorsi offerti. Affinchè sia efficace è necessario che la formazione sia progettata in modo funzionale e trasparente. Per concludere, l’e-learning, per essere un valido strumento di apprendimento, dovrebbe: - offrire un'ampia gamma di percorsi per l’aggiornamento professionale in ambito aziendale, ma anche essere uno strumento efficace per la formazione scolastica e universitaria; - offrire opportunità di formazione che siano motivanti e coinvolgenti per il discente e di stimolo all'interazione per la comunità di apprendimento; - offrire, attraverso i docenti e i tutor on line, un supporto consistente e informale, sia sotto forma di istruzioni e contenuti specifici, sia sotto forma di sostegno emotivo e affettivo in itinere; - stimolare gli utenti alla collaborazione per offrire aiuto reciproco nella soluzione dei problemi proposti; - tracciare i percorsi di apprendimento dei discenti e registrare i risultati ottenuti nei momenti di valutazione formativa e sommativa; 416 Capitolo dodicesimo - consentire la creazione di percorsi di apprendimento personalizzati rispondenti alle esigenze e agli obiettivi specifici del singolo utente. Capitolo tredicesimo Basi di dati e Sistemi Informativi 13.1. Sistemi Informativi e Sistemi Informatici L’informazione rappresenta oggi uno dei beni più preziosi di una organizzazione, in particolare per quel concerne il supporto alle varie attività “aziendali”, siano esse organizzative e di controllo, decisionali e di pianificazione oppure operative. L’informazione è un caso molto particolare di risorsa su cui operano tutte le organizzazioni: tutti i “processi” aziendali, - intesi come insiemi di attività correlate per la realizzazione di un risultato definito e misurabile che coinvolge più risorse e attraversa più strutture - per poter operare, hanno bisogno di informazioni. L’insieme delle informazioni gestite - generate, utilizzate, elaborate – dall’insieme dei processi aziendali costituisce il cosiddetto Sistema Informativo. Un sistema informativo è costituito da più elementi - procedure, mezzi, persone, etc…- , tra cui avvengono interazioni determinanti ai fini del conseguimento degli obiettivi del sistema, che possiamo indicare nella produzione e gestione delle informazioni. In questa ottica, il Sistema Informativo costituisce una parte del sistema organizzativo, visto come un insieme di risorse (persone, informazioni, denari, materiali, etc..) e regole necessari per il raggiungimento degli obiettivi aziendali. Quando è possibile individuarlo in forma esplicita, un sistema informativo risulta composto dalle seguenti categorie di elementi: - un patrimonio di dati: come è noto i dati rappresentano la materia prima (grezza) con cui si producono le informazioni; - un insieme di procedure per l’acquisizione e il trattamento di dati e per la produzione di informazioni; - un insieme di persone che sovrintendono a tali procedure (nel senso che le svolgono di persona, o le alimentano con i dati necessari, oppure gestiscono le apparecchiature che svolgono le procedure in modo automatico); - un insieme di mezzi e strumenti necessari al trattamento, trasferimento, archiviazione, eccetera, di dati e informazioni; - un insieme di principi generali, di valori e di idee di fondo che caratterizzano il sistema e ne determinano il comportamento. In figura 1 sono rappresentate alcune delle componenti base di un moderno sistema informativo. 418 Capitolo tredicesimo Figura 1 - Componenti di un Sistema Informativo Spesso, nella pratica, si tende a confondere il termine “Sistema Informativo” con quello “Sistema Informatico”. Anche se i relativi concetti sono strettamente correlati, essi presentano tuttavia delle differenze sostanziali. Un sistema informativo esiste da molto prima dell’invenzione dei sistemi informatici, e tuttora, molti aspetti di un sistema informativo non sono implementati dai sistemi informatici. Un sistema informatico ha a che fare con gli aspetti tecnologici dei sistemi di elaborazione dell’informazione, in particolare per quanto attiene l’hardware, il software e le reti di comunicazione. Facendo propria una terminologia oggi molto usata, un sistema informatico è, in altri termini, l’insieme delle tecnologie (note anche con l’acronimo ICT, Information and Communication Technology) a supporto della acquisizione, della elaborazione, della memorizzazione, del recupero, della condivisione e della trasmissione dell’informazione all’interno di una qualsiasi organizzazione. In figura 2 vengono riportate quelle che sono le relazioni funzionali tra azienda, organizzazione, sistema informativo e sistema informatico. In altre parole, un sistema informativo rappresenta il modo con cui, attraverso le tecnologie, si raccolgono, elaborano, memorizzano, usano e distribuiscono le informazioni, mentre il sistema informatico rappresenta l’insieme delle tecnologie (hardware, software, reti) di supporto alla gestione delle informazioni. Figura 2 - Sistema Informativo vs Sistema Informatico Per meglio comprendere le caratteristiche di un sistema informativo e del relativo sistema informatico a supporto, si consideri l'esempio di un Comune dotato di un sistema per il rilevamento giornaliero del livello di smog dovuto al traffico cittadino. Le fasi per la produzione e gestione dell’informazione risultano in tale caso quelle elencate di seguito: - un insieme di centraline di disposte in vari punti della città rilevano ed acquisiscono giornalmente i valori di smog consente (acquisizione ed aggiornamento dell’informazione); Basi di dati e Sistemi Informativi 419 - invio dei dati mediante un’apposita rete di comunicazione ad un elaboratore o server centrale che li immagazzina in un appositi archivi digitale, noti anche col nome di basi di dati (archiviazione delle informazioni); - elaborazione ed analisi dei dati mediante appositi software da parte degli impiegati, che dai vari uffici comunali accedono ad essi (elaborazione dell’informazioni); - presentazione dei risultati delle analisi giornaliere dei dati al sindaco, agli assessori ed ai membri della giunta (comunicazione dell’informazione). Nell’esempio mostrato mentre il sistema informativo è costituito dall’insieme complessivo di persone (dipendenti comunali, sindaco, assessori, etc..), mezzi e procedure necessari alla produzione e gestione dell’informazione relativa all’inquinamento cittadino, quello informatico riguarda, invece nello specifico, le macchine hardware (server, centraline di rilevamento, etc…), i programmi software (programmi per la gestione di archivi, programmi per l’elaborazioni dei dati, etc…) e l’apparato di rete (rete comunale) necessari a supportare in maniera efficace ed efficiente l’intero processo di analisi del livello di smog. Tale analisi impatta sulle attività decisionali del comune: in particolare, a seconda del livello di inquinamento riscontrato, il sindaco potrebbe prendere la decisione di vietare la circolazione per determinati giorni della prossima settimana oppure di attendere i risultati dell’analisi dei prossimi giorni prima di intraprendere alcuna decisione. E' chiaro che l’assenza di un sistema informatico di supporto non pregiudica l’esistenza del sistema informativo, ma ne comprometta l’efficienza: tutte le procedure di acquisizione ed elaborazione dei dati risulterebbero “manuali” e più lente, l’archiviazione dei dati avverrebbe su supporto cartaceo, la comunicazione dell’informazioni risulterebbe molto più complicata. Nel paragrafo successivo mostreremo brevemente una breve storia dei sistemi informativi aziendali, per meglio comprendere la funzionalità che oggi sono messe a disposizione da tale tipologia di sistemi. 13.1.1. Sistemi Informativi aziendali: l’evoluzione storica I primi studi sull’automazione delle componenti di un sistema informativo risalgono agli anni 70. A tale proposito, i primi sistemi (sistemi proprietari) furono sviluppati per lo più da software house per la gestione di singole esigenze aziendali: contabilità, controllo di gestione, gestione del personale, gestione della produzione, gestione dei magazzini, gestione della distribuzione, gestione acquisti, gestione vendite, gestione progetti, gestione della manutenzione di impianti, etc… Il passo successivo si ha poi negli anni 80, quando si sviluppano i cosiddetti MRP (Manufacturing Requirements Planning), ovvero sistemi che permettono la copertura non di una sola, ma di più aree funzionali, con integrazione stretta e con funzionalità di pianificazione di materiali e risorse. In particolare i MRP si preoccupano prevalentemente di supportare i processi primari di produzione come l’attività in linea di produzione, la gestione degli approvvigionamenti dei materiali, la gestione del magazzino, etc… Negli anni successivi fanno poi la comparsa i sistemi ERP (Enterprise Resource Planning), che rappresentano oggi la soluzione standard per i moderni sistemi informativi aziendali. Essi costituiscono un’estensione dei sistemi MRP capaci di affiancare all’area di gestione logistica (magazzini ed approvvigionamento) 420 Capitolo tredicesimo funzionalità per la gestione dei processi di vendita e di processi non direttamente legati alla produzione (contabilità, gestione personale, manutenzione impianti, gestione progetti, etc…). A tutt’oggi i sistemi ERP consentono la gestione integrata di una serie di processi aziendali, coprendo tutte le aree che possono essere automatizzate e /o monitorate all’interno di un’azienda. Tra le tante cose, tali sistemi permettono di programmare logiche di ordini automatici ai fornitori veramente sofisticate, tanto da tener conto dei tempi di consegna e di messa in produzione del prodotto, ottimizzando la rotazione dei materiali nei magazzini e minimizzando le giacenze in magazzino. Le tendenze attuali delle varie organizzazioni per ciò che concerne i sistemi informativi sono quelle di integrare sistemi informativi eterogenei e relativi a differenti realtà in sistemi di tipo centralizzato e di rendere accessibili le informazioni direttamente su web, utilizzando la rete Internet come principale strumento di diffusione delle informazioni stesse. 13.2. Analisi e progettazione di un Sistemi Informativo Così come un qualsiasi altro sistema, visto come un insieme di componenti che interagiscono fra loro al fine di raggiungere un determinato obiettivo, un sistema informativo, prima di potere essere realizzato, necessita di una corretta fase di analisi progettazione. La progettazione e sviluppo di un sistema informativo possono essere analizzati sotto due differenti punti di vista: quello dell’utente finale e quello di chi realizza le procedure automatizzate di supporto (sistema informatico). Da un lato, infatti, gli utenti finali devono specificare le caratteristiche dei processi aziendali che dovranno essere supportati dal sistema informativo; dall’altro lato, invece, analisti e programmatori hanno il compito di verificare la fattibilità tecnica delle specifiche richieste e concretizzarla in termini di applicativi efficienti. Nella pratica l’analisi e progettazione di un sistema informativo è una procedura articolata che si compone di più fasi, che costituiscono quello che comunemente viene definito ciclo di vita di un sistema informativo. Di seguito vengono descritte le 6 fasi. - Studio di fattibilità: serve a definire, dopo un’analisi preliminare degli obiettivi realizzativi, in maniera più precisa possibile i costi delle varie alternative possibili e stabilire le priorità della realizzazione delle varie componenti del sistema (scheduling delle attività). Di norma, in tale fase, viene effettuato un progetto di massima per valutare la convenienza della realizzazione in termini di costi e benefici. - Raccolta e analisi dei requisiti: consiste nell'individuazione e nello studio delle proprietà e delle funzionalità che il sistema informativo dovrà possedere. Questa fase richiede un'interazione con gli utenti finali del sistema e produce una descrizione completa ma generalmente informale dei dati coinvolti e delle procedure su di essi. Vengono inoltre stabiliti i requisiti software e hardware del sistema informativo. - Progettazione: si divide generalmente in progettazione dei dati, progettazione delle applicazioni e progettazione dell'architettura tecnica di sistema. Nella prima si individua la struttura e l'organizzazione che i dati dovranno avere, nella seconda si definiscono le caratteristiche degli applicativi atti ad automatizzare le procedure individuate. Queste due Basi di dati e Sistemi Informativi 421 attività sono complementari e possono procedere in parallelo o in cascata. Le descrizioni dei dati e delle applicazioni prodotte in questa fase sono formali e fanno riferimento a specifici modelli. La progettazione dell'architettura tecnica di sistema, infine, rappresenterà l'infrastruttura individuandone le caratteristiche in termini di sistemi (server), connettività e sicurezza. - Implementazione e test componenti: consiste nella realizzazione del sistema informativo secondo la struttura e le caratteristiche definite nella fase di progettazione. Viene costruita e popolata la base di dati e viene prodotto il codice dei programmi. In tale fase i singoli componenti software del sistema sono testati singolarmente - Integrazione, validazione e collaudo: serve a verificare, a valle dell’integrazione dei componenti del sistema, il corretto funzionamento e la qualità del sistema informativo. La sperimentazione deve prevedere, per quanto possibile, tutte le condizioni operative. - Funzionamento e manutenzione: in questa fase il sistema informativo diventa operativo ed esegue i compiti per i quali era stato originariamente progettato. Se non si verificano malfunzionamenti o revisioni delle funzionalità del sistema, questa attività richiede solo operazioni di gestione e manutenzione. Va precisato che il processo di progettazione, come mostrato in figura 3, non è quasi mai strettamente sequenziale, in quanto spesso durante l'esecuzione di una attività citata bisogna rivedere decisioni prese nell'attività precedente. Quello che si ottiene è un ciclo di operazioni. Inoltre alle attività citate si aggiunge quella di “prototipizzazione”, che consiste nell'uso di specifici strumenti software per la realizzazione rapida di una versione semplificata del sistema informativo, con la quale sperimentare le sue funzionalità. La verifica del prototipo può portare a una modifica dei requisiti e una eventuale revisione del progetto. Figura 3 - Ciclo di vita di un Sistema Informativo 422 Capitolo tredicesimo 13.2.1. Le basi di dati Le basi di dati o database rappresentano la tecnologia standard messa a disposizione dei moderni sistemi informativi per ciò che concerne l’archiviazione/memorizzazione, organizzazione e recupero delle informazioni all’interno di una data organizzazione. Le basi di dati costituiscono un aspetto fondamentale della vita di ogni giorno. Se si vanno ad analizzare tutte le più comuni operazioni che può effettuare una persona nel corso di una giornata, ci si rende conto di interagire con una o più basi di dati: l’accesso alla rubrica di un telefonino, l’acquisto di prodotti con carte di credito, la prenotazione alberghiera presso un agenzia di viaggio, il pagamento di una assicurazione, l’iscrizione ad un corso, l’immatricolazione all’Università, sono tutto eventi che in vario modo richiedono l’uso di collezioni di dati che devono essere memorizzate in modo persistente e organizzate a servizio di una determinata organizzazione. Dagli esempi sopra esposti è possibile fornire una prima definizione intuitiva di base di dati: una base di dati è una collezione di dati tra di loro logicamente correlati e dotati di una opportuna descrizione ed appartenenti una certa organizzazione. In altri termini, le basi dati sono collezioni di dati utilizzate per rappresentare le informazioni di interesse di una organizzazione. Le basi di dati sono inoltre collezioni di dati solitamente: (i) di grandi dimensioni, (ii) condivise da più applicazioni/utenti diversi che devono poter accedere alle stesse informazioni secondo adeguate modalità, (iii) persistenti, ovvero le informazioni sono una risorsa con un tempo di vita molto più lungo delle procedure pensate per la loro gestione. Si noti che nella definizione data, una base di dati deve poter mantenere non solo i dati, ma anche una descrizione degli stessi. Per questo motivo spesso si parla di collezione di dati “autodescrittiva”, cosa che possibile ottenere se un data base mette a disposizione non solo un modo di organizzare gli archivi di dati ma anche un catalogo, ovvero un dizionario di dati o, per dirla in modo informatico, un insieme di dati che descrivono i dati (metadati). 13.2.2. I Data Base Management Systems Affinché una base di dati costituisca effettivamente un valido supporto ai vari processi aziendali, risultano necessari la definizione e sviluppo di procedure automatizzate per l’accesso e la gestione dei dati all’interno della basi di dati stessa. Fino all’avvento dei linguaggi di programmazione di terza generazione (anni ‘60), gli archivi di una qualsiasi procedura automatizzata erano realizzati mediante un insieme di file sequenziali residenti su memoria di massa. Ogni procedura agiva su un proprio archivio (base di dati), e, di conseguenza se altre applicazioni dovevano accedere alla stesse informazioni era necessario creare ulteriori copie dei medesimi archivi. Questo approccio, che prevedeva un’organizzazione dei dati “orientata ai file” generava i seguenti problemi: 1. ridondanza dei dati: gli stessi dati sono ripetuti più volte in più archivi; Basi di dati e Sistemi Informativi 423 2. aggiornamento dei dati: ogni volta che un dato presente in un archivio veniva aggiornato, era necessario apportare le stesse modifiche agli archivi correlati; 3. dipendenza dei dati dai processi: ogni procedura automatizzata “vede” i dati in una forma che può essere diversa rispetto a quella di altre applicazioni; 4. scarso supporto alla concorrenza: l’accesso di più applicazioni o utenti agli stessi file era gestito mediante le primitive del file system del sistema operativo. Da quanto detto, si comprende la difficoltà di realizzare una gestione efficiente di grosse moli di informazioni condivise mediante un insieme di applicazioni o procedure “indipendenti” che di volta in volta devono essere sviluppate per gestire i propri dati sotto forma di insieme di file. Al contrario, occorre pensare ad un unico strato software che gestisce in maniera integrata tutti i dati, garantendo l’esecuzione di una serie di operazioni in modo efficiente ed efficace. Questo insieme di programmi è il cosiddetto Data Base Management System o DBMS (figura 4). Figura 4 - Data Base Management System o DBMS Un DBMS rappresenta quindi un insieme di programmi che permette di definire, creare, mantenere e controllare una base di dati. Con l’introduzione dei DBMS (anni 70) risulta più semplice ed efficiente gestire collezioni di dati che siano grosse, persistenti e condivise, garantendo nel contempo le seguenti funzionalità: - definizione dei dati: ovvero specificare i tipi dei dati, le loro strutture, i loro vincoli; - manipolazione dei dati: ovvero recuperare i dati specifici, nonché il loro aggiornamento; - accesso controllato al data base: possibilità di condividere le informazioni tra più utenti, garantendo nel contempo protezione dei dati (da guasti, malfunzionamenti e/o da accessi non autorizzati) e manutenzione evolutiva del sistema. Da un punto di vista architetturale un DBMS può essere quindi visto come uno strato software capace di gestire collezioni di dati residenti in memoria di massa (file), ampliando quelle che sono le funzionalità base di gestione dei file di un sistema operativo. L’insieme costituito dal software DBMS e dalla base di dati sottostante prende il nome di sistema di basi di dati. Un DBMS, pertanto, facilita gli utenti per l’utilizzo della propria banca dati: esso funge da interfaccia trai programmi applicativi e la base di dati supportando le operazioni di interrogazione e modifica su di essa, nonché la condivisione dei dati. In un DBMS, i dati sono di solito descritti a tre livelli differenti di astrazione, come si evince dalla figura 5. Pertanto, una base di dati consiste in un livello esterno, un livello logico ed un livello fisico. Tutte le informazioni relative a tali schemi sono 424 Capitolo tredicesimo memorizzate all’interno del catalogo di sistema. In particolare, procedendo dal livello più interno a quello più esterno, si ha la seguente schematizzazione: - schema fisico: è lo schema che specifica i dettagli della memorizzazione fisica dei dati e specifica come tali informazioni sono effettivamente memorizzate su dispositivi fisici di memorizzazione di massa. In particolare, a questo livello viene descritto come sono memorizzati i dati (array di record) in uno o più file, e, quali tipi di strutture sono necessarie per velocizzare l’accesso fisico agli array (indici di accesso). - schema logico: è lo schema che descrive come sono organizzati i dati dal punto di vista logico-concettuale. In particolare lo schema logico può essere visto come un insieme di record con le loro descrizioni, come illustrato in figura 6. - schema esterno: scopo di uno schema esterno è quello di personalizzare l’accesso ai dati relativamente ad un singolo utente o ad una classe di utenti. In particolare si noti che mentre un database ha sempre un singolo schema logico ed un singolo schema fisico, può possedere differenti schemi esterni che costituiscono, in effetti, una o più “viste” dei dati. Figura 5- Livelli di astrazione forniti da un DBMS su un database Il concetto di vista è particolarmente utile e con essa, per il momento, si intende concettualmente lo schema visibile da una classe di utenti o da un utente, schema che non corrisponde necessariamente a dati memorizzati nel DBMS. Figura 6 - Schema logico Basi di dati e Sistemi Informativi 425 Il vantaggio fondamentale di una organizzazione siffatta di un DBMS è la cosiddetta data - application independence. Se si indica con il termine programma applicativo un programma che interagisce con un database attraverso particolari richieste fatte al DBMS, è possibile con i tre livelli ottenere che le applicazioni siano isolate dal modo in cui i dati siano organizzati. In particolare, (i) attraverso il meccanismo delle viste, le applicazioni sono rese indipendenti dallo schema logico della base di dati: cambiando lo schema di un DB non necessario cambiare le applicazioni a patto che a queste vengono fornite viste di dati che corrispondono agli schemi logici a cui precedentemente le applicazioni si riferivano; (ii) attraverso lo schema logico, si è garantiti dalla differente implementazione fisica dei dati: un applicativo o un utente può tranquillamente operare sul database anche se nel tempo viene cambiato il modo in cui i dati sottostanti sono organizzati. Possiamo concludere che lo schema a livelli di un DBMS permette di realizzare concetti di information hiding (occultamento dell’informazione) tramite il quale i dati sono a vari livelli resi visibili ad una applicazione esterna. 13.2.3. L’evoluzione dei DBMS A livello dello schema logico risulta fondamentale individuare il modo in cui i dati sono organizzati e la struttura usata per renderli comprensibili ad un elaboratore (modello dei dati). Il modello dei dati più diffuso è senza dubbio oggi il modello relazionale, basato sul concetto di relazione - spesso rappresentata attraverso una tabella. Si esaminano tuttavia quelli che sono stati storicamente i modelli dei dati più utilizzati nel corso degli anni. - modello gerarchico (anni ’60): nel modello gerarchico i dati sono organizzati in gerarchie attraverso l’uso di strutture dati ad albero; - modello reticolare (inizio anni ’70): nel modello reticolare i dati sono organizzati in un reticolo attraverso l’uso di strutture dati a grafo; - modello relazionale (definito inizio anni ’70; implementato fine anni ’70: i dati sono organizzati in relazioni, ovvero un insieme di record aventi la stessa struttura logica; tali relazione, sotto certe condizioni, sono a tutti gli effetti delle tabelle; - modello object oriented: i dati sono organizzati sotto forma di oggetti software e di legami tra di essi; - modello object relational: il modello rimane relazionale (struttura tabellare), ma la struttura della tabella è generalizzata al fine di contenere oggetti oltre che valori elementari. Oggigiorno vi sono diverse centinaia di DBMS relazionali che girano sia su mainframe che su ambienti di Personal Computer, a partire da applicazioni semplici come Access di MS Office, Visual FoxPro, a sistemi di medie prestazione come mySQL e SQLServer, a sistemi complessi per medie/grosse aziende come DB2 di IBM ed Oracle della Oracle Corporation. La tendenza attuale è di implementare modelli object relational a servizio delle applicazioni più recenti basati su rete (internet). 426 Capitolo tredicesimo 13.2.4. Le funzionalità di un DBMS Nel 1969 il Data Base Task Group (DBTG) ha definito i compiti principali di un DBMS che consistono nel: - definire i dati, attraverso un opportuno linguaggio di definizione dei dati detto Data Definition Language (DDL); - manipolare, gestire i dati, attraverso un opportuno linguaggio di gestione detto Data Manipulation Language (DML). Un DBMS esegue dei programmi particolari detti transazioni. Una transazione, dal nostro punto di vista molto astratto, è una qualsiasi esecuzione di un programma utente in un ambiente DBMS e costituisce una unità atomica (non divisibile) di modifiche fatte allo stato di una base di dati. Da quanto detto, una qualsiasi transazione o termina in uno stato finale previsto dal programma in esecuzione (attraverso quello che in gergo viene detto commit) o porta il sistema ad uno stato precedente all’esecuzione della transazione attraverso un abort. Sistemi di basi di dati basati su transazioni sono anche detti On Line Transaction Processing, o OLTP. Si noti che talora le transazioni possono anche contenere solo interrogazioni: in tal caso, la atomicità è sempre assicurata in quanto una interrogazione non modifica lo stato di una base di dati. Le operazioni di modifica classiche sono le operazioni di (i) insert, permette di inserire un nuovo oggetto all’interno di un database; (ii) update, permette di modificare un oggetto preesistente all’interno di un database; (iii) delete, permette di cancellare un oggetto all’interno di un database. Le operazioni devono poter essere fatte garantendo il principio che una qualsiasi modifica della base di dati deve essere persistente. Qualunque cosa accada, sia a livello di crash fisico che di sistema, deve poter essere garantito che la transazione effettuata sia permanentemente memorizzata nel data base. Per quanto attiene le operazioni di interrogazione, un DBMS deve poter gestire operazioni di interrogazioni (query), attraverso le quali un utente cerca informazioni interne alla base di dati e si aspetta di avere risultati che siano coerenti e consistenti (ovvero efficaci) nel minor tempo possibile (in modo efficiente). Un DBMS deve poter gestire allora l’esecuzione efficiente di una query attraverso una ricerca di informazioni su una collezione di dati per lo più sempre molto grande. Concludiamo questa breve introduzione ai DBMS cercando di riassumere in maniera sintetica, relativa ai vantaggi (ma anche agli svantaggi) dell’approccio basato su DBMS. - Controllo della ridondanza dei dati e consistenza: un approccio basato su DBMS tende ad eliminare duplicazioni inutili di dati (ridondanza) su vari file, cercando di impedire che la stessa informazione sia presente più volte nel repository dei dati. In questo modo si riduce il rischio di inconsistenza: se una informazione è presente una sola volta dentro un database, una sua modifica rende immediatamente disponibili a tutti il nuovo valore. Una duplicazione di dati genera, invece, in generale inconsistenza, là dove una modifica dello stato del database deve poter effettuare modifiche su tutte le occorrenze di quella informazione. Basi di dati e Sistemi Informativi - - - - 427 Condivisione: i dati di una stessa organizzazione sono logicamente condivisi, anche se con viste diverse, da tutti gli utenti - e soprattutto da tutte le applicazioni – relative a quella organizzazione. Integrità dei dati: per integrità dei dati ci si riferisce alla consistenza e alla validità dei valori contenuti in base di dati. Il concetto di integrità dei dati fortemente legato (nel modello relazionale) al concetto di vincoli o constraints. Gestione efficiente delle operazioni: il DBMS fornisce le funzionalità per l’esecuzione efficiente di richieste di interrogazione e aggiornamento (transazioni) sulla base di dati. A tale proposito, il DBMS fornisce funzionalità di controllo della concorrenza, per garantire che transazioni concorrenti operino correttamente sui dati, senza interferenze reciproche. Affidabilità: il DBMS fornisce funzionalità di salvataggio e ripristino (in gergo, backup e recovery) dei dati contenuti nella base di dati, per garantire che non ci siano perdite di dati anche in caso di guasti e malfunzionamenti hw/sw durante l’esecuzione di transazioni. Sicurezza: Per sicurezza di un DB si intende la prevenzione dell’accesso ai dati ad utenti non autorizzati. Un approccio DBMS permette di definire politiche di accesso ai dati per utenti e per profili di utente. Costi: Il costo di un DBMS dipende dalla complessità del sistema da gestire e dalle funzionalità che devono essere garantite. Tipicamente si va da oltre centinaia di dollari a centinaia di migliaia o milioni di dollari per il solo software. A tali costi diretti vanno poi aggiunti quelli indiretti legati, da un lato, all’acquisizione della configurazione hardware adeguata a sfruttare a pieno tutto le funzionalità del software, e, dall’altro, alla formazione del personale predisposto all’uso degli strumenti suddetti. 13.2.5. Ricerche in un database: introduzione al linguaggio SQL Il modello relazionale è permette, come accennato, di rappresentare una base di dati attraverso un insieme di tabelle, che ne definiscono la struttura, caratterizzate da un insieme di nomi di attributi, ed un insieme di vincoli, che servono a garantire la coerenza delle informazioni all’interno della base di dati stessa. La strutturazione dei dati in tabelle permette di semplificare notevolmente, non solo l’organizzazione delle informazioni, ma anche l’accesso alle stesse. A tale proposito tutti i DBMS relazionali mettono a disposizione un apposito linguaggio, denominato SQL (Structured Query Language) per l’interazione con una base di dati. Le operazioni supportate dal linguaggio SQL sono di vario tipo: - operazioni per la creazione di oggetti (tabelle, indici, utenti, etc…) e definizione di vincoli (la parte dell’SQL che si occupa di tali operazioni prende il nome, come detto, di DDL); - operazioni per l’inserimento, cancellazione e modifica dei dati (la parte dell’SQL che si occupa di tali operazioni prende il nome, come detto, di DML); - operazioni per la ricerca di informazioni, conosciute anche col nome di query. Per istanze di basi di dati di grosse dimensioni si avverte la necessità di introdurre meccanismi per il recupero veloce dell’informazione di interesse. Spesso, infatti, 428 Capitolo tredicesimo per le interrogazioni che si effettuano su una base di dati, non occorre conoscere tutto il contenuto delle tabelle, ma solo poche tuple. Per tale motivo è stato introdotto il linguaggio SQL con l’obiettivo di semplificare e velocizzare l’accesso alle informazioni di interesse. Un’interrogazione alla base di dati o query, espressa nel linguaggio SQL, permette di estrarre dalla base di dati solo l’insieme di record di interesse per le applicazioni o gli utenti che interagiscono con il database. Il risultato di un’interrogazione SQL è quindi una tabella che comprende tutti dati che soddisfano i requisiti definiti nell’interrogazione. In altri termini, SQL è un linguaggio dichiarativo. Questo significa che un comando SQL specifica il risultato che si intende ottenere e non la sequenza di istruzioni necessaria per raggiungerlo. A puro titolo di esempio e per completezza, una query SQL ha la forma: select [Lista Attributi] from [Lista Tabelle] where [Condizione Logica] - dopo la clausola select vanno specificati i campi di interesse di cui si vogliono visualizzare i valori; - dopo la clausola from vanno specificate le tabelle dove si trovano i valori di interesse; - dopo la clausola where va specificata la condizione logica che tutte le tuple appartenenti al risultato dovranno soddisfare. Se ad esempio si considera la tabella: GIOCATORE(CodTessera, Nome, Cognome, Squadra) ovvero una tabella avente per nomi di attributi CodTessera Nome, Cognome, Squadra, se vogliamo determinare il nome ed il cognome di tutti i giocatori che giocano nel “Napoli”, la query SQL che permette di estrarre le informazioni di interesse è: select Nome, Cognome from GIOCATORE where Squadra=’Napoli’ SQL permette di definire una serie di operazioni anche estremamente complesse, che sono particolarmente adatte al modello relazionale dei dati e che per sinteticità omettiamo, rimandando a testi specializzati per una loro trattazione. Appendice A Vademecum dei principali termini usati in informatica In questa sezione vengono elencati i principali termini utilizzati nel mondo dell’informatica di consumo, al fine di fornire una guida veloce e sintetica delle componenti fondamentali dei calcolatori elettronici, con particolare riferimento alla fascia dei Personal Computer. In particolare vengono mostrate, senza alcuna pretesa di completezza: a) una classificazione dei moderni calcolatori b) la struttura interna del personal computer c) una panoramica delle periferiche di ingresso uscita di uso più comune 14.1. Fasce di Computer Personal Computer (PC): sono i normali computer da casa e da ufficio, si usano per lo più come elaboratori di testo (word processor), per reperire o gestire informazioni (Internet, basi di dati), come strumenti da ufficio (amministrazione, programmi gestionali), per la comunicazione (email), per la grafica o i giochi. Costituiscono oggi la tipologia di computer più diffusi. Workstation: sono computer più potenti spesso presenti in laboratori di ricerca; possono essere usati per il calcolo e la programmazione oppure per funzionalità di grafica avanzata. Mainframe: sono computer usati in grandi aziende, e, in generale, ovunque ci sia bisogno di gestire in maniera centralizzata complessi sistemi di computer distribuiti. 430 Appendice A Supercomputer: sono computer con elevate prestazioni in termini di potenza elaborativa; si usano in ambienti con necessità di calcolo avanzato. Se poi si concentra l’attenzione all'ambito dei soli computer per uso personale, esiste una tradizionale ulteriore classificazione in base alla forma e alle dimensioni della macchina: Home Computer: sono i normali PC da casa e ufficio, con cassa orizzontale o verticale. Notebook: sono i cosiddetti computer portatili, utili per chi deve spostarsi spesso per lavoro avendo sempre il proprio computer a portata di mano. Sono dotati di una batteria che consente un'autonomia di alcune ore per lavorare anche durante gli spostamenti. Palmtop o Pocket PC: sono dei computer di capacità ridotta nati dall'evoluzione delle agende elettroniche tascabili. Oltre alle normali funzioni delle agende (appuntamenti, rubrica telefonica, calcolatrice), i palmari sono in grado di svolgere alcune funzioni base dei personal computer, come la navigazione in Internet, la posta elettronica, l'elaborazione di testi, ecc. 14.2. Componenti Interni di un Computer Aprendo la cassa di un computer è possibile scorgere quelli che sono i componenti interni (collegati da una marea di cavi e fili) di un calcolatore. Essi sono opportunamente alimentati da un’ apposita scatola dell'alimentazione provvista di trasformatore elettrico e relativa ventola di raffreddamento (poste di solite nella parte posteriore del calcolatore). Di seguito è riportata una breve descrizione delle principali componenti. Scheda Madre (Motherboard): La scheda madre rappresenta, insieme al processore, il componente principale di un calcolatore. Essa può essere vista come una sorta di “connettore” fra tutti i componenti interni di un computer, inoltre, grazie a circuiti particolari (chipset, bios), una scheda madre è in grado anche di eseguire il controllo delle varie parti. In alcuni computer le schede madri svolgono anche le funzioni audio, video e rete (in gergo, audio, video e Vademecum termini 431 rete sono “integrati” nella scheda madre). In altri invece tali funzionalità sono delegate ad apposite schede di espansione. Sulla scheda madre si trovano inoltre le prese per la connessione dell'hard disk e ai lettori dei dischi mobili (floppy e CD). Microprocessore: spesso identificato con la CPU, è il nucleo del computer; si tratta, come già visto, del componente che esegue le istruzioni dei vari programmi e sovrintende al funzionamento dell’intera macchina. È il microprocessore che esegue tutte le elaborazioni, gestisce il trasferimento di dati attraverso la memoria e i dischi e attiva/disattiva i componenti della macchina. Il ritmo di lavoro del microprocessore è scandito da un apposito segnale elettrico (detto “clock”) generato internamente al computer e costituito da rapidissimi impulsi che si ripetono centinaia di milioni di volte per secondo. La velocità del clock (e quindi del microprocessore) si misura oramai in GigaHertz (GHz, miliardi di impulsi per secondo). Memoria RAM (Random Access Memory): è la memoria, come già visto, dove vengono conservati i dati e le istruzioni dei programmi in esecuzione; si tratta di una memoria veloce (ovvero i tempi di accesso per l’operazioni di lettura/scrittura sono bassissimi), ma “volatile”, ovvero, che si cancella completamente quando si spegne il computer. La capacità della RAM si misura in MegaByte (Mbyte) o GigaByte (Gbyte), ovvero milioni o miliardi di byte. Se la RAM non è sufficiente capiente per contenere tutti i dati necessari, il computer esegue delle onerose operazioni di trasferimento temporaneo (“swap”) di parte dei dati dalla memoria centrale a quella di massa, al fine di liberare dello spazio per l’elaborazione dei nuovi dati. È chiaro però che questa operazione tende a rallentare tutte le altre operazioni. Per evitare tale fenomeno è sufficiente aggiungere più RAM. La capacità della RAM, quindi, insieme alla velocità del microprocessore, sono i due parametri che più pesantemente influiscono sulle prestazioni della macchina. Hard Disk: è la memoria permanente del computer, in cui si conservano tutti i dati e i programmi. Viene usato dunque come memoria di immagazzinamento dati (in gergo “memoria di massa”). La sua capacità si misura in GigaByte (Gbyte), ovvero in miliardi di byte. Attualmente gli hard disk vengono realizzati secondo due diverse tecnologie: EIDE (Enhanced Integerated Drive Electronics) e SCSI (Small Computer Systems Interface); 432 Appendice A i primi sono più economici, i secondi sono più veloci. La velocità del disco in genere non influisce molto sulle prestazioni della macchina, a meno che questa non venga impiegata per funzioni che richiedono frequenti operazioni di lettura/scrittura di dati (questo accade ad esempio per macchine che gestiscono alcuni servizi di rete centralizzati, come la posta elettronica o per i cosiddetti “database server”). Drive: sono gli sportelli in cui si inseriscono il floppy disk, o il CD, o qualsiasi altro tipo di disco. Contengono una testina di lettura/scrittura tramite cui avviene il trasferimento dei dati fra disco e macchina. Il drive per il floppy è ormai un componente standard che non presenta varianti significative, invece i drive per CD esistono in vari modelli che si differenziano soprattutto per la velocità di lettura. Schede di Espansione: si tratta di schede che espandono le funzionalità della scheda madre per pilotare dispositivi interni od esterni. La più importante è la scheda video su cui si connette il monitor. Dalla scheda video dipendono il numero di colori del monitor, la risoluzione massima, la velocità grafica (di giochi, filmati, ecc.) e tutto ciò che riguarda in generale le prestazioni grafiche. Molto diffusa è anche la scheda audio, attraverso cui il computer è in grado di produrre o registrare suoni. Per le connessioni dirette alla rete (senza modem) occorre invece dotarsi di una scheda di rete. Infine la scheda SCSI (letto “scasi”) consente di pilotare dispositivi che richiedono una particolare velocità nel trasferimento dei dati. Esistono poi numerosi altri tipi di schede, create per funzioni particolari, quali ad es. la scheda di acquisizione video per collegarsi alla televisione, ad un videoregistratore o ad una videocamera, e molte altre ancora di uso specialistico. BIOS (Basic Input/Output System): è un componente che fa parte integrante della scheda madre. Il suo scopo è quello di gestire la fase di accensione del computer. Il BIOS conserva in una ROM la sequenza di istruzioni di avvio che viene eseguita automaticamente ad ogni accensione del computer. L'operazione di avvio è detta “boot”, e passa per 3 fasi successive: test di funzionamento del sistema; attivazione dell'hardware installato; verifica della presenza del sistema operativo e suo caricamento. In una piccola memoria RAM (alimentata da una batteria interna) sono conservate le informazioni relative all'hardware installato. Vademecum termini 433 ROM (Read Only Memory): Contrariamente alla RAM, la memoria ROM non esiste in forma di componente separato e individuale, esistono invece numerose piccole ROM incorporate all'interno dei vari circuiti integrati (sulla scheda madre, sulle schede d'espansione, ecc.) come nel BIOS. 14.3. I dispositivi di input e output Tutti i dispositivi che mettono in comunicazione il computer con il mondo esterno sono detti genericamente periferiche oppure dispositivi periferici di Input/Output (o di I/O, o di Ingresso/Uscita). Alcuni dispositivi sono solo di ingresso perché inviano dati al computer ma non ne ricevono mai (ad esempio il mouse e la tastiera), altri sono solo di uscita perché ricevono dati dal computer senza inviarne mai (come il monitor, la stampante e le casse audio), altri sono contemporaneamente di ingresso e di uscita (come i dischi). I dispositivi più comuni sono: tastiera, mouse, monitor, lettore CD-Rom, floppy, stampante, casse, modem, masterizzatore. Alcuni dei dispositivi di I/O, possono essere collegati direttamente al calcolatore attraverso apposite prese dette “porte di ingresso/uscita (I/O)”. Le porte di I/O sono una serie di prese, localizzate sul lato posteriore del computer, che vengono utilizzate per collegare alla macchina tutti dispositivi esterni (monitor, tastiera, mouse, ecc.). Tipicamente sono poste direttamente sulla scheda madre le seguenti porte: porte PS/2 per il collegamento del mouse e della tastiera; porta seriale per il modem, o in generale per i vari dispositivi che non richiedono flussi di dati molto veloci porta parallela per la stampante, ma in generale è adatta per qualunque dispositivo che richieda un flusso di dati più veloce rispetto alla capacità della porta seriale; porta USB (Universal Serial Bus), di recente introduzione, è adatta per connettere al computer qualunque tipo di dispositivo compatibile (mouse, tastiera, pen-drive, etc…). Di contro, altri dispositivi per poter essere collegati alla macchina, richiedono la presenza di una “scheda d’espansione” inserita all’interno del computer. Ad esempio il monitor richiede solitamente la presenza di una scheda video, le casse richiedono una scheda audio, ecc. Le schede di espansione che vengono montate sulla scheda madre rendono quindi disponibili molte altre porte, fra cui le principali sono: - porta video (talvolta integrata direttamente sulla scheda madre, soprattutto nei modelli di marca) per connettere il monitor al computer; 434 Appendice A - porta di rete per collegare la macchina direttamente ad una rete di computer, senza usare il modem. Ne esistono di vari tipi, ma ormai la presa RJ45 ha di fatto rimpiazzato tutte le altre; - porta SCSI per dispositivi che richiedono un flusso di dati molto veloce (scanner, masterizzatore esterno, ecc.). Di seguito si riporta una rapida carrellata dei principali dispositivi di input output di un calcolatore. Tastiera: collegata al calcolatore attraverso una porta PS/2 appositamente dedicata o USB, permette l’immissione dei vari caratteri attraverso digitazione . I simboli riconosciuti dal computer ma non presenti sulla tastiera, possono essere comunque inseriti tenendo premuto Alt e digitando il corrispondente “codice ASCII” col tastierino numerico Mouse: Il mouse fu introdotto assieme ai sistemi operativi di tipo grafico (Macintosh, Windows, ecc.) per semplificare l'invio dei comandi verso il computer, comandi che in precedenza venivano impartiti unicamente attraverso la tastiera. Lo spostamento del mouse controlla il movimento di un “puntatore” sullo schermo, mentre i tasti inviano il comando. Il mouse, come la tastiera, si collega al PC o attraverso una porta PS/2 appositamente dedicata o mediante USB. Monitor: il monitor è la principale interfaccia di comunicazione fra l’utente e l’attività del computer. I monitor più diffusi sono quelli a tubo catodico, che funzionano sullo stesso principio dei televisori. Esistono poi i monitor piatti a cristalli liquidi (LCD), a maggiore costo, che producono un'immagine molto più nitida e non emettono nessuna radiazione. Alcuni modelli comprendono al loro interno anche le casse audio ed il microfono. Esistono infine modelli di monitor sensibili al tocco di una speciale penna luminosa, in modo da poter essere usati anche come una tavoletta grafica, altri sono invece sensibili al semplice tocco delle dita (si trovano soprattutto in luoghi pubblici, dove il mouse o la penna luminosa avrebbero vita breve). Il monitor si collega al PC attraverso la porta che si trova sulla scheda video. Stampanti: attualmente si dividono in due principali categorie: - laser, usano una tecnologia simile a quella delle fotocopiatrici, sono adatte per grossi volumi di lavoro perché riescono a stampare molto velocemente e silenziosamente, offrendo inoltre Vademecum termini 435 la migliore qualità di stampa; a getto d’inchiostro, producono stampe di qualità leggermente inferiore rispetto alle stampanti laser, sono generalmente più lente, ma anche più economiche e di dimensioni più contenute. La stampa avviene spruzzando sulla carta un sottilissimo getto d’inchiostro liquido. Esistono poi stampanti per usi professionali o tipografici, come le stampanti a sublimazione e thermal-wax per riproduzioni di altissima qualità. Una particolare categoria di stampanti sono i plotter che usano dei pennini ad inchiostro per disegnare su fogli di grande formato. Servono per il disegno tecnico e sono usati perciò negli studi professionali e nei centri di progettazione tecnica. Le stampanti si collegano al PC attraverso la porta parallela o USB. Casse: La scelta delle casse deve avvenire in funzione della scheda audio: se la scheda audio comprende solo le funzioni di base si useranno casse standard, se invece è capace di riproduzioni audio di alta qualità, effetti audio particolari come il Dolby surround, ecc., si potranno usare casse di qualità superiore e impianti audio sofisticati. Masterizzatori: Sono i dispositivi usati per la scrittura su CD. Funzionano anche come normali lettori di CD. Come gli hard disk (e altri dispositivi). Esistono masterizzatori EIDE (più economici) e masterizzatori SCSI (più veloci). Scanner: si tratta di dispositivi che consentono di acquisire testo e immagini stampate su carta per trasferirle all’interno del computer. I modelli più versatili consentono anche l’acquisizione direttamente da diapositiva o da negativo fotografico. Gli scanner migliori usano la tecnologia SCSI per collegarsi al computer. Modem: si usa per la trasmissione e ricezione di dati attraverso la linea telefonica ed in particolare per la connessione ad Internet. I modem possono essere esterni al computer (collegati con un cavo) oppure interni (in forma di scheda di espansione), ma in questo ultimo caso presentano spesso problemi di incompatibilità col resto dell'hardware. La velocità con cui i modem sono in grado di scambiare i dati si misura in Kbit/secondo (Kbps) ovvero il numero di bit che il modem riesce a trasferire in un secondo. Esistono tre tipi principali di modem, a seconda del tipo di linea telefonica disponibile: - 436 Appendice A - Standard, per la normale linea telefonica a 56Kbps. Quando il modem è collegato la linea è occupata e non è possibile usare il telefono (di fatto, il collegamento alla rete tramite modem costituisce a tutti gli effetti una normale telefonata); - ISDN, raggiungono i 128 Kbps, ma necessitano della linea ISDN, la quale consente anche l'uso del telefono mentre il modem è collegato sacrificando però metà della velocità (64 Kbps); altrimenti si può scegliere di dedicare l'intera linea al modem; - ADSL, raggiungono i 640 Kbps, ma necessitano della linea ADSL (una tecnica di trasmissione su linea telefonica particolare). L'ADSL costituisce un collegamento permanente 24 ore su 24 con la rete e non interferisce in nessun modo col telefono. Videocamere: è utile per catturare immagini fisse o in movimento ed è collegata al calcolatore tramite la scheda video. Si va da videocamere professionali per riprese di alta qualità, a piccole videocamere dette webcam usate per trasmettere riprese video attraverso la rete. Appendice B Esercizi di programmazione 15.1. Premessa Il capitolo elenca un insieme di esercizi da progettare utilizzando uno dei linguaggi di programmazione presentati nel libro. Nel testo degli esercizi sono state indicate solo le principali funzionalità di elaborazione, lasciando al programmatore la libertà di decidere sulle modalità di inserimento dei dati di prova e di presentazione dei risultati prodotti. Tutti gli esercizi proposti iniziano con frasi del tipo “assegnati” o “data una struttura” che sottintendono che per i dati indicati devono essere previste funzionalità specifiche di assegnazione dei valori iniziali; d’altro canto, non si fa sempre riferimento al fatto che i dati prodotti devono essere presentati in uscita in quanto si presuppone che per poter controllare la correttezza dell’algoritmo tale operazione sia fondamentale. Tutti gli esercizi devono quindi essere progettati prevedendo una struttura del programma finale del tipo: inserimento dati di prova algoritmo stampa dei risultati Il lettore è libero di prevedere che l’inserimento del valore dei dati avvenga da tastiera o da un file in precedenza generato. Tale seconda ipotesi è da preferire nei casi in cui il programma è in fase di testing o debugging e i dati da inserire sono tanti (si pensi ad una matrice). L’aver inserito i dati in un file ha significativi vantaggi, che possiamo elencare nel seguito: i dati non devono essere reinseriti ad ogni prova del programma ma solo in fase di generazione del file che avviene al di fuori di esso; i dati possono essere controllati prima del loro uso evitando una delle più frequenti cause di errore del programma: una loro errata interpretazione nella fase di inserimento; gli stessi dati possono essere usati per provare programmi diversi; la generazione del file, infine può essere fatta con un qualsiasi editor di testo nel caso i dati vengano prelevati da un text file. Nel progettare un qualsiasi esercizio, lo studente deve fare riferimento ai principi della programmazione strutturata che hanno permeato i capitoli precedenti. In 438 Appendice B particolare, l’algoritmo deve essere disegnato pensando a condizioni di tipo generale nelle quali gli esempi di INPUT/OUTPUT presentati sono solo casi particolari. Inoltre si devono cercare soluzioni parametriche che facilitino la manutenzione dei programmi utilizzando quanto più possibile, definizioni di costanti che diano ad esse un nome e ponendo tali dichiarazioni tra le prime righe del testo sorgente. I programmi vanno poi progettati privilegiando leggibilità e correttezza: la leggibilità è di fondamentale importanza per il riuso delle soluzioni in lavori di gruppo. A tal fine si ribadiscono i seguenti principi generali: gli identificatori devono avere nomi che facciano capire l’uso che se ne vuol fare; la nidificazione delle strutture di controllo deve far ricorso alla indentazione per rendere più chiara la struttura dell’intero programma; i commenti devono essere non solo presenti ma soprattutto chiari; inoltre devono essere articolati in motivazioni ed asserzioni; la modularità deve ispirare l’intero progetto per cui funzionalità importanti devono essere organizzate in procedure o funzioni. Anche se gli esercizi sono di piccole dimensioni, si deve approntare una documentazione dell’algoritmo dalla quale si comprendano almeno quali sono le variabili di input, quali sono invece quelle di output e quali sono quelle necessarie al calcolo, ed infine come il procedimento si sviluppa descrivendolo ad un adeguato livello di astrazione. E’ altresì importante notare che la classificazione degli esercizi è stata fatta per indicare quale è la struttura dati su cui principalmente esercitarsi. 15.2. 1) 2) 3) Esercizi sulle variabili non strutturate Sia assegnato un numero intero in base dieci, effettuare la conversione in una qualsiasi altra base. INPUT Numero = 14 Base = 2 OUTPUT 1110 Dati tre numeri, verificare quale di essi ha il valore più grande. INPUT 3 24 8 OUTPUT 24 Dato un numero intero verificare se è primo INPUT 24 OUTPUT No Esercizi 4) 5) 6) 439 Dati tre numeri verificare se uno di essi è il prodotto degli altri due. INPUT 3 24 8 OUTPUT si Date tre lettere verificare se sono tutte diverse. INPUT abc OUTPUT No Dati tre numeri verificare se possono essere le misure dei lati di un triangolo e, in caso affermativo, dire anche di quale tipo (scaleno, isoscele, equilatero, rettangolo) INPUT 345 OUTPUT scaleno rettangolo 15.3. Esercizi sui vettori 7) Dato un vettore determinare se i valori dei suoi elementi sono tutti uguali INPUT [3 24 8] OUTPUT no 8) Dato un vettore determinare se i valori dei suoi elementi sono ordinati INPUT [24 8 4 1] OUTPUT si 9) Dato un vettore V ordinato ed un elemento a, effettuare l’inserimento di a nel vettore rispettando l’ordinamento INPUT [3 24 88] 55 OUTPUT [3 24 55 88] 10) Dati due vettori V1 e V2 ordinati nello stesso modo, generare per fusione dei primi un terzo vettore V3 due che abbia lo stesso ordinamento INPUT V1 = [1 3 5 7] V2 = [2 3 4 6] 440 11) 12) 13) 14) 15) 16) Appendice B OUTPUT V3 = [1 2 3 3 4 5 6 7] Dati i valori del riempimento N e degli elementi di un vettore V di numeri reali determinare: la somma degli elementi del vettore. la media degli elementi del vettore. il minimo degli elementi del vettore. il massimo degli elementi del vettore. Dato un vettore di numeri reali ordinato e due suoi elementi a e b, tali che a<=b, memorizzare in un vettore di uscita tutti gli elementi del vettore di ingresso compresi tra a e b. INPUT [1,3,6,8,70] a=2 b=8 OUTPUT [3,6] Dato un vettore di numeri effettuare lo shift (spostamento) a destra con l’inserimento di uno zero in testa degli elementi del vettore di ingresso (con perdita dell’elemento di coda) INPUT [1,2,3,4] OUTPUT [0,1,2,3] Dato un vettore di numeri effettuare lo shift (spostamento) a sinistra con l’inserimento di uno zero in coda degli elementi del vettore di ingresso (con perdita dell’elemento di testa) INPUT [1,2,3,4] OUTPUT [2,3,4,0] Dato un vettore di numeri effettuare lo shift (spostamento) circolare a destra degli elementi del vettore di ingresso (in questo caso l’elemento di coda va in testa). INPUT [1,2,3,4] OUTPUT [4,1,2,3] Dato un vettore di numeri interi memorizzare in primo vettore di uscita solo i numeri pari e in un secondo vettore di uscita solo i numeri dispari. INPUT v_in=[1 2 33 11 8] Esercizi 17) 18) 19) 20) 441 OUTPUT pari=[2 8] dispari=[1 33 11] Siano assegnati i due vettori (V1 e V2) di numeri interi. Verificare se all’interno di uno dei due vettori (VY) esiste una successione di valori in posizioni consecutive tali che ogni suo elemento sia compreso tra due elementi successivi di una successione di elementi dell’altro vettore (VX): VX[i]<VY[j]<VX[i+1] e VX[i+1]<VY[j+1]<VX[i+2] Si noti che, perché la relazione sussista, la successione di elementi VY deve avere almeno due elementi e la successione di elementi VX deve contenere il doppio degli elementi di VY. In caso la verifica sia positiva, generare e stampare il vettore che si ottiene fondendo le due successioni nel rispetto della relazione trovata: …., VX[i], VY[j], VX[i+1], VY[j+1], VX[i+2], …. Siano assegnati due vettori di numeri interi con ugual riempimento. Verificare che per ogni coppia V1i e V2j (con V1i appartenente al primo vettore e V2j al secondo) esista una delle due condizioni: una sequenza delle ultime cifre (almeno due) di V1i coincida con una sequenza di cifre iniziali di V2j come si può riscontrare nelle seguenti coppie (2321, 21456), (6357,6357), (12908,2908); una sequenza delle ultime cifre (almeno due) di V2i coincida con una sequenza di cifre iniziali di V1j. Per ciascuna delle coppie che soddisfa una delle due condizioni riportare la sequenza di cifre in comune, il tipo di condizione e la posizione i e j all’interno dei vettori V1 e V2. Siano assegnati tre vettori (V1, V2 e V3) di numeri interi. Verificare la presenza nel vettore V3 di tutti e soli gli elementi diversi sia del primo che del secondo con la condizione che le posizioni dispari di V3 siano occupate dagli elementi diversi di V1 o V2 e quelle pari dagli elementi diversi del restante vettore. Nel caso sussistano tutte le condizioni richieste stampare il messaggio “1 pari e 2 dispari” per indicare la presenza degli elementi diversi del primo vettore nelle posizioni pari del terzo e la presenza degli elementi diversi del secondo vettore nelle posizioni dispari; il messaggio “1 dispari e 2 pari” in caso contrario, o infine “non sussiste la condizione” nel caso non si verifichi quanto richiesto. INPUT V1: 5 4 8 5 9 3 4 9 V2: 0 8 3 4 0 7 7 9 8 0 3 7 8 4 4 V3: 0 3 3 9 7 8 4 4 9 5 8 OUTPUT 1 pari e 2 dispari Dato un vettore di interi estrarre e stampare, ad ogni passo di un ciclo, l’elenco dei valori diversi contenuti in esso. Al primo passo il vettore è assegnato in ingresso; ad ogni passo successivo il vettore da considerare è quello ottenuto nel passo precedente mediante l’eliminazione dei valori diversi. L’algoritmo termina quando il vettore da elaborare è vuoto. 442 Appendice B INPUT 1 12 3 12 3 655 6 4 655 6 12 OUTPUT passo Vettore da elaborare 1 1,12,3,12,3,655,6,4,655,6,12 2 12,3,655,6,12 3 12 15.4. Vettore estratto 1,12,3,4,655,6 12,3,655,6 Esercizi sulle matrici 21) Data una matrice quadrata determinarne il valore più grande e la sua posizione di riga e di colonna. INPUT 1 6 1 7 6 4 5 2 7 10 2 1 OUTPUT 3 3 10 22) Data una matrice A di numeri interi, memorizzare in un vettore di uscita il massimo di ogni riga INPUT 1 6 6 4 1 7 5 2 2 7 10 1 OUTPUT 7 6 10 23) Data una matrice A di numeri interi: 1. segnalare errore nel caso di matrice non quadrata 2. memorizzare in un vettore di uscita gli elementi positivi (maggiori di 0) della diagonale, solo nel caso in cui la matrice sia quadrata 3. calcolare la media del vettore determinato al punto precedente. INPUT 1 4 9 15 2 1 8 2 1 2 0 4 8 6 1 5 Esercizi 443 OUTPUT v _ out 1 5 media 3 24) Data una matrice A di numeri interi positivi, memorizzare in un vettore di uscita la media di ogni colonna INPUT 1 6 15 1 3 4 5 2 5 1 2 3 OUTPUT 3 4 7 2 25) Data una matrice A di numeri interi positivi e due indici r1 e r2, memorizzare in una matrice di uscita la matrice di ingresso con le righe di indici r1 e r2 scambiate. INPUT 1 6 15 1 3 4 5 2 5 2 1 3 5 2 r1 r2 1 2 OUTPUT 3 4 1 6 15 1 5 2 1 3 26) Gestire una matrice quadrata di ordine N per: determinare e memorizzare i massimi di ciascuna riga; determinare e memorizzare i minimi di ciascuna colonna; calcolare la differenza tra i massimi e minimi relativi alle stesse posizioni; stampare una tabella riportante massimi, minimi e loro differenze. 27) Data una matrice quadrata di ordine N, determinare se al suo interno ci sono colonne i cui elementi coincidono in valore e posizione con quelli della diagonale principale. 28) Estrarre da una matrice di NxM elementi di tipo intero le posizioni di riga e di colonna dei valori negativi. 444 Appendice B 29) Contare tra gli elementi della diagonale principale di una matrice rettangolare tutti i valori uguali ad un valore preassegnato. 30) Data una matrice di numeri reali: calcolare la trasposta della matrice. compattare la matrice nel caso vi siano righe uguali (ovvero se la matrice ha due righe uguali ne viene eliminata una). INPUT 123 456 456 OUTPUT 123 456 31) Data una matrice di numeri memorizzare: in un primo vettore di uscita, una dietro l’altra, le righe pari. in un secondo vettore di uscita, una dietro l’altra, le righe dispari. INPUT 123 456 789 OUTPUT v1=[4 5 6] v2=[1 2 3 7 8 9] 32) Sia assegnato un vettore V di RIEMP numeri interi. Generare una matrice con tre colonne e tante righe quante sono le terne di elementi in posizioni consecutive di V che godono della proprietà che l'elemento centrale risulta essere contemporaneamente maggiore degli elementi nelle posizioni precedente e seguente (V[i-1]<V[i]<V[i+1]), e infine verificare se nelle sue colonne esiste una delle seguenti relazioni: a) gli elementi sono tutti uguali; b) gli elementi rispecchiano l'ordinamento crescente; c) gli elementi rispecchiano l'ordinamento decrescente. Nel caso in cui una delle relazioni esista, stampare un messaggio riportante il tipo di relazione individuata e la posizione di colonna dove essa è stata individuata. INPUT 10 20 15 30 15 21 30 40 15 17 18 19 50 15 16 OUTPUT 20 15 30 30 15 21 40 15 17 50 15 16 La colonna 1 gode della proprietà b) Esercizi 445 La colonna 2 gode della proprirtà a) La colonna 3 gode della proprietà c) 33) Sia assegnata una matrice quadrata di N elementi di tipo intero. Supposto: che gli elementi della prima riga seguiti nell’ordine da quelli dell’ultima colonna, da quelli dell’ultima riga (a partire dall’ultima colonna fino alla prima) e infine da quelli della prima colonna (anche qui a partire dall’ultima riga per finire con la prima) formino un anello di elementi più esterno della matrice; che all’interno di tale anello ne esistano altri che si riducono di una dimensione fino a degenerare nell’elemento centrale (quando esiste per N dispari); e che ciascuno degli N/2 anelli sia individuato a partire dalle posizioni degli elementi della diagonale principale comprese proprie nell’intervallo 1..(N/2) (con la posizione 1 indicante l’anello più esterno); effettuare la rotazione antioraria degli elementi dell’anello i-esimo e quella oraria dell’anello j-esimo con i e j assegnati da tastiera. INPUT 6 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 1 3 OUTPUT 11 12 13 14 15 21 10 17 18 19 20 27 16 23 30 24 26 33 22 29 31 25 32 39 28 35 36 37 38 45 34 40 41 42 43 44 34) Si diano per una matrice di ordine n le seguenti definizioni: 1. l’elemento di posto (1,1) sia detto vertice superiore sinistro VSS; 2. l’elemento di posto (1,n) sia detto vertice superiore destro VSD; 3. l’elemento di posto (n,1) sia detto vertice inferiore sinistro VIS; 4. l’elemento di posto (n,n) sia detto vertice inferiore sinistro VID; 5. il minore di ordine m, con m<n e con VSS uguale, sia detto minore superiore sinistro MSS; 6. il minore di ordine m, con m<n e con VSD uguale, sia detto minore superiore destro MSD; 7. il minore di ordine m, con m<n e con VIS uguale, sia detto minore inferiore 446 Appendice B sinistro MIS; 8. il minore di ordine m, con m<n e con VID uguale, sia detto minore inferiore destro MID. Allora, assegnate due matrici di ordini rispettivamente N1 e N2, e fissato l’ordine M, con M<N1 e M<N2, individuare l’esistenza dei minori su indicati che abbiano valori uguali. Eventualmente si generalizzi estraendo tali minori per un qualsiasi valore di M compreso nell’intervallo ]1,N[ con n valore minimo tra N1 e N2. INPUT N1 e MAT1 8 54326426 13299768 88745531 43587534 56789554 54656832 16784561 08967418 M=3 N2 e MAT2 6 543264 132946 887321 989546 111167 712089 OUTPUT MSS di MAT1 uguale a MSS di MAT2 MIS di MAT1 uguale a MID di MAT2 35) Sia assegnata una matrice di interi con valori 0 o 1. La matrice riporta l’immagine del campo di gioco di una battaglia navale in cui le configurazioni dei due seguenti minori di ordine tre: 111 111 111 101 111 111 indicano rispettivamente le navi da colpire e le trappole mortali del nemico da evitare. Dopo aver fissato la matrice riportante il piano di gioco, indicare per ogni mossa consistente nella indicazione di una posizione di riga e di colonna se: è stata affondata una nave mediante l’individuazione del suo centro; è stata semplicemente colpita una nave; si è caduti in una trappola cadendo in uno qualsiasi dei suoi punti. Terminare con la condizione di vittoria quando tutte le navi sono state affondate o con quella di sconfitta quando si cade in una trappola. 36) Sia data una matrice di caratteri di R righe e C colonne. Verificare se su righe consecutive vi sono sequenze di vocali uguali a partire dalla stessa colonna e indicare quante volte la sequenza si è presentata in quella posizione, la riga in cui si è presentata la prima volta e la colonna da cui è iniziata la sequenza. Esercizi 447 input 1 2 3 a e i 2 2 a 3 a e i 3 a o u x y t 3 a o u f h 6 8 a o u h 7 8 9 output la sequenza aei si è presentata 2 volte a partire d riga 1, col. 4 la sequenza aou si è presentata 3 volte a partire dalla riga 3, colonna 1 37) Sia assegnata una matrice quadrata contenente NxM numeri interi. Individuare all’interno della matrice assegnata la presenza di elementi che: a) per x posizioni successive di una stessa riga devono soddisfare la relazione d’ordine Mat[i,j] < Mat[i, j+1] b) e che confermino la stessa proprietà a) per un totale di y righe successive alla riga i a partire sempre dalla stessa posizione di colonna, ma x+1 volte per la riga i+1, x+2 volte per la riga i+2, e infine x+y-1 volte per l’ultima riga. Qualora risulti x>1 e y>1, bisognerà stampare dapprima gli elementi costituenti il trapezio rettangolo così individuato e successivamente verificare se la stessa proprietà a) vale per ciascuna delle colonne del trapezio. INPUT 8 99 66 12 23 66 23 12 11 10 11 20 12 88 54 13 76 20 22 30 11 22 76 42 44 30 33 40 34 98 98 63 35 40 44 50 22 33 32 76 87 11 55 66 11 21 11 83 65 OUTPUT 10 20 11 22 20 30 30 33 40 40 44 50 55 66 70 23 33 70 44 44 88 88 78 44 22 22 11 22 99 31 10 viene anche soddisfatta la condizione sulle colonne 15.5. Esercizi sui record 38) Data una matrice sparsa generare un elenco che riporti le posizioni di riga e di colonna ed il valore degli elementi non nulli. (si definisce matrice sparsa una matrice in cui gli elementi non nulli sono pochi) 448 Appendice B INPUT 0 6 0 1 0 4 0 0 0 0 0 3 OUTPUT Riga Colonna Valore 1 2 6 1 4 1 2 2 4 3 4 3 39) Data una rubrica telefonica effettuare: l’inserimento di un nuovo anagrafico; la modifica di uno preesistente la eliminazione di uno preesistente la ricerca di un numero telefonico la visualizzazione della rubrica 40) Sia assegnata una matrice di NxM numeri interi con M>>N. Dopo aver individuato nella matrice le diagonali dei minori di ordine N x N, con N elementi tutti di valore uguale, stampi l’elenco delle posizioni di colonna e dei valori del primo elemento di siffatte diagonali riportando, nel caso alcune di esse si riproponessero con gli stessi valori in posizioni consecutive, il numero delle rispettive ripetizioni. INPUT 3 20 23465812333443322234 12345681433344331223 91234878163334433222 OUTPUT Colonna Valore Ripetizioni 1 2 1 2 3 1 3 4 1 6 8 1 7 1 1 9 3 3 12 4 2 14 3 2 17 2 2 41) Siano assegnati i valori di due numeri interi N1 e N2 con Cn1 cifre il primo numero e Cn2 il secondo. Operando il prodotto di N1 per N2, generare una matrice di Cn2+1 righe contenenti nelle prime Cn2 righe i risultati, Esercizi 449 opportunamente incolonnati, dei prodotti delle Cn2 cifre per le Cn1 cifre, e nella ultima riga le cifre del risultato del prodotto N1xN2. Comunque si produca la matrice, se ne effettui la stampa in modo che risulti chiaro il procedimento necessario per calcolare N1 per N2. Si raccomanda, per una corretta impostazione del progetto, di introdurre almeno le due procedure seguenti: 1) procedura “conversione_base” per la determinazione delle cifre componenti un valore intero; 2) procedura “tabellina” per il calcolo del prodotto di una cifra per un’altra con determinazione del risultato come coppia di cifre (l’unità e la decina). INPUT 1369 256 OUTPUT 1369x 256= ---------8214 6845 2738 ---------350464 42) Scrivere un programma per la gestione delle merci contenute in un magazzino. Per ogni merce si vogliono gestire le informazioni relative a l'identificativo, il nome, la descrizione e la quantità residua nel magazzino. In particolare si realizzino delle funzioni per: a) l'inserimento da tastiera di una nuova merce, b) la stampa a video del contenuto del magazzino, c) la gestione degli ordini che consistono prima nella ricerca della merce nel magazzino e poi nel decremento della quantità residua. 43) Scrivere un programma per la gestione di cartelle cliniche. Per ogni cartella clinica si vogliono gestire le informazioni relative al codice, il nome e il cognome del paziente, e, la descrizione dell suo stato clinico. In particolare si realizzano delle funzioni per: a) l'inserimento da tastiera di una nuova cartella, b) la stampa a video di una cartella di un dato paziente, c) la gestione dei ricoveri che consistono nell'aggiornamento della descrizione di una cartella clinica di un dato paziente. (Utilizzare un unica funzione che ricerca un paziente in base al suo codice) 15.6. Esercizi sulle stringhe 44) Data una stringa: memorizzare in una stringa di uscita la stringa d’ingresso invertita determinare, fissato l’alfabeto delle lettere (a,b,c,d,e,f,…v,w,z), quali e quante sono le lettere effettivamente presenti nella strina (si consiglia di creare per il 450 Appendice B confronto un vettore alfabeto) INPUT hello OUTPUT olleh a 0, b 0,…,e 1, …, h 1,…, l 2, …, o 1,…, z 0 45) Date due stringhe: segnalare se le due stringhe sono uguali o meno e nel caso non siano uguali determinare il numero di caratteri e i caratteri per cui differiscono INPUT ciao cimaro OUTPUT NON SONO UGUALI diff=2 mr 46) Sia assegnato un testo. Individuare la seguente relazione tra due parole consecutive del testo: tra le due parole deve esistere una sequenza di lettere che sia la terminazione della prima parola e l’inizio della seconda. Stampare quindi l’elenco delle parole che godono della proprietà indicata verificando anche l’eventuale presenza della relazione sia con la parola che precede che con quella che segue. INPUT un altro trovatello camminava in casa sapendo dove andare reperendo i quaderni OUTPUT altro trovatello casa sapendo sapendo dove andare reperendo 47) Sia assegnato un testo organizzato in righi separati. Individuare e tabellare le sottosequenze composte da almeno due caratteri disposti in ordine strettamente crescente. In particolare si stampino le sottosequenze accompagnate dalla loro occorrenza nel testo. INPUT ZZZABCDXCCCCCCGPOGDB XXXXXXABCDXAKPRSTSSSSAAAAA ZSTKJIXYZCGPO IXYZABPRSTSSSSAAAABCDX Esercizi 451 OUTPUT ABCDX 3 CGPO 2 AKPRSTS 1 IXYZ 2 48) Sia data una sequenza di stringhe. Definita come parola una sequenza di caratteri separati da un o più spazi bianchi, per ogni stringa individuare le parole che si ripetono più volte all’interno di essa. Nell’ipotesi che il numero di parole individuate sia minore di 100, stampare l’elenco delle parole individuate e la parola più frequente. INPUT andrea giacomo giovanni andrea marina bambi carletta bambi bambi maria giacomina OUTPUT andrea bambi La parola ‘bambi’ è la piu frequente essendo comparsa 3 volte. 49) Sia assegnata una sequenza di cifre decimali. verificare la presenza di sottosequenze di lunghezza data L che a partire dalla posizione L+1 abbiano le cifre disposte in modo da risultare l’anagramma delle prime L cifre. Si escludano dalla verifica le sottosequenze che risultano essere identiche alla sottosequenza costituita dalle prime L cifre. Per ogni sottosequenza individuata se ne riporti la stampa con la posizione della prima cifra ed eventualmente un computo delle occorrenze di quelle tra loro uguali. INPUT 12345895432123451239876001254321345 L=5 OUTPUT Sottosequenza Posizione 54321 8 23451 13 34512 14 45123 15 12543 26 54321 28 21345 31 (opzionale) Sottosequenza Occorrenza 54321 2 23451 1 34512 1 45123 1 12543 1 21345 1 452 Appendice B 50) Siano assegnati tre vettori V1, V2 e V3 contenenti parole. Individuare le parole di V1 che hanno: 1. tutte le vocali uguali a quelle di una parola di V2, 2. e le consonanti uguali a quelle di una parola di V3. Nel caso esistano le condizioni precedenti, stampare la terna di parole riportando per ciascuna di esse la posizione assunta nel vettore di appartenenza. INPUT V1 V2 V3 porta casa talete solitario arto parata latte esiste fase parere pena peso aglio pallino esempio paese matematica lago pizza pasta saluto OUTPUT porta 1 arto 2 parata 2 latte 3 pena 4 talete 1 aglio 5 pallino 5 lago 6 51) Sia assegnato un testo nel quale sia stata cifrata una sequenza di numeri interi. Estrarre i numeri al massimo di cinque cifre nascosti nel testo tenendo conto che ogni frase del testo ne contiene uno e che le cifre di ciascuno di essi sono date dal conteggio delle vocali presenti all’interno della frase (conteggio supposto minore di 10 per ogni vocale) e considerate secondo l’ordine stabilito dalla successione delle posizioni da esse occupate. In particolare, se la prima vocale che compare nella frase è la i, il numero di tutte le i fornisce la prima cifra del numero, se la seconda vocale diversa è la u, il numero complessivo di u determinerà la seconda cifra del numero e così via. Esempio: Questa prova sembra ueaoaea 1231 essere davvero eeeaeo 411 semplice eie 21 52) Sia assegnata una matrice di parole. Eliminare le righe e le colonne che contengono almeno una coppia di parole per le quali si dimostri che: 1. sono diverse; 2. l’una è l’anagramma dell’altra; 3. la più corta è l’anagramma di una parte della più lunga e non una sua sottosctringa. Si tenga presente che una stessa parola può essere nelle relazioni specificate con altre parole sia della stessa riga che della stessa colonna. Infine prima di procedere alla eliminazione delle righe e delle colonne, stampare per ciascuna riga e colonna l’elenco delle parole in relazione tra loro. Esercizi 453 INPUT 6 5 casa esame ancora porta pera gesso OUTPUT Per le righe esame porta Per le colonne Male Matrice ridotta casa ancora pera gesso 15.7. partire piega mela mese pippo pluto parto sedia processore foglio perfetto quaderno mela parto etto vassoio male cane faro troppa pino fiore menta mese troppa lame piega pluto foglio quaderno vassoio faro fiore lana lame platea pera lame partire pippo processore raso Esercizi sui file 53) Dati due file di gradi dimensioni contenenti ognuno elementi tra loro ordinati, generare un terzo file per fusione dei primi due senza introdurre buffer in memoria centrale 54) Dati due file di gradi dimensioni contenenti ognuno elementi tra loro diversi, verificare quali sono gli elementi in comuni tra essi. 55) Sia assegnato un file contenente un vettore “sparso” di numeri reali, memorizzare in un vettore solo gli elementi non nulli. 56) Sia assegnato un file di numeri reali: leggere e memorizzare in un vettore gli elementi del file d’ingresso. effettuare l’ordinamento del vettore generare un file d’uscita contenente il vettore ordinato. 57) Sia assegnato un file testo contenente un racconto organizzato in righi lunghi al massimo 80 caratteri ciascuno. Esaminare il testo contenuto nel file per memorizzare l’elenco delle parole che si ripetono in righi consecutivi fornendo per ciascuna di esse l’indicazione del rigo in cui compare per la prima volta e il numero di righi successivi in cui si ripete (solo il numero di righi consecutivi e non quante volte eventualmente si ripete nello stesso rigo). Nel caso siano presenti parole uguali con le caratteristiche citate, se ne riporti solo quella con 454 Appendice B numero maggiore di ripetizioni o una qualsiasi tra quelle con lo stesso valore di ripetizione. Stampare, infine, l’elenco ordinato per valori decrescenti del numero di ripetizioni. INPUT nel mezzo del cammin di nostra vita sono caduto nel fosso del mare e del cielo il quale cielo era di colore azzurro più azzurro del cielo di casa nostra e azzurro del mare della vita passata a cercare di superare l’esame che difficile non sembra. OUTPUT parola rigo ripetizioni cielo 2 3 azzurro 3 3 nel 1 2 del 1 2 di 3 2 58) Siano assegnati i valori degli elementi di tre vettori distinti in tre text file diversi residenti su memoria di massa. Nell’ipotesi di non poter istanziare un quarto vettore, operare l’ordinamento di tutti gli elementi dei tre vettori in senso crescente e senza ripetizioni di valori uguali. Per effettuare l’ordinamento si estenda l’algoritmo della selezione (select sort) cercando ad ogni passo il valore minimo appartenente all’insieme formato da tutti e tre i vettori e lo si accodi ad un file text; successivamente si stampi il contenuto del file così generato. INPUT File 1: 10 22 11 44 10 33 55 File2: 55 22 19 99 77 66 11 45 File3: 90 12 34 11 23 45 11 23 56 77 85 OUTPUT 10 11 12 19 22 23 33 34 44 45 55 56 66 77 85 90 99 Indice dei termini A algoritmi di base in C algoritmo ALU ALU (Aritmetic Logic Unit) ambiente MATLAB ambienti di programmazione integrati ARPANET array array bidimensionale array monodimensionale ASCII assemblatore automa a stati finiti 228 107 61 57 329 323 409 176 179 176 42 84 109 B Backus Normal Form basi di dati browser bus 122 432 415 63 C cache calcolabilità campionamento caricatore carte sintattiche Central Processing Unit (CPU) ciclo del processore client/server 72 111 39 317 122 56 62 407 456 Appendice C 65 13 329 390 26 28 113 61 37 136 135 135 137 61 clock codifica Command Window commutazione a pacchetto complemento a due complemento a uno complessità computazionale Control Unit (CU) convergenza digitale costrutti condizionali costrutti di controllo costrutti di sequenza costrutti iterativi CPU D 163 433 322 324 133 44 131 399 367 dato DBMS debugging DEV-C++ dichiarazion DOC documentazione dei programmi Domain Name System (DNS) DOS E eccesso 2l -1 E-mail equivalenza funzionale Ethernet 28 401 137 387 F file184 firmware frasi di commento FTP funzione 68 134 401 145 H hardware HTML HTTP 56 412 400 Indice termini 457 I immagini bitmap immagini vettoriali Informatica information hiding informazione ingegneria del software Internet internetworking interprete interruzioni ISO-OSI istruzioni istruzioni elementari di calcolo ed assegnazione 48 49 12 162 163 124 403 392 319 71 394 115 135 J Job Control Language 365 K kernel 369 L linguaggio assemblativo linguaggio C linguaggio di programmazione linguaggio HTML linker Linux Local Area Network 84 195 120 44 315 367 386 M Mac OS Macchina di Turing memoria memoria centrale memorie memorie di massa metadato metalinguaggi Metropolitan Area Network mezzi trasmissivi M-file editor 368 111 56 60 58 60 53 122 391 381 334 458 Appendice C microprocessori middleware motori di ricerca Mp3 MPEG 77 70 415 52 51 O 367 364 OS/2 overhead P parametri attuali parametri formali procedura processo processo di compilazione processo di traduzione progettazione dei programmi programma programmazione nel linguaggio assemblativo programmazione strutturata protocolli di comunicazione puntatore 148 148 145 370 311 310 127 120 89 123 392 182 Q quantizzazione 39 R RAM rappresentazione binaria rappresentazione in virgola mobile rete client-server rete intranet rete TCP/IP reti broadcast reti di calcolatori reti peer to peer reti punto a punto ricorsione ROM 59 15 33 385 402 397 385 383 386 385 159 59 Indice termini 459 S schedulatore segno e modulo sistema di numerazione Sistema Informatico Sistema Informativo Sistema Operativo software sostituzione per riferimento sostituzione per valore sottoprogrammi SQL (Structured Query Language) strutture di controllo. swapping 371 26 20 428 427 359 69 149 149 145 437 115 365; 369; 373 T TCP/IP Telnet tesi di Church-Turing testing throughput tipo booleano tipo carattere tipo coda tipo intero tipo per enumerazione tipo pila tipo reale tipo stringa di caratteri tipo strutturato tipo strutturato record tipo subrange tipo tabella topologia a bus topologia a ring topologia a stella tourn around time trattabilità 397 401 112 320 363 167 169 193 170 173 192 171 179 174 181 173 193 387 388 387 363 113 U UNIX URL 367 413 460 Appendice C V 153 56 visibilità Von Neumann W Wide Area Network Windows World Wide Web 390 367 411 X XML 54 Gli autori Angelo Chianese si laurea nel 1980 in Ingegneria Elettronica presso l’Università degli Studi di Napoli Federico II. Dal novembre del 2005 è in servizio presso la stessa Università in qualità di professore straordinario. Dal 1984 afferisce al Dipartimento di Informatica e Sistemistica dell'Università degli Studi di Napoli“Federico II dove svolge attività didattiche e di ricerca. Dal 2000 è membro del Consiglio di Amministrazione dell'Università. Dal 2003 è responsabile del progetto IDEA per lo sviluppo delle tematiche dell' elearning. E’ stato consulente del Ministro MUR per l’e-learning. I suoi attuali interessi di ricerca riguardano i settori della computer vision, del pattern recognition, delle basi di dati multimediali, della rappresentazione e gestione della conoscenza, dell’information retrieval in Antonio Picariello si laurea nel 1991 in Ingegneria Elettronica presso l'Unviersità di Napoli “Federico II”. Nel 1993 ottiene una borsa di studio presso il CNR - Istituto Ricerca Sui Sistemi Informatici Paralleli, Napoli. E’ dottore di Ricerca in Ingegneria Informatica nel 1998, Ricercatore Universitario dal 1999 al 2004 e Professore Associato dal 2005 ad oggi, presso il Dipartimento di Informatica e Sistemistica dell'Università di Napoli Federico II. I suoi interessi di ricerca attuali riguardano i seguenti settori: computer vision, processing di immagine biomediche, pattern recognition, basi di dati multimediali ed integrazione di informazioni multimediali, gestione ed estrazione delle conoscenza, gestione delle transazioni in ambienti mobili e sviluppo di applicazioni per la video sorveglianza. E' stato coinvolto in vari progetti di ricerca internazionali (in particolare con l’Università del Maryland), nazionali e locali ed è autore di numerose pubblicazioni su riviste e congressi internazionali, testimonianti le attività di ricerca svolte. Nel 2006 ha vinto, assieme ai 462 Autori ambienti di e-learning. colleghi dell’Università del Ha partecipato alla organizzazione di Maryland, il premio “Computer diversi convegni nazionali ed World Horizon Award” per le ricerche nel campo internazionali. Ha partecipato o è stato migliori Ha inoltre responsabile di diversi progetti di dell’Informatica. ricerca. È autore di numerose partecipato all’organizzazione ed è pubblicazioni su riviste e congressi stato nel comitato di programma di internazionali, testimonianti le attività di diversi convegni nazionali ed internazionali. ricerca svolte. È autore di diversi libri. È attualmente docente dei corsi di E’ attualmente docente per i corsi di Fondamenti di Informatica e Basi di Programmazione I e Basi di Dati per Dati per il corso di laurea di Ingegneria il corso di laurea di Ingegneria Informatica presso l’Università degli Informatica presso l’Università degli Studi di Napoli “Federico II”. Studi di Napoli Federico II. Email: picus@unina.it Email: angchian@unina.it Vincenzo Moscato ha ricevuto nel 2002 la laurea con lode in Ingegneria Informatica presso l'Università degli Studi di Napoli “Federico II”. Nel 2005 ha acquisito il titolo di Dottore di Ricerca in Ingegneria Informatica ed Automatica presso la medesima Università, dove attualmente lavora in qualità di assegnista di ricerca in collaborazione col gruppo di ricerca sui Sistemi Informativi Multimediali del Dipartimento di Informatica e Sistemistica.I suoi interessi di ricerca attuali riguardano le seguenti tematiche: basi di dati multimediali ed integrazione di informazioni multimediali, gestione delle transazioni in ambienti mobili, sviluppo di applicazioni per la video sorveglianza, integrazione di reti di sensori ed information retrieval in ambienti di e-learning. E' stato coinvolto in vari progetti di ricerca internazionali, nazionali e locali ed è autore di varie pubblicazioni su riviste e congressi internazionali, testimonianti le attività di ricerca svolte. E’ stato docente nell’anno accademico 2005/2006 del corso di Sistemi Operativi per il corso di Laurea in Ingegneria delle Telecomunicazioni presso l’Università degli Studi di Napoli “Federico II” e dal 2002 effettua assistenza didattica per i corsi di Fondamenti di Informatica e Basi di Dati nella medesima Università. Email: vmoscato@unina.it Sito web di riferimento: www.campus.unina.it u o r g Un viaggio nel mondo dei BIT u o r d g i t o r i t o r u o r e i l i l i A. Chianese, V. Moscato, A. Picariello ALLA SCOPERTA DEI FONDAMENTI DELL’INFORMATICA Alla scoperta dei fondamenti dell’informatica e g e l i ANGELO CHIANESE VINCENZO MOSCATO ANTONIO PICARIELLO d g d i t o r e i e i i t o r u o r d e e i e l i