Capitolo 1 - Gianluca Rossi

Gianluca Rossi
P ROGRAMMAZIONE DEI CALCOLATORI
APPUNTI DELLE LEZIONI
D
RA
FT
(ottobre 2014)
D
RA
FT
Capitolo 1
RA
FT
Problemi, algoritmi, macchine e
linguaggi di programmazione
D
Il computer, nelle sue numerose incarnazioni, e` diventato un elemento essenziale
della societ`a moderna ed indispensabile strumento della nostra quotidianit`a. L’uso del
calcolatore, infatti, e` uscito dai campi tradizionali del calcolo scientifico per entrare
in tutte le aree della nostra quotidianit`a.
Regna, tuttavia, una certa ambiguit`a sul concetto diffuso di informatica e, per questo, e` importante capire che cosa essa non e` . Chiunque intenda intraprendere percorsi
formativi universitari nel campo dell’informatica deve sapere che questa disciplina ha
poco in comune con l’“alfabetizzazione informatica” ovvero saper usare un computer
per scrivere un testo, navigare in Internet, usare la posta elettronica o installare una
stampante: sarebbe come dire che studiare astrofisica consista nell’imparare a usare un telescopio. Allo stesso modo, l’informatica non consiste semplicemente nello
scrivere programmi per calcolatore, anche se e` naturale aspettarsi da un informatico
la capacit`a di farlo in modo corretto ed efficiente.
In base alla definizione fornita dal gruppo di lavoro dell’Association for Computing Machinery nel 1989, l’informatica e` “lo studio sistematico dei processi algoritmici che descrivono e trasformano l’informazione: la loro teoria, analisi, progettazione, efficienza, implementazione e applicazione.” Da questa definizione risulta
chiaro che l’informatica e` un complesso di conoscenze scientifiche e tecnologiche
che permettono di realizzare quello che si potrebbe chiamare metodo informatico o
algoritmico: cos`ı come il metodo scientifico pu`o essere riassunto nel formulare ipotesi che spieghino un fenomeno e nel verificare tali ipotesi mediante l’esecuzione di
esperimenti, il metodo informatico consiste nel formulare algoritmi che risolvano un
problema, nel trasformare questi algoritmi in sequenze di istruzioni (programmi) per
le macchine e nel verificare la correttezza e l’efficacia di tali programmi analizzandoli
ed eseguendoli.
PdC
4
Capitolo 1 – Problemi, algoritmi, macchine e linguaggi di programmazione
Il metodo informatico si sviluppa in una serie di fasi successive ce possiamo
schematizzare come segue.
RA
FT
1. Formulazione del modello. Dall’interazione con esperti di uno specifico settore applicativo viene formulato un modello matematico che rappresenti il pi`u
fedelmente possibile il particolare problema oggetto di studio. Questa fase pu`o
risultare particolarmente difficile, a causa soprattutto dei diversi linguaggi che,
generalmente, vengono usati in diversi contesti applicativi.
2. Analisi del modello e progettazione dell’algoritmo. Questa e` probabilmente la
fase pi`u “creativa” dell’intero processo per la quale l’informatico ha a disposizione un bagaglio molto vasto di tecniche di progettazione, ma che comunque
richiede abilit`a e fantasia combinate con rigore e impegno.
3. Implementazione. In questa fase e` necessario conoscere almeno un linguaggio
di programmazione che consenta all’informatico di “descrivere” l’algoritmo
stesso al calcolatore, chiedendo a quest’ultimo di eseguirlo.
4. Verifica del programma. Ha lo scopo di individuare i cosiddetti bug, ovvero errori concettuali e/o implementativi: nel caso tali errori vengano rilevati sar`a necessario modificare la progettazione dell’algoritmo e/o la sua
implementazione.
5. Verifica del modello. Si verifica che le soluzioni ottenute siano veramente utili
per la risoluzione del problema originario: se cos`ı non fosse vorrebbe dire
che la modellazione del problema effettuate durante la prima fase nascondeva
delle debolezze. Occorre, quindi, modificare il modello e ricominciare l’intero
processo.
D
Da quanto detto sopra si evince che l’applicazione del metodo algoritmico richiede delle conoscenze e competenze di vario genere che l’aspirante informatico dovr`a
necessariamente far sue.
• conoscenze matematiche e logico-deduttive, per proporre soluzioni precise e
corrette e per realizzarle in un linguaggio di programmazione;
• conoscenze ingegneristiche, che permettano di saper modellare il problema in
esame, di modulare la soluzione proposta sviluppandola con tecniche che ne
garantiscano la manutenibilit`a;
• conoscenze di carattere interdisciplinare, per essere in grado di sviluppare
strumenti per settori della societ`a tra i pi`u disparati;
PdC
Programmazione dei calcolatori
• conoscenze di carattere etico, per capire le problematiche di sicurezza, riservatezza e legalit`a che insorgono nello sviluppo di tali strumenti.
RA
FT
Trovare il giusto modello. Per chiarire quanto descritto sinora, vediamo un primo
esempio di applicazione del metodo algoritmico, in cui la scelta, non immediata, del
modello implica, quasi automaticamente, lo sviluppo di un algoritmo estremamente
semplice.
Il filosofo Romagnolo del quindicesimo secolo Paolo Guarini ide`o il seguente
rompicapo basato sul gioco degli scacchi.
m0m
2
0Z0
1
M0M
3
a
b
c
M0M
2
0Z0
1
m0m
3
a
b
c
D
Qual e` la sequenza di mosse pi`u breve che consente ai cavalli di passare dalla configurazione a sinistra della figura a quella a destra? Ricordiamo che negli scacchi il
cavallo muove di due posizioni in orizzontale ed una posizione in verticale o viceversa. Nella figura che segue sono rappresentate con cerchio bianco le 8 possibili
posizioni raggiungibili dal cavallo bianco.
Z0Z0Z0Z
0Z0Z0Z0
Z0Z0Z0Z
0Z0M0Z0
Z0Z0Z0Z
0Z0Z0Z0
Z0Z0Z0Z
Una possibile soluzione a questo rompicapo potrebbe essere quella di provare
tutte le possibili sequenze di mosse dei quattro cavalli selezionando tra di esse quella
5
PdC
6
Capitolo 1 – Problemi, algoritmi, macchine e linguaggi di programmazione
RA
FT
pi`u breve che soddisfi i requisiti del rompicapo stesso. Tuttavia, in tal modo ci renderemmo quasi immediatamente conto che il numero di tali possibili sequenze e` molto
elevato e che, quindi, tale algoritmo sarebbe del tutto inutilizzabile da un punto di
vista pratico.
La soluzione di questo rompicapo pu`o essere ottenuta agevolmente se il problema
viene posto in termini diversi ma equivalenti. Da una casella della scacchiera un
cavallo pu`o raggiungere un insieme di caselle determinato dalla regola esposta in
precedenza. Ad esempio se un cavallo si trova nella casella bianca della riga in basso
(casella b1) si pu`o spostare soltanto in una delle due caselle nere della riga in alto
(caselle a3 e c3) a meno che queste non siano gi`a occupate da un altro cavallo.
Quindi esiste una relazione diretta tra la casella b2 e le caselle a3 e c3; tale relazione
rappresenta la possibilit`a per un cavallo di spostarsi tra le caselle in questione. Si
noti che tale relazione e` simmetrica in quanto un cavallo che giace su a3 o c3 pu`o
spostarsi nella casella b1. Questa relazione tra le caselle della scacchiera pu`o essere
descritta graficamente nel modo che segue.
a2
c3
c1
b1
b3
a3
a1
c2
D
Questa rappresentazione evidenzia con una linea la relazione di raggiungibilit`a tra
caselle: un cavallo pu`o spostarsi tra due caselle con una mossa solo se queste sono
adiacenti ovvero collegate con da linea. Si osservi che non compare la casella b2 in
quanto questa non pu`o essere raggiunta da nessun’altra casella.
Ora possiamo posizionare i nostri cavalli sul diagramma a cerchio anzich´e sulla
scacchiera e riformulare il rompicapo di Guarini.
n
a2
N
N
c3
a2
c1
b1
b3
a3
a1
N
c2
n
n
c3
c1
b1
b3
a3
a1
n
c2
N
PdC
Programmazione dei calcolatori
RA
FT
Potendo spostare un cavallo dalla sua casella ad una adiacente libera, qual e` la sequenza di mosse pi`u breve che consente ai cavalli di raggiungere la configurazione a
destra della figura partendo da quella a sinistra?
La rappresentazione grafica del rompicapo di Guarini e la sua successiva riformulazione, ci permettono di progettare abbastanza facilmente un algoritmo risolutivo del rompicapo stesso. A tale scopo, iniziamo con l’osservare che i cavalli devono
muoversi nella stessa direzione, altrimenti, prima o poi, questi saranno costretti ad
incrociarsi e quindi a occupare la stessa casella. In base a tale osservazione, e` naturale progettare il seguente procedimento algoritmico: spostiamo un cavallo alla volta di
una casella in senso orario (oppure antiorario) fintanto che i cavalli bianchi in a1 e c1
vadano rispettivamente in c3 e a3 e i cavalli neri in c3 e a3 vadano rispettivamente
in a1 e c1. Con quattro mosse, i quattro cavalli raggiungono le posizioni finali per
un totale, quindi, di sedici mosse. Di seguito sono mostrate le prime otto mosse che
permettono di “ruotare” la disposizione dei cavalli di 90 gradi in senso anti-orario.
Lo studente pu`o a questo punto dedurre le successive otto mosse che consentiranno
di raggiungere la configurazione finale.
m0m
2
0Z0
1
M0M
3
mNZ
2
NZ0
1
ZnZ
3
Z0M
2
0Zn
1
ZnM
3
3
a
b
c
D
3
a
b
c
3
a
b
c
mNm
2
0Z0
1
Z0M
3
ZNZ
2
NZn
1
ZnZ
3
m0M
2
0Zn
1
Z0M
3
a
a
a
b
b
b
c
c
c
mNm
NZ0
1
Z0Z
2
a
b
c
Z0Z
NZn
1
ZnM
2
a
b
c
m0M
0Z0
1
m0M
2
a
b
c
7
PdC
8
Capitolo 1 – Problemi, algoritmi, macchine e linguaggi di programmazione
RA
FT
L’esempio appena descritto mostra come la formulazione del giusto modello, con
cui rappresentare il problema sotto esame, sia una passo particolarmente importante
del metodo algoritmico, in quanto pu`o rendere la progettazione dell’algoritmo molto
pi`u semplice. Nell’esempio la progettazione dell’algoritmo di rotazione dei cavalli e`
stata una naturale conseguenza del modello grafico adottato per la formulazione del
problema.
Un altro esempio
di questo tipo e` il problema del calcolo della radice quadrata
√
di un numero x: √
x = y se e solo se y2 = x. Questa definizione non ci dice nulla
su come calcolare x; dal punto di vista di un matematico essa e` ineccepibile ma
dal punto di
vista di un informatico che si propone di sviluppare un algoritmo per il
√
calcolo di x e` poco utilizzabile. Per risolvere il problema occorre cambiare modello
e rappresentare la radice quadrata di x in un altro modo. Per fortuna la matematica ci
dice anche che se z0 e` un qualunque numero positivo allora
√
x = lim zn
dove zn =
n→+∞
x
1
zn−1 +
.
2
zn−1
√
Questo modo alternativo di indicare il numero x e` sicuramente meno intuitivo ma
suggerisce un metodo per il calcolo effettivo della radice cercata:
1. Stabilisci un margine di errore eps > 0;
2. Fissa z = x;
3. Fintanto che |z2 − x| > eps assegna a z il nuovo valore
4. Concludi approssimando
1
2
z + zx ;
√
x con il valore di z.
D
Questa descrizione risulta essere abbastanza formale e priva di ambiguit`a tanto da poter essere utilizzata da una persona che non conosce cosa sia la radice quadrata di un
numero ma sia in grado di eseguire operazioni elementari. Non solo, codificata in un
certo modo potrebbe essere eseguita da una macchina in modo del tutto automatico.
Ecco un esempio:
1 eps = 0.0001; z = x;
2 W H I L E ( abs(z*z - x) > eps) {
3
z = (z + x/z)/2;
4 }
5 R E T U R N z;
La riga 1 codifica i primi due punti della prima descrizione: abbiamo scelto un valore
arbitrario per eps, abbiamo definito una variabile z a cui abbiamo assegnato il valore
di x. Le righe da 2 a 4 sono caratterizzate dalla parola chiave WHILE: qui non si
PdC
Programmazione dei calcolatori
RA
FT
fa altro che codificare il terzo punto della precedente espressione ovvero si continua
ad eseguire il corpo del WHILE, vale a dire tutto ci`o che e` racchiuso tra le parentesi
graffe, fintanto che l’espressione tra parentesi tonde risulta essere vera, ovvero il
valore assoluto di z2 − x risulta maggiore di eps. Quando l’espressione tra parentesi
tonde diventa falsa si smette di eseguire la riga 3 e si passa ad eseguire la riga 5 che
e` in qualche modo la stessa cosa dell’ultimo punto della prima descrizione.
L’esempio precedente mostra un primo esempio non banale di algoritmo. Questo
lo possiamo definite come una descrizione dettagliata e non ambigua della sequenza
di operazioni “elementari” che permette, a partire dai dati iniziali (input), di ricavare
in un tempo finito la soluzione (output) del determinato problema. Quando codificato
in maniera opportuna, l’algoritmo diventa un programma che pu`o essere eseguito in
modo automatico da un agente (un calcolatore, un dispositivo meccanico, un circuito
elettronico, e perch´e no, un essere umano).
D
Limiti del metodo algoritmico Per ogni problema e` sempre possibile trovare un
algoritmo che lo risolva? La risposta a questa domanda, ovvero esistono dei problemi per i quali non esistono algoritmi che li risolvano, e` positiva. Uno di questi e` il
problema della fermata. Prima di definire questo problema osserviamo che un programma potrebbe non terminare ovvero divergere. Ad esempio se nell’algoritmo per
il calcolo della radice quadrata dimenticassimo la riga 3 il valore di z resterebbe invariato e quindi il test del WHILE non sarebbe mai falso. Quindi ha senso domandarsi
se un programma con un determinato input converge (ovvero termina) o diverge. Il
problema della fermata lo riassumiamo nel seguente modo: dato un programma P
ed un suo dato di ingresso x e` possibile stabilire in maniera automatica se P con input x termina? Alternativamente, esiste un algoritmo che con dati di ingresso P e x
restituisce TRUE se P con input x termina e FALSE altrimenti?
In alcuni casi e` possibile dimostrare che il programma termina
come nel caso
√
del nostro algoritmo per la radice quadrata: poich´e zn tende a n allora da un certo
punto in poi z2 − x 6 eps per ogni eps positivo.
Dimostreremo che, in generale, non pu`o esistere un algoritmo che sia in grado
di stabilire se un arbitrario programma con un arbitrario dato di ingresso converge.
Come vedremo pi`u avanti nel capitolo, un programma e` codificato mediante una
sequenza di simboli presi dallo stesso alfabeto utilizzato per codificare i dati. Quindi
un programma pu`o essere a sua volta un dato di ingresso di un altro programma o
parte di esso. Supponiamo che esista un programma chiamato Termina che risolve
il problema della fermata, ovvero Termina(P,x) restituisce TRUE se P con input
x termina e FALSE altrimenti. Poich´e questo programma esiste, pu`o essere utilizzato
come sotto-programa di una altro programma.
9
PdC
10
Capitolo 1 – Problemi, algoritmi, macchine e linguaggi di programmazione
Paradosso(P) {
W H I L E ( Termina( P, P ) == T R U E ) {
}
}
RA
FT
Il programma Paradosso con input P termina se e solo se il programma P con input
P non termina. Se, come abbiamo assunto, il programma Termina esiste questo ha
una codifica e quindi anche il programma Paradosso ne ha una. Questa codifica
pu`o essere usata come input per lo stesso Paradosso. Il programma Paradosso
con input la sua codifica, ovvero Paradosso(Paradosso) pu`o terminare oppure
divergere.
• Se Paradosso con input Paradosso termina allora Termina(Paradosso,
Paradosso) restituisce FALSE, ovvero Paradosso con input Paradosso
non termina.
• Se Paradosso con input Paradosso diverge allora Termina(Paradosso,
Paradosso) restituisce TRUE, ovvero Paradosso con input Paradosso
termina.
Concludendo, abbiamo trovato un input per il programma Paradosso che fa
divergere il programma quando questo termina oppure lo fa terminare quando questo
diverge. Ovviamente un programma che si comporta in modo cos`ıbizzarro non pu`o
esistere.
D
Il paradosso e` stato innescato dall’aver assunto l’esistenza del programma Termina,
quindi dobbiamo concludere che questo non pu`o esistere, ovvero che il problema
della fermata non ammette nessun algoritmo. Questo risultato e` stato ottenuto dal
matematico inglese Alan Turing nel 1937 ispirato dal lavoro del logico tedesco Kurt
G¨odel sull’incompletezza dei sistemi formali.
Efficienza degli algoritmi Ora si consideri il problema di determinare se un numero intero n > 2 sia primo o meno. La definizione di primalit`a ci dice che il numero n
e` primo se e solo se e` divisibile solo per 1 e per se stesso. Questa induce un algoritmo
che possiamo descrivere nel seguente modo.
PdC
Programmazione dei calcolatori
d = 2;
W H I L E ( d < n && n % d != 0) {
d = d+1;
}
I F ( d == n ) {
R E T U R N T R U E;
} ELSE {
R E T U R N F A L S E;
}
RA
FT
1
2
3
4
5
6
7
8
9
D
Assegniamo ad una variabile chiamata d il valore 2 (riga 1) e, fintanto che essa non e`
uguale a n e (&& ) non divide n (riga 2), il valore di questa viene incrementato di 1
(riga 3). La condizione del WHILE e` composta da due sotto-condizioni legate dall’operatore && (si legge and): la condizione e` vera se e solo se entrambe le condizioni
che la compongono sono vere, vale a dire se d < n e, allo stesso tempo, d non divide
n ovvero il resto della divisione di n per d e` diverso da (!=) 0. Quando la condizione
del WHILE diventa falsa (d = n oppure d divide n) usciamo dal ciclo WHILE ed eseguiamo la riga 5 dove viene eseguita l’istruzione IF che funziona nel modo che segue:
se la condizione tra parentesi tonde e` vera viene eseguita l’istruzione all’interno della
prima coppia di graffe che segue (riga 6) altrimenti viene eseguita l’istruzione all’interno della seconda coppia di graffe, quelle dopo ELSE, (riga 8). Infatti, se all’uscita
del WHILE il valore della variabile d e` n vuol dire che non abbiamo trovato divisori di
n e quindi concludiamo dicendo TRUE, ovvero n e` primo. Altrimenti il ciclo WHILE
e` stato interrotto perch´e l’ultimo valore di d divide n pertanto concludiamo dicendo
FALSE , ovvero n e` non primo.
Questo algoritmo nel peggiore dei casi esegue n − 2 divisioni. Si osservi che
questo numero potrebbe essere abbassato√drasticamente in quanto almeno uno degli
eventuali divisori di n deve essere al pi`u n (in caso contrario il prodotto risultante
sarebbe certamente√maggiore di n). Quindi, n e` primo se e solo se non ha divisori
compresi tra 2 e n. Quindi possiamo migliorare il nostro algoritmo riducendo
notevolmente il numero di operazioni eseguite nel caso peggiore.
Stiamo toccando un altro importante aspetto dell’informatica ovvero l’efficienza
degli algoritmi: visto che pi`u sono le operazioni eseguite maggiore e` il tempo di esecuzione dell’algoritmo, si richiede che gli algoritmi, oltre ad esser corretti, eseguano
il minor numero di operazioni, ovvero siano efficienti.
1.1
La macchina di Von Neumann
Gli algoritmi come quelli appena descritti, affinch´e possano essere eseguiti in modo
automatico, devono essere opportunamente codificati in modo che l’esecutore auto-
11
PdC
12
Capitolo 1 – Problemi, algoritmi, macchine e linguaggi di programmazione
Memoria
CPU
(Central Processing Unit)
Unità
Aritmetico
Logica
RA
FT
Unità
di
Controllo
accumulatore
Input/Output
Figura 1.1 La macchina di Von Neumann
matico sia in grado di comprenderli. La codifica dell’algoritmo prende il nome di
programma. Infine nel codificare gli algoritmi dobbiamo stabilire quantomeno un
modello astratto di calcolatore in grado di eseguire il programma.
Tale modello e` la macchina di Von Neumann ideata dal matematico John Von
Neumann negli anni ’40 e poi implementata negli anni successivi. Lo schema della macchina di Von Neumann e` illustrato in Figura 1.1. Questa si compone delle
seguenti parti.
D
• La memoria che conserva sia il programma (ovvero l’algoritmo opportunamente codificato) che i dati su cui deve lavorare il programma. Pu`o essere
vista come un vettore di celle ognuna delle quali contiene una istruzione del
programma oppure un dato. Ogni cella e` identificabile attraverso un indirizzo
che non e` altro che un indice del vettore. Una visione pi`u suggestiva della memoria1 e` quella di accomunarla ad un grosso foglio a quadretti dove ogni quadretto contiene una informazione che pu`o essere un dato oppure un frammento
di programma.
• La CPU ovvero l’unit`a di elaborazione composta da tre elementi principali:
– L’unit`a aritmetico logica (ALU) esegue le istruzioni elementari come
quelle aritmetiche e logiche;
1 Si veda “Informatica, la mela si pu`
o mordere” di Pierluigi Crescenzi su Alias, inserto de “il
Manifesto del 24 Giugno 2012”.
PdC
Programmazione dei calcolatori
– L’unit`a di controllo recupera le istruzioni in memoria secondo l’ordine
logico stabilito dall’algoritmo e permette la loro esecuzione;
– L’accumulatore e` una memoria interna della CPU che viene utilizzata per
contenere gli operandi delle istruzioni eseguite dalla ALU.
• L’input/output (I/O) costituisce l’interfacciamento del calcolatore verso l’esterno (tastiera, dischi, video...).
RA
FT
• Il bus di comunicazione e` il canale che permette la comunicazione tra le unit`a
appena descritte.
D
Torniamo all’algoritmo per la verifica della primalit`a descritto all’inizio di questo
capitolo. Codificato in modo opportuno, l’algoritmo e` memorizzato in una porzione
di memoria della macchina e l’intero in ingresso (o input) in un “quadratino” del
foglio a quadretti che indicheremo con n. Al momento in cui il programma viene
eseguito, l’unit`a di controllo preleva la prima istruzione (riga 1) del programma che
dovr`a essere eseguita dalla CPU: nel nostro caso e` l’istruzione di assegnamento d = 2
(assegna il valore 2 alla variabile d), quindi la CPU scriver`a 2 in un “quadratino”
del foglio che convenzionalmente chiameremo d. Normalmente l’unit`a di controllo
esegue l’istruzione successiva all’ultima gi`a eseguita. In questo caso viene eseguita
l’istruzione nella riga 2. Questa e` una istruzione di controllo che permette di alterare
il flusso del programma in funzione del verificarsi o meno di una condizione. In
particolare, se il valore contenuto in d e` minore o uguale del valore contenuto in
n e il valore contenuto in d non divide quello contenuto in n (operazioni eseguite
dall’unit`a aritmetico logica all’interno della CPU) allora l’unit`a di controllo eseguir`a
l’istruzione nella riga 3 altrimenti quella in riga 5. La riga 3 aggiunge uno al valore
contenuto in d e lo sovrascrive in d. Infine nella riga 5 troviamo un’altra istruzione
di controllo che in base al risultato di una condizione dirotta il flusso del programma
sulla riga 6 o sulla riga 8. L’operazione RETURN pu`o esser vista come “scrivi in
output il risultato della computazione”.
1.2
Al di la` dell’astrazione
La memoria di un calcolatore e` un vettore di celle contenenti informazioni codificate
in modo opportuno. Poich´e vengono utilizzati circuiti integrati l’unica informazione
possibile e` data dallo stato elettrico dei componenti (semplificando, c’`e corrente - non
c’`e corrente). Questo tipo di informazione pu`o essere rappresentata in astratto usando
due simboli 0 e 1 per i due stati: ogni cella di memoria contiene una sequenza fissata
di questi valori denominati bit. Questi due simboli rappresenteranno l’alfabeto del
13
PdC
14
Capitolo 1 – Problemi, algoritmi, macchine e linguaggi di programmazione
RA
FT
calcolatore per mezzo del quale esprimere i programmi e i dati (sistema binario). Col
termine byte e` indicata una sequenza di 8 bit. Un byte rappresenta il taglio minimo
di informazione che un calcolatore e` in grado di gestire. Quindi una cella di memoria
contiene un numero intero di byte (per i modelli attuali di personal computer questi
sono 4 o 8). Una cella di memoria e` anche indicata come parola o word.
Il linguaggio macchina descrive come sono rappresentate le istruzioni e i dati
che costituiscono i programmi che possono essere eseguiti direttamente dalla CPU.
Supponiamo per semplicit`a che ogni istruzione occupi soltanto una cella di memoria
(in generale possono essere pi`u grandi). Ogni istruzione sar`a composta di due parti
principali: una parte iniziale chiamata codice operativo che indica il tipo di azione
che si deve eseguire ed una seconda parte costituita dagli operandi. Per esempio
l’istruzione
1 I F ( valore <= 0 ) {
2
istruzione1;
3 } ELSE {
4
istruzione2;
5 }
pu`o avere la seguente codifica
100110
valore
ind1
ind2
D
dove i primi 6 bit rappresentano il codice operativo, i restanti bit rappresentano gli
operandi che in questo caso sono tre anche questi espressi in binario. Il primo operando e` il valore confrontato con 0, il secondo e` indirizzo in cui si trova istruzione1
e l’ultimo e` l’indirizzo di memoria in cui e` memorizzato istruzione2. Si osservi che ogni operando deve occupare un numero predeterminato di bit affinch´e sia
possibile distinguere ogni singolo operando.
Nel linguaggio macchina puro gli indirizzi utilizzati come operandi nelle istruzioni (e che si riferiscono a dati o posizioni all’interno del programma stesso) devono
essere indirizzi veri. Quindi il programma cambia a seconda della posizione a partire
dalla quale questo viene memorizzato. Tuttavia, al momento della progettazione del
programma e` pi`u comodo utilizzare delle etichette per indicare sia gli indirizzi di memoria utilizzati per i dati che quelli all’interno del programma. Inoltre anche i codici
operativi delle istruzioni “sulla carta” possono essere rappresentati da dei codici pi`u
semplici da ricordare. Queste rappresentazioni mnemoniche possono essere formalizzate cos`ı da costituire un’astrazione del linguaggio macchina chiamato linguaggio
assembly. Soltanto al momento della effettiva memorizzazione del programma in
memoria per la sua esecuzione sar`a necessario sostituire i codici mnemonici e le etichette con i codici operativi e gli indirizzi reali. Questa operazione viene eseguita
meccanicamente da un programma chiamato assembler.
PdC
Programmazione dei calcolatori
1.2.1
Codificare i dati
Sia che il programma venga scritto utilizzando direttamente il codice macchina, sia
che venga scritto in assembly e poi tradotto con un assembler, sia - come vedremo
pi`u avanti - che venga scritto utilizzando un pi`u comodo linguaggio ad alto livello il
risultato e` lo stesso: una sequenza di 0 e 1 che definisce sia i programmi che i dati.
In questa sezione descriveremo brevemente come vengono codificati i dati.
RA
FT
Gli interi Siamo abituati a rappresentare i numeri interi positivi utilizzando il sistema decimale, ovvero attraverso una sequenza di cifre da 0 a 9. Il valore rappresentato
da una cifra all’interno della sequenza dipende dalla posizione che assume la cifra
nella sequenza stessa. Per esempio si consideri il numero 1972, la cifra 9 vale 900
ovvero 9 × 102 dove 2 e` proprio la posizione della cifra 9 all’interno della sequenza
(si assume che la cifra pi`u a destra sia in posizione 0). Quindi
1972 = 1 × 103 + 9 × 102 + 7 × 101 + 2 × 100 .
In definitiva nel sistema decimale il valore dalla cifra in posizione k equivale al valore
associato alla cifra moltiplicato per 10 elevato alla k. L’intero 10 ovvero il numero
di cifre di cui disponiamo rappresenta la base del sistema. Tuttavia questo numero
non ha nulla di magico e quindi pu`o essere sostituito con qualsiasi altra base.
Nel sistema binario abbiamo a disposizione solo due cifre quindi la cifra in
posizione k dalla destra ha un valore che vale b × 2k , dove b vale 0 o 1. Ad esempio
= 1 × 210 + 1 × 29 + 1 × 28 + 1 × 27 + 0 × 26 + 1 × 25 +
1 × 24 + 0 × 23 + 1 × 22 + 0 × 21 + 0 × 20
= 1024 + 512 + 256 + 128 + 32 + 16 + 4 = 1972.
11110110100
D
Quanto appena detto ci fornisce un metodo per convertire numeri binari in numeri
decimali, ma come avviene il processo inverso?
D’ora in poi adotteremo la convenzione di indicare la base di un numero come pedice del numero stesso quando questa non sar`a chiara dal contesto. Ad esempio dalla
precedente conversione deduciamo che 111101101002 ≡ 197210 . Ora il nostro
problema sar`a il seguente: dato un intero N in base 10, qual e` la sua rappresentazioni
in base 2, ovvero qual e` la sequenza binaria B tale che N10 ≡ B2 ? Supponiamo di
conoscere la rappresentazione binaria di bN/2c, sia questa B 0 = bk0 bk0 −1 . . . b00 allora
se b0 e` il resto della divisione di N per 2 (ovvero b0 ∈ {0, 1}) si ha
N
= 2bN/2c + b0
= 2 bk0 × 2k + . . . + b00 × 20 + b0
= bk0 × 2k+1 + . . . + b00 × 21 + b0 × 20 .
15
PdC
16
Capitolo 1 – Problemi, algoritmi, macchine e linguaggi di programmazione
Quindi N10 = (bk0 bk0 −1 . . . b00 b0 )2 ovvero la rappresentazione binaria di N e` data dalla rappresentazione binaria di bN/2c seguita - a destra - dal bit che rappresenta il
resto della divisione di N per 2. Queste osservazioni inducono un algoritmo per la
conversione di qualunque intero N nella sequenza binaria corrispondente.
RA
FT
1 k = 0;
2 WHILE ( N > 0 ) {
3
bk = N % 2;
4
N = N/2;
5
k = k+1;
6 }
7 R E T U R N bk bk−1 ...b0
Nella tabella che segue troviamo i valori di N, bN/2c e bk ottenuti con l’algoritmo se
N = 13.
N
13
6
3
1
bN/2c
bk
6
3
1
0
b0 = 1
b1 = 0
b2 = 1
b3 = 1
Quindi 1310 = 11012 .
La codifica degli interi negativi pu`o essere fatta in diversi modi, quello pi`u semplice prevede di utilizzare uno dei bit che si ha a disposizione come bit di segno. Per
esempio se il bit pi`u significativo di una sequenza e` lo 0 allora il numero che segue
e` positivo altrimenti e` negativo. Altri modi per rappresentare interi negativi sono la
rappresentazione complemento a 2 e la rappresentazione in eccesso.
D
I razionali Analogamente a quanto avviene per la rappresentazione in base 10, la
cifra b−i in posizione i a destra della virgola vale b × 2−i . I numeri razionali possono essere rappresentati in modo molto semplice utilizzando la notazione a virgola
fissa. Ovvero, se si hanno a disposizione n bit per rappresentare tali numeri, una
parte di essi (diciamo k meno significativi) vengono utilizzati per la parte razionale
mentre i restanti n − k vengono utilizzati per la parte intera. Se ad esempio n = 8 e
k = 2 la sequenza 10010101 rappresenta il numero 100101.012 che corrisponde
a 37 + 2−2 = 37.2510 . Questa rappresentazione ha lo svantaggio di “sprecare” bit
nel caso in cui, per esempio, si rappresentano interi oppure nel caso in cui i numeri da
rappresentare sono molto piccoli (tra 0 e 1). Questo problema viene risolto usando
una codifica della rappresentazione scientifica del numero, ovvero il numero x viene
scritto come m × 2e e le singole parti vengono codificate usando interi. Si ottiene
una rappresentazione in virgola mobile del numero. Lo standard adottato (IEEE 754)
PdC
Programmazione dei calcolatori
rappresenta un numero in virgola mobile utilizzando di 32 bit. Il primo di questi
e` il bit di segno; i successivi 8 vengono utilizzati per l’esponente e2 , i restanti per
rappresentare la mantissa m3 .
RA
FT
I caratteri ed il codice ASCII I caratteri (lettere, cifre, punteggiatura, spaziatura)
vengono rappresentati attraverso un sistema di codifica a 7 bit denominato codice
ASCII4 (American Standard Code for Information Interchange.). Poich´e con 7 bit si
possono ottenere 27 diverse sequenza binarie, ne consegue che i caratteri rappresentabili col codice ASCII sono 128. Inoltre ogni sequenza binaria del codice ASCII
pu`o essere vista come un numero intero da 0 a 127 questo implica che esiste una corrispondenza uno-a-uno tra gli interi da 0 a 127 ed i caratteri. Nella tabella che segue
sono mostrati i codici ASCII dei caratteri stampabili, i caratteri mancanti sono spazi,
tabulazioni, ritorno-carrello e altri caratteri di controllo. I codici sono rappresentati
in forma decimale.
Car
!
”
#
$
%
&
’
(
)
*
+
,
.
/
0
1
2
3
D
Cod.
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
2 L’esponente
Cod.
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
Car
4
5
6
7
8
9
:
;
¡
=
¿
?
@
A
B
C
D
E
F
Cod.
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
Car
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Cod.
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
Car
Z
[
\
]
ˆ
‘
a
b
c
d
e
f
g
h
i
j
k
l
Cod.
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
pu`o essere negativo: la rappresentazione utilizzate e` quella in eccesso.
mantissa e` della forma 1.y, si rappresenta solo la parte dopo la virgola.
4 pronuncia ’aschi’ o ’asci’
3 La
Car
m
n
o
p
q
r
s
t
u
v
w
x
y
z
{
—
}
∼
17
PdC
18
Capitolo 1 – Problemi, algoritmi, macchine e linguaggi di programmazione
Si osservi che i caratteri alfabetici minuscoli hanno codici consecutivi (il carattere
‘a’ ha codice 97, ’b’ ha codice 98 e via dicendo). La stessa cosa vale per i caratteri
maiuscoli e per i caratteri rappresentanti i numeri da 0 a 9.
1.3
I linguaggi ad alto livello
D
RA
FT
Il linguaggio macchina ed il linguaggio assembly presentano diversi inconvenienti.
Sono dipendenti dall’architettura: ogni processore ha un suo insieme di istruzioni
specifico quindi il codice scritto non e` portabile. I programmi sono di difficile lettura
quindi non adatti alla descrizione di algoritmi complessi.
Per semplificare il compito ai programmatori sono stati introdotti i linguaggi di
programmazione ad alto livello. Un linguaggio di programmazione ad alto livello
e` definito in modo da essere svincolato dall’architettura del calcolatore; inoltre pu`o
utilizzare elementi del linguaggio naturale rendendo il linguaggio pi`u semplice da
utilizzare. Infine, grazie alla sua astrazione da una specifica architettura, i suoi programmi possono essere facilmente trasportati da una macchina ad un altra. Tuttavia
il linguaggio deve avere regole precise e non ambigue cos`ı che i programmi possano
essere tradotti in linguaggio macchina e quindi essere eseguiti da un calcolatore.
Come e` stato detto precedentemente, un programma pu`o essere visto come un
dato; inoltre un programma produce in output dati, quindi non deve sorprendere l’esistenza di un programma che prende in input un programma scritto in un linguaggio
e restituisce in output un altro programma equivalente al primo codificato in un altro
linguaggio. Questo significa che possiamo utilizzare un programma per tradurre un
programma scritto in un linguaggio ad alto livello nel suo equivalente in linguaggio
macchina. Questo processo di traduzione automatica e` detto compilazione ed il programma che esegue il processo e` detto compilatore. E` questo il processo che instanzia
un programma astratto scritto con un linguaggio ad alto livello su una specifica architettura. Attraverso questo processo siamo in grado di far eseguire un programma P
scritto nel linguaggio ad alto livello L su le architetture A1 , A2 , . . . , Ak tutte diverse tra
di loro. Per far questo e` sufficiente avere un compilatore Ci che trasforma programmi
scritti nel linguaggio L in codice macchina per l’architettura Ai .
P → Ci → Pi
Ovvero il compilatore Ci prende in input il programma P scritto nel linguaggio L e
produce in output il programma Pi scritto in codice macchina per l’architettura Ai .
Ovviamente i programmi P e Pi devo essere equivalenti!
Nella descrizione degli algoritmi fin qui presentati abbiamo utilizzato un formalismo ad alto livello la cui sintassi ricorda i linguaggi di programmazione della famiglia
PdC
Programmazione dei calcolatori
D
RA
FT
del C, C++ o Java. Tuttavia i linguaggi di programmazione ad alto livello pi`u comuni
condividono gli stessi concetti di base che descriveremo nei capitoli che seguono.
19