Uploaded by sikove7839

Alla scoperta dei fondamenti dellinformatica by Angelo Chianese

advertisement
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.