Sei sulla pagina 1di 53

-1-

1 Introduzione

1.1 Budino di nocciole


Ingredienti: 700 ml. di latte; 6 uova; 200 gr di nocciole sgusciate; 180 gr di zucchero; 150 gr di savoiardi; 20 gr di burro; Odore di vaniglia.

Preparazione: Sbucciate le nocciole nell'acqua calda e asciugatele bene al sole o al fuoco; indi pestatele finissime nel mortaio con lo zucchero versato poco per volta. Mettete il latte al fuoco e quando sar entrato in bollore sminuzzateci dentro i savoiardi e fateli bollire per cinque minuti, aggiungendovi il burro. Passate il composto dallo staccio e rimettetelo al fuoco con le nocciole pestate per sciogliervi dentro lo zucchero. Lasciatelo poi ghiacciare per aggiungervi le uova, prima i rossi, dopo le chiare montate; versatelo in uno stampo unto di burro e spolverizzato di pan grattato, che non venga tutto pieno, cuocetelo in forno o nel fornello e servitelo freddo. Questa dose potr bastare per nove o dieci persone. (Artusi)

Immaginate una cucina, una certa quantit di ingredienti, utensili per cucinare, fornelli, forno, un cuoco (possibilmente umano). Preparare un budino di nocciole un processo che parte dagli ingredienti, viene portato avanti dal cuoco, con l'aiuto di un forno e di altri strumenti e, cosa significativa, in accordo con le istruzioni di una ricetta. Gli ingredienti corrispondono, nella terminologia che useremo nel proseguimento di questo corso, all'input del processo. Il budino , abbastanza ovviamente, l'output. La ricetta l'algoritmo. In altre parole, l'algoritmo prescrive le attivit che costituiscono il processo attraverso cui a partire dall'input (ingredienti) si arriva all'output (budino). La ricetta, o l'algoritmo, se scritta in maniera formale, come all'inizio di questo capitolo, corrisponde a ci che viene chiamato software, o programma, mentre gli utensili, il forno e, in questo caso, lo stesso cuoco, vanno sotto il nome di hardware.

-2Nella prima parte di questo corso ci occuperemo di algoritmi, ossia di come utilizzare gli ingredienti per ottenere un budino. Come vedremo in seguito, l'analogia tra un algoritmo computazionale e una ricetta pu essere spinta solo fino a un certo punto, oltre il quale cessa di essere illuminante e diventa un mero gioco di parole. Per esempio, sia un computer che l'hardware-cucina sono in grado di compiere solo operazioni elementari: il computer, come vedremo pi in dettaglio nel seguito di questo corso, pu operare direttamente solo sui bit, ossia su interruttori che possono essere o accesi o spenti, mentre l'hardware-cucina pu sbucciare, pestare, mescolare, cuocere e misurare quantit, ma non pu, direttamente, creare un budino dal nulla. Un primo (importantissimo) problema che si presenta, sia nel caso del computer che in quello dell'hardware-cucina, quello del livello di dettagli al quale dobbiamo scendere perch la serie di istruzioni che costituiscono l'algoritmo abbia un senso, e ci permetta di arrivare al risultato, sia che si tratti di un budino o del calcolo molto complesso per la costruzione della struttura portante di un ponte. Prendiamo per esempio l'istruzione "pestare le nocciole finissime nel mortaio". Perch l'algoritmo non dice "ridurre le nocciole a particelle grandi al massimo un decimo di millimetro"? Semplicemente perch questo livello di dettaglio eccessivo per lo scopo che ci prefiggiamo, che quello di ottenere una pasta omogenea di nocciole. In altre parole possiamo dire che in questo caso l'hardware gi sa cosa significa "pestare le nocciole finissime", e non ha bisogno di ulteriori dettagli. Consideriamo un altro esempio, pi vicino al resto di quel che studieremo durante il corso: la moltiplicazione tra due numeri interi. Supponiamo che ci chiedano di moltiplicare 528 per 46. Sappiamo esattamente (o almeno lo spero) cosa fare. Moltiplichiamo 6 per 8, che d 48, Scriviamo 8 e riportiamo 4; quindi moltiplichiamo 6 per 2 e aggiungiamo il 4 del riporto, e questo ci d 16. Scriviamo 6 a sinistra dell'8 e riportiamo 1, eccetera. Qui possiamo porci la stessa domanda di prima, relativa al grado di sbriciolamento delle nocciole. Perch moltiplichiamo 6 per 8 e non invece aggiungiamo 8 volte 6 a s stesso? La risposta, abbastanza ovvia, che gi sappiamo come moltiplicare 6 per 8, e non abbiamo bisogno di ricorrere alla definizione elementare di moltiplicazione. Al contrario, ma allo stesso modo, potremmo chiederci perch non moltiplichiamo direttamente 528 per 46, senza ricorrere a un algoritmo. Alcune persone riescono a farlo: queste persone corrispondono a dei cuochi che sanno preparare un perfetto budino di nocciole senza leggere la ricetta. In altre parole, usando l'algoritmo della moltiplicazione stiamo dicendo che l'hardware (in questo caso noi stessi) in grado di compiere certe operazioni elementari (moltiplicare 6 per 8, riportare 4, eccetera) ma non capace di moltiplicare 528 per 46 "al volo". Questo esempio mostra la necessit di mettersi subito d'accordo sulle azioni basilari che un algoritmo deve essere in grado di prescrivere. Senza questa specificazione inutile cercare di stabilire un algoritmo per un qualsiasi dato problema. Naturalmente, problemi diversi sono associati a diversi tipi di azioni basilari. Nel caso della cucina, le azioni basilari sono mescolare, pestare, cuocere, pesare, eccetera. Nel caso della moltiplicazione di due numeri grandi le azioni basilari si riducono a moltiplicazioni di numeri minori di 10, riporti, somme, eccetera. Nel caso degli algoritmi di cui ci occuperemo in seguito, e qui arriviamo al limite dell'analogia con le ricette di cui parlavamo prima, le azioni basilari di cui stiamo parlando dovranno essere specificate con chiarezza e precisione. Non saremo in grado di accettare istruzioni tipo "montare le chiare a neve". L'idea che un certo cuoco ha di chiare montate a neve pu essere decisamente diversa da quella di un altro cuoco. Le istruzioni dovranno essere chiaramente distinte dalle non-istruzioni. "Questa dose potr bastare per nove o dieci persone" non , per esempio, un'istruzione che serve a preparare il budino. Frasi ambigue tipo "che non venga tutto pieno" (met? tre quarti? nove decimi?) non trovano posto in algoritmi che vanno

-3poi realmente eseguiti sui calcolatori. Le ricette, per dirla tutta, rispetto agli algoritmi per calcolatore dnno troppe cose per scontate, la pi notevole delle quali che un essere umano (il cuoco) fa parte dell'hardware. Nel disegnare algoritmi per calcolatori non potremo permetterci questo lusso, e dovremo cercare di essere molto pi stringenti e precisi. Nel seguito ci occuperemo principalmente di problemi per i quali possibile una precisa formalizzazione, quasi sempre matematica: se un dato problema cos chiaramente comprensibile ed esprimibile, sar in generale possibile definire una strategia di soluzione basata sullapplicazione sistematica di ben precise regole operative che consentir di ottenere il risultato atteso a partire dai dati disponibili. In molti casi sar possibile affidare lapplicazione delle regole di soluzione a un esecutore altamente specializzato (un calcolatore), in grado di svolgere il compito con estrema rapidit.

1.2 Un po' di storia


Tra il 400 e il 300 a.C. il matematico greco Euclide invent un algoritmo per trovare il Massimo Comun Divisore (MCD) di due numeri interi positivi. A scanso equivoci ricordiamo che il MCD di X e Y il pi grande numero intero che divide esattamente (e cio senza resto) sia X che Y. Per esempio, il MCD di 32 e 12 4. Vediamo quali possibili algoritmi possiamo utilizzare per trovare il MCD di due numeri interi assegnati X e Y. Una prima possibilit quella di applicare direttamente la definizione matematica di MCD. In questo caso lalgoritmo da seguire (Algoritmo A): 1. calcolo gli insiemi D(X) e D(Y) dei divisori di X e Y; 2. costruisco linsieme intersezione di D(X) e D(Y); 3. determino il valore massimo dellinsieme intersezione. Vediamo questo algoritmo in azione nel caso dellesempio di prima, ossia il MCD di X=32 e Y=12. Abbiamo che D(X) = [1, 2, 4, 8, 16, 32], mentre D(Y) = [1, 2, 4, 6, 12]. Linsieme intersezione dato da [1, 2, 4], quindi MCD(32,12) = 4. Unaltra possibilit la seguente (Algoritmo B): 1. individuo il valore minimo (M) tra X e Y; 2. se M divide esattamente sia X che Y allora MCD(X,Y) = M [fine algoritmo]; 3. altrimenti decremento M di 1 e ripeto il passo precedente, al pi fino al valore M = 1. Nel caso del nostro esempio abbiamo che M=12. 32/12 non d un numero intero come risultato, quindi passo a M=11. Ma 11 non va ancora bene, e continuo a decrementare M finch arrivo a M=4, per il quale ho 32/4 = 8, 12/4 = 3, quindi MCD(32,12) = 4. L'algoritmo di Euclide per il MCD il primo algoritmo non banale, in ordine di tempo, di cui ci sia giunta notizia. Questo algoritmo parte da una propriet precisa del MCD di due numeri interi. La propriet la seguente:

-4-

se X, Y, Q, e R sono numeri interi, con X Y e X = Q * Y + R, allora linsieme intersezione di D(X) e D(Y) uguale allinsieme intersezione di D(Y) e D(R). Quindi il problema di trovare il valore massimo nellinsieme intersezione di D(X) e D(Y) pu essere ridotto al problema pi semplice di trovare il massimo nellinsieme intersezione di D(Y) e D(R). Sulla base di questa propriet possibile scrivere lalgoritmo di Euclide (Algoritmo E) per il MCD: 1. R = MOD(X,Y); [MOD il resto della divisione intera]

2. Se R = 0 allora Y il MCD(X,Y), altrimenti calcola MCD(Y,R). Vediamo questo algoritmo allopera sul nostro esempio favorito. Abbiamo MOD(32,12)=8 (perch 32 = 12 * 2 + 8), quindi calcoliamo MOD(12,8)=4 (perch 12 = 8 * 1 + 4), e infine MOD(8,4)=0 (perch 8 = 4 * 2), quindi MCD(32,12)=4. La parola algoritmo deriva dal nome di un matematico arabo, Mohammed alKhowarizmi, che visse nel nono secolo della nostra era e trov alcuni procedimenti sequenziali per la moltiplicazione e la divisione di numeri interi. Il suo nome fu latinizzato in Algorismus, e da qui ad algoritmo il passo breve.

1.3 Algoritmi corti, processi lunghi


Supponiamo che ci venga fornita la lista del personale di una compagnia. In questa tabella troviamo, per ogni riga, il nome dell'impiegato, un campo di dettagli personali (indirizzo, codice fiscale, eccetera) e il salario mensile. Siamo interessati a conoscere la somma totale dei salari mensili degli impiegati. Ecco l'algoritmo che potremmo usare: 1. annotiamo da qualche parte il numero 0; 2. procedendo lungo la lista, sommiamo il salario di ciascun impiegato al numero annotato; 3. quando la lista finisce, produciamo in output il numero annotato. E' abbastanza facile rendersi conto che questo algoritmo "funziona", ossia un algoritmo corretto per il problema che ci siamo posti. Il numero "annotato" (per esempio su un pezzo di carta) all'inizio contiene zero. Dopo il primo impiegato conterr, quindi, il salario mensile del primo impiegato, dopo il secondo la somma dei salari del primo e del secondo, eccetera. E' interessante notare che il testo di questo algoritmo corto (e per di pi di lunghezza fissata), mentre il processo che detto algoritmo descrive e controlla pu essere arbitrariamente lungo: basti pensare a un'azienda con un milione di impiegati. Ancor pi interessante notare che l'algoritmo funziona sia per aziende con pochi impiegati che per aziende con moltissimi impiegati: basta fornire all'algoritmo la giusta lista (input), per quanto lunga essa sia. Non solo: indipendentemente dalla quantit di impiegati, per ogni azienda c' bisogno di un solo "oggetto" (il numero annotato, che corrisponde alla somma progressiva dei salari) per compiere il lavoro assegnato. Naturalmente il valore di

-5questo numero potr essere piccolo o grande, a seconda delle dimensioni dell'azienda, del numero degli impiegati e della consistenza del loro stipendio.

1.4 Problemi algoritmici


Siamo arrivati al punto di aver fissato un determinato algoritmo che "funziona" in molti casi diversi, con processi che possono essere corti o lunghi a seconda dell'input che l'algoritmo riceve in pasto. Anche il semplicissimo algoritmo che abbiamo esposto nella sezione precedente pu avere un numero molto alto di possibili input: ditte individuali (una sola persona), aziende con milioni di dipendenti, aziende in cui alcuni dei salari sono nulli, altre in cui sono uguali, altre ancora in cui i dipendenti ricevono uno stipendio negativo (cio pagano per il piacere di poter lavorare). L'algoritmo dello "stipendio totale" funziona in realt per un numero infinito di possibili input diversi. C' infatti un numero infinito di possibili liste di impiegati perfettamente accettabili, e l'algoritmo dovrebbe essere in grado di sommare gli stipendi in ciascuna di queste. Ci stiamo scontrando con un altro limite dell'analogia tra algoritmi e ricette: nel caso della ricetta gli ingredienti sono fissati una volta per tutte, e sebbene la stessa ricetta possa essere utilizzata infinite volte (colesterolo permettendo), l'output della ricetta sempre lo stesso budino, per nove o dieci amanti del budino (e delle nocciole). Tuttavia in questo caso potremmo generalizzare la ricetta, cio potremmo specificare, invece di 200 gr. di nocciole e 20 di burro, X gr di nocciole e X/10 gr di burro (e di conseguenza per gli altri ingredienti), con il risultato finale che la quantit di budino in output baster per X/20 persone. In questo caso la ricetta si riavvicinerebbe allo spirito dell'algoritmo. Un'altra problematica legata all'input di un algoritmo riguarda la sua "legalit", o correttezza. Questo per esempio significa che una lista delle piante ospitate nel giardino botanico di Ginevra non va bene come input per il nostro algoritmo dello "stipendio totale", cos come le aringhe affumicate non costituiscono un ingrediente accettabile di un qualsiasi budino alle nocciole degno di questo nome. In termini pi generali, le ricette, o gli algoritmi, sono soluzioni a certi tipi di problema, chiamati problemi computazionali, o algoritmici. Nel caso degli stipendi, per esempio, il problema pu essere completamente specificato dal fatto di chiedere che un certo numero (la somma degli stipendi) sia prodotto a partire da una lista, legale e di lunghezza qualsiasi, di impiegati: tale lista, per poter essere accettata in input dall'algoritmo, deve possedere una serie di caratteristiche (la prima colonna contiene i nomi degli impiegati, la seconda lo stipendio). Questo problema pu essere visto come la ricerca di una scatola nera (black box): la scatola "mangia" l'input (la lista degli impiegati) e produce un output (la somma di tutti gli stipendi). Ci che definisce la scatola nera la serie di operazioni elementari che dobbiamo compiere sulla lista per ottenere il risultato, o in altre parole il modo in cui il risultato dipende dagli elementi in input. Possiamo dire, forse un po' tautologicamente, che un problema algoritmico risolto quando abbiamo trovato un algoritmo che produce il risultato voluto a partire da un input dato. In questo caso la scatola nera stata riempita da un contenuto (l'algoritmo A): detta scatola "funziona" secondo l'algoritmo A. Dato A, la scatola nera produce l'appropriato output a partire da qualsiasi input legale, eseguendo i processi che sono prescritti e governati da A. La parola "qualsiasi", nella frase precedente ("qualsiasi input legale") di un'importanza fondamentale. Non siamo interessati a soluzioni che per alcuni tipi di input

-6(legale) non funzionano. Come esempio estremo, immaginiamo per il problema dello "stipendio totale" il seguente algoritmo: 1. produci zero come output. Questo algoritmo funziona solo per una ristrettissima classe di aziende. Un altro aspetto importante da non sottovalutare riguarda il tempo di esecuzione, da parte dell'hardware di turno, di ciascuna azione basilare, o operazione, prescritta dall'algoritmo. Risulta infatti necessario, anche se apparentemente ovvio, richiedere che ciascun passo elementare dell'algoritmo possa essere portato a termine in un tempo finito. Nel caso contrario l'algoritmo non terminerebbe mai, risultando quindi di scarsa se non nulla utilit pratica.

1.5 Un tentativo di riassunto


Per riassumere, un problema algoritmico consiste in: una caratterizzazione di un insieme legale, anche se infinito, di possibile input; la specifica dell'output desiderato in funzione dell'input.

Si assume che sia data a priori una descrizione dei passi elementari possibili, o equivalentemente una configurazione hardware e la specifica delle azioni elementari che l'hardware stesso pu eseguire. La soluzione a un problema algoritmico (o computazionale) consiste nell'algoritmo stesso, composto da istruzioni elementari che prescrivono le azioni da compiere, scelte tra le azioni possibili e legali. L'algoritmo, quando viene eseguito in seguito all'immissione di un qualsiasi input legale, risolve il problema, producendo l'output richiesto. Va notato che le regole di un qualsiasi algoritmo sono in genere applicate su rappresentazioni degli oggetti fondamentali che vivono nello spazio in cui lalgoritmo opera. Nel caso del MCD, lalgoritmo di Euclide opera su X, Y eccetera, che sono rappresentazioni di particolari numeri interi (32 e 12, nel nostro caso). Il messaggio fondamentale di questo capitolo riguarda la natura e la definizione di algoritmo e di problema algoritmico: un algoritmo sostanzialmente un insieme di regole che, eseguite ordinatamente, permettono di risolvere un problema a partire dai dati a disposizione (input). Perch questo insieme di regole possa considerarsi un algoritmo a tutti gli effetti deve rispettare alcune propriet: Non ambiguit: le istruzioni devono essere univocamente interpretabili dallesecutore dellalgoritmo, che sia un calcolatore (come pi spesso accade) o un essere umano; Eseguibilit: lesecutore deve essere in grado, con le risorse a disposizione, di eseguire ogni istruzione in un tempo finito; Finitezza: lesecuzione di un algoritmo deve terminare in un tempo finito per ogni insieme di valori in input.

E' importante capire che gli esempi che abbiamo sviluppato in questo capitolo introduttivo (la ricetta e la moltiplicazione di due interi) non rendono giustizia alla considerevole complessit del problema generale di trovare algoritmi soddisfacenti per un dato problema. Non bisogna pensare che le cose siano cos semplici come sono state presentate. E

-7se sono state presentate in maniera cos semplice solo a scopo esemplificativo. I problemi algoritmici di interesse pratico possono risultare incredibilmente complessi, e possono richiedere anni di lavoro da parte di un equipe di specialisti per poter essere risolti in maniera soddisfacente. Addirittura alcuni problemi non possono essere assolutamente risolti in maniera soddisfacente, mentre altri non ammettono nessuna soluzione ( possibile stabilire con un algoritmo quale sar il cambio Euro/Dollaro il primo gennaio del 2100?). E ci che peggio, per certi problemi non sappiamo neppure se possano essere risolti algoritmicamente o meno.

-8-

2 Algoritmi e dati

Sappiamo gi che gli algoritmi contengono istruzioni elementari selezionate con cura che prescrivono le azioni basilari che devono essere eseguite al fine di ottenere un certo risultato in output a partire da un certo input. Non abbiamo parlato del modo in cui queste istruzioni sono arrangiate (???) nellalgoritmo, in modo tale che chi poi si incaricher di eseguire materialmente lalgoritmo (probabilmente un calcolatore) possa immaginare lordine preciso nel quale le azioni elementari devono essere eseguite. Non abbiamo neanche discusso gli oggetti manipolati da queste azioni elementari. Lesecuzione di un algoritmo pu essere pensato come portato avanti da un piccolo robot, o un processore, che chiameremo Corrintorno. Il processore riceve istruzione di correre qui e l facendo questo e quello, dove questo e quello sono proprio le azioni basilari dellalgoritmo. Nellalgoritmo dello stipendio totale del capitolo precedente al piccolo Corrintorno stato ordinato di prendere nota del numero 0 e poi di cominciare a lavorare sulla lista di impiegati, trovando gli stipendi e aggiungendoli, uno a uno, al numero annotato allinizio. Dovrebbe risultare chiaro che lordine in cui le azioni elementari sono eseguite cruciale. E di unimportanza fondamentale non solo che le azioni elementari siano chiare e non ambigue, ma anche che lo stesso criterio di chiarezza e non ambiguit sia applicato al meccanismo che controlla la sequenza in cui le istruzioni elementari sono eseguite. Lalgoritmo deve quindi contenere istruzioni di controllo per spingere il processore (il nostro Corrintorno) in questa o quella direzione, a seconda dei casi, dicendogli chiaramente cosa fare passo per passo.

2.1 Strutture di controllo


Il controllo sulla sequenza delle operazioni in genere svolto con laiuto di un insieme di istruzioni chiamate strutture di controllo di flusso, o pi semplicemente strutture di controllo. Avvertenza: poich tutti i linguaggi di programmazione utilizzano ampiamente linglese per le loro parole-chiave, nel seguito le strutture di controllo saranno specificate in inglese (la prima volta che sono presentate anche in italiano, per facilitare la traduzione). Anche la ricetta del budino alle nocciole contiene diverse istruzioni o strutture di controllo, come le seguenti: Sequenza diretta: sono della forma fai A e poi B (do A followed by B). Nella

-9ricetta: inserire le chiare montate a neve dopo aver amalgamato i rossi duovo); Salto condizionale: sono della forma se succede Q allora fai A altrimenti fai B (if Q then do A else do B), o semplicemente se succede Q allora fai A (if Q then do A), in cui Q qualche tipo di condizione. Nella ricetta: sminuzzare i savoiardi se il latte bolle, altrimenti continuare a scaldare il latte).

Queste due strutture di controllo, sequenza diretta e salto, non spiegano come un algoritmo di lunghezza prefissata possa eseguire processi arbitrariamente lunghi, a seconda dellinput. Un algoritmo che contenga solo sequenze dirette e salti condizionali pu solo prescrivere processi di lunghezza prefissata, poich nessuna parte dellalgoritmo pu essere eseguita pi di una volta. Strutture di controllo che permettono allalgoritmo di eseguire processi arbitrariamente lunghi sono nascoste anche nella ricetta del budino, ma sono di gran lunga pi esplicite nellalgoritmo di stipendio totale. Queste strutture sono genericamente chiamate iterazioni, o costrutti di loop (loop significa cappio, ossia una cosa che torna su s stessa come la corda di un cappio), e possono presentarsi in diverse maniere. Qui ne descriviamo due: Iterazioni limitate: sono della forma fai A esattamente N volte (do A exactly N times), in cui N un numero; Iterazioni condizionali: sono della forma fai A fino a che non si verifica la condizione Q (do A until Q), oppure finch la condizione Q vera fai A (while Q do A). Nella ricetta, implicitamente: battere le chiare duovo finch non sono montate a neve).

Quando abbiamo descritto lalgoritmo dello stipendio totale siamo rimasti sul vago relativamente a come la parte principale dellalgoritmo dovesse essere svolta: abbiamo scritto qualcosa del tipo scorri tutta la lista, aggiungendo lo stipendio dellimpiegato corrente al numero annotato. In realt, per descrivere con precisione lalgoritmo (e ogni algoritmo va descritto con estrema precisione) avremmo dovuto utilizzare un costrutto iterativo, specificando esattamente in questo modo a Corrintorno il modo in cui scorrere la lista degli impiegati. Assumiamo che insieme alla lista sia data in input anche la sua lunghezza, ovvero il numero degli impiegati, N. In questo caso possibile utilizzare un costrutto del tipo iterazione limitata, che porta al seguente algoritmo: 1. annota 0; 2. punta al primo stipendio della lista; 3. fai le cose che seguono N-1 volte; 3.1. somma lo stipendio a cui stai puntando al numero annotato; 3.2. punta al prossimo stipendio; 4. somma lo stipendio a cui stai puntando al numero annotato; 5. produci il numero annotato come output. Le cose che seguono al punto 3. si riferiscono naturalmente notato subito il livello di indentazione di questi punti, che sono scritti Quello dellindentazione un trucco che useremo spesso, anche concretamente a scrivere programmi, per connotare i cicli iterativi, o codice. ai punti 3.1. e 3.2. Va pi a destra degli altri. quando ci troveremo in generale i blocchi di

Gli studenti sono incoraggiati a cercare di capire come mai al punto 2. stiamo usando

- 10 N-1 invece di N, e perch stiamo sommando separatamente lultimo stipendio. E da notare che lalgoritmo fallisce se la lista vuota (cio se N = 0) perch in questo caso la seconda parte del punto 1. non ha alcun significato. Se linput non include il numero N di impiegati, dobbiamo ricorrere a uniterazione condizionale, del tipo while (la lista non finita) somma stipendio. In questo caso per dobbiamo specificare qual il segnale che ci dice che la lista finita.

2.2 Diagrammi di flusso


Abbiamo gi detto (ma non lo ripeteremo mai abbastanza) che i vari passi di un algoritmo devono essere specificati nel modo pi chiaro possibile, soprattutto se a eseguire lalgoritmo sar poi in concreto un calcolatore. Tutti ci siamo trovati nella penosa situazione di dover montare un mobile seguendo le poche istruzioni scritte in svedese su un foglietto mal disegnato: in quel momento, anche se non lo sapevate, avete ardentemente desiderato, al posto del foglietto in svedese, un algoritmo ben congegnato e ben descritto, che spiegasse nella maniera pi semplice possibile i vari passi da eseguire per ottenere, a partire da una serie di tavole di legno e di viti generalmente della misura sbagliata, una libreria stabile con minimi rischi di crollo. Sembra quindi necessario, nella costruzione di un algoritmo, salire a un livello di formalizzazione superiore a quello impiegato nella descrizione dellalgoritmo dello stipendio totale nella sezione precedente (da questo punto di vista analogo al famigerato foglietto scritto in svedese). Alla fine del processo vedremo che il grado di formalizzazione massima, dal punto di vista risolutivo, consiste nello scrivere un programma, in un certo linguaggio di programmazione, che implementi lalgoritmo. Prima di arrivare a quel punto dobbiamo superare un paio di stadi intermedi. Il primo di questi livelli consiste in una visualizzazione grafica. Esistono molti metodi per visualizzare in maniera grafica un algoritmo. Il pi usato e certamente il pi efficace quello dei diagrammi di flusso (in inglese flow charts). In un diagramma di flusso i vari passi di un algoritmo sono rappresentati da diversi elementi grafici, la cui forma rende visivamente immediato il tipo di operazione che si sta compiendo in quel particolare punto dellalgoritmo. Gli elementi grafici sono poi uniti da linee direzionate (cio dotate di frecce): lalgoritmo scorre nella direzione delle frecce (per questo sono chiamati diagrammi di flusso). Gli elementi grafici pi comunemente usati nei diagrammi di flusso sono:

start
un ovale, per rappresentare linizio (start) o la fine (stop) dellalgoritmo;

azione
un rettangolo, per rappresentare unazione vera e propria (per esempio lassegnazione di un valore a una variabile);

- 11 -

?
una losanga, per rappresentare una condizione. In Figura 1 disegnato, a mo di esempio, il diagramma di flusso dellalgoritmo dello stipendio totale, mentre in Figura 2 e 3 sono rappresentati, rispettivamente, i diagrammi di flusso dellalgoritmo del diretto superiore (vedi sezione ???) e dellalgoritmo di Euclide per il Massimo Comun Divisore.

start

annota 0

punta al primo elemento della lista aggiungi lo stipendio corrente al numero annotato s output somma

la lista finita?

no

punta al prossimo elemento della lista

stop

FIGURA 6 Diagramma di flusso dellalgoritmo dello stipendio totale.

Notiamo che osservando un diagramma di flusso ci accorgiamo subito della presenza nellalgoritmo di un ciclo: se seguendo le frecce troviamo un percorso che ci riporta in un punto del programma che abbiamo gi attraversato, siamo in presenza di un ciclo.

2.3 Linguaggio di programmazione naturale


Il secondo passo di formalizzazione che dobbiamo compiere, prima di arrivare a un linguaggio di programmazione vero e proprio che possa essere usato congiuntamente a un

- 12 calcolatore per implementare un algoritmo, consiste nellintroduzione di un linguaggio di programmazione che chiameremo naturale. Questo linguaggio di programmazione generico (LP) se da un lato , come vedremo presto, molto vicino a un vero e proprio linguaggio di programmazione (almeno per quel che riguarda luso delle variabili e delle strutture di controllo), dallaltro se ne discosta drasticamente per quel che riguarda il livello di dettaglio in cui si scende per descrivere certe operazioni di alto livello, e da questo punto di vista il risultato netto assomiglia pi a un diagramma di flusso che a un programma vero e proprio. Per fare un esempio concreto, loperazione di input non sar descritta, a livello di LP, con lo stesso dettaglio necessario in un linguaggio di programmazione vero e proprio affinch linput avvenga poi concretamente e in maniera corretta, ma sar genericamente descritta dallistruzione: Input( x, y, z ); dove x, y e z sono le variabili che si stanno leggendo in input. Per descrivere in un minimo didettaglio il LP conveniente iniziare con degli esempi. Il primo esempio che vedremo loramai famoso lalgoritmo dello stipendio totale: Input( lista ); somma := 0; while ( not lista.finita ) { p := lista.prossimo_elemento; somma := somma + p.stipendio; } Output( somma ); Notiamo subito le istruzioni Input e Output, che come abbiamo detto restano molto generiche (il LP non ci dice come effettivamente avvenga linserimento dei dati, n come il risultato finale sia mostrato in output allutente). Notiamo anche luso della struttura di controllo while ( condizione ), seguita da un cosiddetto blocco di codice, incluso tra parentesi graffe. In generale possiamo dire che per scrivere un algoritmo in LP possiamo usare i seguenti elementi: variabili, ossia nomi logici associati agli oggetti sui quali il nostro algoritmo deve operare (nellesempio sopra: lista, somma e p); istruzione Input( lista variabili ); istruzione Output( lista variabili ); ciclo condizionale while ( condizione ) { blocco di codice }; listruzione while fa s che il blocco di codice contenuto tra le parentesi graffe sia ripetuto di seguito finch non si avvera la condizione nelle parentesi tonde; ciclo limitato for i = 1 to N { blocco di codice }; questo ciclo viene usato per eseguire il blocco di codice esattamente N volte. A ogni iterazione la variabile

- 13 i viene incrementata di uno; istruzione if ( A ) { blocco 1 } else { blocco 2 }; questa istruzione comporta lesecuzione del blocco 1 se la condizione A vera: altrimenti sar eseguito il blocco 2; istruzione di assegnazione nome_variabile := valore; luso del simbolo := per differenziare lassegnazione dalla condizione di uguaglianza; operatori logici: not, and, or, =, <= >= > metodi, della forma nome_variabile.nome_metodo: nellesempio sopra riportato lista.finita un metodo che ci dice se la lista finita oppure no. Va notato che il LP non specifica in nessun modo il concreto funzionamento di un metodo (ossia come questo metodo viene implementato in pratica). Per esempio, invocando il metodo p.stipendio otteniamo lo stipendio dellimpiegato il cui profilo contenuto nella variabile p, ma non sappiamo come abbiamo fatto per ottenerlo (in altre parole non siamo scesi fino al livello di dettagli necessario per capire come ottenere questo valore). funzioni, della forma nome_funzione( lista parametri ). La funzione esattamente come un metodo, solo che un metodo in qualche senso appartiene a una variabile (o pi propriamente a una classe di variabili) mentre la funzione opera sulla lista dei parametri che le passiamo e restituisce un certo risultato: possiamo pensare a una funzione come a un sottoalgoritmo, di cui al momento della stesura dellalgoritmo vero e proprio non ci interessa conoscere i dettagli di funzionamento. Va senza dire che quando si scriveranno veri e propri programmi occorrer specificare in dettaglio anche le istruzioni che definiscono i metodi e le funzioni.

2.4 Variabili e array


Nelle sezioni precedenti abbiamo spesso usato il concetto di variabile, a volte anche implicitamente, ma finora non abbiamo mai detto cosa sia effettivamente una variabile. Una variabile un po come una stanza dalbergo: non va confusa con chi la occupa. In altre parole possiamo pensare a una variabile come a un contenitore (astratto) dentro il quale poter mettere i dati sui quali lalgoritmo deve operare. Nellesempio in LP sopra riportato, abbiamo usato listruzione di assegnazione somma := 0; e anche listruzione somma := somma + p.stipendio; Qestultima istruzione particolarmente importante, perch spiega bene il diverso significato da attribuire a una variabile a seconda che la variabile stessa si trovi a destra o a sinistra delloperatore di assegnazione: se la variabile a sinistra la dobbiamo intendere come contenitore, mentre se a destra stiamo usando il valore in essa contenuto. Nellistruzione somma := somma + p.stipendio; stiamo prima sommando il valore contenuto nella variabile somma al valore restituito dal

- 14 metodo p.stipendio, e poi stiamo assegnando alla variabile somma il valore cos ottenuto. Un modo particolarmente furbo di raggruppare variabili quello di metterle in un array, o vettore. Per continuare con la metafora del contenitore, possiamo dire che un array un contenitore di contenitori. Vediamo gli array allopera in una versione modificata dellalgoritmo dello stipendio totale:

Input( lista ); N := lista.lunghezza; somma := 0; for i = 1 to N { p := lista[i]; somma := somma + p.stipendio; } Output( somma ); In questo caso la variabile lista pensata come un vettore (array) ordinato: gli elementi dellarray, che sono le variabili vere e proprie, sono ottenute appendendo il loro numero progressivo, racchiuso da parentesi quadre, al nome dellarray, e in questo modo lista[1] contiene il primo elemento della lista, lista[2] il secondo e cos via, fino allultimo elemento, che sar lista[N].

2.5 Cicli annidati


Non inusuale, nella risoluzione di problemi algoritmici, trovarsi a dover considerare la costruzione di cicli annidati, ovvero di un ciclo che si svolge interamente dentro un altro ciclo. Per costruire un esempio, modifichiamo la domanda del problema dello stipendio totale: richiediamo un algoritmo che trovi la somma degli stipendi di tutti i dipendenti il cui stipendio sia maggiore dello stipendio del diretto superiore. Immaginiamo che in input ci sia la tabella seguente:

Nome Annalisa Emanuele Giorgio Marco

Superiore Annalisa Annalisa Emanuele

Stipendio (Euro) 2.000 3.000 1.000 4.000

Vediamo che il diretto superiore sia di Emanuele che di Giorgio Annalisa, ma solo Emanuele guadagna pi di Annalisa. Inoltre Marco guadagna pi di Emanuele, che il suo diretto superiore. Lalgoritmo deve fornire come risposta, in questo caso, 7.000 Euro (cio la

- 15 somma degli stipendi di Emanuele e Marco). In figura 2 abbiamo il flow chart dellalgoritmo che ci serve. Vediamo immediatamente che i cicli annidati si mostrano visivamente come due percorsi chiusi uno dentro laltro.

- 16 -

start somma := 0

p := primo eleme nto della lista

q := primo eleme nto della lista

p.stip > q.stip? no

q sup. di p? no

somma := somma + p.stip q ultimo? no q := prossimo

outp ut so mma

p ultimo?

no

p := prossimo

stop

FIGURA 6 Flow chart dell'algoritmo dello "stipendio totale modificato"

- 17 -

3 Cenni sullarchitettura calcolatori

dei

Alla domanda che cos un calcolatore? possibile rispondere in molte maniere diverse, ciascuna relativa al diverso punto di vista dal quale si guarda alloggetto in questione. In questo corso il punto di vista prevalente sar quello che consente di rispondere alla domanda in questa maniera: un calcolatore una macchina programmabile, per tramite della quale possibile scrivere ed eseguire programmi. Potremmo anche dire che un calcolatore una macchina che tramite i programmi permette di risolvere problemi algoritmici. Ladozione di questo punto di vista non ci impedir di considerare di tanto in tanto, e soprattutto a scopo esemplificativo, altri punti di vista: per esempio quello di un semplice utente, per il quale un calcolatore una macchina che esegue programmi. Daltronde, dal punto di vista tecnologico, un calcolatore un sistema elettronico molto complicato.

3.1 Programmi, applicazioni


Nel senso in cui li useremo durante questo corso, i termini programma e applicazione sono sinonimi. Un programma, in generale, oltre a svolgere determinati compiti (e cio risolvere ad esempio un problema algoritmico), costituir anche linterfaccia tra lutente e il calcolatore. In un programma possibile identificare, in tutta generalit, i seguenti elementi: informazioni gestite dal programma (acquisizione, memorizzazione, visualizzazione); operazioni che possono essere eseguite per manipolare le informazioni. In generale le caratteristiche di un programma consistono in questi pochi fatti fondamentali: un programma permette ai suoi utenti di perseguire un particolare scopo; gestisce un insieme di dati e di informazioni; consente di elaborare le informazioni attraverso operazioni; una particolare operazione pu essere eseguita solo se sono soddisfatte le condizioni che ne abilitano lesecuzione; ciascuna operazione va richiesta con la sua modalit; lutente interagisce con lapplicazione richiedendo lesecuzione di una sequenza di operazioni, una operazione alla volta. La possibilit di eseguire su un medesimo calcolatore applicazioni diverse rende il calcolatore una macchina che pu essere utullizzata da un utente per la risoluzione di problemi

- 18 anche molto diversi tra loro. Per poter risolvere un particolare problema usando un programma, lutente deve essere in grado di fornire al programma stesso le istruzioni dettagliate su come il problema debba essere risolto. In alternativa pu sempre scriversi un programma tutto suo. Dal punto di vista dellutente, con riferimento allesecuzione di un applicazione, le istruzioni che possibile richiedere al calcolatore di eseguire sono quelle corrispondenti alle richieste di esecuzione delle operazioni fornite dallapplicazione. Inoltre ciascun programma pu essere caratterizzato dallinsieme di operazioni che permette di eseguire (e dalle regole per usarle) e dalla tipologia di informazioni che permette di gestire. La capacit di comprendere e usare un programma largamente indipendente dalla comprensione del funzionamento del calcolatore, esattamente come si riesce a guidare la macchina senza sapere nulla sul funzionamento del motore. Ma allo stesso modo in cui consigliabile conoscere almeno le basi del funzionamento del motore per guidare una macchina, cos sarebbe preferibile avere almeno una vaga idea di come funziona un calcolatore, prima di provare a utilizzare o addirittura a scrivere programmi.

3.2 Macchine virtuali


Una Macchina Virtuale una macchina che non c, ma che si comporta come se esistesse. Questaffermazione un po paradossale ha bisogno di essere spiegata. Quando diciamo che la Macchina Virtuale non c intendiamo dire che non esiste allo stato materiale: non possiede leve, interruttori, circuiti, eccetera. Ma al tempo stesso, dal punto di vista dellutilizzatore, come se esistesse: lutilizzatore pu impartire comandi a una Macchina Virtuale, pu girare un programma eccetera. In altre parole una Macchina Virtuale un sistema astratto che si comporta a tutti gli effetti come un vero sistema. Ad esempio, il sistema operativo di un calcolatore una Macchina Virtuale, che usa come base lHardware della Macchina per funzionare. I software applicativi, invece, sono Macchine Virtuali che utilizzano come base sia lHardware che il sistema operativo. Possiamo pensare a un calcolatore come a un sistema gerarchico di Macchine Virtuali: ogni Macchina Virtuale, per funzionare, ha bisogno della Macchina Virtuale di livello precedente. Alla base di questa catena troveremo ovviamente lhardware della Macchina, ossia una Macchina Reale, che alla fine lunico vero motore di qualsiasi programma o applicazione. Ciascuna Macchina Virtuale, ovvero ciascun livello, fornisce una serie di operazioni e di metodi che sono pi semplici, o pi facilmente apprendibili, dei metodi e delle operazioni disponibili al livello inferiore, anche se ovviamente questi metodi semplici sono implementati in termini dei metodi del livello sottostante. Possiamo dire che una Macchina Virtuale ha due interfacce: una che guarda verso linterno (il livello inferiore, pi complicato, pi vicino alla Macchina Reale), e laltra che guarda verso lesterno (il livello superiore se esiste pi semplice, pi intuitivo, pi vicino allutente finale). Per esempio il sistema operativo di un calcolatore una Macchina Virtuale che avvolge le complicazioni dellhardware e presenta allutente uninterfaccia molto probabilmente grafica e facilmente utilizzabile. Lutente sposta il mouse, clicca su unicona e si apre una finestra: cio lutente e il sistema operativo si parlano a un livello molto astratto, molto comprensibile da parte dellutente. Daltro canto, perch alloperazione doppio click su unicona corrisponda la reazione apertura di una finestra, il sistema operativo costretto anche a dialogare con la Macchina Reale, nel linguaggio proprio della Macchina stessa (che appunto il Linguaggio Macchina: istruzioni codificate sotto forma di una serie di 0 e di 1).

- 19 Come vedremo in seguito, il linguaggio di programmazione Java definisce una sua propria Macchina Virtuale (JVM, ossia Java Virtual Machine) che permette allo stesso programma Java di essere eseguito su piattaforme (hardware) diverse.

3.3 Macchina di Von Neumann


Per comprendere larchitettura generale di un calcolatore faremo uso di un modello, chiamato Macchina di Von Neumann. Ogni calcolatore realmente esistente , dal punto di vista astratto, una macchina di Von Neumann: la differenza consiste nel dettaglio di implementazione delle singole componenti dei calcolatori reali. In altre parole la macchina di Von Neumann modellizza in maniera generale lo schema che ogni calcolatore deve seguire per essere appunto considerato un calcolatore. In una macchina di Von Neumann distinguiamo quattro grandi componenti, che andiamo ad elencare: CPU, ovvero Central Processing Unit, ovvero Unit Centrale di Calcolo: il cuore della macchina, ossia il luogo in cui vengono prese le decisioni sul flusso del programma, vengono coordinate le azioni delle altre componenti del calcolatore e vengono compiute le operazioni. Memoria centrale: il luogo in cui risiedono i programmi in esecuzione e i dati su cui i programmi operano. Bus: la navetta che trasporta le informazioni da una parte allaltra dal calcolatore. Dispositivi di Input/Output: tutto ci che permette linterazione tra lutente e il calcolatore: tastiera, mouse, schermo, stampante, eccetera.

tastiera

mouse

schermo

BUS

Memoria Centrale

CPU

FIGURA 6 Schema di una macchina di Von Neumann.

- 20 -

3.4 Logica e Aritmetica


Come abbiamo gi detto, la CPU costituisce il cuore di un calcolatore. In figura 2 possiamo vedere lo schema generico di una CPU. In esso troviamo: Controller: lunit che controlla il flusso del programma, e che istruisce lALU sulle operazioni da compiere. Nel controller distinguiamo: o registro PC (Program Counter): tiene il conto del numero di operazioni o o effettuate; registro IR (Instruction Register): contiene, in forma binaria, il codice della prossima operazione da effettuare; registro PSW (Processor State Word): contiene lo stato della macchina (se c

stato un errore in unoperazione, per esempio); ALU (Arithmetic-Logic Unit, ovvero Unit Aritmetico-Logica): il luogo in cui effettivamente avvengono le operazioni; Register Stack (pila dei registri): i registri contengono i dati su cui lALU opera; MAR (Memory Address Register): contiene lindirizzo di memoria da cui estrarre il prossimo dato su cui si deve operare o in cui salvare il risultato di unoperazione gi effettuata; MDR (Memory Data Register): registro di transito per il valore prelevato dalla memoria, prima che venga scaricato nel Register Stack, o sulla strada contraria (dal Register Stack alla memoria).

MAR

PC IR

Controller PSW

MDR

register register register register register

ALU

FIGURA 6 Schema di una CPU

- 21 Una CPU dei nostri giorni contiene una quarantina di milioni di transistor. I transistor sono assemblati a gruppi per formare le cosiddette porte logiche, che implementano le operazioni logiche come AND, OR, NOT e cos via. Quel che vogliamo vedere adesso lidea del funzionamento di una porta logica. Non scenderemo in dettaglio nel modo di funzionamento di un transistor, ma ci accontenteremo dellidea di base, e useremo un modello formato da un circuito elettrico, due interruttori e una lampadina, e quello che ci domanderemo : possiamo costruire il circuito in modo che si comporti come un operatore AND, oppure come un operatore OR? Ricordiamo brevemente la definizione degli operatori AND e OR. Abbiamo a che fare con quantit (o proposizioni) che possono avere solo due valori: Vero (V) e Falso (F). Chiamando P e Q due di queste quantit, ci chiediamo quale sia il valore di P AND Q e di P OR Q. La proposizione (P AND Q) sar vera solo se sono vere entrambe le quantit P e Q, mentre la proposizione (P OR Q) sar vera se anche una sola delle due quantit vera. In altre parole otteniamo la seguente tabellina: P V V F F Q V F V F P AND Q V F F F F V V V P OR Q

Vogliamo ora vedere come costruire dei circuiti che implementino le porte logiche AND e OR. Consideriamo il circuito in figura 3. Notiamo che i due interruttori sono in serie: come risultato avremo che la corrente nel circuito scorrer solo se gli interruttori saranno entrambi chiusi. Se interpretiamo (interruttore chiuso) = Vero e (interruttore aperto) = Falso, abbiamo che la lampadina si accende (cio il risultato Vero) in corrispondenza di un AND logico tra gli interruttori.

- 22 -

FIGURA 6 Circuito elettrico che implementa una porta logica AND

Nel caso della figura 4, invece, gli interruttori sono in parallelo. Questo significa che per far accendere la lampadina sar necessario chiudere solo uno dei due interruttori, e ci comporta che il circuito implementa la porta logica OR.

FIGURA 6 Circuito elettrico che implementa una porta logica OR

Nella CPU di un calcolatore non ci sono circuiti come quelli che abbiamo appena presentato: ci sono per dei circuiti integrati basati sui transistor, che effettuano lo stesso tipo di operazioni. Esistono altri tipi di porte logiche oltre AND e OR: per gli scopi che ci prefiggiamo vogliamo ricordare solo loperatore logico NOT, che applicato a una proposizione ne cambia il valore di verit (ossia NOT Vero = Falso e viceversa), e lOR Esclusivo, o XOR, che a differenza dellOR inclusivo vero solo se le proposizioni P e Q sono una vera e laltra

- 23 falsa. Il motivo per il quale ci stiamo interessando ai circuiti logici che le operazioni che lALU pu compiere sui bit sono implementate con circuiti logici. Pi tardi daremo un semplice esempio di circuito logico che permette di sommare due numeri scritti in notazione binaria. Prima di arrivare a tanto dobbiamo spendere due parole sul modo in cui il calcolatore gestisce i numeri. Noi esseri umani siamo abituati fin dalla tenera infanzia a contare in base 10. I personaggi dei cartoni animati probabilmente usano una base 8, perch hanno generalmente 4 dita per mano. Ma che significa in realt contare in una certa base B? Significa che per rappresentare un numero qualsiasi abbiamo a disposizione esattamente B simboli (cifre), la posizione delle quali allinterno del numero assume un significato particolare (notazione posizionale): una cifra nella k-esima posizione (a partire da destra) significa che nello sviluppo del numero che stiamo considerando quella cifra moltiplica la (k-1)-ma potenza della base B. In base 10 abbiamo a disposizione 10 simboli, che sono: 0, 1, 2, 3, 4, 5, 6, 7, 8 e 9. Se scriviamo il numero 1234, in realt stiamo intendendo:

1234 = 1 * 103 + 2 * 102 + 3 * 101 + 4 * 100

Nel seguito, per non confonderci, quando scriveremo un numero in una certa base B lo scriveremo in questo modo: numeroB , e quindi 1234 in base 10 lo scriveremo 123410 . Ora, in un calcolatore la quantit minima di informazione chiamata bit, che una contrazione di binary digit, ossia cifra binaria. Un bit una quantit che pu avere solo due valori: spento o acceso, falso o vero, e quindi in definitiva 0 o 1. Avendo a disposizione solo 2 simboli, un calcolatore costretto a contare in base 2. Vediamo come conterebbe un calcolatore, e partiamo da 02. Il prossimo numero 12. E dopo gi sorgono i problemi, perch non abbiamo a disposizione il simbolo 22. Per analogia, cosa succede in base 10 quando arriviamo al limite dei simboli, cio quando arriviamo a 9? Subito dopo abbiamo 10, ossia riportiamo a 0 il contatore delle unit e incrementiamo quello delle decine (ossia passiamo alla potenza di 10 superiore). In base 2 dobbiamo fare esattamente la stessa cosa, quindi

210 = 102

Continuando a contare, dobbiamo incrementare il contatore delle unit, quindi arriviamo a 310 = 112, e poi abbiamo di nuovo esaurito i simboli, e dobbiamo scalare verso sinistra di unulteriore potenza della base, ottenendo 410 = 1002. Rivediamo ancora il tutto con un esempio: abbiamo il numero in base 2

100101112

e vogliamo sapere quant questo numero in base 10. Dobbiamo partire da destra,

- 24 esattamento come abbiamo fatto per il caso 1234 in base 10, e contare le potenze di 2 (ossia di 102):

100101112 = 1 * 1002 + 1 * 1012 + 1 * 1022 + 0 * 1032 + 1 * 1042 + 0 * 1052 + 0 * 1062 + 1 * 1072,

Occorre stare attenti a considerare che 1 * 1072 in realt significa, in decimale, 1 * 2710. Quindi in definitiva

100101112 = 15110.

Possiamo anche porci la domanda al contrario: dato un numero in base 10, come possiamo esprimerlo in base 2? Vediamolo con un esempio: abbiamo il numero 104, e vogliamo esprimerlo in base 2. Dobbiamo prima di tutto chiederci qual la pi grande potenza di 2 minore o uguale a 104: la risposta ovviamente 64, cio 26. Possiamo quindi scrivere

104 = 64 + 50.

A questo punto operiamo ricorsivamente, e ci chiediamo qual la pi grande potenza di 2 minore o uguale a 50. La risposta 32 (25). Scriviamo quindi

104 = 64 + 32 + 18.

Siamo arrivati al 18, che possiamo scomporre, in potenze di 2, come 16 + 2. In definitiva abbiamo

104 = 64 + 32 + 16 + 2 = 26 + 25 + 24 + 21

Per scrivere il nostro numero in binario partiamo da destra: notiamo che nello sviluppo

- 25 non c la potenza 20, quindi la prima cifra a destra sar 0. Poi c un 1, perch troviamo 21 nello sviluppo: quindi vediamo che mancano sia 22 che 23. In definitiva otteniamo

10410 = 11100102.

Dal punto di vista del calcolatore, unaltra base importante la base 16, o esadecimale. Per contare in base 16 abbiamo bisogno di 16 simboli (ovvero 16 cifre) per costruire un qualsiasi numero: occorre quindi introdurre nuovi simboli rispetto alle consuete cifre da 0 a 9. I simboli necessari sono fornite dalle prime lettere dellalfabeto latino. Abbiamo quindi la seguente corrispondenza:

A16 = 1010 B16 = 1110 C16 = 1210 D16 = 1310 E16 = 1410 F16 = 1510

Perch importante la base 16? Perch il contenuto di un byte (ossia di una serie consecutiva di 8 bit) pu essere scritto come una coppia di numeri esadecimali, nel modo che andiamo adesso a vedere: prendiamo 8 bit (ossia, come abbiamo detto, un byte) e dividiamolo in una parte inferiore (i quattro bit a destra) e in una parte superiore (i quattro bit a sinistra).

Notiamo che il numero pi grande che possiamo inserire in quattro bit 11112, che corrisponde a 1510 e quindi in definitiva a F16. Ossia abbiamo che il contenuto di 4 bit pu essere espresso come una singola cifra esadecimale. Vediamo quindi che possiamo scrivere i quattro bit pi a destra come

10112 = 1110 = B16,

e i quattro bit pi a sinistra come

11012 = 1310 = D16.

Abbiamo quindi che lintero byte pu essere scritto come DB16. A questo punto

- 26 potremmo domandarci se quel che abbiamo fatto pienamente legale, ossia se dal punto di vista del numero completo vero che 1101101116 = DB16. La risposta s, e la dimostrazione lasciata come esercizio. Si noti che tutto questo reso possibile dal fatto che 16 una potenza esatta di 2.

Possiamo ora discutere come lALU riesca per esempio a sommare 2 numeri interi, scritti in forma binaria, utilizzando sostanzialmente solo le porte logiche che operano sui bit che compongono i numeri. Ripetiamo che nella CPU sono presenti microcircuiti che sebbene molto diversi in pratica dai circuiti elettrici con la lampadina che abbiamo presentato prima, operano per con la stessa idea di base. Introduciamo ora i seguenti simboli grafici per rappresentare le porte logiche: Porta AND:

p q

p AND q

Porta OR:

p q
Porta XOR:

p OR q

p q

p XOR q

Vediamo ora tutte le possibilit che ci si presentano quando dobbiamo sommare due bit P e Q: P 0 1 0 1 Q 0 0 1 1 P+Q 0 1 1 10

Notiamo subito che nel quarto caso otteniamo come risultato 2 bit (10), un po come quando sommiamo due numeri a una cifra in base 10 e otteniamo un risultato maggiore di 10. Per motivi di simmetria aggiungiamo uno zero (ininfluente) ai primi tre risultati, ottenendo la tabella

- 27 -

P 0 1 0 1

Q 0 0 1 1

P+Q 00 01 01 10

Ora, possiamo chiamare il bit pi a destra SOMMA e il bit pi a sinistra RIPORTO, ottenendo lulteriore tabella:

P 0 1 0 1

Q 0 0 1 1

SOMMA 0 1 1 0

RIPORTO 0 0 0 1

Notiamo subito che la SOMMA si comporta come se fosse stata ottenuta da una porta XOR, mentre il RIPORTO si comporta come se fosse stato ottenuto da una porta AND. Possiamo quindi disegnare il seguente circuito, che avr come effetto (output) quello di sommare (con riporto) i due bit in ingresso (input):

p q

SOMMA

RIPORTO

FIGURA 6 Schema dell'addizionatore Incompleto

A partire dai due elementi XOR e AND, che ottengono rispettivamente la SOMMA e il RIPORTO, possiamo costruire un primo circuito integrato che chiameremo Addizionatore Incompleto (o AI):

AI

- 28 Laddizionatore incompleto perch ci permette s di sommare due bit, ma non ci permette di sommare numeri binari formati da un numero arbitrario di cifre. Infatti, dopo aver sommato le due cifre a destra, e aver ottenuto un eventuale riporto, ci troviamo nella condizione di dover sommare tre cifre binarie: il circuito che permette di sommare tre cifre binarie quello raffigurato in Figura 6: la linea R(in) porta il riporto di una precendente operazione di somma tra due bit. LAI in basso a sinistra somma le due cifre successive del numero binario, p2 e q2. La somma di questa operazione va a sommarsi, nellAI al centro, con R(in), producendo S(out) (la somma dei due bit pi il riporto della somma precedente) e un riporto che dobbiamo ancora sommare col riporto della somma dei secondi due bit. E facile vedere, facendo una tabellina di tutti i casi possibili, che la linea che esce in basso dallAI centrale e la linea che esce in basso dallAI di sinistra non possono essere tutte e due uguali a 1: quindi per sommarle baster farle entrare in un circuito OR, che in questo caso specifico si comporta come uno XOR, cio come un addizionatore.

R(in) p2 q2 AI AI

S(out) R(out)

FIGURA 6 Schema dell'Addizionatore Completo

Dovrebbe essere chiaro a questo punto che assembrando di seguito diversi circuiti come quelli mostrati in figura 6 possibile, tramite solo operazioni logiche sui bit, sommare numeri interi di grandezza qualsiasi.

- 29 -

4 Introduzione a Java

4.1 Linguaggi di programmazione


I primi calcolatori, negli anni 50 del secolo scorso, potevano venir programmati solo in Linguaggio Macchina, ossia inserendo ogni istruzione direttamente come quel particolare codice numerico che il calcolatore in grado di interpretare come listruzione stessa. Il primo linguaggio di programmazione degno di questo nome stato lAssembler: lAssembler molto vicino al Linguaggio Macchina, ma ha il vantaggio che le istruzioni sono rappresentate da brevi sigle (generalmente di tre o quattro lettere) mnemoniche. Per esempio, listruzione di somma viene rappresentata dalla sigla ADD, listruzione di salto (lequivalente del GOTO) dalla sigla JMP. LAssembler, oltre a non essere di facile interpretazione, non un linguaggio strutturato, e anche se i programmi scritti in Assembler risultano di solito molto efficienti (le istruzioni hanno un rapporto uno a uno con le operazioni che la CPU in grado di compiere, ed per questo motivo che i sistemi operativi dei calcolatori - ossia quel programma che rappresenta linterfaccia primaria tra lhardware della macchina e il mondo esterno - sono stati per molto tempo scritti in Assembler) sono per anche difficilmente decodificabili e modificabili. Ben presto la necessit di avere linguaggi ad alto livello, ossia pi simili ai linguaggi umani che al linguaggio binario della macchina, e che quindi lasciassero ai programmatori la possibilit di scrivere programmi pi leggibili e pi facilmente modificabili, ha portato alla creazione di linguaggi specializzati: il FORTRAN per i calcoli scientifici, il COBOL per i programmi finanziari, il PASCAL (molto valido dal punto di vista didattico), e molti altri. Una prima rivoluzione nel campo della programmazione si avuta con lavvento del linguaggio C: un linguaggio di alto livello, strutturato ed efficiente, in grado di sostituire lAssembler per la creazione e la scrittura di sistemi operativi. La potenza del C anche il fattore che lo rende un linguaggio difficile da maneggiare con disinvoltura: abbastanza facile, per il programmatore non particolarmente esperto, commettere errori devastanti. Con il C++ poi, unevoluzione del C, entriamo finalmente nel campo dei linguaggi orientati agli oggetti, che rappresentano la seconda rivoluzione informatica degli scorso decennio.

4.2 Java
Uno dei problemi incontrati da chi scrive software quello della portabilit: idealmente si vorrebbe che un determinato programma, scritto in un determinato linguaggio di programmazione, possa girare su qualsiasi piattaforma, ossia su qualsiasi tipo di hardware con qualsiasi sistema operativo. Di fatto questo obiettivo molto difficile da realizzare: occorre tenere presente che un programma scritto, poniamo, in C, prima di poter essere capito e quindi eseguito dalla macchina ha bisogno di essere compilato: ossia occorre trasformare le

- 30 istruzioni ad alto livello (scritte in un linguaggio molto simile allinglese) in istruzioni di basso livello (codice binario, ossia linguaggio macchina) che la CPU possa comprendere ed eseguire. I dettagli della compilazione naturalmente dipendono dal tipo di macchina su cui si sta lavorando, e ogni hardware diverso richiede in generale istruzioni in linguaggio macchina diverse. Inoltre, cosa da non trascurare data limportanza oramai acquisita dalle interfacce user-friendly dei programmi, laspetto visivo del programma dipende dallinterfaccia grafica utilizzata dal sistema operativo. Java risolve il problema della portabilit eliminando, in qualche senso, la compilazione, o almeno riducendola a una semi-compilazione. Un programma scritto in Java, prima di poter essere eseguito, deve venir trasformato in bytecode, ossia in un formato binario che per non viene direttamente compreso dalla CPU del calcolatore su cui si sta lavorando. Il bytecode, per poter essere eseguito, va dato in pasto alla Java Virtual Machine, che lo traduce al volo in istruzioni che la macchina in grado di interpretare. In altre parole Java introduce una nuova interfaccia (ossia una Macchina Virtuale, vedi capitolo precedente) tra il programma scritto ad alto livello e il Linguaggio Macchina.

4.3 Prolegomena
Per arrivare a eseguire un programma in Java prima di tutto necessario che sul calcolatore siano installati il compilatore Java e la Java Runtime Environment (ossia lAmbiente Java di Esecuzione Programmi). Poi, occorrer scrivere il programma in un file. Per far questo ci si pu servire di un qualsiasi editor di testo (tipo il Notepad). E importante sottolineare che un file Java deve necessariamente avere unestensione .java (cos come i file di Word hanno unestensione .doc). Una volta scritto il programma occorre compilarlo: il compilatore legge il file Java inserito e genera un nuovo file per ogni classe presente nel programma: i nuovi file avranno il nome della classe corrispondente e lestensione .class (questi file di tipo class contengono il bytecode). Dopo aver compilato il programma occorre invocare la Macchina Virtuale di Java per eseguirlo. Per esemplificare il tutto, poniamoci lobiettivo di scrivere un programma che stampi sullo schermo un breve messaggio, per esempio Questo e il mio primo programma in Java. Mettiamoci nel caso concreto e tuttaltro che improbabile in cui il calcolatore su cui stiamo lavorando abbia un qualche tipo di Windows (95, 98, 2000, etc) come sistema operativo. Come prima cosa, per essere ordinati, creiamo una Nuova Cartella sul disco C: chiamandola Corso. Apriamo il Notepad e inseriamo le righe seguenti:

class Esempio { public static void main( String args[] ) { System.out.println( Questo e il mio primo programma in Java ); } }
Abbiamo appena scritto un programma Java. Adesso salviamolo con il nome Esempio.java dentro la cartella Corso appena creata. A questo punto occorre aprire una finestra di comandi del DOS, cambiare directory in modo da posizionarsi nella stessa cartella in

- 31 cui abbiamo salvato il file Java e dare il comando

javac Esempio.java

Se non compaiono messaggi derrore significa che il nostro programma non presenta errori di sintassi, e quindi il compilatore ha potuto produrre il file Esempio.class. A questo punto possiamo invocare la Macchina Virtuale di Java per eseguire il programma:

java Esempio

e nella riga immediatamente inferiore comparir la scritta Questo e il mio primo programma in Java (vedi figura).

Pu essere utile, per scrivere programmi in Java (ma anche in altri linguaggi di programmazione), usare i cosiddetti IDE, ossia Integrated Development Environment (Ambienti Integrati di Sviluppo), cio delle applicazioni che mettono a disposizione tutti gli strumenti che sono necessari per scrivere il programma stesso (quindi un editor di testi), per compilarlo e per eseguirlo. Nel caso del presente corso abbiamo scelto unapplicazione Freeware (ossia gratuita), Jcreator.

- 32 -

4.4 Unocchiata pi attenta al primo programma


Riguardiamo con una certa attenzione il programma che abbiamo appena scritto: per comodit inserir nel listato del programma i numeri di linea, che non devono essere presenti nel codice sorgente che va poi compilato.

1. class Esempio { 2. public static void main( String args[] ) { 3. System.out.println( Questo e il mio primo programma in Java ); 4. } 5. }
Nella prima riga utilizziamo la parola chiave class per dichiarare che tutto quello che segue, dallapertura delle parentesi graffe fino alla chiusura alla riga 5, la definizione di una nuova classe. Esempio un identificatore, che costituisce il nome della classe. La riga 2 contiene linizio del metodo main. Notiamo che il metodo definito come public static void: tutte queste parole chiave verranno definite in seguito. Per il momento non vogliamo occuparci dei particolari, ma solo della struttura generale, che verr poi indagata pi a fondo. Dentro le parentesi tonde, subito dopo main, sono contenuti i parametri che possiamo passare al programma dalla linea di comando. Questi parametri sono sempre contenuti in un vettore (args[]) di tipo String (una stringa, come vedremo meglio in seguito, una sequenza di caratteri racchiusa da doppi apici). Tutti i programmi Java devono avere almeno una classe che contenga un metodo main: il metodo main il punto di ingresso nel programma, e cio quando eseguiamo un programma Java il sistema operativo passa il controllo al programma, che inizia la sua esecuzione eseguendo, una per una, le istruzioni contenute nel metodo main. Come si pu facilmente notare dal listato del programma, il corpo del metodo main , come il corpo della classe che lo contiene, delimitato da parentesi graffe. In Java un gruppo di linee di codice racchiuse tra parentesi graffe rappresentano un blocco di programma. In realt in questo semplicissimo programma il metodo main costituito da una sola istruzione, quella contenuta nella riga 3. In questa istruzione utilizziamo la classe System, tramite un suo oggetto, System.out, un oggetto predefinito della libreria di sistema di Java che permette di inviare un output allo schermo. In questo caso usiamo il metodo println delloggetto System.out: questo metodo accetta come parametro una stringa (la serie di caratteri racchiusa tra doppi apici), e la sua azione consiste nello stamparla sullo schermo. Notiamo subito due cose importanti: la prima, che per invocare un metodo di un certo oggetto si scrive il metodo delloggetto seguito da un punto e poi dal nome del metodo (System.out . println qui gli spazi sono stati inseriti per chiarezza). La seconda che per terminare unistruzione, in Java come in molti altri linguaggi, si usa il punto e virgola: in altre parole unistruzione pu essere scritta su pi righe diverse, con la convenzione che listruzione stessa deve necessariamente terminare con un simbolo di punto e virgola. In questa sezione, analizzando un semplice programma Java, sono stati introdotti velocemente e alla buona una quantit di concetti la cui spiegazione costituir il succo del seguito del corso.

- 33 -

4.5 Tipi di dati, variabili, array


Un linguaggio di programmazione non sarebbe tale se non permettesse di definire delle variabili. Abbiamo gi incontrato, nelle sezioni precedenti, il concetto di variabile in ambito informatico. Una variabile pu essere vista come un contenitore di qualcosa, e quindi deve possedere un nome (per poterci riferire a essa) e un valore (il contenuto). Dovrebbe risultare chiaro che un contenitore adatto a ospitare una variabile intera (un numero senza cifre dopo la virgola) pu non essere adatto a ospitare una variabile con cifre decimali (o come diremo spesso in seguito, una variabile di tipo floating point, ossia in virgola mobile). quindi opportuno introdurre il concetto di tipo di variabile: variabili di un certo tipo possono ospitare solo un certo tipo di oggetti. Esisteranno quindi, dal punto di vista numerico, tipi interi e tipi in virgola mobile. Java definisce otto diversi tipi semplici (o elementari): byte, short, int, long, char, float, double e boolean. Questi tipi possono essere classificati in quattro grandi gruppi: Numeri Interi: byte, short, int e long; Numeri in virgola mobile: float e double; Caratteri: char; Valori logici: boolean.

4.5.1 Tipi interi


Dovrebbe essere chiaro a tutti cosa sia un numero intero. I quattro tipi che Java definisce per contenere numeri interi differiscono tra loro per la quantit di bit impiegata per rappresentare il numero stesso, e quindi in ultima analisi da quanto grande pu essere un numero contenuto in una variabile di un certo tipo. Il tipo byte, il pi piccolo, utilizza un byte1, appunto, per rappresentare un certo numero intero. Poich un byte composto da otto bit, sembrerebbe che il numero pi grande rappresentabile con un byte sia 255 (ossia, in rappresentazione binaria, 11111111). In realt le cose non stanno cos, perch in Java tutti i tipi hanno un segno, e quindi uno degli otto bit usato per decidere se il numero rappresentato dal byte negativo o positivo. Quindi un byte pu contenere numeri interi che vanno da 128 a 127 (in tutto, come ovvio, 256 possibili valori). Un tipo short, invece, utilizza due byte, ossia 16 bit, e pu quindi contenere numeri interi che vanno da 32 768 a 32 767 (perch? E un utile esercizio per lo studente calcolare quanti possibili valori diversi possono assumere 16 bit). Il tipo int, quello pi frequentemente usato per rappresentare numeri interi, pu contenere numeri che vanno da 2 147 483 648 a 2 147 483 647, e a questo scopo utilizza 4 byte, ossia 32 bit. Se c bisogno di rappresentare numeri ancora pi grandi si pu usare il tipo long, che utilizza 64 bit ed in grado di rappresentare numeri interi nellintervallo da 9 223 372 036 854 775 808 a 9 223 372 036 854 775 807.

4.5.2 Tipi in virgola mobile


Esistono due tipi per rappresentare i numeri in virgola mobile, e si differenziano, 1 In Java questo non strettamente vero (lambiente run-time di Java libero di usare la
quantit di bit che preferisce per definire un tipo byte), ma in ogni caso un tipo byte pu contenere solo numeri da 128 a 127, ossia a tutti gli effetti si comporta come se fosse composto da otto bit. Lo stesso vero per gli altri tipi.

- 34 analogamente al caso degli interi, per la grandezza dei numeri che possono contenere. Il tipo float utilizza quattro byte e pu contenere numeri fino a circa 10 38, mentre il tipo double utilizza otto byte e il numero massimo che pu contenere equivale a circa 10308. Ovviamente i due tipi si differenziano anche per la precisione dei calcoli: se bisogna affrontare un problema matematico che richiede una grande accuratezza numerica certamente consigliabile usare il tipo double per rappresentare numeri in virgola mobile.

4.5.3 Il tipo carattere


Il tipo char, in Java, stato studiato al fine di contenere la maggior parte dei caratteri alfabetici, compresi quelli del greco, del cirillico, dellarabo, dellebraico e di alcune lingue nonideogrammatiche dellestremo oriente, come il katakana. Per questo, a differenza del C, in cui il tipo char utilizza un solo byte, in Java il char utilizza due byte.

4.5.4 Tipo logico


Capita molto di frequente, in un programma, di dover stabilire se una condizione sia vera o meno. Il tipo di variabile in grado di contenere il valore di verit di un enunciato deve poter contenere solamente due valori, vero (true) o falso (false). Questo tipo di variabile in Java si chiama boolean: a una variabile di tipo boolean possono essere assegnati solamente i valori true o false.

4.6 Variabili
Allinterno di un programma Java, come per qualsiasi linguaggio di programmazione, la variabile lunita base di memorizzazione di un dato. Una variabile definita dalla combinazione di un identificatore (nome della variabile), un tipo e un inizializzatore opzionale. Inoltre tutte le variabili hanno un campo dazione, che ne definisce la visibilit, e una durata.

4.6.1 Dichiarazione di una variabile


Prima di utilizzare una variabile necessario dichiararla. La forma base per la dichiarazione di una variabile la seguente:

tipo

identificatore

[= valore];

dove le [] indicano una parte opzionale della dichiarazione. Vediamo degli esempi:

int a; int b = 5; float r; double piGreco = 3.141592;

- 35 E possibile dichiarare pi variabili di uno stesso tipo nella stessa riga, separandone i nomi con una virgola:

int a, b=5, c=123; Una variabile pu anche essere definita dinamicamente, come nellesempio che segue:

class Dinamica { public static void main( String args[] ) { double a = 3.0, b = 4.0; double c = Math.sqrt( a*a + b*b ); System.out.println( Ipotenusa = + c ); } }
In questo semplice programma vengono dichiarate tre variabili locali, a, b e c. a e b vengono inizializzate ai valori rispettivamente 3.0 e 4.0 durante la dichiarazione, mentre c inizializzata dinamicamente (Math.sqrt(x) una funzione matematica che ritorna la radice quadrata dellargomento x).

4.6.2 Ambito di visibilit e durata delle variabili


Java consente di dichiarare una nuova variabile in qualsiasi punto del programma, con lavvertenza che lambito di visibilit di una variabile (cio il contesto allinterno del quale la variabile visibile e utilizzabile) corrisponde al blocco di programma nel quale la variabile dichiarata (un blocco comprende anche eventuali sottoblocchi, ed delimitato da parentesi graffe. Ogni gruppo di linee di codice delimitato da parentesi graffe aperte e chiuse un blocco di programma, quindi possiamo dire che la variabile vive finch non viene chiuso il blocco in cui stata dichiarata e definita.

4.6.3 Array, o vettori


Un array (in italiano vettore) un gruppo di variabili dello stesso tipo a cui viene fatto riferimento usando un nome comune, e offre un sistema comodo per raggruppare informazioni correlate. E possibile accedere a un particolare elemento dellarray medianto il relativo indice. Nel corso ci occupiamo esclusivamente di array monodimensionali, anche se in generale possibile definire array con due indici (matrici) o pi. La forma generale per la dichiarazione di un array monodimensionale la seguente:

tipo nome-array[];

in cui tipo definisce il tipo base di ciascun elemento dellarray. Per esempio, se vogliamo dichiarare un array di interi che andr a contenere i giorni in ciascun mese, possiamo scrivere

- 36 int giorniNelMese[];

Occorre fare attenzione al fatto che dopo la dichiarazione di un array in realt ancora non esiste nessun array. Per creare un array infatti occorre dire esplicitamente al compilatore quanti elementi vogliamo raggruppare nellarray, con listruzione:

giorniNelMese = new int[12]; che utilizza la parola chiave int. Dopo questa istruzione esister un array che contiene dodici elementi, ma occorre tenere presente che lindice va da 0 (primo elemento dellarray) a 11 (ultimo elemento dellarray). Quindi se vogliamo dire che i giorni nel mese di gennaio sono 31 dobbiamo scrivere

giorniNelMese[0] = 31; mentre se vogliamo inizializzare il valore relativo a giugno occorre scrivere

giorniNelMese[5] = 30; Un metodo alternativo per dichiarare allallocazione di memoria, il seguente: un array, che unisce la dichiarazione

int giorniNelMese[] = new int[12];

4.7 Stringhe
In Java le stringhe, ossia sequenze di caratteri racchiuse tra doppi apici, non sono tipi semplici, ma sono implementate come una classe. Tuttavia limportanza delle stringhe nella programmazione in generale cos grande che sembra necessario fornire una breve introduzione alle stringhe prima di aver introdotto il concetto di classe. Per dichiarare una variabile di tipo stringa si ricorre allistruzione

String str = Questa e una stringa.;

Da notare che listruzione

System.out.println( Stringa str = + str );

produrrebbe sullo schermo, in questo caso, il seguente output:

- 37 -

Stringa str = Questa e una stringa.

4.8 Istruzioni di controllo


Le istruzioni di controllo che presenteremo in queste dispense sono, per semplicit di esposizione, solo 3 (if, while e for), anche se Java dispone di altre tipologie di istruzioni di controllo. Con le tre istruzioni che stiamo per presentare per possibile scrivere una gran quantit di programmi.

4.8.1 Istruzione if
La forma pi semplice dellistruzione if la seguente:

if ( condizione ) { righe di codice; ... ... } In cui condizione unespressione booleana, che pu essere solo vera o falsa. Quando durante lesecuzione del programma si arriva allistruzione if vista sopra, se condizione vera allora verranno eseguite le righe di codice racchiuse tra la parentesi graffe, mentre se falsa lesecuzione continuer dalla riga seguente la chiusura delle parentesi graffe. Vediamo un semplicissimo esempio di utilizzo di istruzione if:

class EsempioIf { public static void main( String args[] ) { int x = 10; int y = 20; if ( x < y ) { System.out.println( x e minore di y ); } if ( x > y ) { System.out.println( x e maggiore di y ); } } }

- 38 -

Poich 10 minore di 20, la prima condizione vera, e la stringa x e minore di y verr stampata sullo schermo, mentre la seconda condizione falsa, e quindi la stringa x e maggiore di y non verr stampata. Una variante dellistruzione if la seguente

if ( condizione ) { codice1; ... ... } else { codice2; ... ... }


In questo caso se condizione vera vengono eseguite le righe di codice1, mentre se falsa vengono eseguite le righe di codice2. Una terza variante :

if ( condizione1 ) { codice1; ... ... } else if ( condizione2 ) { codice2; ... ... } else if ( condizione3 ) { codice3; ... } ... ... ... } else { codiceAlternativo; ... ... }
Da notare che gli else if possono essere tanti a piacere. Il significato dovrebbe essere chiaro: se la condizione1 vera viene eseguito codice1, altrimenti si passa al prossimo else if, e quindi se la condizione2 vera viene eseguito codice2, e cos via: se nessuna delle condizioni vera verr eseguito il blocco codiceAlternativo relativo a else.

- 39 -

4.8.2 Istruzione for


Con listruzione for in Java vengono implementati i cicli finiti. La forma dellistruzione for la seguente:

for ( istruzioneA; condizione; istruzioneB ) { righe di codice; ... ... }


e funziona in questa maniera. Quando il flusso del programma arriva alla riga contenente listruzione for, la prima cosa che viene fatta lesecuzione dellistruzioneA. Dopo di che viene valutata la condizione: se condizione vera, vengono eseguite le righe di codice racchiuse dalle parentesi graffe. Alla chiusura delle parentesi, il flusso del programma ritorna alla riga che contiene il for, e viene eseguita listruzioneB; dopo di che viene nuovamente valutata la condizione, e se la si trova ancora vera le righe di codice verranno eseguite nuovamente, e cos via. Il ciclo termina quando condizione cessa di essere vera, e lesecuzione del programma continua dalla riga immediatamente seguente la chiusura delle parentesi graffe. Facciamo un esempio:

class EsempioFor { public static void main( String args[] ) { int i; int n = 5; for ( i = 0; i < n; i = i + 1 ) { System.out.println( Il valore di i e: + i ); } } }
Se eseguito, questo programma dar il seguente output:

Il valore di i e 0; Il valore di i e 1; Il valore di i e 2; Il valore di i e 3; Il valore di i e 4;

Chiaramente, quando i diventa uguale a 5 la condizione (i < n) non pi vera, e il ciclo si interrompe. Notiamo che listruzione i = i + 1 pu essere scritta in forma pi concisa come i++. Loperatore ++ ha leffetto di incrementare di uno la variabile intera a cui applicato.

- 40 -

4.8.3 Istruzione while


Con listruzione while in Java vengono implementati i cicli condizionali. La forma di questa istruzione la seguente:

while ( condizione ) { righe di codice; ... ... }


e funziona in questa maniera: quando si incontra listruzione while, viene valutata la condizione, e se la si trova vera vengono eseguite le righe di codice. Alla chiusura delle parentesi graffe il flusso del programma torna allistruzione while e valuta di nuovo la condizione, eseguento nuovamente le righe di codice se condizione ancora vera, e cos via fino a che la condizione diventa falsa. Quando la condizione diventa falsa il programma salta alla riga immediatamente successiva alla chiusura delle parentesi graffe. Come esempio, riscriviamo il programma EsempioFor con un while:

class EsempioWhile { public static void main( String args[] ) { int i = 0; int n = 5; while ( i < n ) { System.out.println( Il valore di I e: + I ); i++; } } }
Loutput lo stesso del programma EsempioFor.

4.9 Operatori
Java offre un ambiente molto ricco per quel che riguarda gli operatori. In generale gli operatori possono essere suddivisi in quattro grandi gruppi: aritmetici, binari, relazionali e logici. Nel seguito descriveremo tutti i gruppi elencati a eccezione degli operatori binari.

4.9.1 Operatori aritmetici


Gli operatori aritmetici vengono utilizzati nelle espressioni matematiche con le stesse modalit con cui vengono impiegati nellalgebra ordinaria. Gli operandi devono essere di tipo numerico (interi, floating point e anche caratteri, dato che il tipo char sostanzialmente un sottoinsieme degli interi). La tabella seguente elenca gli operatori aritmetici disponibili in Java:

- 41 -

OPERATORE + * / % ++ += -= *= /= %= --

RISULTATO Addizione Sottrazione Moltiplicazione Divisione Modulo Incremento Assegnazione addizione Assegnazione sottrazione Assegnazione moltiplicazione Assegnazione divisione Assegnazione modulo Decremento

DESCRIZIONE c = a + b; c = a b; c = a * b; c = a / b; c = a % b; // c il resto della divisione tra a e b c++; // c viene incrementato di 1 c += a; // c viene incrementato di a c -= a; // c viene decrementato di a c *= a; // c viene moltiplicato per a c /= a; // c viene diviso per a c %= a; // c = resto di c/a c--; // c viene decrementato di 1

Tutte le operazioni aritmetiche di base si comportano come ci si potrebbe ragionevolmente attendere. Da ricordare che loperatore Divisione tra interi comporta un risultato intero (senza parte frazionaria).

4.9.2 Operatori di relazione


Gli operatori di relazione determinano, come dice il nome, la relazione che un operando ha con laltro. Pi specificatamente servono a determinare luguaglianza o lordinamento tra due operandi, come illustrato nella seguente tabella: OPERATORE == != > >= < <= RISULTATO Uguale a Diverso da Maggiore di Maggiore di o uguale a Minore di Minore di o uguale a DESCRIZIONE a == b; // true se a uguale a b a != b; // true se a diverso da b a > b; // true se a maggiore di b a >= b; // true se a maggiore o uguale a b a < b; // true se a minore di b a <= b; // true se a minore o uguale a b

Da notare che tutti i risultati di queste operazioni sono valori di tipo boolean, e come tali possono e devono essere utilizzati nelle espressioni che controllano le istruzioni if e i cicli while. E da notare che loperatore di uguaglianza formato da due segni = posti accanto: questo per distinguerlo dalloperatore di assegnazione (un solo =).

4.9.3 Operatori logici


Gli operatori logici che introdurremo in questa sottosezione (non tutti quelli presenti in Java) funzionano solo se applicati a tipi boolean: OPERATORE && || ! RISULTATO AND logico OR logico NOT DESCRIZIONE a && b; // true se a AND b sono veri a || b; // true se a OR b sono veri !a; // true se a false

- 42 -

4.9.4 Precedenza degli operatori


Come nella matematica ordinaria che si fa con carta e penna, anche in Java gli operatori hanno una precedenza: in particolare, in unespressione algebrica, la moltiplicazione e la divisione hanno la precedenza su somma e sottrazione. Lespressione d = a + b * c; viene valutata in questo modo: prima c viene moltiplicato per b, il risultato di questa operazione sommato ad a e il risultato finale messo in d. Se lintenzione quella di moltiplicare per c la somma a+b occorre usare le parentesi tonde: d = (a + b) * c; Occorre prestare attenzione al mescolamento tra divisione e moltiplicazione: avendo le due operazioni la stessa precedenza, vengono eseguite nellordine in cui sono scritte. Quindi lespressione d = a / b * c; molto diversa dallespressione d = a / ( b * c ); In caso di dubbio sempre consigliabile usare le parentesi tonde per definire le precedenze delle operazioni in unespressione algebrica complicata.

- 43 -

5 Introduzione alle classi

La classe il "nucleo" di Java. il costrutto logico su cui si basa tutto il linguaggio Java perch definisce la forma e la natura di un oggetto. In quanto tale, la classe costituisce la base della programmazione orientata agli oggetti in Java. Qualunque concetto si desideri implementare in un programma Java deve essere incapsulato in una classe.

5.1 Concetti fondamentali sulla classe


Le classi sono state utilizzate anche nel capitolo precedente, ma finora stata impiegata solo la loro forma pi rudimentale. Le classi create nel capitolo 4 esistono semplicemente per incapsulare il metodo main(), che stato utilizzato per dimostrare i concetti fondamentali della sintassi di Java. Come verr spiegato successivamente, le classi sono in realt pi potenti di quelle limitate illustrate finora. Forse, il concetto pi importante da comprendere relativamente a una classe che essa definisce un nuovo tipo di dati, che successivamente pu essere utilizzato per creare oggetti di quel tipo. Quindi una classe un modello di un oggetto, mentre l'oggetto un'istanza di una classe. Poich un oggetto un'istanza di una classe, i due termini oggetto e istanza verranno spesso utilizzati in modo intercambiabile.

5.1.1 La forma generale di una classe


Quando si definisce una classe, si dichiarano la sua forma e la sua natura esatte, specificando i dati che essa contiene e il codice che agisce su tali dati. Anche se le classi molto semplici possono contenere solo codice o solo dati, moltissime classi del mondo reale contengono entrambi. Come verr spiegato, il codice di una classe defmisce l'interfaccia ai propri dati. Una classe viene dichiarata mediante l'utilizzo della parola chiave class. Le classi utilizzate finora sono esempi molto limitati della sua forma completa. In realt le classi possono essere molto pi complesse. La forma generale di una definizione di class illustrata di seguito:

class nomeclasse { tipo variabile-istanza1; tipo variabile-istanza2;

...
tipo variabile-istanzaN;

- 44 -

tipo nome-metodo1( elenco-parametri ) { Corpo del metodo1; } tipo nome-metodo2( elenco-parametri ) { Corpo del metodo2; } ... tipo nome-metodoN( elenco-parametri ) { Corpo del metodoN; } }
I dati, o le variabili, definiti allinterno di una classe sono chiamate variabili distanza. Il codice vero e proprio, ossia le istruzioni per operare sulle variabili, e quindi sui dati, contenuto allinterno dei metodi. Nel loro insieme, variabili e metodi di una classe sono chiamati membri della classe. Nella maggior parte dei casi, solo i metodi di una certa classe possono agire sui dati della stessa classe, quindi sono i metodi a determinare in che modo possono essere utilizzati i dati di una classe.

5.2 La classe Persona: i costruttori


Invece di dare nozioni astratte sulle classi, cercheremo di imparare qualcosa sul loro funzionamento attraverso un esempio concreto. Immaginiamo quindi di voler scrivere un programma in Java per implementare una rudimentale ma funzionante agendina del telefono. Per capire di quali classi abbiamo bisogno, dobbiamo considerare il fatto che a ogni oggetto che fa parte del nostro problema dobbiamo assegnare una classe. Occorre dunque prima capire quali tipi di oggetto servono per definire unagenda del telefono. La prima classe che consideriamo verr chiamata Persona: un oggetto di tipo Persona dovr contenere i tipici dati di una persona che finiscono in unagenda del telefono, ossia nome, cognome e numero di telefono. Scriviamo quindi la classe Persona 2:

class Persona { String nome; String cognome; String telefono; Persona( String unNome, String unCognome, String unTelefono ) { nome = unNome; cognome = unCognome; telefono = unTelefono;
2 Nota Bene: durante le prove pratiche di laboratorio la classe Persona stata chiamata Elemento.

- 45 -

} }
E importante ricordare che una dichiarazione class ha il solo effetto di definire un modello, e non di creare un oggetto. Per creare un oggetto concreto di tipo Persona (ossia, come si dice in gergo java, per instanziare la classe Persona) si dovr scrivere, in un metodo di unaltra classe, listruzione

Persona p = new Persona( n, c, t ); in cui n, c e t sono variabili (o letterali) di tipo stringa che contengono rispettivamete il nome, il cognome e il numero di telefono della Persona i dati della quale vanno immessi nellagenda. Per fare un esempio concreto, potremmo scrivere

Persona p = new Persona( Mario, Rossi, 06/1234567 ); oppure

String n = Mario; String c = Rossi; String t = 06/1234567; Persona p = new Persona( n, c, t );

In entrambi i casi il risultato netto sar che la variabile p.nome conterr la stringa Mario, p.cognome la stringa Rossi e p.telefono la stringa 06/1234567. Notiamo che nella classe Persona c un metodo che ha lo stesso nome della classe, a cui passiamo tre variabili di tipo stringa: questo metodo si chiama costruttore, e viene usato appunto per costruire loggetto. In altre parole quando scriviamo new Persona( n, c, t ) stiamo chiamando il metodo Persona della classe Persona, ossia il costruttore della classe.

5.3 Utilizzo della classe Persona: la classe Agenda


Unagenda in s stessa si configura come una lista (ordinata) di nomi di persone con accanto il loro numero di telefono. Nel nostro caso quindi dobbiamo costruire una classe Agenda che contenga una tale lista, ed ovvio implementare questa lista con un vettore che contiene un certo numero di oggetti di tipo Persona. Quando usiamo unagenda possiamo sostanzialmente fare tre cose: 1. inserire un nuovo numero di telefono; 2. cercare il numero di telefono di qualcuno;

- 46 3. sfogliare lagenda. Nello scrivere la classe Agenda dovremo creare tre metodi che implementino nel mondo di Java le tre azioni che possiamo compiere nel mondo reale.

class Agenda { private Persona lista[]; final int MAX = 20; int n; Agenda() { lista = new Persona[MAX]; n = 0; } public void inserisci( String ilNome, String ilCognome, String ilTelefono ) { if ( n < MAX ) { lista[n] = new Persona( ilNome, ilCognome, ilTelefono ); n++; } else { System.out.println( "*** ERRORE: impossibile aggiungere numeri di telefono." ); System.out.println( " L'Agenda e' piena" ); } } public void stampa() { System.out.println( "\n\nLista dei numeri presenti nell'Agenda:\n\n" ); for ( int i = 0; i < n; i++ ) { System.out.println( "\t" + lista[i].nome + " " + lista[i].cognome + "......." + lista[i].telefono ); } System.out.println( "\n" ); } public void cerca( String ilCognome ) { int i; boolean trovato = false; for ( i = 0; i < n; i++ ) { if ( lista[n].cognome.equals( ilCognome ) ) { System.out.println( "\t" + lista[i].nome + " " + lista[i].cognome + "......." + lista[i].telefono ); trovato = true;

- 47 -

} } if ( ! trovato ) { System.out.println( ilCognome + non presente nellAgenda ); } } }


Notiamo prima di tutto che la classe Agenda contiene tre variabili distanza: il vettore lista, che contiene oggetti di tipo Persona, e che dichiariamo come private perch non vogliamo che venga usato direttamente allesterno di questa classe: il numero intero MAX, che indica il numero massimo di persone che possiamo inserire nellAgenda, dichiarato come final, che equivale a dire che il suo valore (20) non potr essere cambiato: il numero intero n che indica quante persone abbiamo effettivamente inserito nellagenda. Il costruttore Agenda() non riceve nessuna variabile in ingresso, e si limita a:

dire che la quantit di numeri di telefono inseriti pari a zero (n = 0); creare il vettore lista. Il metodo inserisci dichiarato public, perch deve poter essere chiamato

dallesterno di questa classe, e void, cio non restituisce nessun oggetto; riceve in ingresso tre variabile di tipo stringa (ilNome, ilCognome, ilTelefono). Prima di tutto questo metodo controlla che lagenda non sia piena: quindi if ( n < MAX ) crea un nuovo oggetto di tipo Persona, usando le tre variabili di tipo stringa ricevute dal metodo, e lo inserisce nella n-esima posizione del vettore lista (la posizione corrente). Dopo queste operazioni incrementa il valore di n (perch abbiamo aggiunto una persona allagenda). Se invece lagenda gi piena (perch abbiamo gi inserito 20 numeri) si limita a segnalare lerrore sullo schermo. Il metodo stampa molto semplice. Si limita a eseguire un ciclo for che ha come effetto quello di stampare sullo schermo i nomi, i cognomi e i numeri di telefono delle persone presenti nellagenda. Il metodo cerca invece il pi complicato dei tre. Notiamo che riceve in ingresso un oggetto di tipo stringa (ilCognome) che rappresenta il cognome da cercare nellagenda. Inizializza una variabile boolean (trovato) al valore false, per indicare che il cognome che cerchiamo non stato ancora trovato. Dopo di che inizia un ciclo for per scorrere tutte le persone presenti nellagenda, e controlla se il cognome delli-esima Persona corrisponde al cognome (ilCognome) che vogliamo cercare. Per svolgere questo controllo utilizza il metodo equals( String s ) della classe String. Notiamo infatti che lista[i].cognome un oggetto di tipo String. A questo oggetto applichiamo il metodo equals( ilCognome ) che restituisce un valore true se lista[i].cognome (la stringa oggetto al quale stiamo applicando il metodo equals) uguale a ilCognome (la stringa che stiamo passando come parametro al metodo equals), e restituisce un valore false altrimenti. Quindi if ( lista[n].cognome.equals( ilCognome ) ) allora scriviamo sullo schermo il nome, il cognome e il numero di telefono della persona trovata e mettiamo uguale a true la variabile

- 48 trovato, per segnalare che abbiamo trovato il cognome che cercavamo.

5.4 Unire le classi in un programma: Rubrica


Finora, nella nostra esplorazione del programma di agenda telefonica, abbiamo visto la classe Persona e la classe Agenda: tempo di mettere tutto quanto insieme e di costruire una classe che contenga un metodo main (punto di inizio del programma) e che usi in maniera appropriata le classi che abbiamo appena costruito. Presentiamo quindi la classe Rubrica:

import java.io.*; class Rubrica { public static void main( String args[] ) throws IOException { Agenda a = new Agenda(); Reader r = new Reader(); boolean continua = true; while ( continua ) { char c = menu( r ); if ( c == 'q' ) { continua = false; } else if ( c == 'i' ) { inserisci( r, a ); } else if ( c == 'c' ) { //cerca(); } else if ( c == 's' ) { a.stampa(); } } } public static char menu( Reader lettore ) throws IOException { System.out.println( "\n\n System.out.println( " [i] telefono; " ); System.out.println( " [c] System.out.println( " [s] System.out.println( " [q] return lettore.readChar(); } Fai una scelta:\n\n" ); Inserisci nuovo numero di Cerca un numero di telefono;" ); Stampa l'Agenda;" ); Esci dal programma." );

- 49 -

public static void inserisci( Reader lettore, Agenda lAgenda ) throws IOException { System.out.println( "\n\n" ); System.out.print( "Inserisci il Nome: " ); String nome = lettore.readString(); System.out.print( "Inserisci il Cognome: " ); String cognome = lettore.readString(); System.out.print( "Inserisci il Numero di Telefono: " ); String telefono = lettore.readString(); lAgenda.inserisci( nome, cognome, telefono ); } }
Notiamo subito che il file Rubrica.java inizia con una dichiarazione di import;

import java.io.*;

Poich abbiamo bisogno di leggere un input da tastiera, durante il programma, stiamo importando tutte le librerie di input/output. Notiamo anche che stiamo usando la classe Reader (che abbiamo scritto durante le lezioni in Laboratorio) e che non sar descritta in nessun dettaglio. Ai nostri scopi baster dire che un oggetto di classe Reader, propriamente usato, ci permette di leggere numeri e stringhe che inseriamo con la tastiera durante lo svolgimento del programma. Anche le varie clausole di gestione degli errori (throws IOException) non saranno discusse. La cosa importante da notare in questa classe il fatto che serve come un semplice contenitore del metodo main, che a sua volta utilizza un metodo menu (cio un metodo che permette allutente di scegliere quale operazione compiere, di volta in volta: inserimento, ricerca o stampa.

5.5 La classe String


Le stringhe sono cos importanti, in generale, nella programmazione, che non sembra inutile guardare con un certo dettaglio come vengono gestite da Java. Java definisce la classe String che permette di manipolare stringhe, ossia sequenze di caratteri delimitati da doppi apici. Cos se vogliamo assegnare alla variabile s il valore Mario Rossi possiamo usare le seguenti alternative:

String s = Mario Rossi; String s = new String( Mario Rossi );

- 50 Esistono altri metodi per inizializzare una stringa, che qui per non prenderemo in considerazione. Quel che vogliamo ottenere la conoscenza di alcuni metodi che possiamo applicare a un oggetto di tipo String per ottenere certi risultati.

5.5.1 Il metodo length()


Il metodo length() permette di conoscere la lunghezza di una stringa, ossia quanti caratteri contiene. Ad esempio, se abbiamo String s = Mario Rossi; int lunghezza = s.length();

la variabile lunghezza conterr il valore 11 (tanti quanti sono i caratteri di Mario Rossi, spazio incluso ovviamente).

5.5.2 Il medodo indexOf( char c );


Questo metodo permette di conoscere in che posizione compare, se compare, il carattere c in una stringa. Ad esempio, se s la solita stringa Mario Rossi,

int i = s.indexOf( M );

dar come risultato che la variabile i contiene il valore 0 (le posizioni dei caratteri partono da 0 e arrivano a N-1, se N la lunghezza della stringa), mentre listruzione

int j = s.indexOf( r );

far s che j contenga il valore 2. Da notare che

int k = s.indexOf( s );

dar come risultato 8 (cio viene considerata la prima s della stringa) mentre listruzione

int q = s.indexOf( z );

dar come risultato 1 (perch il carattere z non contenuto nella stringa).

5.5.3 Il metodo lastIndexOf( char c );


Questo metodo funziona come indexOf, solo che restituisce la posizione in cui il

- 51 carattere c compare per lultima volta nella stringa. Quindi

int i = s.lastIndexOf( s );

restituir il valore 9;

5.5.4 Il metodo charAt( int i );


Questo metodo restituisce il carattere nella posizione i-esima. Quindi

char c = s.charAt( 1 );

far s che la variabile c contenga il valore a.

5.5.5 Il metodo substring()


Questo metodo pu essere usato in pi di un modo (ossia in gergo Java si dice che sovraccarico) e in ogni caso serve ad estrarre un porzione di stringa da una stringa data. Un primo modo di usarlo il seguente:

String cognome = s.substring( 6 ); Se passiamo una sola variabile intera al metodo substring, diciamo k, il metodo restituisce la sottostringa che parte dal carattere nella posizione k-esima. Nel caso dellesempio sopra la variabile cognome conterr la stringa Rossi. Un altro modo di utilizzare substring il seguente:

String nome = s.substring( 0, 4 );

I due numeri interi rappresentano lindice iniziale e lindice finale della sottostringa da estrarre. In questo caso nome conterr Mario.

5.5.6 Il metodo trim()


Questo metodo consente di eleminare da una stringa tutti gli spazi vuoti eventualmente presenti allinizio e alla fine della stringa stessa. Quindi

String sSpazi =

Mario Rossi

String s = sSpazi.trim();

far s che s contenga solamente Mario Rossi.

- 52 -

5.5.7 I metodi toLowerCase(), toUpperCase()


Sono metodi che servono a trasformare una stringa tutta in minuscolo (toLowerCase) o tutta in MAIUSCOLO (toUpperCase);

String minuscolo = s.toLowerCase(); // restituisce mario rossi String maiuscolo = s.toUpperCase(); // restituisce MARIO ROSSI;

5.5.8 Concatenare stringhe


Per concatenare due o pi stringhe si pu usare loperatore +.

String nome = Mario; String cognome = Rossi; String s = nome + + cognome; // restituisce Mario Rossi

5.5.9 Il metodo equals();


Come abbiamo gi visto il metodo equals serve a comparare due stringhe, e ritorna true in caso le due stringhe da comparare siano uguali, false in caso contrario. Quindi

boolean b = s.equals( Mario Rossi ); // restituisce true boolean b = s.equals( MARIO ROSSI ); // restituisce false

Se vogliamo comparare due stringhe indipendentemente dalle maiuscole e minuscole dobbiamo usare equalsIgnoreCase:

boolean b = s.equalsIgnoreCase( MARIO ROSSI ); // restituisce true

5.5.10Il metodo compareTo()


Questo metodo serve a capire lordine alfabetico di due stringhe. Ad esempio

int i = s.compareTo( Mario Rossi );

restituisce valore 0 (le due stringhe sono uguali), mentre listruzione

int i = s.compareTo( Giuseppe Verdi );

restituisce un valore > 0 (perch s, cio Mario Rossi, alfabeticamente maggiore di Giuseppe Verdi). Al contrario,

- 53 -

int i = s.comapreTo( Sergio Bianchi );

restituisce un valore < 0.