Note per il corso di Fondamenti di Informatica I

Vincenzo Grassi
Note per il corso
di Fondamenti di Informatica I
a.a. 1999/2000
Facoltà di Ingegneria, Università di Roma “Tor Vergata”
Capitolo 1
Rappresentazione di tipi di dato astratti
2
Introduzione
Questo capitolo costituisce una integrazione delle lezioni tenute in aula durante lo svolgimento del corso,
riguardanti la definizione di alcuni tipi di dato astratti e la loro codifica utilizzando i meccanismi messi a
disposizione dal linguaggio C++. Il suo scopo principale è di presentare in maniera più organica e precisa quanto
esposto durante le lezioni. Si rimanda quindi a queste ultime per la parte riguardante la “cornice” (il concetto di
tipo di dato astratto) in cui inquadrare tale capitolo.
Anche se la codifica presentata riguarda il linguaggio C++, il lettore dovrebbe essere in grado facilmente di
tradurre tale codifica in un altro linguaggio, utilizzando i meccanismi (più o meno “potenti” di quelli C++) messi a
disposizione.
Obiettivo di questo capitolo è anche quello di far vedere come, a prescindere dal linguaggio adottato, una
certa “filosofia” di progettazione, basata sul concetto di tipo di dato astratto, consenta di arrivare alla
realizzazione di algoritmi risolutivi con migliori caratteristiche di modularità e riusabilità.
Mi scuso per qualunque errore o svista che possa apparire in questo capitolo. Ogni commento e
suggerimento sarà comunque gradito.
Vorrei infine ringraziare Vittoria de Nitto Personè per gli utili suggerimenti e commenti durante la stesura di
questo capitolo.
3
Il tipo di dato astratto “lista”
1. Introduzione
Una “lista” rappresenta una sequenza ordinata di elementi omogenei, la cui quantità può variare
dinamicamente. Operazioni tipiche effettuate su un oggetto di tipo lista sono quelle di inserzione,
rimozione e modifica di un elemento. Tali operazioni possono essere eseguite qualunque sia la
posizione dell’elemento nella lista. Quindi, nell’eseguire tali operazioni, è necessario specificare in
qualche modo la posizione coinvolta.
Nella definizione che daremo si assumerà che tale posizione è specificata esplicitamente,
tramite un intero che rappresenta il numero d’ordine dell’elemento interessato, dove ad ogni elemento
della lista è assegnato un numero d’ordine che va da 1 a n (numero corrente di elementi nella lista). Si
noti che il numero d’ordine di un elemento (ovvero, la sua posizione nella lista) può cambiare nel
tempo, in conseguenza di operazioni di inserzione o rimazione di altri elementi nella stessa lista.
Graficamente, un oggetto di tipo lista può essere visualizzato come in Fig. 1, dove n è la lunghezza
corrente della lista α.
α = [a1 a2 a3 … an-1 an]
Figura 1
2. Definizione del tipo di dato
Nella letteratura si possono incontrare varie definizioni del tipo di dato astratto lista (in particolare,
delle operazioni primitive definite su questo tipo di dato). Quella che adottiamo non ha nessuna
pretesa di essere “migliore” di altre. E’ semplicemente una definizione “ragionevole”, ma altre
definizioni lo sono altrettanto.
Nella nostra definizione, il tipo di dato lista risulta caratterizzato dai seguenti domini e operazioni
primitive:
D = {lista, T_elem_lis, integer}
F = {length, insert, remove, element}
dove:
T_elem_lis: insieme (generico) degli oggetti utilizzabili come elementi di una lista;
lista: insieme degli oggetti di tipo lista;
integer: insieme dei numeri interi.
Le operazioni primitive in F hanno la seguente caratterizzazione funzionale e semantica:
- length : lista → integer length(α) == n
(dove n è la lunghezza della lista; se la lista è vuota, n vale 0)
4
- element : lista x integer → T_elem_lis
definizione: restituisce il valore dell’i-esimo elemento di α;
element(α, i) == ai
precondizione: length(α)>0 and 1≤i≤length(α)
- insert : lista x T_elem_lis x integer → lista
definizione: inserisce un elemento nella posizione i-esima della lista;
insert(α, b, i) == [a1 a2 … ai-1 b
(nuovi indici degli elementi dopo l’inserzione:
ai … an-1 an]
1 2 … i-1 i i+1 …
n n+1)
precondizione: 1≤i≤length(α)+1
se i==length(α)+1 il nuovo elemento diventa l’ultimo della lista
- remove : lista x integer → lista
definizione: rimuove un elemento dalla posizione i-esima della lista;
remove(α, i) == [a1 a2 … ai-1 ai+1 … an-1 an]
(nuovi indici degli elementi dopo la rimozione:
1 2 … i-1
i … n-2 n-1)
precondizione: length(α)>0 and 1≤i≤length(α) )
3. Codifica C++ del tipo astratto lista
In questa sezione viene presentata una possibile codifica del tipo lista, usando i meccanismi
disponibili nel linguaggio C++. Si noti che in tutti i casi in cui compare la scritta “… // errore: …” si
intende che, in una effettiva codifica, tale frase andrà sostituita da un comando opportuno (per
esempio: stampa di un messaggio di segnalazione, assegnazione di un valore ad un parametro
aggiuntivo (per riferimento) usato per segnalare al chiamante l’impossibilità di eseguire l’operazione (in
questo caso sarà responsabilità del chiamante prendere azioni opportune), ecc.).
3.1. Codifica astratta
Usando la filosofia della programmazione a oggetti, e quindi il meccanismo delle classi del C++,
possiamo definire una codifica “astratta” del tipo di dato lista. Il termine “astratta” fa riferimento alla
possibilità di definire e codificare in C++ algoritmi su liste indipendentemente dal particolare modo in
cui gli oggetti (e le corrispondenti operazioni primitive) del tipo di dato lista sono effettivamente
realizzati. Infatti, il meccanismo delle classi del C++ ci consente di definire un’interfaccia “pubblica”
indipendente dal particolare meccanismo usato per codificare oggetti di tipo lista e le corrispondenti
operazioni primitive. Tale interfaccia pubblica può poi essere utilizzata nella definizione e codifica in
C++ di algoritmi su liste.
La codifica astratta può essere definita come segue:
5
typedef … T_elem_lis;
class lista
{private :
//strutture dati per rappresentare una lista
...
public :
void init_lis();
int length();
T_elem_lis element(int i);
void insert(T_elem_lis b,int i);
void remove(int i);
};
void lista::init_lis()
{//codifica dell’ operazione init_lis()
}
int lista::length()
{//codifica dell’ operazione length()
}
T_elem_lis lista::element(int i)
{//codifica dell’ operazione element(…)
}
void lista::insert(T_elem_lis b,int i)
{//codifica dell’ operazione insert(…)
}
void lista::remove(int i)
{//codifica dell’ operazione remove(…)
}
In questa codifica, si noterà la presenza, a fianco delle operazioni primitive, di una operazione
init_lis() il cui scopo è quello di inizializzare in modo opportuno le strutture dati utilizzate per
rappresentare una lista. Tale funzione d e v e essere chiamata prima di qualunque uso di un oggetto di
tipo lista.
Come esempio di applicazione di quanto detto, viene presentata nel seguito una funzione che
risolve il seguente problema:
data una lista di valori (per esempio, numeri interi) ordinati in senso crescente, inserire un nuovo
valore rispettando l’ordinamento crescente.
6
lista ins_in_ord(lista l, T_elem_lis b)
{if (l.length()==0 || b<l.element(1))
l.insert(b,1);
else if (b>l.element(l.length()))
l.insert(b, l.length()+1);
//se la lista è vuota oppure b è
//più piccolo di ogni elemento
//della lista, b deve essere
//inserito in prima posizione;
//b risulta maggiore del più
//grande elemento della lista, e
//quindi viene inserito in ultima
//posizione
else {int i;
//inserimento in posizione “intermedia”
i=1;
while (i<=l.length() && l.element(i)<b)
i++;
l.insert(b,i);
}
return l;
}
Come si può notare, questa funzione restituisce come risultato la lista di partenza modificata
inserendo in modo ordinato il nuovo elemento. Questa funzione potrebbe essere utilizzata in un
programma così fatto:
typedef int T_elem_lis;
siano
//si assume che gli elementi della lista
//numeri interi
class lista
{private :
//strutture dati per rappresentare una lista
public :
void init_lis();
int length();
T_elem_lis element(int i);
void insert(T_elem_lis b,int i);
void remove(int i);
};
void lista::init_lis()
{//codifica dell’ operazione init_lis()
}
int lista::length()
{//codifica dell’ operazione length()
}
T_elem_lis lista::element(int i)
{//codifica dell’ operazione element(…)
}
void lista::insert(T_elem_lis b,int i)
{//codifica dell’ operazione insert(…)
}
void lista::remove(int i)
{//codifica dell’ operazione remove(…)
}
7
main ()
{int k;
lista l;
l.init_lis();
for (k=1;k<5;k++)
l.insert(k,k);
for (k=1;k<=l.length();k++)
cout <<l.element(k)<<' ';
cout <<'\n';
l=ins_in_ord(l,3);
l=ins_in_ord(l,7);
for (k=1;k<=l.length();k++)
cout <<l.element(k)<<' ';
cout <<'\n';
}
Al lettore viene lasciato di verificare che l’uscita prodotta da questo programma è:
1
2
3
4
1
2
3
3
4
7
Come si può facilmente notare, dopo aver definito come int il generico tipo T_elem_lis, il
programma principale e la funzione ins_in_ord risultano indipendenti dalla codifica del tipo lista, e
quindi anche da eventuali modifiche di tale codifica che interessino solo la parte private (ovvero, il
modo di rappresentare concretamente una lista e le operazioni primitive si di essa).
Tale caratteristica mostra come l’adozione dell’approccio “a oggetti” nella realizzazione di
soluzioni di problemi (come l’inserimento in ordine appena presentato) facilita grandemente il “riuso” di
tali soluzioni all’interno di differenti situazioni, dove, per esigenze particolari, possono essere adottate
codifiche differenti dello stesso tipo di dato (ovviamente, deve valere il vincolo che l’interfaccia adottata
(cioè la parte public) sia la stessa in tutte le situazioni).
3.2. Codifica concreta (sequenziale)
La codifica concreta che presentiamo è basata sull’idea di rappresentare una lista di M elementi
usando M posizioni contigue di un array di N elementi. Dato che un array è una struttura statica nel
linguaggio C++, conviene definire N>M, in modo opportuno, per consentire una eventuale “crescita”
dinamica della lista. La condizione N>M implica che non tutti gli elementi dell’array saranno, in generale,
occupati da elementi della lista; quindi, la codifica deve includere anche una informazione su quale
parte dell’array è effettivamente utilizzata.
Una possibile codifica del dominio lista è quindi la seguente:
8
class lista
{private :
const int N = …;
T_elem_lis elem[N];
int primo,lungh;
public :
//parte “pubblica”
};
In questa codifica, “primo” e “lungh” delimitano la porzione effettivamente utilizzata dell’array.
Per usare in modo efficiente lo spazio disponibile, conviene considerare l’array come una
“struttura circolare” di N elementi numerati da 0 a N-1, dove:
- il successore di un elemento in posizione i è quello in posizione (i+1) % N;
- il predecessore di un elemento in posizione i è quello in posizione (i-1+N) % N.
Esempio 1
Se N = 5, si ha:
- successore(2) = (2+1) % 5 = 3; successore(4) = (4+1) % 5 = 0;
- predecessore(2) = (2-1+5) % 5 = 1;
predecessore(0) = (0-1+5) % 5 = 4;
4
0
1
3
2
Figura 2
Fine esempio 1
Esempio 2
Nella Figura 3 sono mostrate due possibili configurazioni di una struttura dichiarata di tipo lista
usata per rappresentare la lista di interi [4, 5, 10, 12, 7]:
9
2
5
0
primo
N-2
5
primo
0
10
elem
1
12
7
lungh
elem
1
2
4
2
3
5
10
3
4
5
6
lungh
4
12
5
6
7
7
7
N-2
N-2
N-1
N-1
4
5
Figura 3
Fine esempio 2
Passiamo ora a realizzare in C++ le operazioni primitive del tipo astratto lista usando questa
codifica sequenziale.
void lista::init_lis()
{lungh=0;primo=1;}
int lista::length()
{return lungh;}
T_elem_lis lista::element(int i)
{if (lungh>0 && i<=lungh && i>=1)
return elem[(primo+i-1)%N];
else … ;
//errore: indice fuori dai limiti
}
10
void lista::insert(T_elem_lis b, int i)
{if (lungh==N)
… ;
//errore: array pieno
else if (i>lungh+1 || i<=0)
… ;
//errore: indice fuori dai limiti
else {if (i==1) //inserzione in prima posizione
{elem[(primo-1+N)%N]=b;
primo=(primo-1+N)%N;
}
else if (i==lungh+1)
//inserzione in ultima
//posizione
elem[(primo+lungh)%N]=b;
else
//inserzione interna
{int j;
for (j=(primo+lungh)%N;j!=(primo+i-1)%N;
j=(j-1+N)%N)
elem[j]=elem[(j-1+N)%N];
elem[(primo+i-1)%N]=b;
}
lungh=lungh+1;
}
}
void lista::remove(int i)
{if (i>lungh || i<1)
… ;
//errore: indice fuori dai limiti
else if (i==1) //rimozione dalla prima posizione
{primo=(primo+1)%N;
lungh=lungh-1;
}
else if (i==lungh) //rimozione dall'ultima posizione
lungh=lungh-1;
else
//rimozione interna
{int j;
for (j=(primo+i-1)%N;j!=(primo+lungh-1)%N;j=(j+1)%N)
elem[j]=elem[(j+1)%N];
lungh=lungh-1;
}
}
La codifica sequenziale appena descritta presenta i seguenti inconvenienti:
• ogni oggetto di tipo lista occupa sempre uno spazio proporzionale a N, anche se, ad un certo
istante, può essere costituito da un numero molto più piccolo di elementi;
• il costo dell’esecuzione delle operazioni di inserzione e rimozione è alto, poichè si richiede
uno spostamento di elementi, per mantenere la necessaria contiguità fisica.
Questi due inconvenienti possono essere superati usando codifiche degli oggetti di tipo lista
che non cercano di rappresentare la “contiguità logica” tra elementi della lista (cioè il fatto che
l’elemento i-esimo della lista è preceduto dall’(i-1)-esimo e seguito dall’(i+1)-esimo) mediante la
“contiguità fisica” degli elementi in una struttura di tipo array. Tali codifiche non verranno presentate in
questa sede.
Terminiamo questa sezione presentando due diverse codifiche di una funzione che risolve il
seguente problema su liste:
11
data una lista e un valore di tipo T_elem_lis, la funzione deve restituire il valore 1 se l’elemento è
presente nella lista, e 0 altrimenti.
(E’ da notare che, secondo la convenzione del C++, questo corrisponde a restituire i valori logici
“vero” se l’elemento è presente nella lista, e “falso” altrimenti)
int ricerca_1(lista l, T_elem_lis e)
{if (l.length()==0)
return 0;
//e non è presente perchè la lista è vuota
else {int i;
i=1;
while (i<=l.length() && l.element(i)!=e)
i++;
if (i>l.length())
return 0;
//e non è presente perchè la lista è stata
//esaminata tutta senza successo
else return 1;
}
}
int ricerca_2(lista l, T_elem_lis e)
{if (lungh==0)
return 0;
//e non è presente perchè la lista è vuota
else {int i;
i=1;
while (i<=lungh && element([(primo+i-1)%N]!=e)
i++;
if (i>lungh)
return 0;
//e non è presente perchè la lista è stata
//esaminata tutta senza successo
else return 1;
}
}
Data la codifica del tipo lista presentata in questa sezione, la funzione ricerca_1( ) è
corretta, mentre ricerca_2( ) non lo è. Perchè? In che modo andrebbe modificata la definizione
della classe lista affinchè la funzione ricerca_2( ) risulti corretta? Il lettore è invitato a sperimentare
concretamente possibili soluzioni, per verificarne la correttezza.
12
Il tipo di dato astratto “matrice”
1. Definizione del tipo di dato astratto
Una “matrice” (a n dimensioni) rappresenta una collezione di elementi omogenei, ognuno dei
quali è identificato univocamente da una n-upla particolare (indice dell’elemento). Operazioni tipiche
effettuate su un oggetto di tipo matrice sono quelle di acquisizione e modifica del valore di un
elemento della matrice, identificato tramite la n-upla associata.
Il tipo di dato matrice risulta quindi caratterizzato dai seguenti domini e operazioni primitive:
D = {matrice, T_elem_mat, indice}
F = {valore, modifica}
dove:
- matrice: insieme degli oggetti di tipo matrice (n-dimensionale);
- T_elem_mat: insieme (generico) degli oggetti utilizzabili come elementi di una matrice;
- indice = indice1 x indice2 x … x indicen : insieme di n-uple di indici;
Un singolo elemento di una matrice è quindi individuato da una n-upla <i1, i2 ,… , in>, i k ∈indice k ,
k = 1, 2, …, n.
Le operazioni primitive in F hanno la seguente caratterizzazione funzionale:
- valore : matrice x indice → T_elem_mat
(restituisce il valore dell’elemento identificato da “indice”)
- modifica: matrice x indice x T_elem_mat→ matrice
(modifica
la
matrice rimpiazzando il valore dell’elemento identificato da
“indice” con il valore dato)
Esempio 1
Consideriamo il problema del calcolo del prodotto di matrici reali bidimensionali. Come sappiamo
date due matrici Amxn = [aij] e Bnxp = [bij], si definisce come prodotto la matrice Cmxp = [cij] tale che:
n
cij =
∑
k=1
aik ·b kj
Definiamo un algoritmo per il calcolo del prodotto di matrici, pensato per un “esecutore” che
“conosce” il tipo astratto matrice e le operazioni aritmetiche su numeri reali.
Si ha quindi:
indice_1 = {1, 2, …, m} x {1, 2, …, n}
indice_2 = {1, 2, …, n} x {1, 2, …, p}
indice_3 = {1, 2, …, m} x {1, 2, …, p}
ℜ}
B ∈ matrice(indice_2, ℜ }
C ∈ matrice(indice_3, ℜ }
(dove ℜ rappresenta l’insieme dei numeri reali).
A ∈ matrice(indice_1,
13
algoritmo:
date A e B, esegui:
per ogni i ∈ {1, 2, …, m} :
per ogni j ∈ {1, 2, …, p} :
modifica(C, (i,j), 0)
//inizializza a zero l’elemento (i,j) di C
per ogni k ∈ {1, 2, …, n} :
modifica(C, (i,j), valore(C,(i,j))+valore(A, (i,k))·valore(B, (k,j))
fine algoritmo
Fine esempio 1
2. Codifica C++ del tipo astratto matrice
In questa sezione vengono presentate alcune possibili codifiche del tipo astratto matrice,
usando i meccanismi messi a disposizione dal linguaggio C++.
2.1. Codifica usando il meccanismo array
E’ abbastanza ovvio realizzare che il meccanismo array è stato introdotto in C++ (come anche in
altri linguaggi) principalmente per codificare il tipo astratto matrice. Questo in particolare significa che il
C++ mette a disposizione una sintassi “facilitata” e una realizzazione efficiente delle operazioni
primitive del tipo di dato. Per semplicità, limitiamo il discorso a matrici bidimensionali, ma, ovviamente, le
considerazioni fatte valgono per matrici con qualsiasi numero di dimensioni. Si ha quindi :
• codifica del dominio:
const int N1 = … ;
const int N2 = … ;
typedef … T_elem_mat;
typedef
T_elem_mat
//cardinalità del dominio indice_1;
//cardinalità del dominio indice_2;
//definizione del tipo degli elementi
//della matrice;
matrice[N1][N2];
Si noti che in C++ gli indici di un array [N1][N2] sono vincolati ad assumere valori tra 0 e N1-1
(o N2-1); nel caso che, per il problema considerato (come nell’Esempio 1), il dominio degli indici non
corrisponda a questa convenzione, bisognerà preoccuparsi di stabilire una opportuna corrispondenza
tra indici “astratti” e indici “concreti”.
• codifica delle operazioni primitive:
dato, ad esempio, un ambiente definito dalle seguenti dichiarazioni:
matrice A;
int i1, i2;
si ha:
- “modifica(A, (I1,I2), E)” è rappresentato come:
A[i1][i2] = E;
(dove E è una espressione C++ di tipo T_elem_mat)
- “valore(A, (I1,I2))” è rappresentato da:
A[i1][i2]
(usato all’interno di una opportuna espressione C++).
14
Esempio 2
Codifichiamo, usando la rappresentazione appena vista, l’algoritmo per il prodotto di matrici
descritto nell’Esempio 1. Per semplicità, consideriamo il caso di matrici quadrate NxN. Si ha:
const int N = … ;
typedef … T_elem_mat;
typedef T_elem_mat matrice[N][N];
void prodmat1(matrice a, matrice b, matrice c)
{//calcolo del prodotto di matrici N*N c=a*b
int i,j,k;
for (i=0;i<N;i++)
for (j=0;j<N;j++)
{c[i][j]=0;
for (k=0;k<N;k++)
c[i][j]=c[i][j]+a[i][k]*b[k][j];
}
}
Si noti che l’algoritmo non è codificato come funzione di tipo matrice perchè in C++ una
funzione non può assumere come valore un oggetto codificato tramite array; il risultato viene quindi
restituito tramite il parametro c (si ricordi che in C++ tutti i parametri formali di tipo array sono
implicitamente per riferimento).
Fine Esempio 2
2.2. Codifica astratta del tipo matrice
Come detto nella sezione precedente, il meccanismo array è pensato (e quindi ottimizzato) per
la codifica di oggetti di tipo matrice. Questo, tra l’altro, implica l’uso di una sintassi (quella vista per la
rappresentazione delle operazioni) strettamente dipendente dal meccanismo utilizzato. Una maggiore
“astrazione” dalla codifica (con conseguente possibilità di modificare la codifica senza dover modificare
la codifica degli algoritmi risolutivi) la si otterrebbe se il tipo astratto matrice venisse codificato
utilizzando la filosofia della programmazione a oggetti, e quindi il meccanismo delle classi del C++. In
questo modo diventa possibile definire un’interfaccia “pubblica” indipendente dal particolare
meccanismo usato per codificare oggetti di tipo matrice e le corrispondenti operazioni primitive.
Tale codifica “astratta” potrebbe essere definita come segue:
const int N1 = … ;
//cardinalità del dominio indice_1;
const int N2 = … ;
//cardinalità del dominio indice_2;
typedef … T_elem_mat; //definizione del tipo degli elementi
// della matrice;
class matrice
{private :
//strutture dati per rappresentare una matrice
public :
void init_mat(T_elem_mat v);
T_elem_mat valore(int i, int j);
void modifica(int i, int j, T_elem_mat v);
};
15
void matrice::init_mat(T_elem_mat v)
{//codifica dell’ operazione init_mat(); il valore v viene assegnato
//a tutti gli elementi della matrice
}
T_elem_mat matrice::valore(int i, int j)
{//codifica dell’ operazione valore()
}
void matrice::modifica(int i, int j, T_elem_mat v)
{//codifica dell’ operazione modifica()
}
Usando questa codifica astratta, l’algoritmo per il prodotto tra due matrici può essere riscritto
come segue (assumendo, per semplicità, N1 == N2):
void prodmat2(matrice a, matrice b, matrice& c)
{//calcolo del prodotto di matrici
int i,j,k;
for (i=0;i<N1;i++)
for (j=0;j<N1;j++)
{c.modifica(i,j,0);
for (k=0;k<N1;k++)
c.modifica(i,j,c.valore(i,j)+a.valore(i,k)*b.valore(k,j));
}
}
E’ facile notare come, rispetto alla procedura prodmat1(…) dell’Esempio 2, questa nuova
funzione risulta indipendente dalla particolare codifica adottata per il tipo matrice. Si noti anche che,
questa volta, il parametro formale c utilizzato per trasmettere all’esterno il risultato deve essere
dichiarato esplicitamente come parametro per riferimento. L’utilità di poter scrivere algoritmi in modo
indipendente dalla codifica del tipo di dato cui si fa riferimento risulterà più chiaro, quando, nel seguito,
verrà presentata una nuova possibile codifica del tipo matrice, che in alcune situazioni risulta più
vantaggiosa di quella tramite array.
La possibilità di cambiare la codifica utilizzata senza dover cambiare la codifica degli algoritmi
risolutivi comporta un indubbio vantaggio in termini di modificabilità e flessibilità. E’ bene avvertire,
però, che nel caso del tipo matrice il vantaggio è controbilanciato da una pesante perdita di efficienza.
Ogni operazione primitiva, in questa codifica, richiede l’esecuzione di una chiamata di funzione,
mentre questo non succede nel caso della codifica della Sezione 2.1 basata sull’uso diretto del
meccanismo array (ogni operazione primitiva comporta in quel caso, sostanzialmente, solo un accesso
in memoria, che richiede molto meno tempo per essere eseguito). La differenza, nel caso di algoritmi
complessi che richiedono l’esecuzione di un gran numero di operazioni primitive (per esempio,
somme e prodotti di matrici di grandi dimensioni) è sostanziale.
2.3. Codifica concreta tramite array
In questa sezione viene mostrato come la codifica tramite array di matrici può essere “inglobata”
nella codifica astratta basata sul meccanismo delle classi definita nella sezione precedente. E’ bene
16
comunque rimarcare quanto detto alla fine di quella sezione: se si sceglie di adottare il meccanismo
array per la codifica, e non si hanno fortissime esigenze di modificabilità della rappresentazione, la
codifica della Sezione 2.1 è senz’altro da preferire, in termini di efficienza di esecuzione.
La codifica di questa sezione è quindi data sostanzialmente a scopo dimostrativo.
const int N1 = … ;
//cardinalità del dominio indice_1;
const int N2 = … ;
//cardinalità del dominio indice_2;
typedef … T_elem_mat; //definizione del tipo degli elementi
//della matrice;
class matrice
{private :
T_elem_mat m[N1][N2];
public :
void init_mat(T_elem_mat v);
T_elem_mat valore(int i, int j);
void modifica(int i, int j, T_elem_mat v);
};
void matrice::init_mat(T_elem_mat v)
{int i,j;
for (i=0;i<N1;i++)
for (j=0;j<N2;j++)
m[i][j] = v;
}
T_elem_mat matrice::valore(int i, int j)
{return m[i][j];
}
void matrice::modifica(int i, int j, T_elem_mat v)
{m[i][j] = v;
}
I
3. Codifica compatta di matrici sparse
Per matrice sparsa si intende una matrice in cui la “maggior parte” degli elementi hanno uno
stesso valore, detto valore dominante.
Esempio 4
E’ molto facile che la matrice dei coefficienti di un sistema di equazioni lineari, in molti casi reali,
sia una matrice sparsa. Un tipico sistema di equazioni è, ad esempio, il seguente:
n
∑
j=1
aij xj = bi,
i = 1, 2, …, n
 > 0 se |i - j| ≤ 1
con aij 
altrimenti
= 0
In questo caso la matrice A = [aij] risulta formata da n2 elementi di cui:
- 3n-2 elementi diversi da 0;
- n2- (3n-2) elementi uguali a 0.
17
Quindi, 0 è il valore dominante,e solo 3n -2 elementi risultano “significativi” ai fini della soluzione
dell’equazione.
Fine Esempio 4
La codifica di matrici sparse usando il meccanismo array, come visto nella Sezione 2, comporta
necessariamente, per le caratteristiche intrinseche del meccanismo, uno spreco di locazioni di
memoria, usate per memorizzare elementi con valore dominante che, come nell’esempio appena
visto, risultano non significativi. Si pone quindi l’esigenza di ideare codifiche alternative, che
consentano di ridurre lo spreco di spazio. L’idea base di queste codifiche è di provare a rappresentare
solo gli elementi con valore diverso da quello dominante. Tali codifiche vengono dette “codifiche
compatte”. Nel seguito vengono riportate alcune possibili codifiche. Anche in questa sezione, per
semplicità, ci limitiamo al caso di matrici bidimensionali.
3.1. Codifica a lista
Questa codifica si basa sull’idea di estrarre dalla matrice gli elementi significativi, e di organizzarli
in una sequenza ordinata. Per identificare nella sequenza ogni elemento della matrice, al suo valore
dovremo associare la corrispondente coppia di indici.
Esempio 5
Consideriamo la seguente matrice 5x5:

c
B = d



a
b
e
g
h


f



La sua rappresentazione compatta secondo l’idea appena descritta risulta essere la seguente:
[(a,1,2) (b,1,3) (c,2,1) (d,3,1) (e,3,4) (f,3,5) (g,4,3) (h,5,3)]
Fine Esempio 5
Se la rappresentazione di una matrice è quella appena descritta, si pone il problema di come
eseguire le operazioni primitive; in questo caso, evidentemente, non possiamo più fare affidamento
su meccanismi già realizzati nella “macchina C++” (come per la codifica della Sezione 2), ma dobbiamo
programmare esplicitamente tali operazioni.
Sia β la sequenza di elementi [valore, indice_riga, indice_colonna] che rappresenta in modo
compatto una generica matrice B. Algoritmi che realizzano le operazioni primitive possono essere
schematizzati come segue:
18
•
valore(B, i, j) :
scandisci β partendo dall’inizio, esaminando ogni elemento x ;
se esiste x tale che indice_riga(x) = i e indice_colonna(x) = j
allora restituisci valore(x)
altrimenti restituisci valore dominante
• modifica(B, i, j, e):
scandisci β partendo dall’inizio, esaminando ogni elemento x ;
se esiste x tale che indice_riga(x) = i e indice_colonna(x) = j
allora se e = valore dominante
allora elimina x dalla sequenza β
altrimenti valore(x) ← e
altrimenti se e ≠ valore dominante
allora aggiungi alla sequenza β l’elemento (e, i, j)
E’ facile rendersi conto che la sequenza β usata per rappresentare una matrice B può essere
vista come un oggetto appartenente al tipo astratto lista, dove il tipo degli elementi è costituito dalle
triple (valore, indice_riga, indice_colonna). Gli algoritmi schematizzati sopra possono quindi essere
descritti in maniera più precisa come segue, facendo uso delle operazioni primitive sul tipo lista:
• valore(B, i, j) :
k ← 1;
finchè (k ≤LENGTH(β)) e
(ELEMENT(β,k).indice_riga≠i o ELEMENT(β,k).indice_colonna≠j)
k ← k+1
//ricerca di un elemento di indici (i,j)
se k≤ LENGTH(β)
allora restituisci (ELEMENT(β, k).valore
altrimenti restituisci valore dominante
•modifica(B, i, j, e):
k ← 1;
finchè (k ≤ LENGTH(β)) e
(ELEMENT(β,k).indice_riga≠i o ELEMENT(β,k).indice_colonna≠j)
k ← k+1
//ricerca di un elemento di indici (i,j)
se k≤ LENGTH(β)
allore se e=valore dominante
allora β ← REMOVE(β,k)
altrimenti β ← REMOVE(β, k)
β ← INSERT(β, [e,i,j], k)
altrimenti se e≠valore dominante
allora β ← INSERT(β, [e,i,j], LENGTH(β)+1) //il nuovo elemento viene
//aggiunto come ultimo della
lista
Possiamo ora dare una codifica C++ della rappresentazione compatta appena descritta,
conforme alla codifica astratta della Sezione 2.2 (quindi, quello che codificheremo sarà la parte
“private” di quella classe e le relative operazioni primitive. Questa codifica utilizza, al suo interno, una
codifica C++ del tipo astratto lista. A questo scopo può essere usata la codifica del tipo lista data nel
relativo capitolo, o qualunque altra codifica che si conformi alla stessa codifica astratta.
19
• codifica della classe:
const int N1 = … ;
//cardinalità del dominio indice_1;
const int N2 = … ;
//cardinalità del dominio indice_2;
typedef … T_elem_mat; //definizione del tipo degli elementi
//della matrice;
typedef struct {T_elem_mat val;
int riga;
int colonna;
} T_elem_lis;
class lista
{private :
//tipo degli elementi della lista
//utilizzata per rappresentare la
//matrice
//una qualunque codifica del tipo lista
public :
void init_lis();
int length();
T_elem_lis element(int i);
void insert(T_elem_lis b,int i);
void remove(int i);
};
class matrice
{private :
lista l;
//contiene i valori diversi da quello dominante
T_elem_mat elem_dom; //valore dell’elemento dominante
public :
void init_mat(T_elem_mat v);
T_elem_mat valore(int i, int j);
void modifica(int i, int j, T_elem_mat v);
};
Come si può vedere dalla codifica data, una matrice viene rappresentata tramite una struttura a
due campi, uno dei quali contiene il valore dominante, e l’altro è la lista dei valori (con relativi indici)
diversi dal valore dominante. In pratica, il compattamento è stato ottenuto “collassando” tutti gli
elementi con valore dominante nel singolo campo “elem_dom”. Con riferimento alla matrice
dell’Esempio 4, la codifica di tale matrice risulterebbe essere quella rappresentata graficamente in
Figura 1, immaginando che il valore dominante sia espresso come x..
elem_dom :
l:
x
(a ,1,2) (b ,1,3) (c ,2,1) (d ,3,1) (e ,3,4) (f ,3,5) (g ,4,3) (h ,5,3)
Figura 1
• codifica delle operazioni primitive:
//è necessario premettere la codifica delle operazioni su lista, che
//verranno utilizzate per realizzare le operazioni primitive su matrice
void lista::init_lis()
{//codifica dell’ operazione init_lis()
}
20
int lista::length()
{//codifica dell’ operazione length()
}
T_elem_lis lista::element(int i)
{//codifica dell’ operazione element(…)
}
void lista::insert(T_elem_lis b,int i)
{//codifica dell’ operazione insert(…)
}
void lista::remove(int i)
{//codifica dell’ operazione remove(…)
}
void matrice::init_mat(T_elem_mat v)
{elem_dom = v;
l.init_lis();
}
T_elem_mat matrice::valore(int i, int j)
{int k;
k=1;
while (k<=l.length() &&
(l.element(k).riga!=i || l.element(k).colonna!=j))
k++;
if (k<=l.length())
return (l.element(k)).val;
else return elem_dom;
}
void matrice::modifica(int i, int j, T_elem_mat v)
{T_elem_lis t; int k;
k=1;
while (k<=l.length() &&
(l.element(k).riga!=i || l.element(k).colonna!=j))
k++;
if (k<=l.length()) //se vero l'elemento di indice (i,j) e' gia' presente
if (v==elem_dom)
l.remove(k);
else {l.remove(k);
t.riga=i; t.colonna=j; t.val=v;
l.insert(t,k);
}
else
//l'elemento di indice (i,j) non e' presente
if (v!=elem_dom)
{t.riga=i; t.colonna=j; t.val=v;
l.insert(t,1); //convenzione: si inserisce in prima posizione
}
}
Possiamo ora discutere vantaggi e svantaggi di questa codifica rispetto a quella tramite il
meccanismo array della Sezione 2. Prendiamo in considerazione matrici quadrate n x n con k elementi
diversi dal valore dominante:
• spazio occupato:
codifica con array: proporzionale a n2;
codifica compatta a lista: proporzionale a 3k;
21
• complessità delle operazioni primitive:
codifica con array: proporzionale a 1;
codifica compatta a lista: proporzionale a k;
Facciamo alcune osservazioni.
Dal punto di vista dello spazio occupato, la codifica compatta è conveniente solo se k < n2/3. C’è
inoltre da considerare che, per codifiche di liste come quelle viste nel relativo capitolo, l’occupazione di
spazio può essere superiore a 3k (se scegliamo un N>k per avere la possibilità di aggiungere nuovi
elementi).
Dal punto di vista della complessità delle operazioni, quelle nel caso “array” costano 1 perchè
sono indipendenti dalla dimensione della matrice (vedi modalità di accesso a una struttura array
memorizzata nella memoria di una macchina C++); nel caso compatto a lista, invece, può succedere di
dover scandire l’intera lista (k elementi) prima di concludere l’operazione (per esempio nel caso
l’elemento di indice (i,j) non sia presente nella lista perchè ha valore dominante). C’è inoltre da
considerare che, nel caso di codifica compatta, le operazioni primitive devono necessariamente
essere realizzate come procedure e funzioni, con conseguente innalzamento del costo. Nella sezione
seguente viene presentata una codifica compatta alternativa, con l’obiettivo di ridurre il costo delle
operazioni primitive, a prezzo di un leggero aumento dello spazio occupato.
3.2. Codifica a vettore di liste
L’idea alla base di questa codifica è quella di organizzare in liste separate gli elementi
“significativi” (diversi dal valore dominante) di ogni riga, e di fornire punti di accesso separati per ogni
lista. In questo modo, le operazioni di scansione di una lista per ricercare un elemento di indice (i, j)
richiedono un numero di passi pari al più al numero di elementi significativi in una riga, e non nell’intera
matrice.
Esempio 5
Consideriamo di nuovo la matrice 5x5:

c
B = d



a
b
e
g
h


f



La sua rappresentazione compatta secondo l’idea descritta in questa sezione risulta essere la
seguente:
22
riga 1: (a, 2) (b, 3)
riga 2 : (c, 1)
riga 3 : (d, 1) (e, 4) (f, 5)
riga 4 : (g, 3)
riga 5 : (h, 3)]
Fine Esempio 5
Come si può vedere anche dall’esempio, la lista che rappresenta la riga i-esima è formata da
elementi ognuno dei quali è una coppia di tipo (valore, indice_colonna). La ricerca dell’elemento di
indice (i, j) della matrice avviene quindi accedendo alla lista che rappresenta la riga i-esima, e
scandendola finchè non si trova, eventualmente, un elemento con indice_colonna=j.
Possiamo a questo punto dare direttamente una codifica C++ di questa rappresentazione. Si
noti che nella codifica, i nomi di alcuni “tipi” C++ sono uguali a nomi usati nella codifica a lista unica della
Sezione 3.1, ma hanno una definizione diversa in questa nuova codifica.
• codifica del la classe:
const int N1 = … ;
//cardinalità del dominio indice_1;
const int N2 = … ;
//cardinalità del dominio indice_2;
typedef … T_elem_mat; //definizione del tipo degli elementi
//della matrice;
typedef struct {T_elem_mat val;
int colonna;
} T_elem_lis;
class lista
{private :
//tipo degli elementi della lista
//utilizzata per rappresentare la
//matrice; notare la differenza
//rispetto alla definizione di
//Sezione 3.1
//una qualunque codifica del tipo lista
public :
void init_lis();
int length();
T_elem_lis element(int i);
void insert(T_elem_lis b,int i);
void remove(int i);
};
class matrice
{private :
lista l[N1];
//vettore di liste; la lista i-esima contiene
//i valori della riga i-esima diversi da
//quello dominante
T_elem_mat elem_dom; //valore dell’elemento dominante
public :
void init_mat(T_elem_mat v);
T_elem_mat valore(int i, int j);
void modifica(int i, int j, T_elem_mat v);
};
Come si può vedere dalla codifica data, una matrice viene rappresentata come una struttura a
due campi, uno dei quali contiene il valore dominante, e l’altro è un vettore di liste, ognuna delle quali
contiene gli elementi significativi di una riga, con il relativo indice di colonna.
23
Con riferimento alla matrice dell’Esempio 5, la codifica di tale matrice risulterebbe essere quella
rappresentata graficamente in Fig. 2, immaginando anche qui che il valore dominante sia espresso
come x.
elem_dom :
l:
(a
(c
(d
(g
(h
x
,2) (b ,3)
,1)
,1) ( e ,4) (f ,5)
,3)
,3)
Figura 2
• codifica delle operazioni primitive:
//è necessario premettere la codifica delle operazioni su lista, che
//verranno utilizzate per realizzare le operazioni primitive su matrice
void lista::init_lis()
{//codifica dell’ operazione init_lis()
}
int lista::length()
{//codifica dell’ operazione length()
}
T_elem_lis lista::element(int i)
{//codifica dell’ operazione element(…)
}
void lista::insert(T_elem_lis b,int i)
{//codifica dell’ operazione insert(…)
}
void lista::remove(int i)
{//codifica dell’ operazione remove(…)
}
void matrice::init_mat(T_elem_mat v)
{int k;
elem_dom = v;
for (k=0;k<N1;k++)
l[k].init_lis();
}
24
T_elem_mat matrice::valore(int i, int j)
//si noti la forte somiglianza con la realizzazione della stessa
//funzione nel caso di rappresentazione a lista unica
{int k;
k=1;
while (k<=l[i].length() && l[i].element(k).colonna!=j)
k++;
if (k<=l[i].length())
return (l[i].element(k)).val;
else return elem_dom;
}
void matrice::modifica(int i, int j, T_elem_mat v)
//si noti anche qui la forte somiglianza con la realizzazione della
//stessa funzione nel caso di rappresentazione a lista unica
{T_elem_lis t; int k;
k=1;
while (k<=l[i].length() && l[i].element(k).colonna!=j))
k++;
if (k<=l[i].length()) //se vero l'elemento di indice (i,j) e' gia'
//presente
if (v==elem_dom)
l[i].remove(k);
else {l[i].remove(k);
t.colonna=j; t.val=v;
l[i].insert(t,k);
}
else
//l'elemento di indice (i,j) non e' presente
if (v!=elem_dom)
{t.colonna=j; t.val=v;
l[i].insert(t,1); //convenzione: si inserisce in prima posizione
}
}
Si può facilmente verificare che il costo delle operazioni diventa proporzionale al numero di
elementi significativi in una riga (che sarà minore del numero totale di elementi significativi nell’intera
matrice). Il costo può essere in molti casi ulteriormente ridotto se la lista che rappresenta ogni singola
riga viene ordinata per indice di colonna crescente. In questo caso il costo medio di ricerca
dell’elemento di indice (i, j) diventa proporzionale a (num. elementi significativi in una riga)/2.
Ovviamente, una organizzazione per indici di colonna crescenti richiede che le operazioni di
inserzione nella lista siano realizzate in modo da rispettare l’ordinamento.
Infine, una ultima considerazione, per rimarcare i vantaggi in termini di flessibilità e modificabilità
di algoritmi risolutivi progettati secondo la filosofia della programmazione a oggetti (utilizzando il
meccanismo delle classi offerto dal C++). Se si considera la procedura “prodmat2” per il prodotto di
matrici data nell’Esempio 3, si può facilmente verificare che essa può essere utilizzata con una
qualunque delle rappresentazioni di matrici fornite (usando classi con la stessa parte “pubblica”), a
differenza della procedura “prodmat1” dell’Esempio 2 che può essere utilizzata solo con
rappresentazione basata direttamente sul meccanismo array. Rimangono ovviamente valide le
considerazioni sulla maggiore velocità nella esecuzione delle operazioni nel caso di quest’ultima
rappresentazione.
25
Capitolo 2
Linguaggio “assembler” e architettura della macchina
26
Introduzione
Anche questo capitolo non intende costituire un testo autosufficiente, ma va inteso come integrazione
delle lezioni tenute regolarmente in aula durante lo svolgimento del corso.
Lo scopo delle lezioni a cui questo capitolo fa riferimento è di fornire una idea più precisa di come algoritmi
espressi in un formalismo di “alto livello” (come è, ad esempio, il C++) possono essere concretamente eseguiti da
una macchina reale.
L’approccio seguito a tale scopo è in due passi.
Viene prima presentato un nuovo formalismo astratto (chiamato AS-86) di livello “più basso” rispetto al
C++, e viene mostrato come i fondamentali costrutti del C++ possono essere tradotti in sequenze di operazioni
semanticamente equivalenti dell’AS-86.
Viene poi presentata l’architettura di una possibile macchina reale (chiamata 8086-S) e viene mostrato
come algoritmi specificati in AS-86 possono essere tradotti in sequenze di operazioni semanticamente
equivalenti nel linguaggio (chiamato LM) eseguibile dall’8086-S.
I nomi scelti per indicare la macchina e i linguaggi non sono casuali. La macchina 8086-S e LM non sono
altro che una versione notevolmente semplificata della nota famiglia di processor 80x86 e del loro linguaggio
macchina, rispettivamente, mentre il linguaggio AS-86 è ispirato ai corrispondenti linguaggi “assembler”.
La presentazione di questi due punti segue un ordine inverso rispetto a quello abitualmente seguito (prima
l’architettura della macchina e poi il suo linguaggio “assembler”, che viene presentato come una semplice
versione “simbolica” del linguaggio macchina). La presentazione “inversa” adottata intende invece mettere
l’accento sulla stratificazione di “livelli virtuali” che avvolgono una macchina reale e che rendono possibile
l’esecuzione di formalismi anche molto “distanti” dal linguaggio macchina. L’importanza di tale concetto di base
mi è sembrato un motivo sufficiente a giustificare la “forzatura” insita nell’approccio seguito.
Questo capitolo, per la sua natura, si presenta incompleto in particolare per quanto riguarda la
formalizzazione di molti dei concetti espressi.
In un caso (traduzione da C++ in AS-86) tale mancata formalizzazione rappresenta una scelta in gran
parte obbligata, in quanto richiederebbe strumenti che verranno acquisiti in corsi più avanzati. Spero comunque
di aver dato l’idea che tale traduzione è “possibile”.
Negli altri casi, (specificazione della sintassi e semantica di AS-86 e LM) la formalizzazione è solo
parziale. Questo è dovuto alla natura di “lavoro in corso” di tali note, e spero non sia tale da compromettere la
comprensione delle idee base.
Ogni commento e suggerimento sarà comunque gradito.
27
Il linguaggio AS-86
Un algoritmo può essere specificato usando differenti formalismi, ognuno dei quali può essere
“eseguito” da un opportuno “automa” (si ricorda che per automa si può intendere una qualunque
entità in grado di comprendere ed eseguire un algoritmo espresso in un certo formalismo; quindi un
automa può essere un essere umano, una macchina astratta, una macchina concreta, …).
Consideriamo come esempio, il seguente algoritmo (banale) per calcolare il massimo tra due
numeri A e B. Un primo modo per esprimerlo può essere nel formalismo dei diagrammi a blocchi:
leggi A
leggi B
si
no
A≥B?
max <-- A
max <-- B
scrivi max
l
Figura 1
Lo stesso algoritmo può essere espresso in un altro formalismo a noi noto, il C++, eseguibile da
una “macchina C++”.
main ()
{int A, B, MAX;
cin >> A >> B;
if (A>=B)
MAX = A;
else MAX = B;
cout << MAX;
}
Figura 2
Mettiamo in evidenza alcune caratteristiche che questo ultimo formalismo richiede per
specificare ed eseguire un algoritmo:
a. i dati utilizzati devono essere dichiarati esplicitamente;
b. la macchina C++ esegue il programma in accordo alla seguente regola base:
1. la prima istruzione del programma è l’istruzione corrente
2. esegui l’istruzione corrente;
3. l’istruzione immediatamente successiva è la nuova istruzione corrente;
4. ritorna a 2.
28
La regola base può essere alterata usando opportuni costrutti di controllo che consentono di
“saltare” (if … … else) o “iterare” (for, while, do-while) blocchi di istruzioni. In effetti, si può pensare
che questi costrutti consentono di alterare il valore di una sorta di puntatore implicito che la macchina
C++ usa per tenere traccia di quale è l’istruzione corrente.
Esempio 1 - Se A=5, B=7, il “puntatore implicito” nel programma di Fig. 2 assume in sequenza
questi valori:
1 → cin >> A >> B;
if (A>=B)
MAX = A;
(nota come viene “saltata” l’istruzione MAX = A;)
2 → else MAX = B;
3→
cout << MAX;
_____________________________
Consideriamo ora la realizzazione dello stesso algoritmo di Figura 1 e 2 in un nuovo formalismo,
che chiamiamo AS-86, non ancora definito:
MAX
THEN:
FINE:
DW ?
<leggi AX>
<leggi BX>
CMP AX,BX
JGE THEN
MOV MAX,BX
JMP FINE
MOV MAX,AX
<scrivi MAX>
} dichiarazione
|
|
|
> operazioni eseguibili
|
|
Figura 3
Non essendo ancora stato definito il formalismo AS-86, questo algoritmo risulta probabilmente
di difficile comprensione. Lo commentiamo qui di seguito, per dare una prima idea (informale) delle
caratteristiche particolari di questo nuovo formalismo:
AX, BX e MAX sono operandi;
CMP, JGE, MOV, JMP sono operazioni eseguibili;
THEN, FINE sono oggetti speciali, detti etichette (il loro significato verrà spiegato più avanti);
Si può quindi notare:
• solo uno degli operandi è dichiarato esplicitamente (MAX);
29
• le operazioni hanno un formato standard:
<etichetta> :
<nome operazione>
|← CAMPO ETICHETTA →|←
CAMPO OPERATORE
<operandi>
→|← CAMPO OPERANDI →|
(opzionale)
(uno o due; se due, il primo è anche
la “destinazione” del risultato)
In AS-86 la regola standard di esecuzione è analoga a quella del C++. A differenza del C++
però, il puntatore all’istruzione corrente è un puntatore esplicito , nel senso che ha un nome (PC) e il
suo valore può essere modificato esplicitamente. Infatti, a differenza del C++, la regola standard viene
alterata in AS-86 usando un diverso meccanismo, quello delle istruzioni di salto, mediante le quali
viene assegnato un nuovo “valore” al puntatore PC. Una istruzione di salto ha il seguente formato:
J<cond>
<etichetta>
dove <cond> è una sequenza di uno o due caratteri alfabetici che specifica una condizione
logica (secondo un codice che verrà dato più avanti). Il significato di una istruzione di salto è il
seguente:
se <cond> è vera,
allora assegna a PC il valore di puntatore alla istruzione nel cui campo etichetta compare
<etichetta>;
altrimenti, assegna a PC il valore di puntatore alla istruzione successiva a quella di salto.
MP: condizione sempre vera (salto “incondizionato”)
L: minore
LE: minore o uguale
G: maggiore
GE: maggiore o uguale
EQ: uguale
NZ: diverso
Tabella 1 - Codici delle condizioni di salto
Quindi, nel programma di Figura 3, dopo l’istruzione
CMP AX,BX
il cui significato è: "confronta il valore di AX e BX", si esegue l’istruzione
JGE THEN
che ha il significato di salto condizionato all’esito del confronto appena fatto (in questo caso, si
salta se risulta AX≥BX). Se il salto non viene eseguito (cioè se AX<BX), allora si esegue
MOV MAX, BX
30
il cui effetto è di assegnare a MAX il valore di BX, e subito dopo si esegue l’istruzione di salto
incondizionato
JMP FINE
per evitare di eseguire l’istruzione MOV MAX,AX.
Dopo questa presentazione informale, presentiamo formalmente il linguaggio AS-86. Questo
significa definire:
• dati rappresentabili;
• sintassi e semantica della dichiarazione di dati;
• sintassi e semantica delle operazioni disponibili.
Dati rappresentabili
E’ possibile utilizzare dati appartenenti a due tipi diversi:
DW:
DB:
interi compresi tra ± 215;
{
interi compresi tra ±2
7
caratteri (fino a 256 caratteri diversi)
Dichiarazioni
Diamo ora la sintassi della dichiarazione esplicita di dati di tipo DB o DW.
<dich-cost>::= <nome> EQU <val-def>
<dich-var>::= <nome> <tipo> <corpo-dich>
<tipo> ::= DB | DW
<corpo-dich> ::= <lista> | <sequenza>
<lista> ::= <valore> | <valore>, <lista>
<valore> ::= <val-def> | <val-indef>
<val-indef> ::= ?
<val-def> ::= -215 | … | -1 | 0 | 1 | 2 | … | 215-1 | A | B | C | … | Z
<sequenza> ::= <dim-sequenza> DUP (<valore>)
<dim-sequenza> ::= 1 | 2 | 3 | 4 | …
Diamo ora alcuni esempi di dichiarazioni ricavabili dalla sintassi appena descritta. La semantica
della dichiarazioni di costanti, variabili, “liste” e “sequenze” è analoga alla semantica delle
corrispondenti dichiarazioni C++ (associazione tra nome e “oggetto” dichiarato). Gli esempi che
seguono evidenziano tale analogia.
Dichiarazione di costante
NUM EQU 10
(associa al nome NUM il valore costante 10)
CAR EQU 'A'
(associa al nome CAR il valore costante 'A')
31
Dichiarazioni di variabili di tipo DB
VAR DB ?
(dichiarazione di var. di nome VAR; la cella di memoria associata a tale nome ha
valore indefinito)
NUM DB 23
(dichiarazione di var. di nome NUM; la cella di memoria associata a tale nome è
inizializzata con il valore 23)
LISTANUM DB 12, 7
(dichiarazione di “lista” di (due) variabili; la 1a è identificata dal nome
LISTANUM, e la cella di memoria associata è inizializzata con il
valore 12; la 2a (per convenzione dell’AS-86) è identificata
dal “nome” LISTANUM+1, e la corrispondente cella di
memoria è inizializzata con il valore 7)
LISTANUM:
12
LISTANUM+1 :
7
Figura 4
LISTACAR DB 'A','B','C' (dichiarazione di “lista” di (tre) variabili; la 1a è identificata dal nome
LISTACAR, e la cella di memoria associata è inizializzata con il
valore 'A'; la 2a (per convenzione dell’AS-86) è identificata
dal “nome” LISTACAR+1, e la corrispondente cella di
memoria è inizializzata con il valore 'B'; la 3a (per convenzione
dell’AS-86) è identificata dal “nome” LISTACAR+2, e la
corrispondente cella di memoria è inizializzata con il valore 'C')
LISTACAR :
'A'
LISTACAR+1 :
'B'
LISTACAR+2 :
'C'
Figura 5
STR DB 10 DUP('A') (dichiarazione di sequenza di (dieci) elementi, tutti inizializzati con il valore
'A'; il 1° è identificato dal nome STR; il 2° (per convenzione
dell’AS-86) è identificata dal “nome” STR+1; … ; il 10° è
identificato dal “nome” STR+9)
32
STR :
'A'
STR+1 :
'A'
STR+2 :
'A'
.
.
.
.
STR+9 :
'A'
Figura 6
Dichiarazioni di variabili di tipo DW
VAR DW ?
(dichiarazione di var. di nome VAR; la cella di memoria associata a tale nome ha
valore indefinito)
NUM DW 154
(dichiarazione di var. di nome NUM; la cella di memoria associata a tale nome è
inizializzata con il valore 154)
LISTANUM DW 207,128,13
(dichiarazione di “lista” di (tre) variabili; la 1a è identificata dal nome
LISTANUM, e la cella di memoria associata è inizializzata con il
valore 207; la 2a (per convenzione dell’AS-86) è identificata
dal “nome” LISTANUM+2 (nota la differenza dal caso
precedente!) e la corrispondente cella di memoria è
inizializzata con il valore 128; la 3 a è identificata dal “nome”
LISTANUM+4 e la corrispondente cella di memoria è
inizializzata con il valore 13)
LISTANUM :
207
LISTANUM+2 :
128
LISTANUM+4 :
13
Figura 7
VET DW 20 DUP(?) (dichiarazione di sequenza di (venti) elementi, tutti non inizializzati; il 1° è
identificato dal nome VET; il 2° (per convenzione dell’AS-86)
è identificata dal “nome” VET+2; il 3° è identificato dal
“nome” VET+4; … ; il 20° è identificato dal “nome” VET+38)
33
VET :
VET+2 :
VET+4 :
.
.
.
.
VET+38 :
Figura 8
Come già accennato nell’esempio presentato in precedenza, il linguaggio AS-86 consente di
utilizzare, oltre ai dati (costanti, variabili,…) dichiarati esplicitamente secondo le modalità appena viste,
anche alcune variabili “predichiarate”. Per variabili predichiarate si intendono dei nomi predefiniti ai
quali è già associato un tipo (p.es. DB) e una cella di memoria. Ne diamo ora l’elenco completo.
Variabili predichiarate
AX, BX, CX, DX: di tipo DW
AH, AL, BH, BL, CH, CL, DH, DL: di tipo DB
SI, DI: di tipo “indice” (l’uso di questo tipo speciale verrà spiegato nel seguito)
SP: di tipo “pila” (il formalismo AS-86 dispone di una pila (non è possibile definirne altre perchè il
tipo “pila” non è inserito tra i tipi utilizzabili nelle dichiarazioni esplicite), il cui riferimento
all’elemento di testa è SP).
Operazioni
La sintassi delle operazioni disponibili nel formalismo AS-86, già data informalmente in
precedenza, è la seguente:
<operazione> ::= <etichetta> : <operatore> <lista operandi> | <operatore> <lista operandi>
<etichetta> ::= <nome>
<operatore> ::= ADD | SUB | MUL | DIV | INC | DEC | CMP | MOV | PUSH | POP
<lista operandi> ::= <operando> | <operando>, <operando>
<operando> ::= <val-def> | <nome> | <nome con indice> | <puntatore>
Questa sintassi (incompleta) ci dice che una operazione è formata da due parti fisse
(<operatore> e <lista operandi>) più una opzionale (<etichetta>); a sua volta la <lista operandi> è
formata da uno o due operandi, ognuno dei quali può essere specificato secondo una delle quattro
modalità indicate (che spiegheremo più avanti).
34
Ovviamente, non tutte le “frasi” corrette secondo questa sintassi hanno un significato nella
semantica del formalismo AS-86. Di seguito, indichiamo quali vincoli devono essere soddisfatti perchè
tali “frasi” abbiano un significato, e quale è tale significato.
• Operatore ADD :
Frase lecita: ADD dst, src (dove src e dst sono due operandi)
Semantica: dst = dst + src
Vincoli: • l’operando dst non può essere specificato come <val-def> oppure come <nome> se
<nome> è un nome di costante
• uno dei due operandi deve essere una variabile predichiarata o una costante
• Operatore SUB :
Frase lecita: SUB dst, src (dove src e dst sono due operandi)
Semantica: dst = dst - src
Vincoli: • l’operando dst non può essere specificato come <val-def> oppure come <nome> se
<nome> è un nome di costante
• uno dei due operandi deve essere una variabile predichiarata o una costante
• Operatore MUL :
Frase lecita: MUL dst, src (dove src e dst sono due operandi)
Semantica: dst = dst x src
Vincoli: • l’operando dst non può essere specificato come <val-def> oppure come <nome> se
<nome> è un nome di costante
• uno dei due operandi deve essere una variabile predichiarata o una costante
• Operatore DIV :
Frase lecita: DIV dst, src (dove src e dst sono due operandi)
Semantica: dst = dst / src
Vincoli: • l’operando dst non può essere specificato come <val-def> oppure come <nome> se
<nome> è un nome di costante
• uno dei due operandi deve essere una variabile predichiarata o una costante
• Operatore INC :
Frase lecita: INC dst
Semantica: dst = dst + 1
Vincoli: • l’operando dst non può essere specificato come <val-def> oppure come <nome> se
<nome> è un nome di costante
35
• Operatore DEC :
Frase lecita: DEC dst
Semantica: dst = dst - 1
Vincoli: • l’operando dst non può essere specificato come <val-def> oppure come <nome> se
<nome> è un nome di costante
• Operatore CMP :
Frase lecita: CMP dst, src (dove src e dst sono due operandi)
Semantica: confronta (e memorizza l’esito del confronto) dst e src
Vincoli: • uno dei due operandi deve essere una variabile predichiarata o una costante
• Operatore PUSH :
Frase lecita: PUSH src
Semantica: inserisce sulla pila (“push”) il valore di src
• Operatore POP :
Frase lecita: POP dst
Semantica: estrae dalla pila (“pop”) il valore di testa e lo assegna a dst
Vincoli: • l’operando dst non può essere specificato come <val-def> oppure come <nome> se
<nome> è un nome di costante
• Operatore MOV :
Frase lecita: MOV dst, src (dove src e dst sono due operandi)
Semantica: dst := src
Vincoli: • l’operando dst non può essere specificato come <val-def> oppure come <nome> se
<nome> è un nome di costante
• uno dei due operandi deve essere una variabile predichiarata o una costante
Nota: uno dei vincoli che compare spesso nelle definizioni appena date (uno dei due operandi
deve essere una variabile predichiarata o una costante) può apparire del tutto “arbitrario” e privo di
giustificazioni. Lo è in effetti, alla luce delle conoscenze che abbiamo a questo punto. Troverà una sua
giustificazione più avanti, quando affronteremo il problema della “traduzione” di algoritmi specificati in
AS-86 in algoritmi specificati in un differente formalismo (“linguaggio macchina”).
Modalità di specificazione degli operandi
Abbiamo visto che la sintassi per specificare operandi è:
<operando> ::= <val-def> | <nome> | <nome con indice> | <puntatore>
Diamo ora la semantica delle varie modalità sintattiche indicate:
36
<val-def> : il valore specificato è quello indicato esplicitamente dall’operando;
<nome> : dobbiamo distinguere due casi:
1• <nome> appare in una dichiarazione di costante:
il valore specificato è quello associato a <nome> nella dichiarazione;
2 • <nome> appare in una dichiarazione di variabile o è il nome di una variabile predichiarata:
il valore specificato è: il valore contenuto nella cella di memoria
associata a <nome>.
<nome con indice> :
la sintassi completa di questa modalità di specificazione degli operandi è:
<nome con indice> ::= <nome> [<indice>]
dove <nome> deve essere il nome di una variabile dichiarata esplicitamente e <indice> deve
essere il nome di una delle due variabili predichiarate di tipo “indice” (DI o SI), oppure una costante
il valore specificato è: il valore contenuto nella cella di memoria
associata a <nome>+(valore di <indice>).
Nota: per il significato di <nome>+(valore di <indice>) vedi nella sezione Dichiarazioni le
modalità di accesso agli elementi di oggetti dichiarati come “sequenza” o “lista”.
<puntatore> :
la sintassi completa di questa modalità di specificazione degli operandi è:
<puntatore> ::= [<indice>] | [<indice>] + <num. intero>
dove <indice> deve essere il nome di una delle due variabili predichiarate di tipo “indice” (DI o
SI), e <num. intero> è un valore intero positivo;
il valore specificato è: il valore contenuto nella cella di memoria
associata a <indice>+(valore di <num. intero>).
Nota: al momento non è ancora noto come associare a una variabile di tipo indice una cella di
memoria. Vedremo più avanti come questo può essere fatto.
Esempio 2
Consideriamo il seguente “frammento” di programma C++:
int
int
.
.
x =
y =
y =
const
x,y ;
k = 10;
y;
k;
30;
37
Un frammento equivalente (cioè con la stessa semantica) in AS-86 è il seguente:
K EQU 10
X DW ?
Y DW ?
.
.
MOV AX, Y
MOV X, AX
MOV Y,K
MOV Y,30
oppure, usando la variabile predichiarata BX al posto di Y:
K EQU 10
X DW ?
.
.
MOV X,BX
MOV BX,K
MOV BX,30
____________
Consideriamo il programma visto prima per il calcolo del massimo tra due numeri:
THEN:
FINE:
MAX DW ?
<leggi AX>
<leggi BX>
CMP AX,BX
JGE THEN
MOV MAX,BX
JMP FINE
MOV MAX,AX
<scrivi MAX>
Abbiamo visto, a suo tempo, come questo programma sia equivalente (abbia la stessa
semantica) ad un programma C++ che fa uso del costrutto if …
… else. Questa equivalenza può
essere generalizzata secondo la seguente regola:
Regola di traduzione 1:
dato un comando C++ del tipo
if
<condizione>
<comando 1>
else <comando 2>
l’equivalente programma AS-86 è:
<test condizione >
(usando CMP … , tipicamente)
J<cond> THEN (salto condizionato, si deve saltare se la condizione è vera)
<operazioni equivalenti a “comando 2”>
JMP ENDIF
38
THEN : <operazioni equivalenti a “comando 1”>
ENDIF : …
_________________
Esempio 3
Calcolo del massimo in un vettore di 10 elementi.
Il frammento di programma C++ che risolve questo problema è il seguente:
int
int
…
max
for
vet [10];
i,max ;
= vet[0];
(i = 1; i<10; i++)
if (max < vet[i])
max = vet[i];
…
Un programma equivalente in AS-86 è il seguente:
VET
MAX
DW 10 DUP (?)
DW ?
.
.
MOV AX, VET
MOV SI, 0
CICLO : ADD SI, 2
CMP AX, VET[SI]
JGE ENDIF
MOV AX, VET[SI]
MOV MAX, AX
ENDIF : CMP SI, 18
JL CICLO
…
(SI funge da indice per il ciclo)
(l’incremento di SI è di 2 perchè l’array è di oggetti di tipo DW)
(AX ≥VET[SI]?)
(salta se la condizione è vera)
(realizza in due passi
l’operazione MAX = VET[SI]
(raggiunto l’ultimo elemento del vettore?)
(salta se l’indice SI non ha raggiunto l’ultimo elemento del vettore)
Nota: gli elementi dell’array VET sono individuabili come mostrato nella seguente figura.
'
VET :
VET+2 :
VET+4 :
.
10 ELEMENTI
.
VET+18 :
Figura 9
_________________________________
L’esempio appena visto, oltre a presentare un ulteriore caso di traduzione di un costrutto if
del C++ in uno equivalente in AS-86, presenta anche una traduzione di un costrutto for
del C++ in
uno equivalente in AS-86. Anche in questo caso possiamo definire una regola generale che fa
corrispondere a un comando for
C++ un equivalente programma in AS-86. La regola è la seguente:
39
Regola di traduzione 2:
dato un comando C++ del tipo:
for (i = k; i≤n; i++)
<comando>
l’equivalente programma AS-86 è:
MOV <indice>, k*incr.
(<indice> è una delle due variabili SI o DI)
CICLO : CMP <indice>, n*incr.
(se l’indice risulta > n, si deve uscire dal ciclo)
JG ENDFOR
<operazioni equivalenti a “comando”>
ADD <indice>, incr.
(incr. è un opportuno incremento dell’indice,
precedente, incr.=2)
JMP CICLO
ENDFOR : ……
nell’esempio
_____________________________
Esempio 4
Dato il seguente comando C++ (dove a, i ed n sono variabili di tipo int):
…
for (i = 3; i≤n; i++)
a = a+i;
…
una sua corrispondente traduzione in AS-86 è:
N
A
…
DW ?
DW ?
MOV SI, 3
CICLO: CMP SI, N
JG ENDFOR
ADD A, SI
ADD SI, 1 (oppure
JMP CICLO
ENDFOR: …
INC SI)
Da notare che in questo caso l’incremento “opportuno” di SI per realizzare un comportamento
equivalente al comando C++ è 1.
____________________________
Esempio 5
Dato un vettore di 5 interi, moltiplicare per due tutti quelli che precedono un elemento (se c’è) di
valore uguale ad un valore “soglia”.
Ad esempio, se “soglia”=8, il vettore dato verrà trasformato come segue:
40
caso a): il vettore contiene l’elemento soglia; la trasformazione si arresta all’elemento precedente;
9
18
6
12
8
8
5
5
15
15
vettore di partenza
vettore trasformato
caso b): il vettore non contiene l’elemento soglia; la trasformazione interessa tutto il vettore;
7
14
11
22
3
6
5
10
12
24
vettore di partenza
vettore trasformato
Un programma C++ che risolve questo problema è il seguente. Nel programma, per semplificare
le operazioni da eseguire, si ricorre all’espediente di aggiungere in fondo al vettore originario un
elemento fittizio, che assume il valore “soglia”.
int vet :6] ;
int soglia, i;
…
cin >> soglia;
vet[5] = soglia;
i := 0;
while (vet[i] != soglia)
{vet[i] = 2*vet[i];
i = i+1;
}
Un programma equivalente in AS-86 è il seguente:
41
VET
DW 6 DUP (?)
.
.
<memorizza in DX il valore “soglia”>
MOV VET[10], DX
(vet[6] = soglia)
MOV SI, 0
CICLO : CMP DX, VET[SI]
(soglia≠VET[SI]?)
JEQ ENDWHILE
(se la condizione è falsa, esci dal ciclo)
MUL VET[SI], 2
(VET[SI] = 2*VET[SI])
ADD SI, 2
(incremento dell’indice)
JMP CICLO
ENDWHILE : …
____________________________
Anche in questo caso possiamo generalizzare l’esempio visto nella seguente regola:
Regola di traduzione 3:
dato un comando C++ del tipo
while (<condizione>)
<comando>
l’equivalente programma AS-86 è il seguente:
CICLO : <test condizione>
Jcond ENDWHILE
(tipicamente, con una istruzione CMP)
(salta se la condizione del while è falsa)
<operazioni equivalenti a <comando>>
JMP CICLO
ENDWHILE : …
____________________________
Esempio 6
Calcolo della media di un vettore di 16 interi.
La versione C++ di un algoritmo che risolve questo problema è la seguente.
int vet [16];
int media, i;
<lettura vettore>;
media = 0;
for (i = 0; i<16; i++)
media = media + vet[i];
media = media/16
La versione equivalente in AS-86 è la seguente.
VET
DW 16 DUP (?)
MEDIA DW ?
<lettura vettore>
MOV MEDIA, 0
MOV SI, 0
42
CICLO : MOV AX, VET[SI]
MEDIA, VET[SI] a causa del vincolo sul tipo di
ADD MEDIA, AX
ADD SI, 2
CMP SI, 30
JLE CICLO
DIV MEDIA, 16
(nota che sarebbe stato scorretto scrivere direttamente ADD
operandi)
(incremento dell’indice)
_______________________________
Esempio 7
Somma di due vettori A e B di n elementi (la somma va in A).
La versione C++ di un algoritmo che risolve questo problema è la seguente.
int const n = … ;
int a[n];
int b[n];
int i;
<lettura di a e b>;
for (i = 1; i<n; i++)
a[i] = a[i] + b[i];
…
L’equivalente versione in AS-86 è la seguente.
<lettura vettori A e B>
MOV SI, 0
CICLO : MOV BX, B[SI]
(nota che sarebbe stato scorretto scrivere direttamente ADD
A[SI], B[SI] a causa del vincolo sul tipo di operandi)
ADD A[SI], BX
ADD SI, 2
CMP SI, DOUBLE_N
(SI < 2*n?)
JL CICLO
(salta se la condizione è vera)
.
.
DOUBLE_N EQU …
(valore pari a 2*n)
N EQU …
A DW N DUP (?)
B DW N DUP (?)
Procedure
Le motivazioni che portano all’introduzione del concetto di “procedura” in un linguaggio di
programmazione ci sono già note. Per poter rendere praticabile l’uso di tale concetto, un linguaggio di
programmazione deve mettere a disposizione:
• costrutti per dichiarare che un segmento di codice è una procedura;
• meccanismi di chiamata e ritorno;
• meccanismi per il passaggio dei parametri e il ritorno dei risultati.
Riassumiamo brevemente come il formalismo C++ risolve questi punti:
A) dichiarazione: diamo per nota la sua sintassi e semantica;
B) chiamata: quando un nome di procedura viene incontrato durante l’esecuzione, il “puntatore
implicito” della macchina C++ si trasferisce sulla prima istruzione eseguibile della procedura;
43
C) ritorno: terminata l’ultima istruzione eseguibile di una procedura, il “puntatore implicito” ritorna
automaticamente a puntare alla istruzione successiva a quella in cui era stata fatta la
chiamata;
D) passaggio parametri: • per valore;
• per riferimento;
E) ritorno risultati:
• per riferimento
• valore ritornato da funzioni.
Nel formalismo AS-86 la soluzione data è la seguente:
A) dichiarazione:
sintassi:
<dich. procedura> ::= <nome> PROC <comando> ENDP
semantica: la sequenza di operazioni <comando> viene associata al nome <nome>;
B) chiamata:
sintassi:
<chiamata di procedura> ::= CALL <nome>
dove <nome> deve essere un nome di procedura;
semantica:
1. PC assume il valore di riferimento alla prima istruzione eseguibile della procedura <nome>;
2. sulla pila (riferimento SP) viene messo il riferimento alla istruzione successiva a CALL
<nome>;
C) ritorno:
sintassi:
<ritorno da procedura> ::= RET
semantica:
si estrae il valore in testa alla pila e lo si assegna a PC
Esempio 8
Cosideriamo questo frammento di programma AS-86:
44
.
.
XX :
YY : …
CALL PROC_1
.
.
PROC_1 PROC
WW : … (1a istruzione di PROC_1)
…
ZZ :
CALL PROC_2
TT : …
…
RET
…
ENDP
PROC_2 PROC
JJ : … (1a istruzione di PROC_2)
…
…
RET
…
procedura PROC_1
procedura PROC_2
ENDP
L’evoluzione del contenuto della pila e del valore di PC e SP in corrispondenza delle varie
chiamate e ritorni è il seguente:
SP→
SP→
YY
SP→
TT
YY
PC :
XX
PC :
WW
(CALL PROC_1)
SP→
YY
PC :
TT
PC :
JJ
(CALL PROC_2)
SP→
PC :
(RET)
YY
(RET)
__________________________________
D) passaggio parametri: tipicamente, avviene usando le variabili predichiarate, secondo la
modalità per valore (alla variabile è associato il valore del parametro} o per riferimento (alla
45
variabile è associato un riferimento alla cella di memoria che contiene il valore del
parametro);
E) ritorno risultati: sempre tramite le variabili predichiarate secondo le stesse modalità del
passaggio di parametri.
Nota: La pila definita nel formalismo AS-86 può essere usata per trasmettere parametri e
ritornare risultati. Dato però che la stessa pila viene anche usata per gestire “esplicitamente” (cioè in
modo visibile a chi scrive l’algoritmo) il meccanismo di chiamata e ritorno, particolare attenzione va posta
per non creare interferenze potenzialmente disastrose tra le due cose.
Esempio 9
Calcolare la media aritmetica delle medie aritmetiche degli elementi di due vettori (si utilizza a tale
scopo una versione leggermente modificata del programma visto nell’ esempio 5).
L’algoritmo di soluzione è articolato in una procedura che riceve in ingresso un vettore e la sua
dimensione e restituisce il valore della media degli elementi del vettore, e un programma principale che
chiama due volte la procedura e poi calcola la media delle due medie così ottenute.
La trasmissione dei parametri e dei risultati avviene usando le seguenti variabili predichiarate:
SI: vettore (passaggio per riferimento)
BX: dimensione del vettore (passaggio per valore);
CX: media degli elementi del vettore (risultato).
Programma principale:
VET1 DW 10 DUP (?)
VET2 DW 20 DUP (?)
MEDIA_MEDIE DW ?
<lettura VET1 e VET2>
MOV SI, OFFSET VET1
MOV BX, 10
CALL CALCOLA_MEDIA
MOV MEDIA_MEDIE, CX
MOV SI, OFFSET VET2
MOV BX, 20
CALL CALCOLA_MEDIA
ADD MEDIA_MEDIE, CX
DIV MEDIA_MEDIE, 2
(incontriamo qui una nuova modalità di specificazione degli
operandi: l’operando OFFSET <nome> assume il valore di
riferimento alla cella di memoria associata a <nome>; in questo
caso quindi, SI assume il valore di puntatore al primo elemento
del vettore VET1)
(in CX è stato posto il risultato)
(preparazione parametri per la 2a chiamata
della procedura)
(il nuovo risultato viene sommato al precedente)
(calcolo della media delle medie)
46
Procedura:
CALCOLA_MEDIA PROC
MOV NUM_ELEM, BX
BX verrà decrementato nel ciclo)
MOV CX, 0
CICLO : ADD CX, [SI]
ADD SI, 2
(salva nella variabile NUM_ELEM il valore trasmesso in
(azzeramento della variabile che conterrà il risultato finale)
(SI viene fatto puntare all’elemento successivo del
vettore)
(decremento di BX)
(BX ≠ 0?)
(salta se la condizione è vera, cioè se la scansione del
vettore non è terminata)
DEC BX
CMP BX, 0
JNZ CICLO
NUM_ELEM
DIV CX, NUM_ELEM
RET
DW ?
ENDP
→
1
[SI+2] →
2
[SI+4] →
3
[SI]
BX;
.
.
.
.
.
.
n
Figura 10
_____________________________
47
Architettura e linguaggio di programmazione di una macchina fisica (8086-S)
In questa sezione presentiamo una macchina in grado di eseguire algoritmi specificati nel
linguaggio (che chiameremo LM) proprio della macchina stessa, e mostriamo come algoritmi specificati
in AS_86 possono essere tradotti in algoritmi equivalenti specificati in LM.
I principali componenti della macchina 8086-S sono mostrati in Fig. 11.
Memoria
Processor
I/O
…
I/O
Figura 11
Memoria
La memoria di questa macchina è costituita da una sequenza di N celle, ognuna identificata da
un numero (indirizzo) progressivo, da 0 a N-1.
0 :
1 :
2 :
.
.
.
.
N-1 :
Ogni cella è costituita da b elementi fisici in grado di assumere due stati diversi
(magnetizzato/non magnetizzato, conduttore/non conduttore, … a seconda delle tecnologie
disponibili). Ogni elemento rappresenta quindi un bit (cifra binaria) di informazione, e ogni cella può
quindi contenere una informazione codificata in forma binaria, usando b cifre binarie.
0
1
2 …
b-1
cella i :
Nell’8086-S (e nei calcolatori attuali) b = 8, e la cella viene chiamata
byte. Come unità di
riferimento della memoria si usa anche la parola, formata da due o quattro byte. Nell’AS-86, una parola
è formata da due byte. Ogni parola è quindi individuata da un indirizzo pari.
48
0 :
2 :
4 :
.
.
.
.
.
.
N-2 :
Ogni cella può essere utilizzata per rappresentare:
• un dato (o parte), numerico o non numerico
• una istruzione eseguibile (o parte)
Per comunicare con le altre unità, la memoria dispone di alcune celle particolari, i registri, che
sono le uniche parti della memoria direttamente accessibili dall’esterno, e i cui “nomi” e funzioni sono i
seguenti:
RWR (2 byte): registro di lettura e scrittura;
AR (2 byte): registro indirizzo;
OPR (2 bit): registro operazione (00 ≡ lettura di un byte;
01 ≡ lettura di una parola;
10 ≡ scrittura di un byte;
11 ≡ scrittura di una parola);
RR (1 bit): registro di “richiesta”;
TR (1 bit): registro di “terminazione”.
0 :
1 :
2 :
.
.
N-1:
RWR
AR
OPR
RR
TR
Figura 12
Il funzionamento della memoria è descritto dal seguente algoritmo (la memoria può quindi
essere vista come una macchina che esegue fisicamente questo unico algoritmo):
49
do
while (RR == 0) do nothing;
switch (OPR)
{case 00 : <lettura byte>; break;
case 01 : <lettura parola>; break;
case 10 : <scrittura byte>; break;
case 11 : <scrittura parola> break;
};
TR = 1;
while (RR == 1) do nothing;
TR = 0;
for ever.
<lettura byte> :
RWR[0..7] = M(AR)
<lettura parola> :
RWR[0..15] = M(AR)
<scrittura byte> :
M(AR) = RWR[0..7]
<scrittura parola> :
M(AR) = RWR[0..15]
dove: RWR[0..n] denota i bit 0..n del registro RWR, e M(AR) denota il byte (o parola) il cui
indirizzo è contenuto in AR.
Quindi, per leggere una parola di indirizzo X dalla memoria una unità esterna (ad esempio il
processor) deve eseguire le seguenti operazioni:
while (TR == 1) do nothing;
AR = X;
OPR = 01;
RR = 1:
while (TR == 0) do nothing;
<preleva parola da RWR>;
RR = 0;
mentre, per scrivere una informazione E codificata con 16 bit (una parola) nella cella di memoria
di indirizzo X una unità esterna deve eseguire le seguenti operazioni:
while (TR == 1) do nothing;
RWR = E;
AR = X;
OPR = 11;
RR = 1:
while (TR == 0) do nothing;
RR := 0;
Processor
Il processor è una macchina formata da tre parti fondamentali:
• UC (unità di controllo): è l’unità che controlla e comanda il funzionamento di tutta la macchina;
50
• UO (unità operativa): è l’unità che esegue, dietro comando di UC, operazioni logico/aritmetiche
(somma, sottrazione, confronto, …);
• Registri: dispositivi in grado di contenere informazione, accessibili direttamente da UC e UO.
I dispositivi di tipo “registro” presenti nell’8086-S sono i seguenti:
Registri generali:
(16 bit) AX, BX, CX, DX;
(8 bit) AL, AH, BL, BH, CL, CH, DL, DH.
Come si nota dalla Fig. 13, i registri a 8 bit non sono altro che il primo e il secondo byte di ogni
registro a 16 bit. Questi registri vengono utilizzati per contenere dati.
Registri indice: (16 bit) SI, DI.
Questi registri vengono utilizzati per contenere indirizzi.
Registro indirizzo istruzione corrente: (16 bit) PC
Utilizzato per contenere l’indirizzo dell’istruzione correntemente eseguita dal processor.
Registro istruzione corrente: (32 bit) CI
Utilizzato per contenere l’istruzione correntemente eseguita dal processor.
Flags: (1 bit)
CF: indicatore di risultato con riporto sul bit di segno
ZF: indicatore di risultato nullo
SF: indicatore di risultato negativo
0
7 8
15
AX
AL
AH
BX
BL
BH
CX
CL
CH
DX
DL
DH
DI
SI
PC
CI
CF
ZF
SF
Figura 13 - registri del processor della macchina 8086-S
L’esecuzione di ogni operazione logico/aritmetica da parte del processor provoca una
opportuna modifica del valore dei flag. Essi vengono quindi utilizzati come indicatori del tipo di risultato
51
ottenuto, tipicamente per scopi di controllo (eventuali trabocchi, …) o per pilotare l’esecuzione di “salti
condizionati” (vedi dopo).
Nota: si può notare come i nomi utilizzati per identificare alcuni registri del processor dell’8086S siano uguali ai nomi delle variabili predichiarate del linguaggio AS-86. La corrispondenza non è
casuale ma, come vedremo più avanti, ha lo scopo di semplificare la “traduzione” di programmi scritti
nel linguaggio AS-86 in programmi scritti in un linguaggio eseguibile dalla macchina 8086-S. In altre
parole, il linguaggio AS-86 è progettato in modo da poter essere “facilmente” eseguito dalla macchina
8086-S.
Il funzionamento del processor è descritto dal seguente algoritmo (in particolare, l’Unità
Controllo può essere vista come una macchina che esegue fisicamente questo algoritmo):
do
{while (TR == 1) do nothing;
AR = PC;
(lettura della istruzione il cui indirizzo è contenuto in PC)
OPR = 01;
RR = 1;
while (TR == 0) do nothing;
CI[15..0] = RWR;
(l’istruzione letta viene messa nei primi 16 bit di CI)
RR = 0;
PC = PC+2;
(PC ora contiene l’indirizzo della parola successiva)
if (<istruzione su due parole>)
(la codifica di alcune istruzioni occupa due parole consecutive
nella memoria dell’8086-S; esaminando la parte già presente
in CI[15..0] si può stabilire in quale caso ci si trova)
{AR = PC;
(lettura della seconda parte dell’istruzione)
OPR = 01;
RR = 1:
while (TR == 0) do nothing;
CI[31 .. 16] = RWR;(la seconda parte dell’istruzione viene messa in CI[31..16])
RR = 0;
PC = PC+2
}
<decodifica ed esecuzione istruzione in CI>;
<trattamento interruzioni>;
}
for ever.
Nell’algoritmo appena presentato, si può notare che la lettura di una parola dalla memoria
avviene secondo le modalità descritte a proposito della memoria. Per quanto riguarda le due parti
designate come
<decodifica ed esecuzione istruzione in CI>, e
<trattamento interruzioni>
la prima verrà presentata più avanti, dopo aver descritto i tipi di istruzioni che possono essere
eseguiti dal processor dell’8086-S, mentre la seconda non sarà oggetto della presente trattazione.
Dalla presentazione dell’algoritmo di funzionamento del processor dovrebbe comunque essere
chiaro come tale algoritmo consenta alla macchina 8086-S di eseguire una sequenza di
istruzioni (cioè un programma) presente nella sua memoria.
52
Istruzioni eseguibili
Il processor dell’8086-S è in grado di eseguire tutte le usuali operazioni logico/aritmetiche
(somma, sottrazione, …). Tali operazioni (istruzioni) sono codificate in forma binaria, su una o due
parole consecutive (16 o 32 bit), dove il primo byte codifica, in ogni caso, il tipo di operazione (codice
operativo).
0
7 8
15 0
7 8
15
C OP
I restanti byte (uno o tre) vengono usati per codificare gli operandi. Le opzioni possibili nella
specificazione degli operandi sono le seguenti:
• l’operando (il suo valore) è contenuto in un registro (modalità registro);
• l’operando (il suo valore) è contenuto nell’istruzione stessa (modalità immediata);
• l’operando (il suo valore) è contenuto in una cella di memoria; in questo caso deve essere
specificato l’indirizzo della cella, che può essere fatto secondo le seguenti modalità:
- l’indirizzo è contenuto nella stessa istruzione (modalità memoria);
- l’indirizzo è ottenuto sommando il contenuto di un registro ad un valore contenuto
nell’istruzione stessa (modalità registro indiretto (+ valore));
Le istruzioni eseguibili dal processor possono essere raggruppate in varie classi che
differiscono fra loro per le modalità di specificazione degli operandi. Presentiamo ora tali classi. In
questa presentazione, utilizzeremo le seguenti notazioni:
COP: codice binario di una operazione logico/aritmetica
R1, R2: codifica binaria del nome di un registro
VALORE: codifica binaria di un valore numerico
op: generico operatore logico/aritmetico
M(ind.): cella di memoria di indirizzo “ind”
0
…
COP
Classe RR (registro-registro)
7 8 … 11 12 … 15
R1
R2
In questa classe sono codificate operazioni binarie o unarie i cui operandi sono entrambi
contenuti in registri.
R1 = R1 op R2; (operazione binaria)
R1 = op R1;
(operazione unaria)
0
…
COP
7 8 … 11 12 … 15
R1
VALORE
Classe RM (registro-memoria)
53
R2
In questa classe sono codificate operazioni binarie o unarie i cui operandi sono contenuti uno in
un registro e l’altro in una cella di memoria.
R1 = R1 op M(R2 + VALORE);
M(R1 + VALORE) = M(R1 + VALORE) op R2;
Da notare nell’indirizzo (Rx + VALORE) che:
• se Rx non corrisponde a un nome “valido”, l’indirizzo è costituito dal solo VALORE (modalità
memoria);
• se VALORE == 0, l’indirizzo è costituito dal solo contenuto di Rx (modalità registro indiretto);
• altrimenti, l‘indirizzo è calcolato come Rx + VALORE (modalità registro indiretto + valore).
E’ facile vedere come tali modalità di specificazione dell’operando in memoria consentano di
realizzare le varie modalità di specificazione degli operandi del linguaggio AS-86 (nome, nome con
indice, puntatore).
0
…
7 8 … 11 12 … 15
COP
R1
VAL_1
VALORE_2
Classe RI (registro-immediato)
In questa classe sono codificate operazioni binarie o unarie i cui operandi sono contenuti uno in
un registro o in una cella di memoria, e l’altro è codificato direttamente nella istruzione (operando
“immediato”)
R1 = R1 op VALORE_2;
M(R1 + VAL_1) = M(R1 + VAL_1) op VALORE_2;
0
…
7 8
…
15
COP
VALORE
Classe SALTO
In questa classe sono codificate istruzioni che modificano il contenuto del registro PC,
provocando quindi un “salto”, cioè una alterazione nel normale flusso sequenziale di esecuzione delle
istruzioni presenti in memoria. Di solito, PC viene modificato in relazione al valore assunto da alcuni
flag.
PC = VALORE;
(salto incondizionato)
if (f(FLAG)== true) PC = VALORE;
(salto condizionato)
dove f(FLAG) è una generica funzione logica del valore dei flag.
Decodifica ed esecuzione istruzione
Esaminiamo ora in qualche dettaglio la parte rimasta non specificata dell’algoritmo eseguito dal
processor. Più esattamente, tale algoritmo, inclusa la parte che presentiamo ora, è eseguito da UC. Si
ricorda che nel byte CI[0..7] di CI è contenuto il codice (COP) dell’operazione che deve essere
eseguita.
54
<decodifica ed esecuzione istruzione in CI>:
switch (<formato operazione in CI[0..7]>)
{case RR : <esegui RR>; break;
case RM : <esegui RM>; break;
case RI : <esegui RI>; break;
case SALTO : <esegui SALTO>;break;
default <segnala errore>
}
<esegui RR> :
{<utilizza CI[8..15] per individuare R1 e R2>;
<invia COP e contenuto di R1 e R2 a UO>;
R1 = <risultato proveniente da UO>;
}
<esegui RM> :
{while (TR == 1) do nothing;
AR = Rx + VALORE; (x == 1, 2)
OPR = <lettura byte (00) o parola (01))>; (lettura del byte o parola di indirizzo Rx +VALORE
RR = 1;
while (TR == 0) do nothing;
<preleva dato da RWR>;
RR = 0;
<invia COP, dato e contenuto di Ry (y == 2, 1) a UO>;
if (x == 2)
(x == 2 significa che il 1° operando è di tipo registro (quindi y == 1)
R1 = <risultato proveniente da UO>;
else
{while (TR == 1) do nothing;
RWR = <risultato proveniente da UO>;
(scrittura nella cella di indirizzo
AR = R1 + VALORE;
R2+VALORE del risultato
OPR = <scrittura byte (10) o parola (11))>; proveniente da UO)
RR = 1:
while (TR == 0) do nothing;
RR = 0;
}
<esegui SALTO> :
if (<salto incondizionato>)(questa condizione viene verificata controllando il COP)
PC = CI[16..31];
(si modifica il valore di PC)
else if (<f(FLAG)> == true)
(f(FLAG) indica una opportuna condizione logica)
PC = CI[16..31];
(PC viene modificato solo se la condizione è vera)
Nota: Ogni cella di memoria può contenere, indifferentemente, la codifica binaria di un dato o
una istruzione. Il significato da dare alla sequenza di bit contenuta in una cella dipende esclusivamente
dall’uso che si fa di essa:
se viene caricata in CI, viene interpretata come la codifica di una istruzione;
se viene caricata in un registro generale, viene interpretata come la codifica di un dato;
se viene caricata in un registro indice, viene interpretata come la codifica di un indirizzo.
55
Traduzione dal linguaggio AS-86 nel linguaggio della macchina 8086-S
Ci occupiamo ora del problema della traduzione di algoritmi specificati usando il formalismo AS86 in algoritmi “equivalenti” specificati nel linguaggio della macchina 8086-S, che indichiamo con LM e
che è costituito da tutte le operazioni eseguibili dalla macchina 8086-S e raggruppate nelle quattro
classi descritte in precedenza. Notiamo che un algoritmo specificato in LM è equivalente a uno
specificato in AS-86 se e solo se per uno stesso valore dei dati in ingresso fornisce gli stessi risultati.
Ricordiamo che ogni operazione presente in un algoritmo scritto in AS-86 è caratterizzata dal
seguente formato:
<etichetta> :
|← CAMPO ETICHETTA →|←
<nome operazione>
CAMPO OPERATORE
<operandi>
→|← CAMPO OPERANDI →|
Un algoritmo equivalente in LM si ottiene generando:
1. una sequenza di operazioni (codificate in binario!) di LM corrispondente alla sequenza di
operazioni in AS-86;
2. aree dati nella memoria della macchina 8086-S dimensionate e inizializzate opportunamente,
corrispondenti alle variabili e costanti dichiarate nell’algoritmo scritto in AS-86.
Come vedremo, la soluzione del punto 2. è immediata. Per quanto riguarda la soluzione del
punto 1., notiamo innanzitutto che essa è grandemente facilitata dalla “vicinanza” tra AS-86 e LM.
Questa riguarda in particolare le operazioni disponibilli, le modalità di specificazione degli operandi, il
meccanismo di controllo del flusso (salti condizionati o no). Grazie a ciò, possiamo eseguire una
traduzione “rigo per rigo”, generando:
a) un codice operativo binario corrispondente alla operazione specificata nel campo operatore
AS-86;
b) codici binari corrispondenti agli operandi specificati in AS-86.
Anche riguardo questi due punti la soluzione è “banale”, tranne che in un caso: quando
l’operando in AS-86 è specificato usando una etichetta. Ricordiamo che in AS-86 una etichetta ha il
valore di “riferimento simbolico” (a un dato o a una operazione). In LM un riferimento a un dato o
istruzione non è altro che un indirizzo della cella di memoria che contiene quel dato o istruzione.
Dobbiamo quindi trovare il modo di tradurre etichette AS-86 in indirizzi opportuni della memoria
dell’8086-S. La soluzione a questo problema è basata sulla seguente osservazione:
immaginiamo che la traduzione nel linguaggio LM dell’algoritmo in AS-86 sia memorizzata a
partire dall’indirizzo 0; sia ETICH una etichetta che compare nel campo etichette dell’algoritmo in AS86; sia k-1 la lunghezza (in byte) della traduzione in LM di ciò che precede la riga di codice AS-86 che
contiene ETICH nel suo campo etichetta; allora l’indirizzo da associare a ETICH è k (vedi Figura 14)
56
AS-86
ETICH :
LM
................
0
................
1
................
2
................
.
................
.
................
k-1
xxxxxxxxx
k
bbbbbbbbbb
(traduzione
binaria
di
xxxxxxxxx)
................
.
................
.
Figura 14
E’ da notare che una etichetta può essere incontrata nel campo operandi di una istruzione in un
algoritmo scritto in AS-86 prima di incontrare la stessa etichetta nel campo etichetta di un’altra
istruzione nello stesso algoritmo (vedi, Nell’Esempio 8, l’etichetta NUM_ELEM nella procedura
CALCOLA_MEDIA). Dato il meccanismo appena descritto di “calcolo” dell’indirizzo da associare a una
etichetta, si crea il problema, nella traduzione “rigo per rigo” da AS-86 in LM, di come affrontare
situazioni di questo genere. La soluzione generale è di scomporre il processo di traduzione in due
successive scansioni dell’algoritmo scritto in AS-86:
1 a scansione: si calcolano gli indirizzi corrispondenti alle etichette;
2 a scansione: si genera il codice binario (LM) che traduce l’algoritmo originario.
1 a scansione
L’algoritmo corrispondente riceve come dati in ingresso:
• il testo in AS-86 dell’algoritmo da tradurre;
• la tabella istruzioni, che per ogni operazione AS-86 fornisce il corrispondento codice di
operazione in LM (binario) e lo spazio (in byte) occupato dalla traduzione dell’intera
operazione (inclusi gli operandi); notare che a uno stesso nome di operazione AS-86
possono corrispondere più codici in LM (formati diversi!);
• la tabella dichiarazioni, che per ogni “dichiarazione” o “delimitatore” AS-86 specifica il nome di
un sottoprogramma del traduttore che esegue le opportune operazioni di gestione;
e produce in uscita:
• la tabella etichette-indirizzi, che associa a ogni etichetta AS-86 il corrispondente indirizzo.
57
CODICE AS-86
FORMATO
CODICE BINARIO (LM)
N. BYTE
ADD
RR
10 00 00 00
2
ADD
RM
11 00 00 00
4
ADD
RI
01 00 00 00
4
SUB
RR
00 10 00 00
2
.
.
.
.
.
.
.
.
Tabella istruzioni
CODICE AS-86
NOME SOTTOPROGRAMMA DI GESTIONE
DB
…………
DW
…………
ENDP
…………
END
…………
.
.
.
.
Tabella dichiarazioni
ETICHETTA
INDIRIZZO
NOME_1
…………
NOME_2
…………
.
.
.
.
.
.
Tabella etichette/indirizzi
In questo algoritmo viene inoltre utilizzato un contatore (LC, location counter) che indica, ad
ogni passo dell’algoritmo, l’indirizzo (a partire da 0) in cui verrà memorizzato nella memoria della
macchina 8086-S il codice binario che traduce l’istruzione (o dato) AS-86 correntemente esaminata.
58
Algorimo prima scansione
LC = 0;
do
{<leggi un rigo del programma AS-86>;
if (<istruzione>)
{if <campo etichetta non vuoto>
<metti in tab. etich./indir. nome etichetta e valore LC>;
<accedi a tab. istruzioni in base a codice operazione e formato e ricava n. byte>;
LC = LC + n. byte;
}
else if (<dichiarazione>)
<esegui sottoprogramma relativo>;
else ERRORE;
}
while <non raggiunta fine programma AS-86>.
Sottoprogramma di gestione per dichiarazioni DB (1a scansione)
if (<campo etichetta non vuoto>)
<metti in tab. etich./indir. nome etichetta e valore di LC>;
<determina n. byte occupati dalla codifica binaria del dato>;
LC = LC + n. byte;
Nota: data la definizione del dominio dei dati di tipo DB in AS-86, ognuno di tali dati può essere
codificato in LM con 8 bit (un byte). Quindi, il numero di byte occupati dalla codifica in LM dei dati
dichiarati in una dichiarazione di tipo DB è pari al numero di tali dati. Invece, ogni dato DW può essere
codificato con 16 bit (2 byte), e quindi lo spazio occupato dalla corrispondente traduzione in LM è pari
a 2x(numero di dati DW).
Esempio 10
Consideriamo questa semplice procedura AS-86 che calcola la somma di due numeri (accanto a
ogni rigo del programma è indicato il valore assunto da LC in quel punto).
LC
SOMMA
NUM1
NUM2
ANS
PROC
MOV AX, NUM1
ADD AX, NUM2
MOV ANS, AX
RET
DW 15
DW 3
DW ?
ENDP
0
0
4
8
12
14
16
18
20
La tabella costruita alla fine della prima scansione è:
59
ETICHETTA
INDIRIZZO
SOMMA
0
NUM1
14
NUM2
16
ANS
18
_________________________________
2 a scansione
L’algoritmo corrispondente riceve come dati in ingresso:
• il testo in AS-86 dell’algoritmo da tradurre;
• la tabella istruzioni;
• la tabella dichiarazioni;
• la tabella etichette-indirizzi, costruita durante la 1a scansione;
• la tabella registri, che fornisce la codifica binaria del nome di ogni registro;
e produce in uscita la traduzione in codice binario (LM) dell’algoritmo in AS-86.
NOME REGISTRO
CODICE
AX
0001
BX
0010
CX
0011
DX
……
AL
……
SI
……
DI
……
Tabella registri
Algoritmo seconda scansione
LC = 0;
do
{<leggi un rigo del programma AS-86>;
if (<istruzione>)
{<accedi a tab. istruzioni in base a codice op. e formato>;
<“assembla” la corrispondente traduzione binaria>;
<memorizza istruzione tradotta>;
LC = LC + n. byte>;
}
else if (<dichiarazione>)
<esegui sottoprogramma relativo>;
else ERRORE;
}
while <non raggiunta fine programma AS-86>
60
“assemblaggio” di una istruzione di formato RM:
COP = codice binario corrispondente al nome dell’operazione AS-86 (da tab. istruz.);
R1 = codice binario che identifica il primo registro (da tab. registri); (0 in caso di reg. non specificato)
R2 = codice binario che identifica il secondo registro (da tab. registri); (0 in caso di reg. non specificato)
if (<nome in campo operandi presente in tab. etich./indir.>)
VALORE = indirizzo; (da tab. etich./indir.)
else ERRORE
a
sottoprogramma realtivo a una dichiarazione DB (2 scansione)
<determina n. dati>;
<memorizza la codifica binaria dei dati>;
LC = LC + n. dati;
Esempio 11
La traduzione in LM del sottoprogramma di somma dell’esempio 10 (prodotta in uscita dalla 2a
scansione) è la seguente (per rendere un po’ più leggibile il codice, adottiamo la notazione (OP-F)bin
per indicare la codifica binaria (riportata in tab. istruzioni) in LM di una operazione AS-86 di nome OP e
formato F.
(MOV-RM)
bin
0001
0000
(0001 è la codifica di AX)
0000
(14 è l’indirizzo corrispondente a NUM1)
0000
0000
1110
(ADD-RM)
bin
0001
0000
0000
0000
0001
0000
(MOV-RM)
bin
0000
0001
0000
0001
0010
0000
0000
0000
(RET- -)
bin
(16 è l’indirizzo corrispondente a NUM2)
(18 è l’indirizzo corrispondente a ANS)
0000
0000
0000
1111
(valore numerico del dato = 15)
0000
0000
0000
0011
(valore numerico del dato = 3)
0000
0000
0000
0000
_____________________________
Nota
Gli algoritmi di prima e seconda scansione presentati calcolano i valori delle etichette assumendo
che il programma verrà caricato in memoria a partire dall’indirizzo 0. Nella realtà questo non succede
quasi mai (gli indirizzi “bassi” sono riservati usualmente al “Sistema Operativo” della macchina). Il
problema può essere risolto in due modi:
61
• se l’indirizzo iniziale è noto al momento della traduzione, basta inizializzare il contatore LC con
questo valore iniziale, piuttosto che 0;
• se l’indirizzo iniziale non è noto al momento della traduzione (e questo è il caso più probabile,
per ragioni di flessibilità nell’uso della macchina), la traduzione verrà fatta usando come
indirizzo iniziale l’indirizzo “fittizio” 0; quando il programma verrà effettivamente caricato in
memoria centrale per l’esecuzione (a partire da un indirizzo iniziale k≥0), occorrerà aggiornare
gli indirizzi sommando ad essi il valore k.
E’ da notare che l’aggiornamento richiesto nel secondo caso può risultare molto gravoso se fatto
“esplicitamente” (potenzialmente, ogni istruzione deve essere aggiornata!). Nella macchina 8086-S
che abbiamo descritto non c’è altra possibilità. Una possibile soluzione più efficiente può essere quella
di predisporre un apposito registro interno al processor (registro base) che viene caricato con il valore
k, e il cui contenuto viene automaticamente sommato al valore del registro AR ogni volta che viene
inviata una richiesta di accesso alla memoria.
62
Capitolo 3
Rappresentazione di numeri
63
Introduzione
In questo capitolo viene presentata, in maniera più organica e precisa, una parte di quanto esposto
durante le lezioni sull’argomento della rappresentazione di numeri. La trattazione è limitata, in queste note, alla
rappresentazione posizionale di numeri interi.
64
Rappresentazione di numeri interi
1. Concetti generali
Sia S = {s0 , s1 , …, sk} un insieme finito di simboli.
Sia S* l’insieme di tutte le sequenze finite costruibili usando simboli appartenenti a S.
Si noti che S* è un insieme infinito.
Esempio
S = {a}
S* = {a, aa, aaa, aaaa, aaaaa, … };
oppure:
S = {a, b}
S* = {a, b, aa, ab, ba, bb, aaa, aab, aba, baa, abb, … }
Fine esempio
Sia N l’insieme dei numeri naturali {0, 1, 2, 3, … }. Una rappresentazione di N è una funzione
iniettiva
r : N → S*
che possiede una funzione inversa (interpretazione)
ℑ : S* → N
tale che, data σ∈S*,
se r(n) = σ
n ∈N
ℑ(σ) = 
indefinito
altrimenti

2. Rappresentazione posizionale
Sia B un numero intero ≥2. Una rappresentazione posizionale in base B viene definita in questo
modo:
S B = {s0 , s1 , …, sB-1 }
rB : N → SB * è definita come:
sn
se0 ≤ n ≤ B-1

rB (n) = (c
 p-1 cp-2 … c0 )B, c i ∈SB, p ≥ 2 se n ≥ B
dove la sequenza (cp-1 cp-2 … c0 )B viene interpretata come:
p−1
ℑN(cp-1 cp-2 … c0 ) =
ℑN(ci)·Bi
i=0
Si noti che, dalla definizione di rB , si ha, banalmente, ℑN(sj) = j, per ogni sj∈S B . Nel seguito, per
∑
semplicità, scriveremo semplicemente j invece di ℑN(sj).
65
Esempio
La rappresentazione in base 10 che siamo abituati ad usare non è altro che un caso particolare
della rappresentazione appena definita, con B=10, S10 = {0, 1, 2, …, 9}. Infatti, data una sequenza
appartenente a S 10 *, p.es. 343, questa viene interpretata come rappresentazione del valore
numerico 3·102 + 4·10 1 + 3·10 0 .
Fine esempio
Si noti che una stessa sequenza di simboli rappresenta, in generale, valori numerici diversi al
variare di B; così, la sequenza 101 rappresenta i seguenti valori numerici (espressi nella usuale
notazione in base 10):
se B=2, rappresenta il valore 1·22 + 0·2 1 + 1·2 0 = (5)10
se B=3, rappresenta il valore 1·32 + 0·3 1 + 1·3 0 = (10)10
se B=10, rappresenta il valore 1·102 + 0·10 1 + 1·10 0 = (101)10 .
3. Proprietà di rappresentazioni posizionali
Dalla definizione di rappresentazione posizionale derivano alcune semplici proprietà.
Potenza k-esima
Il valore Bk in base B è rappresentato dalla sequenza (ck ck-1 ck-2 … c 0 )B , con c k=1 e c i=0,
0≤i<k (ovvero, un 1 seguito da k volte 0); infatti si ha:
k
k-1
ℑN(ck ck-1 ck-2 … c0 )
=
ci·B i = ck·B k +
ci·B i
i=0
i=0
k-1
= 1·B k +
0·B i =Bk
i=0
∑
∑
∑
Per esempio, se B=2, il valore numerico (8)10 , pari a 2 3 , è rappresentato come (1000) 2 ; se
B=3, il valore numerico (27)10 , pari a 33 , è rappresentato come (1000) 3 ;
Massimo numero rappresentabile
Il massimo valore numerico M rappresentabile in base B usando una sequenza σ∈S B * di
lunghezza al più pari a p è dato da:
M = Bp - 1
Infatti si ha:
p-1
M
= max{
∑
ci·B i } =
p-1
∑
i=0
i=0
p
B −1
= (B-1)
= Bp - 1
B−1
max{ci}·Bi =
p-1
∑
i=0
Per esempio, con B=2 e p=4, si ha
66
(B-1)·B i = (B-1)
p-1
∑
i=0
Bi
M = ℑN(1111)2 = 1·23 + 1·22 + 1·21 + 1·20 = (15)10 = 24 - 1;
con B=3 e p=3, si ha
M = ℑN(222) 3 = 2·32 + 2·31 + 2·30 = (26)10 = 33 - 1.
4. Rappresentazione con numero finito di cifre
Supponiamo di porre un limite massimo (pari ad h≥1) alla lunghezza delle sequenze di simboli
utilizzabili in una rappresentazione posizionale di N (da notare che questo è quanto succede
effettivamente nelle macchine che utilizziamo per eseguire calcoli numerici). Questo limite comporta
come conseguenza la possibilità di rappresentare soltanto un sottoinsieme finito dell’insieme N.
Indichiamo con N B,h ⊂ N questo sottoinsieme. In base al risultato della Sezione 3, si ha:
N B,h = {n | n∈N, 0 ≤ n ≤ Bh - 1}
Utilizzare una rappresentazione di N usando un numero finito di cifre porta come conseguenza
che non sempre gli algoritmi che utilizziamo per calcolare le operazioni aritmetiche restituiscono il
risultato corretto. Ad esempio, supponiamo che AlgSomma(.,.) indichi un algoritmo per il calcolo della
somma tra interi (ad esempio, potrebbe essere l’usuale algoritmo che ci è stato insegnato durante la
scuola elementare, basato sulla somma cifra per cifra da destra a sinistra e sulla regola del riporto).
Questo algorimo si applica a due sequenze di simboli (aq-1 a q-2 … a 0 )B e (b s-1 b s-2 … b 0 )B e
restituisce come risultato una nuova sequenza di simboli (ct-1 ct-2 … c0 )B , con t≥max{q, s}, tale che
ℑN(aq-1 aq-2 … a0 )B + ℑN(bs-1 bs-2 … b0 )B = ℑN(ct-1 ct-2 … c0 )B
Nel caso che stiamo considerando di rappresentazione con un numero finito h di cifre,
AlgSomma(.,.) deve necessariamente essere modificato, dando luogo al seguente algoritmo (si noti
che, per il vincolo che ci siamo posti sulla rappresentazione, deve essere q≤h e s≤h):
AlgSommaB,h (.,.):
acquisisci (aq-1 aq-2 … a0 )B e (bs-1 bs-2 … b0 )B ;
determina (ct-1 ct-2 … c0 )B usando AlgSomma(.,.);
se t>h, elimina le cifre ct-1 ct-2 … ch ;
restituisci come risultato la sequenza di cifre restanti.
In altre parole, AlgSommaB,h (.,.) tronca alla h-esima cifra il risultato della somma. Questa
operazione di troncamento può ovviamente portare a situazioni di errore. Più precisamente, l’errore si
verifica se ℑN(aq-1 aq-2 … a0 )B + ℑN(bs-1 b s-2 … b 0 )B ∉N B,h . Questo tipo di errore viene detto
errore di “trabocco” (overflow).
Esempio
Assumiamo B=10 e h=3;
se (a2 a1 a0 )B = (230) e (b2 b1 b0 ) = (381), si ha:
67
AlgSommaB,h (230, 381) = 611 che costituisce il risultato corretto;
invece, se (a2 a1 a0 )B = (621) e (b2 b1 b0 ) = (492), si ha:
AlgSommaB,h (621, 492) = 113 che non è, evidentemente, il risultato corretto.
Nel secondo caso, il risultato corretto sarebbe stato 1113; l’algoritmo produce un risultato
scorretto perchè 1113 rappresenta un valore numerico che va al di là dei limiti della rappresentazione
scelta; infatti, con B=10 e h=3 il massimo intero rappresentabile è pari a 103 - 1 = 999.
Fine esempio
5. Rappresentazione di interi relativi
Finora ci siamo occupati della rappresentazione di numeri naturali, ovvero interi senza segno;
consideriamo ora il problema della rappresentazione
degli interi relativi (insieme Z). La
rappresentazione che siamo correntemente abituati ad utilizzare è la cosiddetta rappresentazione in
modulo e segno, ottenuta premettendo il simbolo + o - alla sequenza di simboli che rappresenta il
valore assoluto del numero. In questa sezione presentiamo una rappresentazione alternativa, detta in
complemento, che può risultare vantaggiosa dal punto di vista della definizione di algoritmi per
l’esecuzione di operazioni aritmetiche. In particolare, consente di utilizzare sostanzialmente il solo
algoritmo di somma per realizzare l’operazione di addizione indipendentemente dal segno degli
operandi; inoltre, anche l’operazione di sottrazione si riconduce sostanzialmente a quella di somma (al
contrario, nella rappresentazione in modulo e segno occorre discriminare il segno deggli operandi:
somma di numeri con lo stesso segno e differenza di numeri di segno opposto si calcolano usando
l’algoritmo di somma; somma di numeri di segno opposto e differenza di numeri dello stesso segno si
calcolano usando un differente algoritmo, quello di sottrazione, anche questo appreso, almeno per la
base 10, nella scuola elementare). Questo semplifica la costruzione di “macchine” per eseguire tali
operazioni.
Premessa fondamentale: la rappresentazione in complemento è definibile solo nel caso si
ponga un vincolo al numero massimo di cifre utilizzabili (come nella Sezione 4).
Sia B la base adottata, e h la quantità massima di cifre utilizzabili. Sia rB : N → S B * la funzione di
rappresentazione definita nella Sezione 2. La rappresentazione in complemento a B dell’insieme Z è
data dalla funzione rc : Z → SB *, definita nel seguente modo:
dato x∈Z,

 rB (x)
rc(x) = 
rB (Bh - x )

se0 ≤ x <
se -
h
B
2
Bh
2
≤ x<0
Da questa definizione, risulta evidente che si riesce a rappresentare soltanto il sottoinsieme
 Bh Bh 

finito di Z costituito dai numeri appartenenti all’intervallo  2 ,+ 2  (questa è, di nuovo, una
conseguenza dell’aver posto un limite al numero di cifre utilizzabili). In maniera informale, l’idea alla
base di questa rappresentazione è quella di suddividere l’intervallo dei naturali [0, B h -1] (ovvero
68
l’insieme N B,h ) in due metà, e di utilizzarne la prima metà per rappresentare i relativi appartenenti
 Bh 
 Bh 
 , e la seconda metà per rappresentare l’intervallo dei relativi - ,-1 (vedi
all’intervallo 0,+

 2 
2 
Figura 1).
h
- B
2
h
B
2
0
Z
h
B
2
0
N
h
B -1
Figura 1
Esempio
Consideriamo il caso B=2, h=3; si possono quindi rappresentare gli interi relativi appartenenti
all’intervallo [-4, +3]; questi valori verranno rappresentati utilizzando le rappresentazioni, definite dalla
funzione rB , dell’intervallo dei naturali [0, 7]. Si ha quindi (per maggiore chiarezza, utilizziamo sempre
una notazione a tre cifre, incluse quindi le cifre 0 non significative (cioè quelle più a sinistra)):
rB (0) = 000
rB (1) = 001
rB (2) = 010
rB (3) = 011
rB (4) = 100
rB (5) = 101
rB (6) = 110
rB (7) = 111
Delle otto sequenze di simboli così ottenute, le prime quattro (cioè 000, 001, 010, 011)
vengono utilizzate per rappresentare l’intervallo [0, +3], e le seconde quattro (cioè 100, 101, 110,
111) per rappresentare l’intervallo [-4, -1], nel seguente modo:
rc(0) = rB (0) = 000
rc(1) = rB (1) = 001
rc(2) = rB (2) = 010
rc(3) = rB (3) = 011
rc(-4) = rB (23 - |-4|) = 100
rc(-3) = rB (23 - |-3|) = 101
rc(-2) = rB (23 - |-2|) = 110
rc(-1) = rB (23 - |-1|) = 111
69
Analogamente, con B=10 e h=2, l’intervallo dei relativi [-50, +49] viene rappresentato tramite
l’intervallo dei naturali [0, 99], nel seguente modo:
rc(0) = rB (0) = 00
rc(1) = rB (1) = 01
rc(2) = rB (2) = 02
rc(3) = rB (3) = 03
…
rc(47) = rB (47) = 47
rc(48) = rB (48) = 48
rc(49) = rB (49) = 49
rc(-50) = rB (10 2 - |-50|) = 50
rc(-49) = rB (10 2 - |-49|) = 51
rc(-48) = rB (10 2 - |-48|) = 52
…
rc(-2) = rB (10 2 - |-2|) = 98
rc(-1) = rB (10 2 - |-1|) = 99
Fine esempio
La funzione inversa di rc (ovvero la funzione di interpretazione, che fornisce il valore numerici
rappresentato da una sequenza di simboli) si definisce, banalmente, nel seguente modo:
ℑZ(ah-1 ah-2 … a0 )B =

 ℑN (ah−1ah−2 …a 0 )

- Bh − ℑN (ah − 1a h − 2 …a 0 )

Bh
se0 ≤ ℑN (ah−1a h−2 …a 0 )<
2
h
B
se
≤ ℑN (ah−1a h−2 …a 0 ) < B h
2
dove ℑN è la funzione di interpretazione definita nella Sezione 2.
(
)
Esempio
Consideriamo il caso B=3, h=3. Le sequenze di simboli utilizzabili per la rappresentazione sono
quindi: 000, 001, 002, 010, 011, 012, 020, 021, 022, 100, …, 212, 220, 221, 222; secondo la
funzione di interpretazione ℑN, queste sequenze rappresentano l’intervallo dei naturali: 0, 1, 2, 3, 4,
5, 6, 7, 8, 9, …, 23, 24, 25, 26. Interpretate secondo alla funzione ℑZ (ovvero, come rappresentazione
in complemento di numeri relativi) queste stesse sequenze rappresentano i seguenti valori numerici:
ℑZ(000) = ℑN(000) = 0, ℑZ(001) = ℑN(001) = 1, …, ℑZ(111) = ℑN(111) = 13, ℑZ(112) = -(33 ℑN(112)) = -13, …, ℑZ(221) = -(33 - ℑN(221)) = -2, ℑZ(222) = -(33 - ℑN(222)) = -1. (Quindi, l’intervallo
dei relativi rappresentabile è [-13, +13]).
70
Se consideriamo invece il caso B=10, h=2, le sequenze di due cifre decimali 00, 01, 02, …, 97,
98, 99 vengono interpretate come:
ℑZ(00) = ℑN(00) = 0, ℑZ(01) = ℑN(01) = 1, …, ℑZ(49) = ℑN(49) = 49, ℑZ(50) = -(102 - ℑN(50)) = 50, …, ℑZ(98) = -(102 - ℑN(98)) = -2, ℑZ(99) = -(102 - ℑN(99)) = -1. (Quindi, l’intervallo dei relativi
rappresentabile è [-50, +49]).
Fine esempio
Per quanto riguarda le operazioni aritmetiche, la rappresentazione in complemento consente di
sommare o sottrarre numeri relativi, indipendentemente
dal loro segno,
facendo
ricorso,
fondamentalmente, al solo algoritmo per l’esecuzione dell’addizione.
Consideriamo inizialmente l’operazione di addizione. Questa viene eseguita utilizzando lo
stesso algoritmo AlgSommaB,h (.,.) definito su numeri naturali. Vediamo di seguito alcuni esempi che
dovrebbero convincere della correttezza di tale affermazione.
Esempio
Consideriamo B=2 e h=4. L’intervallo dei relativi rappresentabile è quindi [-8, +7]. Consideriamo
i seguenti numeri relativi (espressi nella usuale base 10): 2, 3, 4, 7, -3, -4, -1. Le loro rappresentazioni,
nella notazione in complemento in base 2, sono:
rc(2) = 0010, rc(3) = 0011, rc(4) = 0100, rc(7) = 0111, rc(-3) = 1101, rc(-4) = 1100, rc(-1) = 1111.
Consideriamo ora le seguenti operazioni di somma tra i numeri sopra elencati:
2 + 4, 3 + 3, 3 + (-3), 4 + (-3), 7 + (-1), -4 + (-3)
Per eseguirle utilizziamo l’algoritmo AlgSomma2,4 (.,.); il lettore è invitato a verificare che i risultati
forniti nel seguito sono effettivamente quelli ottenuti applicando questo algoritmo (usando l’aritmetica
in base 2!):
2 + 4 ==>
AlgSomma2,4 (0010, 0100) = 0110
il numero rappresentato da 0110 è ℑZ(0110) = +6, quindi il risultato
3 + 3 ==>
calcolato è corretto
AlgSomma2,4 (0011, 0011) = 0110
il numero rappresentato da 0110 è ℑZ(0110) = +6, quindi il risultato
3 + (-3) ==>
calcolato è corretto
AlgSomma2,4 (0011, 1101) = 0000
il numero rappresentato da 0000 è ℑZ(0000) = 0, quindi il risultato
4 + (-3) ==>
calcolato è corretto
AlgSomma2,4 (0100, 1101) = 0001
il numero rappresentato da 0001 è ℑZ(0001) = +1, quindi il risultato
7 + (-1) ==>
calcolato è corretto
AlgSomma2,4 (0111, 1111) = 0110
il numero rappresentato da 0110è ℑZ(0110) = +6, quindi il risultato
71
-4 + (-3) ==>
calcolato è corretto
AlgSomma2,4 (1100, 1101) = 1001
il numero rappresentato da 1001 è ℑZ(1001) = -7, quindi il risultato
calcolato è corretto
Fine esempio
Consideriamo ora la sottrazione; invece di definire un apposito algoritmo, possiamo sfruttare
l’algoritmo per l’addizione in base alla semplice considerazione che, dati n,m∈Z, n-m = n+(-m).
Possiamo quindi calcolare la differenza tra due numeri come somma tra il primo numero e il secondo
cambiato di segno. In una rappresentazione in complemento in base B con h cifre, cambiare di segno
un numero significa effettuare una operazione di complementazione sulla sua rappresentazione.
Concettualmente, l’operazione di complementazione trasforma un numero n nel suo complemento
rispetto a B h , cioè B h -n. Questo sembrerebbe richiedere l’esecuzione di una sottrazione,
contraddicendo quanto detto sopra circa la possibilità, grazie alla rappresentazione in complemento, di
eseguire l’operazione di sottrazione evitando di definire uno specifico algoritmo per la sottrazione. In
effetti, non è necessario eseguire una sottrazione per calcolare Bh -n. Infatti, assumiamo che
(ah-1 ah-2 … a0 ) sia la sequenza di cifre che rappresenta n in base B; si ha allora:
h-1
h-1
h-1
h
h
i
i
B -n = B -1 - n + 1 =
(B-1)·B ai·B + 1 =
((B-1)-ai)·Bi + 1
i=0
i=0
i=0
∑
∑
∑
La complementazione si effettua quindi nel seguente modo:
se (ah-1 ah-2 … a0 ) è la sequenza di cifre che rappresenta un numero n∈Z, allora la sequenza di
cifre che rappresenta Bh -n è calcolata dall’algoritmo ComplB,h (ah-1 ah-2 … a0 ) = AlgSommaB,h ((bh-1
b h-2 … b0 ), (uh-1 uh-2 … u0 )), con
bi = (B-1) - ai, 0≤i≤h-1
se i= 0
1
ui = 
0 se 1≤ i ≤ h -1
In pratica, la complementazione (cioè il calcolo di Bh -n) si ottiene rimpiazzando ogni cifra di n con
il suo complemento rispetto a B-1, e sommando alla fine 1 alla sequenza così ottenuta. Si noti che nel
caso B=2, il complemento di una cifra rispetto a B-1 si calcola banalmente come:
 1 se ai = 0
bi =
se ai =1
0
Esempio
Consideriamo il caso B=2, h=4, i seguenti numeri relativi: +5, -4, +1, e le loro rappresentazioni in
complemento date da rc(5) = 0101, rc(-4) = 1100, rc(1) = 0001.
Consideriamo le seguenti operazioni di sottrazione:
+5 - (+1), -4 - (+1), +1- (-4)
+5 - (+1) ==>
è calcolabile come +5 + (-1); la rappresentazione di -1 si determina
72
complementando la rappresentazione di +1: Compl2,4 (0001) =
AlgSomma2,4 (1110, 0001) = 1111; quindi +5 + (-1) si calcola come
AlgSomma2,4 (0101, 1111) = 0100; il numero rappresentato da 0100 è
ℑZ(0100 ) = +4, quindi il risultato calcolato è corretto;
-4 - (+1) ==>
è calcolabile come -4 + (-1); la rappresentazione di -1 è data da1111; quindi
+4 + (-1) si calcola come AlgSomma2,4 (1100,1111) = 1011; il numero
rappresentato da 1011 è ℑZ(1011) = -5, quindi il risultato calcolato è corretto;
+1- (-4) ==>
è calcolabile come +1 + (+4); la rappresentazione di +4 sidetermina
complementando quella di -4: Compl2,4 (1100) = AlgSomma2,4 (0011, 0001)
= 0100; quindi +1 + (+4) si calcola come AlgSomma2,4 (0001, 0100) = 0101;
il numero rappresentato da 0101 è ℑZ(0101 ) = +5, quindi il risultato calcolato
è corretto.
Consideriamo ora il caso B=10, h=2, i seguenti numeri relativi: +25, -41, -12, e le loro
rappresentazioni in complemento date da rc(25) = 25, rc(-41) = 59, rc(-12) = 88.
Consideriamo le seguenti operazioni di sottrazione:
+25 - (-12), -41 - (-12)
+25 - (-12) ==> è calcolabile come +25 + (+12); la rappresentazione di +12 si determina come
Compl10,2 (88) = AlgSomma10,2 (11, 01) = 12; quindi +25 + (+12) si calcola
come AlgSomma10,2 (25, 12) = 37; poichè ℑZ(37 ) = +37, il risultato calcolato
è corretto;
-41 - (-12) ==>
è calcolabile come -41 + (+12); la rappresentazione di +12 si determina come
Compl10,2 (88) = AlgSomma10,2 (11, 01) = 12; quindi -41 + (+12) si calcola
come AlgSomma10,2 (59, 12) = 71; poichè ℑZ(71 ) = -29, il risultato calcolato è
corretto.
Fine esempio
Si noti che negli esempi appena fatti sulla esecuzione di operazioni aritmetiche usando la
rappresentazione in complemento, il risultato era sempre corretto perchè rientrava nei limiti
dell’intervallo di rappresentazione. Se invece il risultato cade al di fuori di tale intervallo (cioè se è
Bh
Bh
<, oppure ≥
), allora il risultato calcolato sarà ovviamente scorretto.
2
2
Esempio
Consideriamo il caso B=2, h=4. L’intervallo di rappresentazione è quindi [-8, +7]. Consideriamo i
seguenti numeri relativi: +5, +4, -6, -3. Questi numeri appartengono all’intervallo di rappresentazione,
e le loro rappresentazioni in complemento sono date da rc(+5) = 0101, rc(+4) = 0100, rc(-6) = 1010,
rc(-3) = 1101.
Consideriamo le seguenti operazioni: 5 + 4, -6 + (-3), +5 - (-6)
73
5 + 4 ==>
si calcola come AlgSomma2,4 (0101, 0100) = 1001; poichè stiamo utilizzando
la rappresentazione in complemento questo risultato viene interpretato tramite
la funzione ℑZ(.); si ha quindi ℑZ(1001) = -7, che è evidentemente errato;
-6 + (-3) ==>
si calcola come AlgSomma2,4 (1010, 1101) = 0111; poichè stiamo utilizzando
la rappresentazione in complemento questo risultato viene interpretato tramite
la funzione ℑZ(.); si ha quindi ℑZ(0111) = +7, che è evidentemente errato;
+5 - (-6) ==>
si calcola come AlgSomma2,4 (0101, Compl24 (1010)) =
AlgSomma2,4 (0101, 0110) = 1011; poichè stiamo utilizzando la
rappresentazione in complemento questo risultato vienei nterpretato tramite
la funzione ℑZ(.); si ha quindi ℑZ(1011) = -5, che è evidentemente errato.
Analogamente, consideriamo B=10 e h=2, e i seguenti valori numerici: +38, -43, -21. Le loro
rappresentazioni in complemento sono: rc(+38) = 38, r c(-43) = 57, r c(-21) = 79. Consideriamo le
seguenti operazioni aritmetiche: +38 - (-43), -43 - (-21), -43 + (-21).
+38 - (-43) ==> si calcola come +38 + (+43), la rappresentazione di +43 si ottiene
complementando quella di -43: Compl10,2 (57) = 43; quindi
AlgSomma10,2 (38, 43) = 81; poichè stiamo utilizzando la rappresentazione in
complemento questo risultato viene interpretato tramite la funzione ℑZ(.); si ha
quindi ℑZ(81) = -19, che è evidentemente errato;
-43 - (-21) ==>
si calcola come -43 + (+21), la rappresentazione di +21 si ottiene
complementando quella di -21: Compl10,2 (79) = 21; quindi
AlgSomma10,2 (57, 21) = 78; poichè stiamo utilizzando la rappresentazione in
complemento questo risultato viene interpretato tramite la funzione ℑZ(.); si ha
quindi ℑZ(78) = -22, che è evidentemente corretto;
-43 + (-21) ==> si calcola come AlgSomma10,2 (57, 79) = 36; poichè stiamo utilizzando la
rappresentazione in complemento questo risultato viene interpretato tramite
la funzione ℑZ(.); si ha quindi ℑZ(36) = +36, che è evidentemente errato.
Fine esempio
74