Appunti di FORTRAN 77

Transcript

Appunti di FORTRAN 77
Appunti di FORTRAN 77
Maria Grazia Gasparo
Aprile 2011
i
ii
Indice
1 Linguaggio macchina e assembly
1
2 Linguaggi interpretati e compilati
3
3 Storia del FORTRAN
5
4 Perché insegnare il FORTRAN 77?
7
5 Realizzazione di un programma FORTRAN
5.1 Compilazione . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2 Collegamento . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
9
11
6 Elementi di base del FORTRAN
6.1 Alfabeto . . . . . . . . . . . . . . .
6.2 Struttura delle linee . . . . . . . .
6.3 Struttura delle unità di programma
6.4 Costanti, variabili e loro tipo . . .
6.5 Variabili dimensionate (arrays) . .
12
12
12
13
14
18
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7 Espressioni e istruzione di assegnazione
22
7.1 Espressioni aritmetiche . . . . . . . . . . . . . . . . . . . . . . 22
7.2 Assegnazione aritmetica . . . . . . . . . . . . . . . . . . . . . 25
7.3 Fortran 90: operazioni fra arrays . . . . . . . . . . . . . . . . 25
8 Controllo del flusso di esecuzione
8.1 Istruzione GO TO . . . . . . . . . . . .
8.2 Istruzioni IF . . . . . . . . . . . . . . . .
8.2.1 IF logico . . . . . . . . . . . . . .
8.2.2 IF–THEN–ENDIF. . . . . . . . .
8.2.3 IF–THEN–ELSE–ENDIF . . . .
8.2.4 IF concatenati. . . . . . . . . . .
8.3 Fortran 90: istruzione SELECT CASE.
8.4 Istruzione DO . . . . . . . . . . . . . . .
8.5 DO impliciti . . . . . . . . . . . . . . . .
8.6 Fortran 90: DO illimitato e DO WHILE
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
26
27
27
28
28
30
32
34
34
38
39
9 Esempi riassuntivi
40
9.1 Esempio 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
9.2 Esempio 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
9.3 Esempio 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
iii
10 L’istruzione WRITE(u,f )
43
10.1 Scrivere su un file. . . . . . . . . . . . . . . . . . . . . . . . . 44
10.2 Scegliere il formato di scrittura. . . . . . . . . . . . . . . . . . 45
10.3 Alcuni esempi . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
11 Sottoprogrammi
11.1 Sottoprogrammi FUNCTION . . . . . . . . . . . . .
11.2 Sottoprogrammi SUBROUTINE . . . . . . . . . . .
11.3 Associazione fra argomenti muti e argomenti attuali
11.4 Argomenti muti dimensionati . . . . . . . . . . . . .
11.5 Dimensionamento variabile e indefinito per vettori .
11.6 Dimensionamento variabile e indefinito per matrici .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
48
49
54
56
58
60
62
12 Alcune regole di buona programmazione
65
12.1 Istruzioni di scrittura nei sottoprogrammi . . . . . . . . . . . 65
12.2 Istruzioni STOP e RETURN . . . . . . . . . . . . . . . . . . 67
12.3 Arrays di lavoro . . . . . . . . . . . . . . . . . . . . . . . . . . 67
13 Esempi di programmi con
13.1 Esempio 1. . . . . . . .
13.2 Esempio 2 . . . . . . . .
13.3 Esempio 3 . . . . . . . .
13.4 Esempio 4 . . . . . . . .
sottoprogrammi
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
70
70
72
75
77
14 Istruzione EXTERNAL
81
15 Istruzione COMMON
82
Bibliografia
85
iv
1
Linguaggio macchina e assembly
assembly
Un linguaggio di programmazione è un linguaggio utilizzabile per scrivere
algoritmi che il computer può capire (direttamente o tramite una traduzione)
e eseguire. Nell’accezione più semplice, che in questo momento ci basta, un
programma è un algoritmo scritto in un linguaggio di programmazione.
Il fatto che tutte le informazioni debbano essere memorizzate su un computer come sequenze di bits implica che anche le istruzioni di un algoritmo,
per poter essere direttamente eseguite dal computer, devono essere scritte in
un linguaggio il cui alfabeto è composto soltanto dai simboli “0” e “1”. Non
solo, ma la sintassi di questo linguaggio è strettamente dipendente dall’architettura della macchina ed è quindi diversa per ogni processore, o famiglia
di processori. Data questa dipendenza, ci si riferisce a questo linguaggio
con l’espressione generica linguaggio macchina. In un linguaggio di questo
tipo, ad ogni operazione elementare (aritmetica, logica, di assegnazione, di
salto, etc...) corrisponde un codice operativo e ogni operando è identificato
tramite l’indirizzo della locazione in cui è memorizzato1 . Codici operativi e
indirizzi sono sequenze di 0 e 1 di lunghezza prefissata.
Scrivere o leggere un programma in linguaggio macchina è estremamente
difficile: si tratta in generale di programmi molto lunghi e complessi, incomprensibili per i non addetti ai lavori. Il primo passo per superare questa
difficoltà fu la definizione dei cosiddetti linguaggi assembly, che nacquero
praticamente insieme ai primi computers negli anni ’40. Questi sono ancora
linguaggi dipendenti dall’hardware della macchina per cui sono progettati,
perché le istruzioni sono in corrispondenza biunivoca con quelle del linguaggio macchina; d’altra parte sono linguaggi simbolici e il loro utilizzo è
decisamente più facile. Con l’espressione “linguaggio simbolico” si intende
che i codici operativi binari sono sostituiti da codici alfanumerici mnemonici
detti parole chiave (es: ADD per l’addizione, STO per la memorizzazione) e
i riferimenti alle locazioni di memoria avvengono attraverso nomi simbolici,
o identificatori, scelti dall’utente rispettando le regole stabilite dal linguaggio. Per poter essere eseguito, un programma assembly deve essere tradotto
in linguaggio macchina da un opportuno programma detto assembler. È
importante aver presente che i linguaggi assembly non sono nient’altro che
versioni simboliche dei linguaggi macchina e non sono quindi portabili, nel
senso che un programma scritto per un computer non può essere eseguito
su un altro computer con un processore diverso.
Il vero salto di qualità, dal punto di vista della portabilità dei programmi
e della facilità di programmazione, si ebbe negli anni ’50, quando nacquero
i primi linguaggi di programmazione ad alto livello. L’idea di fondo era
definire dei linguaggi la cui sintassi prescindesse dall’architettura di un par1
Un dato può essere anche un’istruzione del programma stesso. Si pensi ad esempio
a un’istruzione di salto “Vai all’istruzione tal dei tali”, nella quale l’operazione è espressa
dalla parola “Vai” e l’operando è l’istruzione tal dei tali.
1
ticolare computer e fosse invece vicina al linguaggio “parlato” usato per
descrivere gli algoritmi. Siccome il tipo di algoritmi e di conseguenza il linguaggio comune utilizzato per descriverli era diverso a seconda degli ambiti
applicativi in cui un programmatore si trovava a lavorare, furono creati diversi linguaggi ad alto livello destinati a settori diversi. I primi linguaggi
furono :
–FORTRAN (FORtran TRANslation), destinato ad algoritmi di natura
matematico/computazionale e sviluppato intorno al 1955 in ambiente IBM
da un gruppo di lavoro guidato da J. Backus.
–LISP (LISt Processing), per applicazioni nell’ambito dell’intelligenza artificiale, sviluppato intorno al 1958 al Massachussets Institute of Technology
da J. McCarthy e collaboratori.
–COBOL (COmmon Business Oriented Language), finalizzato alla stesura
di programmi in ambito commerciale e imprenditoriale, sviluppato nel 1959
da un comitato pubblico-privato noto come Short Range Committee.
–ALGOL 60 (ALGOrithmic Language), nato nel 1958 come IAL (International Algorithmic Language) grazie agli sforzi congiunti di una organizzazione europea (GAMM) e una statunitense (ACM) per creare un linguaggio universale di descrizione degli algoritmi, non orientato a una particolare
classe di problemi. IAL fu successivamente modificato fino a dar luogo a
ALGOL 60, che è stato molto importante non tanto come linguaggio in se
stesso (le versioni commerciali non hanno avuto successo) quanto come primo esempio di linguaggio indipendente dalla macchina e dal problema, con
costrutti sofisticati (scelte, cicli) non ancora previsti dagli altri linguaggi di
alto livello.
–BASIC (Beginners All-purpose Symbolic Instruction Code), sviluppato nel
1964 al Darthmouth College da J. Kemeny e T. Kurtz come linguaggio
didattico, che facilitava l’avvicinamento alla programmazione di studenti di
qualunque provenienza e formazione culturale.
Con l’evolversi della tecnologia e delle architetture dei computers, i linguaggi di programmazione si sono evoluti, spesso dando luogo a numerosi
dialetti anche molto diversi dal linguaggio originale, e molti altri linguaggi sono nati. ALGOL 60 influenzò in molti casi l’ evoluzione dei linguaggi
preesistenti e la definizione di quelli nuovi. Ad esempio, un discendente diretto di ALGOL è il PASCAL, sviluppato negli anni 1971-73 da N. Wirth
come linguaggio per insegnare i principi della programmazione nello spirito
di ALGOL, e molto diffuso nelle scuole statunitensi ed europee negli anni
’70-’80. Negli stessi anni nacque il linguaggio C, ad opera di D.M. Richtie e
K. Thompson, come linguaggio per la programmazione di sistema nell’ambito dello sviluppo del nascente sistema operativo Unix. La fortuna del C
2
e la sua evoluzione sono dovute al fatto che, pur essendo un linguaggio di
alto livello, permette di accedere ad aspetti di basso livello del computer,
come fanno i linguaggi assembly. Negli anni ’80, con l’affermarsi dei Personal computers e delle interfacce grafiche, si sono sviluppati linguaggi orientati
agli oggetti, come C++ (un discendente del C sviluppato intorno al 1979 da
B. Straustrup) e Java (1995, J. Gosling). Vogliamo infine citare MATLAB
(MATrix LABoratory), un linguaggio, o meglio dire un ambiente di programmazione, attualmente molto usato nell’insegnamento dell’analisi numerica e
dell’algebra lineare anche teorica. Il progetto MATLAB partı̀ verso la fine
degli anni ’70 ad opera di C. Moler per permettere ai suoi studenti di usare
le librerie FORTRAN per l’algebra lineare senza dover imparare il linguaggio FORTRAN. Visto il successo dell’operazione, negli anni ’80 la proprietà
fu acquisita dal gruppo Mathworks, che da allora ha curato lo sviluppo e la
vendita di MATLAB. Più o meno negli anni in cui MATLAB diventava un
linguaggio di proprietà, nasceva il linguaggio GNU Octave, che ha molto in
comune con MATLAB ed è open-source.
2
Linguaggi interpretati e compilati
Come già detto per i programmi assembly, anche i programmi scritti in un
linguaggio di programmazione ad alto livello devono essere tradotti nel loro
equivalente in linguaggio macchina per poter essere eseguiti. La traduzione è
molto più onerosa che per i programmi assembly dal momento che i linguaggi
ad alto livello prevedono l’utilizzo di costrutti e istruzioni che non hanno
una controparte diretta nei linguaggi macchina. Ad esempio, la semplice
istruzione di assegnazione
Assegna a c il valore di a+b,
che in un linguaggio più algoritmico potremmo esprimere come
Poni c = a + b,
oppure
c ← a + b,
ha una traduzione immediata del tipo
c=a+b
(1)
in tutti i linguaggi ad alto livello, ma non nel contesto di un linguaggio
macchina, dove ad essa corrisponde una sequenza di più operazioni di basso livello. Per chiarire meglio questo punto, ricordiamo che l’operazione di
assegnazione si divide in due fasi: il calcolo del valore dell’espressione alla
destra del segno di “=” e la memorizzazione del risultato nella locazione associata al nome simbolico indicato a sinistra del segno di “=”. Ricordiamo
3
assegnazione
anche che per eseguire le operazioni aritmetiche i processori utilizzano locazioni speciali chiamate registri, con un numero di bits più alto che nelle
normali locazioni adibite alla memorizzazione dei numeri floating point (ad
esempio, 80 bits contro i 32 o 64 dei numeri reali in precisione semplice o
doppia). Lo scopo di questa maggiore lunghezza è ovviamente quello di accumulare un risultato più preciso, la cui mantissa verrà poi approssimata per
arrotondamento al momento della memorizzazione in una normale locazione
floating point. Supponiamo per semplicità di lavorare su un computer con
un solo registro, dove viene copiato un operando, mentre l’altro resta nella
sua locazione (situazioni più realistiche,
nelle quali i processoriassegnazione
usano più di
Overton
un registro, sono descritte nel libro [3]). Allora, l’istruzione (1) viene spezzata in una sequenza di tre operazioni di un livello sufficientemente basso
da avere una codifica in linguaggio macchina:
1. Copia nel registro il contenuto della locazione corrispondente all’identificatore a;
2. Somma al contenuto del registro il contenuto della locazione individuata
dal nome b, memorizzando il risultato nel registro stesso;
3. Trasferisci il contenuto del registro nella locazione corrispondente all’identificatore c.
Esistono due modalità per la traduzione di un programma in linguaggio
macchina: l’interpretazione e la compilazione. Nel primo caso la traduzione e
l’esecuzione vanno di pari passo: un interprete traduce un’istruzione per volta e, in assenza di errori di sintassi, la esegue immediatamente; le istruzioni
in linguaggio macchina non vengono conservate in memoria. Nel secondo
caso un compilatore traduce tutto il programma, generando un programma
in linguaggio macchina equivalente a quello originale, che può essere conservato in memoria. La compilazione talvolta prevede due passaggi: prima
il programma viene tradotto in linguaggio assembly e poi l’assembler traduce in linguaggio macchina. Sia gli interpreti che i compilatori sono a loro
volta programmi, spesso originariamente scritti in un linguaggio di programmazione, tradotti in linguaggio macchina e definitivamente memorizzati sul
computer.
In linea di principio, dato un qualsiasi linguaggio L e un qualsiasi processore P, si potrebbe progettare sia un interprete che un compilatore per
tradurre programmi scritti in L nel linguaggio macchina di P. In pratica, alcuni linguaggi vengono tipicamente tradotti mediante interpretazione
(linguaggi interpretati) e altri mediante compilazione (linguaggi compilati).
Ad esempio MATLAB e la sua controparte GNU Octave sono interpretati,
mentre FORTRAN, C e C++ sono compilati. In realtà, molti informatici
ritengono superata questa classificazione perché oggigiorno le due modalità
di traduzione spesso sono mescolate; ad esempio, il compilatore traduce il
programma in un linguaggio intermedio fra quello originale e l’assembly, e
4
poi la traduzione in linguaggio macchina viene affidata a un interprete.
3
Storia del FORTRAN
Come già accennato, il FORTRAN fu sviluppato, primo fra i linguaggi di
programmazione ad alto livello, intorno al 1955 all’IBM, destinato all’allora
nuovo computer IBM-704. Il primo compilatore fu elaborato nel 1957. Il
successo di questo linguaggio rivoluzionario, che permetteva ai programmatori di scrivere istruzioni più vicine al linguaggio matematico (e inglese) che
al linguaggio macchina, fu tale che negli anni immediatamente successivi
cominciarono a proliferare dialetti, e corrispondenti compilatori, destinati ai
diversi computers allora disponibili. L’esistenza di dialetti diversi impediva
la portabilità dei programmi, vanificando in larga misura il fatto dell’essersi
affrancati dalla sintassi del linguaggio macchina. Si cominciò allora a progettare uno standard del linguaggio, ovvero un insieme di regole sintattiche
che avrebbero dovuto essere riconosciute da tutti i compilatori: un programma scritto rispettando rigorosamente quelle regole sarebbe stato traducibile
da qualunque compilatore che si adeguasse ad esse, e quindi estremamente
portabile. Nel 1962 fu fondata a tale scopo una commissione nell’ambito
dell’ANSI2 che lavorò fino al 1966, quando vide la luce il primo FORTRAN
standard, noto come FORTRAN 66. Era un linguaggio ancora molto rozzo rispetto ai linguaggi attuali. Basti pensare che non prevedeva istruzioni
per descrivere in modo immediato situazioni di scelta fra più percorsi alternativi, ma soltanto un’istruzione tramite la quale si esegue o meno una
singola azione, in
base al verificarsi o meno di una determinata condizione
iflogico
(cfr. paragrafo 8.2.1). Per intendersi, un costrutto del tipo
Se a > b, allora:
max = a
altrimenti:
max = b
Fine scelta
non aveva un’immediata traduzione in FORTRAN 66; per ottenere il risultato desiderato si doveva concepire l’algoritmo nel seguente modo, equivalente
al precedente ma molto più complicato:
2
L’ANSI (American National Standard Institute) è un’organizzazione fondata nel 1918
con il nome American Engineering Standards Committee allo scopo di sviluppare standards per prodotti, servizi, procedure, etc.. in diversi settori dell’ingegneria. Il nome subı̀
diverse variazioni negli anni, fino a diventare quello attuale nel 1969. Oggi nell’ANSI sono
rappresentate alcune agenzie governative statunitensi, corporations, università e soggetti
privati.
5
Se a ≤ b, vai all’istruzione 10
max = a
vai all’istruzione 20
10 max = b
20 ......
Nel 1969 fu deciso di por mano alla definizione di un nuovo standard
per tener conto delle tante importanti estensioni del FORTRAN 66 fiorite
in quegli anni e anche del fatto che nel frattempo erano nati altri linguaggi,
fra cui il C, che erano decisamente avanti rispetto al FORTRAN 66, ed era
imperativo adeguarsi se non si voleva che il linguaggio morisse. Il nuovo standard, noto come FORTRAN 77, fu pubblicato nel 1978. Le novità rispetto
alla precedente versione erano moltissime. Fra queste ricordiamo l’introduzione di istruzioni di scelta articolate e la possibilità di gestire abbastanza
agevolmente stringhe di caratteri. Il FORTRAN 77 è stato il FORTRAN
per molti anni, e per certi aspetti lo è ancora, in quanto molti programmi
di pubblico dominio sono scritti in FORTRAN 77 e molti programmatori
continuano a usarlo anche se nel frattempo il linguaggio si è ulteriormente
evoluto.
Il successivo standard, noto come Fortran 90, fu pubblicato nel 1992.
Ancora una volta, per stare al passo con altri linguaggi saliti prepotentemente alla ribalta, quali il C++ e il MATLAB, furono introdotte innovazioni
sostanziali: gli autori del nuovo standard hanno voluto cambiare la sigla da
FORTRAN a Fortran, probabilmente per evidenziare che il nuovo standard
è talmente diverso dal precedente da poter essere quasi considerato un nuovo linguaggio. Fra le principali innovazioni ricordiamo: le operazioni sugli
array (variabili dimensionate quali vettori, matrici, etc.) o sezioni di array (ad esempio gruppi di righe e/o colonne di una matrice) che possono
snellire la stesura dei programmi e migliorarne l’efficienza su computers con
adeguate architetture; la gestione dinamica della memoria contrapposta a
quella statica del FORTRAN 773 ; i tipi di dati definiti dall’utente; i puntatori. Gli estensori del Fortran 90 hanno comunque mantenuto il FORTRAN
77 come sottinsieme del nuovo standard, per ovvii motivi di compatibilità:
una scelta diversa avrebbe significato dover buttare alle ortiche o tradurre
tutto il (tanto) software scritto in FORTRAN 77, il che avrebbe probabilmente provocato la rivolta degli utilizzatori. In alcuni ambienti si parla di
Fortran 90/95 invece che Fortran 90. Questo è dovuto al fatto che negli anni
immediatamente successivi al 1992 il Fortan 90 subı̀ un primo aggiustamento, mirato essenzialmente ad eliminare alcune ambiguità che complicavano
la costruzione dei compilatori e rischiavano di compromettere la portabilità
3
Con gestione statica della memoria si intende che la memoria per le variabili coinvolte
in un programma viene allocata dal compilatore, prima dell’esecuzione. Questo implica fra
l’altro che il programmatore deve decidere in fase di stesura del programma le dimensioni
di tutte le variabili dimensionate coinvolte. Con la gestione dinamica invece la memoria
per le variabili viene allocata durante l’esecuzione, via via che si rende necessaria.
6
dei programmi. Si arrivò cosı̀ al nuovo standard Fortran 95, in cui comunque
non ci sono novità di rilievo rispetto al Fortran 90.
Concludiamo questo excursus citando gli aggiornamenti più recenti: il
Fortran 2003, che ha introdotto alcuni elementi di programmazione a oggetti,
e il Fortran 2008 che ne aggiusta le ambiguità.
4
Perché insegnare il FORTRAN 77?
Queste dispense raccolgono gli elementi essenziali di FORTRAN 77, anche
se talvolta verranno indicate delle alternative proprie del Fortran 90 ormai
accettate da molti compilatori oggi in uso. Per tutti i dettagli
che qui non
AGM MR
vengono discussi, gli studenti possono ricorrere ai libri [1] e [2] citati in
bibliografia, o al numeroso materiale reperibile in rete (da prendersi con
molto spirito critico, come tutte le informazioni diffuse in Internet).
Per quanto riguarda i compilatori FORTRAN, faremo spesso riferimento
in queste dispense a due di essi, entrambi liberamente scaricabili da Internet,
che presumibilmente saranno usati dagli studenti a cui questi appunti sono
destinati: il compilatore Open WATCOM, che è utilizzabile in ambiente
Windows e realizza un FORTRAN 77 stretto, e il compilatore gfortran,
che è utilizzabile in ambiente Linux e realizza il Fortran 90/95.
Perché continuare a insegnare uno standard ormai “vecchio” come il
FORTRAN 77? Ci sono diverse risposte a questa domanda.
1) Molte importanti librerie matematiche, prima fra tutte LAPACK per
l’algebra lineare, sono scritte in FORTRAN 77 e, per poterle usare bene,
occorre conoscere questo linguaggio.
2) La gestione statica della memoria prevista dal FORTRAN 77 talvolta
complica un po’ la stesura dei programmi perché il programmatore deve decidere fin dall’inizio le dimensioni di tutte le variabili dimensionate coinvolte
nel programma. Questa necessità fu eliminata nel Fortran 90 introducendo
la possibilità di gestire dinamicamente la memoria, già prevista da altri linguaggi come il C++ e il MATLAB. D’altra parte, la gestione dinamica ha
un potenziale svantaggio pratico, tristemente noto a molti programmatori
che ne hanno fatto esperienza: la quantità di memoria richiesta per l’esecuzione di un programma è spesso decisamente maggiore di quella richiesta
in regime statico, con il risultato che su macchine con risorse limitate (ad
esempio un normale Personal Computer) diventa impossibile far eseguire il
programma.
3) Alcuni compilatori open-source oggi molto usati, come gfortran, consentono l’allocazione dinamica della memoria ma non la realizzano bene.
Il risultato è che l’esecuzione di un programma può essere interrotta dal
sistema per violazioni della memoria anche se non contiene errori.
7
4) Una volta che si conosce bene un linguaggio di programmazione “rigido”
come il FORTRAN 77, diventa assolutamente facile imparare il Fortran 90.
5
Realizzazione di un programma FORTRAN
realizza
Con il termine realizzazione, o implementazione, di un programma scritto
in FORTRAN si intende la sequenza di operazioni da fare per arrivare all’esecuzione del programma. Per i linguaggi compilati come il FORTRAN
le operazioni sono due: compilazione e collegamento. Per spiegare in cosa
consistono queste due operazioni, faremo riferimento al seguente semplice
programma:
PROGRAM ERONE
C Programma che calcola l’area di uno o più triangoli
C usando la formula di Erone
REAL A,B,C,SP,AREA
INTEGER LEGGI
10 PRINT∗, ’immettere le lunghezze dei lati’
READ∗, A,B,C
SP=(A+B+C)/2.
AREA=SQRT(SP∗(SP-A)∗(SP-B)∗(SP-C))
PRINT∗,’area= ’, AREA
PRINT∗,’ancora? (1/0= si/no)’
READ∗,LEGGI
IF(LEGGI.EQ.1) GOTO 10
STOP
END
Lo scopo del programma è calcolare e stampare l’area di uno o più triangoli
con la formula di Erone secondo la quale, ricordiamolo, l’area è data da
p
s(s − a)(s − b)(s − c),
dove a, b, e c sono le lunghezze dei lati e s è il semiperimetro. Nel programma
usiamo i nomi A,B e C per a, b e c e SP per s. Senza entrare nei dettagli,
diamo un cenno al significato di tutte le istruzioni contenute nel programma:
– L’istruzione PROGRAM ERONE dà un nome al programma;
– Le istruzioni REAL A,B,C,SP,AREA e INTEGER LEGGI mettono in evidenza che A,B,C,SP e AREA rappresentano grandezze a valori reali, mentre
LEGGI identifica una grandezza a valori interi;
– Le istruzioni caratterizzate dalla parola chiave PRINT∗, sono istruzioni
di scrittura sul video: le stringhe racchiuse fra apici (come ‘immettere le
lunghezze dei lati’) vengono riprodotte pari pari, mentre delle variabili come
AREA viene stampato il valore;
8
– Le istruzioni caratterizzate dalla parola chiave READ∗, sono istruzioni
di lettura: i dati vengono scritti sulla tastiera separati da virgole o spazi
bianchi, su una o più righe;
– L’istruzione IF(LEGGI.EQ.1) GOTO 10 realizza una situazione di scelta:
se il valore della variabile LEGGI è uguale a 1, viene eseguita l’istruzione
GOTO 10, per effetto della quale l’esecuzione del programma riprende dall’istruzione 10 PRINT∗, ’immettere le lunghezze dei lati’; altrimenti l’esecuzione prosegue con la successiva istruzione STOP che fa fermare il programma. Questo significa che il numero di triangoli non è fissato in anticipo:
dopo aver calcolato l’area di un triangolo, il programma chiede se si deve
continuare; in caso affermativo, si leggono le lunghezze dei lati di un nuovo
triangolo, altrimenti ci si ferma4 .
– L’istruzione END che indica la fine del programma.
5.1
Compilazione
compila
La compilazione consiste nella traduzione del programma FORTRAN, detto programma sorgente, nel suo equivalente in linguaggio macchina, detto
programma oggetto. La traduzione è un’operazione complessa perché coinvolge l’analisi lessicale, sintattica e semantica del programma sorgente. Il
compilatore ha anche il compito di allocare la memoria per il programma.
Per semplificare, possiamo sintetizzare la compilazione nel modo seguente.
Il compilatore effettua una prima scansione del programma, durante la
quale vengono distinti gli identificatori scelti dal programmatore dalle parole
chiave e simboli di operazioni aritmetiche/logiche. Durante questa scansione
viene creata la tavola dei simboli nella quale vengono elencati gli identificatori, ciascuno con i suoi attributi (informazioni utili a interpretarne il
significato nelle fasi successive) e l’indirizzo di memoria assegnatogli.
Osserviamo il programma ERONE, scorrendo il quale il compilatore trova i
seguenti identificatori:
– il nome simbolico ERONE, che identifica il programma;
– i nomi simbolici A, B, C, SP e AREA, che il programmatore ha scelto
per identificare le grandezze variabili su cui il programma opera; il compilatore inserisce nella tavola dei simboli tutti questi identificatori e per ognuno
indica la dimensione (scalare), il tipo (reale) e l’indirizzo della locazione di
memoria che gli viene associata;
4
Dal punto di vista algoritmico, questo si configura come un ciclo while, in cui il
proseguimento o l’interruzione delle ripetizioni dipendono dal verificarsi o meno di una
determinata condizione. Il FORTRAN 77 non prevede un’istruzione che realizzi questo
tipo di ciclo, che pertanto viene realizzato tramite l’istruzione di scelta IF e quella di salto
GOTO. Il Fortran 90 ha invece introdotto un’apposita istruzione.
9
– il nome simbolico LEGGI, scelto dal programmatore per gestire l’interruzione del programma; il compilatore lo inserisce nella tavola dei simboli
indicando la dimensione (scalare), il tipo (intero) e l’indirizzo della locazione
di memoria che gli viene associata.
– il numero 10 che precede l’istruzione PRINT∗, ...; il compilatore classifica questo identificatore come etichetta (in inglese, label) e lo inserisce nella
tavola dei simboli insieme all’indirizzo in memoria dell’istruzione corrispondente;
– il numero 2. nell’istruzione SP=(A+B+C)/2.; il compilatore lo classifica
come identificatore di una costante reale e lo inserisce nella tavola insieme
all’indirizzo della locazione di memoria in cui tale valore è memorizzato;
– il nome simbolico SQRT, che identifica una procedura, più precisamente
una funzione intrinseca, per il calcolo della radice quadrata di un numero
reale5 ; il compilatore lo inserisce nella tavola insieme alle indicazioni utili
per l’aggancio alla procedura nella successiva fase di collegamento;
Dopo aver costruito la tavola dei simboli, il compilatore scandisce nuovamente il programma per effettuare la traduzione vera e propria: le parole
chiave e i simboli di operazioni aritmetiche e logiche vengono sostituiti dal
loro equivalente in linguaggio macchina e gli identificatori vengono sostituiti
dagli indirizzi delle locazioni di memoria ad essi associati durante la prima scansione. Il risultato è il programma oggetto. La traduzione non può
ovviamente essere portata a termine se una o più istruzioni non sono sintatticamente corrette. In questo caso, al posto del programma oggetto viene
generato un’elenco degli errori di sintassi presenti nel programma e delle
istruzioni in cui tali errori si trovano (diagnostico). Usando questo elenco,
il programmatore può correggere gli errori e risottoporre il programma al
compilatore, fino a quando tutti gli errori non siano stati eliminati. La forma in cui il diagnostico viene presentato e il livello di dettaglio variano da
compilatore a compilatore.
Alcuni compilatori prevedono anche la segnalazione, attraverso messaggi di avvertimento (Warning), della presenza nel programma di eventuali
situazioni sospette, che non sono classificabili come errori di sintassi ma
potrebbero nascondere qualche svista del programmatore; ad esempio, se
nello scrivere l’istruzione
PRINT∗,’area= ’, AREA
nel programma ERONE si facesse un errore di battitura e l’istruzione risultasse
PRINT∗,’area= ’, ARRA
5
Ogni compilatore è accompagnato da un insieme di procedure, dette funzioni
intrinseche, che realizzano funzioni matematiche (e non solo) di interesse comune.
10
qualche compilatore avvertirebbe la variabile ARRA viene usata senza che
le sia stato attribuito alcun valore in precedenza (uninitialized variable). La
presenza di situazioni anomale come questa non impedisce la traduzione del
programma e la creazione del programma oggetto; d’altra parte, i messaggi
di Warnings sono molto utili per individuare veri e propri errori, di battitura
o di altra natura. Per quanto riguarda i due compilatori di riferimento, il
gfortran segnala i messaggi di Warning soltanto se usato con l’opzione
–Wall (che raccomandiamo vivamente di usare sempre), mentre l’ Open
WATCOM li segnala automaticamente.
Come impareremo più avanti, un programma FORTRAN è spesso composto da più unità di programma, o moduli sorgenti, ciascuno dei quali è la
traduzione in FORTRAN di un algoritmo. Ad esempio, se dobbiamo scrivere
un programma che prevede la risoluzione di un certo numero di equazioni
di secondo grado, possiamo organizzarlo in due unità di programma: una
che, dati i coefficienti a, b e c di un’equazione, ne calcola le soluzioni; l’altra
che legge i coefficienti di tutte le equazioni, le risolve ad una ad una usando
la prima unità, ed eventualmente stampa i risultati. Il secondo modulo è il
programma principale, quello che gestisce le operazioni; il primo modulo si
configura invece come un sottoprogramma, che non “agisce” autonomamente
ma solo quando chiamato in causa dal programma principale. L’unione dei
due moduli sorgenti forma il programma sorgente. Ebbene, in presenza di un
programma costituito da più unità di programma, il compilatore le traduce
separatamente e indipendentemente l’una dall’altra, creando per ciascuna di
esse una tavola dei simboli e un modulo oggetto (o un diagnostico in presenza di errori di sintassi). Quando tutti i moduli sorgenti sono privi di errori,
l’unione dei moduli oggetto corrispondenti forma il programma oggetto.
5.2
Collegamento
link
Pur essendo scritto in linguaggio macchina, il programma oggetto non è ancora eseguibile. Infatti, il compilatore non è in grado di stabilire a priori le
richieste di memoria di un’unità di programma, e tantomeno del programma nel suo complesso; segue da questo che gli indirizzi memorizzati nella
tavola dei simboli relativa a un’unità di programma sono virtuali, in quanto
fanno riferimento all’indirizzo “0” in cui si considera memorizzata la prima istruzione dell’unità di programma stessa. Trasformare questi indirizzi
virtuali in indirizzi effettivi è il compito principale del linker (il “collegatore”), il quale assolve a questo incarico essenzialmente tramite le seguenti
operazioni:
1) crea una tabella dei moduli oggetto che compongono il programma;
2) assegna un indirizzo effettivo di inizio ad ogni modulo;
11
3) modifica di conseguenza gli indirizzi virtuali all’interno di ogni modulo;
4) cerca in tutti i moduli le istruzioni che fanno riferimento ad altri moduli
e vi inserisce l’indirizzo di inizio dei moduli richiamati.
Se il programma fa riferimento a moduli inesistenti, o se occupa troppa memoria, il linker segnala la situazione di errore, altrimenti genera il
programma eseguibile. A questo punto si può dare avvio all’esecuzione.
6
6.1
Elementi di base del FORTRAN
Alfabeto
L’insieme dei caratteri utilizzabili in un programma FORTRAN è il seguente:
26 lettere maiuscole: A, B, C, ..., W, Z
26 lettere minuscole: a, b, c, ..., w, z
10 cifre: 0, 1, ..., 9
12 caratteri speciali: = + - ∗/ ( ) . , ’ $ :
Alcuni altri caratteri speciali, fra cui ! , < e >, sono stati aggiunti all’alfabeto nel Fortran 90.
Dal punto di vista della scrittura dei programmi, due cose sono importanti:
– I compilatori FORTRAN non fanno distinzione fra lettere maiuscole e minuscole (si dice che il linguaggio è case-insensitive): le parole chiave possono
essere scritte con caratteri maiuscoli o minuscoli indifferentemente e nomi
simbolici come “ANNA”, “Anna” o “anna” sono la stessa cosa;
– Esattamente come quando si scrive in italiano, nello scrivere un programma FORTRAN si devono separare le parole, intese come parole chiave e
identificatori, tramite spazi bianchi, a meno che non ci sia già un simbolo
(ad esempio una virgola o una parentesi aperta o chiusa) che funge da separatore. Durante la prima scansione del programma, il compilatore analizza
le istruzioni in modo da distinguere le parole chiave dai separatori e dagli
identificatori. A questo fine, gli spazi bianchi in eccesso vengono ignorati
(“GOTO 10” o “GOTO
10” sono considerati la stessa cosa); d’altra
parte, l’assenza di uno spazio bianco necessario può causare malintesi, per
cui nella scrittura “GOTO10” il compilatore non riesce a separare la parola chiave dall’identificatore e registra GOTO10 come un identificatore da
inserire nella tavola dei simboli.
6.2
Struttura delle linee
Un programma FORTRAN è una successione di linee su cui sono scritte le
istruzioni (in inglese, statements). Non è prevista punteggiatura per separare
12
un’istruzione dalla successiva: la regola è che, finita un’istruzione, si va a
capo e si inizia a scrivere la successiva.
Le istruzioni vanno scritte sulle linee rispettando il cosiddetto formato
fisso, ereditato dall’epoca in cui non esistevano terminali e tastiere e i programmi venivano scritti usando le schede perforate6 ; questo formato, che è
stato poi abbandonato dal Fortran 90, prescrive le seguenti regole per l’uso
delle colonne:
Colonne da 1 a 5: sono riservate alle eventuali etichette delle istruzioni (cfr.
l’istruzione “10 PRINT∗, ...“ del programma ERONE). Un’etichetta può
essere un qualunque numero naturale da 1 a 99999 e può essere collocata
dovunque in queste colonne.
Colonna 6: abitualmente è vuota; se contiene un (qualsiasi) carattere, la
linea viene considerata dal compilatore una continuazione di quella precedente.
Colonne da 7 a 72: in queste colonne vengono scritte le istruzioni. In virtù
di quanto detto sull’utilizzo degli spazi, un’istruzione può iniziare in qualsiasi
colonna dalla 7 in poi. Se l’istruzione è troppo lunga e va oltre la colonna
72, può essere continuata sulla linea successiva.
Colonne da 73 in poi: sono ignorate dal compilatore.
Ogni programmatore sa che per rendere più comprensibile un programma
è buona regola inserire dei commenti che ne spieghino lo scopo e il flusso. In
FORTRAN 77 questo scopo è raggiunto inserendo delle linee di commento,
ovvero linee che contengono un carattere C o ∗ a colonna 1. In Fortran 90
è considerato commento anche qualunque carattere scritto dopo un punto
esclamativo, in qualunque punto di una linea. I commenti sono ignorati dal
compilatore.
6.3
Struttura delle unità di programma
struttura
Le istruzioni FORTRAN si dividono in due categorie: le istruzioni eseguibili
descrivono azioni che dovranno essere compiute durante l’esecuzione del
programma e vengono tradotte dal compilatore nelle equivalenti istruzioni
del linguaggio macchina; le istruzioni non eseguibili invece forniscono informazioni di cui il compilatore deve tener conto durante la traduzione, ma non
vengono tradotte di per sé (sono istruzioni di questa natura, ad esempio, le
intestazioni delle unità di programma come PROGRAM ERONE e le specificazioni di tipo, come REAL A,B,C,SP,AREA e INTEGER LEGGI). Le
istruzioni non eseguibili diverse da un’intestazione di unità di programma e
6
Ogni scheda serviva a scrivere un’istruzione e aveva 80 colonne, su ciascuna delle quali
veniva perforato un carattere. Le ultime 8 colonne non venivano usate per le istruzioni del
programma, ma per numerare le schede, in modo da poterle rimettere nell’ordine giusto
se per un motivo qualsiasi venivano mescolate.
13
write
dalle FORMAT (di cui parleremo nel paragrafo 10) sono chiamate istruzioni
di specificazione o dichiarative. Tenendo conto di questa classificazione, ogni
unità di programma è divisa in quattro parti:
– l’intestazione, che è la prima istruzione nella quale si specifica se l’unità di
programma è un programma principale o un sottoprogramma e, sottoprog
in questo
caso, di quale tipo di sottoprogramma si tratta (cfr. paragrafo 11). L’intestazione è facoltativa in un programma principale e obbligatoria in un
sottoprogramma;
– la sezione dichiarativa, che raccoglie tutte le eventuali istruzioni dichiarative relative a nomi simbolici usati nell’unità di programma;
– la sezione esecutiva, che raccoglie tutte le istruzioni eseguibili;
– la fine dell’unità di programma, rappresentata dall’istruzione END.
Le istruzioni FORMAT possono comparire in qualsiasi punto dell’unità di
programma. Queste regole sono schematizzate qui sotto, con riferimento in
particolare al programma ERONE.
intestazione
sezione dichiarativa
sezione esecutiva
fine
PROGRAM ERONE
INTEGER LEGGI
REAL A,B,C,SP,AREA
10 PRINT∗, ’immettere le lunghezze dei lati’
..
.
STOP
END
Per ERONE, l’intestazione potrebbe non esserci perché si tratta di un programma principale. Notiamo inoltre che la sezione esecutiva del programma
finisce con l’istruzione STOP, e subito dopo viene l’istruzione END. A questo
proposito, è importante puntualizzare la differenza fra STOP e END.
L’istruzione STOP è un’istruzione eseguibile che corrisponde all’azione
“ferma l’esecuzione del programma”, e può comparire in qualunque punto
dell’unitàs di programma (eventualmente anche in più punti), dovunque
l’algoritmo richieda di fermarsi. L’istruzione END rappresenta invece la
fine fisica dell’unità di programma e compare sempre e soltanto alla fine;
essa non corrisponde a nessuna azione, ma dice al compilatore che l’unità
di programma finisce in questo punto, e qualunque cosa sia scritta dopo la
END non ne fa parte. Un’istruzione STOP che preceda immediatamente la
END può essere omessa.
6.4
Costanti, variabili e loro tipo
Le grandezze su cui un programma opera possono essere costanti o variabili.
Una costante è una grandezza il cui valore è definito prima dell’esecuzione e
14
non può cambiare durante l’esecuzione. Una variabile è invece una grandezza
il cui valore viene definito in fase di esecuzione e può variare nel corso della
stessa. Il compilatore assegna una locazione di memoria sia alle costanti che
alle variabili; la differenza sta nel fatto che nella locazione di una costante
viene subito memorizzato il valore, mentre in quella destinata a una variabile
non viene memorizzato alcun valore 7 .
Sia le costanti che le variabili hanno un tipo che può essere: intero, reale
in precisione semplice (o, semplicemente, reale), reale in precisione doppia
(o, semplicemente, doppia precisione), complesso, logico, carattere.
Costanti. La forma in cui una costante è scritta ne determina tipo e valore.
Una costante intera è un numero senza punti o virgole decimali, eventualmente preceduto da un segno. Sono esempi validi i seguenti:
0 999
–1325
34
+7
12459
mentre non lo sono –1.000 o 1,345 perché contengono il punto o la virgola.
Una costante reale è un numero in cui compare un punto decimale, eventualmente preceduto da un segno. Essa può essere scritta in forma esponenziale o
non esponenziale; nel primo caso l’esponente consiste nella lettera E seguita
da un intero positivo o negativo. Sono esempi validi i seguenti:
10.
–8.05
1.45E–3
25.89E+1
–0.15E0
1.E–6
1.E6
mentre non sono validi 1,04 (perché contiene la virgola invece del punto) e
-12.3E0.5 (perché l’esponente è un numero reale).
Nel formato esponenziale si può evitare il punto decimale; ad esempio 5E3 è un’alternativa valida a 5.E3. Questa possibilità non esisteva in
FORTRAN 66 ed è stata eliminata in Fortran 90.
Una costante doppia precisione si presenta come una costante reale in formato esponenziale, con la lettera D al posto di E. La differenza sta nel
modo in cui il valore viene convertito in binario e memorizzato. Ad esempio, su una macchina a 32 bits la costante 0.1D–4, che corrisponde al valore
0.1 × 10−4 , viene memorizzata su 64 bits, di cui 52 dedicati alla mantissa
e 11 alla caratteristica, mentre la costante 0.1E–4, che corrisponde al solito
valore, viene memorizzata su 32 bits, di cui 23 per la mantissa e 8 per la
caratteristica.
Una costante complessa è data da una coppia di costanti reali separate da
una virgola e racchiuse fra parentesi tonde: la prima costante rappresenta
la parte reale del numero e la seconda la parte immaginaria. Ad esempio,
la costante (–0.1, 2.5E–2) identifica il numero complesso –0.1+0.025i. Il
7
Alcuni compilatori, ma non tutti, inseriscono il valore 0 (zero) nelle locazioni destinate
alle variabili.
15
compilatore riserva a una costante di questo tipo due locazioni di memoria
consecutive atte a contenere la rappresentazione floating point di un numero
reale8 .
Una costante logica può assumere solo due valori, “vero” e “falso”, che in
FORTRAN sono scritti rispettivamente come .TRUE. e .FALSE.
Una costante carattere è una stringa di caratteri racchiusa fra apici. Sono
esempi di costanti carattere le stringhe ’immettere le lunghezze dei lati’,
’area= ’ e ’ancora?
(1/0= si/no)’ che compaiono nel programma ERONE
realizza
del paragrafo 5.
Talvolta è utile identificare una costante con un nome simbolico, pur
senza cambiarne la natura di costante. Immaginiamo di aver scritto un
programma in cui compare molte volte la costante reale 1.E–3, e di volerlo
cambiare in modo che tutte le grandezze reali siano in doppia precisione.
Allora tutte le occorrenze di 1.E–3 devono essere cambiate in 1.D–3 e dobbiamo usare a questo scopo la funzionalità “sostituisci” degli editori di testo.
Un’alternativa è quella di scrivere il programma usando un nome simbolico,
ad esempio COST, al posto della costante 1.E–3 e associare il nome alla
costante tramite l’istruzione dichiarativa PARAMETER che ha in questo
caso la forma
PARAMETER(COST=1.E–3)
Se organizziamo cosı̀ il programma, per cambiare il tipo della costante sarà
sufficiente intervenire sull’istruzione PARAMETER, facendola diventare
PARAMETER(COST=1.D–3)
Il fatto che COST, pur essendo un nome simbolico, identifichi una costante
implica che qualunque istruzione che tenti di cambiarne il valore provocherà
una situazione di errore in fase di compilazione (nel qual caso il modulo
oggetto non viene creato) o in fase di esecuzione (nel qual caso l’esecuzione
del programma viene bloccata dal sistema).
Variabili. Una variabile è identificata da un nome simbolico che può contenere solo lettere e cifre per un massimo di 6 caratteri e deve iniziare con
una lettera. I nomi
A1
B
somma
WORK
n2p1
Norma2
sono validi, mentre
1c
Norma 2
Spaziolavoro
8
n2&1
Alcuni compilatori accettano anche costanti di tipo complesso-doppia precisione, come
ad esempio (1.D2, –5.1D0), a cui vengono riservate due locazioni per numeri floating point
doppia precisione.
16
non lo sono. Alcune restrizioni sono state allentate nei dialetti nati dal
FORTRAN 77 e le estensioni sono state poi recepite in Fortran 90; cosı̀
molti compilatori accettano oggi nomi con un massimo di 31 caratteri e
contenenti il carattere “underscore” ( ).
Cosa dire del tipo di una variabile? Se si vuole che essa sia considerata
dal compilatore intera o reale, si può sfruttare la regola di default seguente,
in base alla quale l’iniziale del nome determina il tipo:
– iniziale da A a H : tipo reale
– iniziale I,J, K, L, M o N : tipo intero
– iniziale da O a Z : tipo reale.
Cosı̀, in assenza di altre indicazioni, il compilatore classifica come reali le
variabili A1, WORK, FLAG, e come intere le variabili NORMA, L, MAX. Da
questo punto di vista, le istruzioni di specificazione REAL A,B,C,SP,AREA
realizza
e INTEGER LEGGI nel programma ERONE del paragrafo 5 sono inutili,
perché i tipi delle variabili coinvolte sarebbero per default quelli specificati.
A questo proposito i programmatori FORTRAN si dividono grosso modo in due categorie. Ci sono quelli che rispettano rigorosamente la regola
di default nella scelta dei nomi delle variabili intere e reali e di conseguenza non hanno bisogno di istruzione dichiarative al riguardo. Al contrario,
ci sono alcuni che, indipendentemente dal rispetto della regola di default,
preferiscono dichiarare il tipo di tutte le variabili; questa scelta di solito ha
un carattere puramente “estetico”, ma acquista maggiore utilità se si utilizza l’istruzione dichiarativa (che non fa parte della standard FORTRAN 77,
ed è invece standard per il Fortran 90)
IMPLICIT NONE
il cui effetto è annullare la regola di default, costringendo quindi a dichiarare
esplicitamente il tipo di tutte le variabili. Siccome in questa situazione il
compilatore segnala un errore se il tipo di una variabile non è specificato,
si ha un controllo immediato sulla presenza di eventuali “errori di battitura” nei nomi simbolici nella sezione esecutiva del programma. L’istruzione
IMPLICIT NONE deve precedere tutte le altre istruzioni dichiarative.
La regola di default contempla soltanto i tipi intero e reale. Per qualunque
altro tipo bisogna ricorrere a un’istruzione di specificazione di tipo usando
il dichiaratore che ci interessa fra
DOUBLE PRECISION, COMPLEX, LOGICAL e CHARACTER.
Le istruzioni in questione vanno inserite nella sezione dichiarativa dell’unità
di programma a cui si riferiscono e sono formate dalla parola chiave, ovvero
il dichiaratore, seguito dall’elenco delle variabili a cui si vuole attribuire quel
tipo. Consideriamo alcuni esempi:
17
DOUBLE PRECISION A, W1, W2, NORMA
COMPLEX RAD
LOGICAL CONDIZ, IND
CHARACTER∗10 NOME
Le prime tre frasi non hanno bisogno di spiegazione. L’ultima dice che
NOME identifica una variabile di tipo carattere di lunghezza 10, e pertanto
i valori che NOME può assumere sono stringhe di al più 10 caratteri.
Non
AGM MR
ci dilungheremo oltre sulle variabili di tipo carattere, rimandando a [1] o [2]
per ulteriori dettagli.
Un altro strumento utilizzabile per specificare il tipo delle variabili è
l’istruzione IMPLICIT, che permette di associare un tipo particolare alle
variabili il cui nome inizia con una particolare lettera dell’alfabeto o con una
lettera appartenente ad un particolare gruppo. Per esempio, le istruzioni
IMPLICIT DOUBLE PRECISION D
IMPLICIT REAL M,N
dicono al compilatore che tutti i nomi simbolici che iniziano per D identificano variabili di tipo doppia precisione e quelli che iniziano per M o N
corrispondono a variabili reali; l’istruzione
IMPLICIT INTEGER (A-C)
attribuisce tipo reale a tutte le variabili il cui nome inizia per A, B o C;
infine l’istruzione
IMPLICIT DOUBLE PRECISION (A-H,O-Z)
dichiara di tipo doppia precisione tutte le variabili il cui nome comincia per
una lettera fra A e H oppure fra O e Z (in pratica tutte le variabili che la
regola di default definirebbe come reali).
6.5
Variabili dimensionate (arrays)
arrays
Finora abbiamo parlato delle variabili FORTRAN come di grandezze scalari,
a cui il compilatore associa una locazione di memoria. In realtà capita
molto spesso di dover tradurre in FORTRAN algoritmi che operano anche
su vettori e matrici e, più raramente, su variabili a tre o più dimensioni
(il massimo numero di dimensioni consentito in FORTRAN 77 è 7). A
questo proposito, dobbiamo imparare due cose: come avvertire il compilatore che un nome simbolico corrisponde a una variabile dimensionata (il
termine tecnico è array) e come tradurre in FORTRAN l’usuale notazione
vettoriale con gli indici. Per quanto riguarda la prima questione, si può
usare un’apposita istruzione dichiarativa, caratterizzata dalla parola chiave
18
DIMENSION, tramite la quale si danno al compilatore tutte le informazioni
che gli occorrono per allocare la memoria e tradurre poi nel modo corretto
le istruzioni eseguibili che coinvolgono elementi dell’array. Consideriamo ad
esempio l’istruzione
DIMENSION X(3), M(4,2)
Essa dice al compilatore che X è un vettore composto da 3 elementi e M è
una matrice di 4 righe e 2 colonne. Il compilatore assegna a X tre locazioni
di memoria consecutive in cui sono memorizzati gli elementi di X: nella
prima c’è l’elemento di indice 1, nella seconda quello di indice 2, e nella
terza quello di indice 3. Per la matrice M il compilatore riserva invece
8 locazioni di memoria consecutive, nelle quali dobbiamo immaginare la
matrice memorizzata per colonne, come descritto nello schema seguente:
locazione:
indici:
1
(1,1)
2
(2,1)
3
(3,1)
4
(4,1)
5
(1,2)
6
(2,2)
7
(3,2)
8
(4,2)
Esattamente come una variabile scalare, cosı̀ anche una variabile dimensionata ha un tipo che viene attribuito dal compilatore in base alle stesse
regole viste per le variabili scalari. Ad esempio, in assenza di dichiarazioni,
X è un vettore reale e M è una matrice intera, con il che si intende che tutti
gli elementi di X sono reali e tutti gli elementi di M sono interi.
Per informare il compilatore che una variabile è dimensionata, si possono
usare anche istruzioni dichiarative diverse dalla DIMENSION; ad esempio
l’istruzione
DOUBLE PRECISION VET1(20)
è equivalente alla coppia di istruzioni
DOUBLE PRECISION VET1
DIMENSION VET1(20)
Negli esempi precedenti gli indici degli elementi dell’array vanno da 1
fino alla dimensione stabilita nella dichiarazione dell’array. D’altra parte,
talvolta può essere comodo poter usare indici che variano fra estremi diversi.
Ad esempio, spesso in matematica si usano scritture del tipo x0 , x1 , . . . , xn
per indicare i primi elementi di una successione, che possono essere assimilati
agli elementi di un vettore x di n + 1 elementi, in cui gli indici partono da
0 e arrivano a n. Oppure, se si deve creare un vettore in cui memorizzare
quanti abitanti di un paese sono nati negli anni dal 1991 al 2010, può essere
comodo usare gli indici da 1991 a 2010 invece che da 1 a 20. In FORTRAN
è possibile dire al compilatore che gli indici non partono da 1, esprimendo
esplicitamente nella dichiarazione dell’array il primo e l’ultimo valore che
l’indice può assumere. Nel primo dei due esempi precedenti, supponendo
n ≤ 10 si potrebbe usare la dichiarazione
19
DIMENSION X(0:10)
e nel secondo esempio
INTEGER ABIT(1991:2010)
Di fronte a dichiarazioni come queste, il compilatore è comunque in grado
di contare il numero di elementi dell’array e allocare la memoria necessaria.
Un elemento di array può essere usato in un programma alla stregua di
una variabile scalare, perché ad esso corrisponde una locazione di memoria
il cui indirizzo è univocamente associato all’indice (o agli indici) che lo contraddistingue. Per fare riferimento a un elemento di array si usa il nome
dell’array seguito da tanti indici quante sono le dimensioni dell’array separati da virgole e racchiusi fra parentesi. Un indice è una costante, una
variabile o un’espressione di tipo intero (parleremo nel prossimo paragrafo
delle espressioni aritmetiche e del loro tipo). Dati un vettore V e una matrice
A, sono ad esempio sintatticamente corretti i seguenti nomi di elementi:
V(1) A(I,J) V(I+1) A(1,J) V(K) A(K,K-1) M(V(1),1)
purché I,J e K siano variabili intere e il vettore V sia anch’esso intero (questa
ipotesi serve perché V(1) è usato come indice in M(V(1),1)). Consideriamo il
seguente programma MEDIE, che calcola e stampa la media dei voti riportati
da un certo numero di studenti in un corso di laurea il cui regolamento
prevede 15 esami.
PROGRAM MEDIE
∗ Programma che calcola la media dei voti riportati da uno o più
∗ studenti per un numero massimo di esami pari a 15
PARAMETER (NM=15)
INTEGER V(NM)
10 PRINT∗, ’immettere il numero N di esami superati’
PRINT∗, ’Attenzione: N deve essere <=’,NM
READ∗, N
PRINT∗, ’immettere i voti’
READ∗, (V(I), I=1,N)
SMEDIA=0.
DO 100 I=1,N
SMEDIA=SMEDIA+V(I)
100 CONTINUE
SMEDIA=SMEDIA/N
PRINT∗,’media= ’, SMEDIA
PRINT∗,’ancora? (1/0= si/no)’
READ∗,LEGGI
IF(LEGGI.EQ.1) GOTO 10
END
20
Per ogni studente si legge il numero n ≤ 15 di esami superati e i relativi
voti, che vengono memorizzati in un vettore V. L’istruzione
READ∗, (V(I), I=1,N)
è la traduzione FORTRAN dell’istruzione “Leggi v1 , . . . , vn ”, tramite la
quale si leggono tutti i voti di uno studente. Osserviamo che se scrivessimo
semplicemente
READ∗, V
la macchina si aspetterebbe in fase di esecuzione 15 dati, indipendentemente
da N, perché la lunghezza di V è NM=15. Le istruzioni
DO 100 I=1,N
SMEDIA=SMEDIA+V(I)
100 CONTINUE
sono la traduzione FORTRAN del ciclo
Per i = 1, . . . , n
Poni media = media + vi .
Nelle istruzioni PRINT che precedono la lettura di N abbiamo fatto in modo che il valore (15) della costante NM venga visualizzato sullo schermo
prima che l’utente inserisca il valore di N. Cosa succederebbe infatti se il
programma venisse usato senza tenere conto della limitazione su N, ad esempio per fare le medie per un corso di laurea dove sono previsti 20 esami? È facile immaginare che si creerebbe una situazione anomala perché
in fase di esecuzione il programma si troverebbe ad operare con elementi
di V con indice maggiore di 15, laddove dalla dichiarazione risulta che gli
indici per V possono andare da 1 a 15 (in gergo, staremmo usando indici
out-of-bounds). All’atto pratico questo significherebbe fare riferimento con
nomi V(16), V(17), etc.. a locazioni di memoria che il compilatore non ha
allocato per il vettore V. La gestione di queste situazioni varia da macchina
a macchina. Dal punto di vista teorico sarebbe possibile per qualunque compilatore inserire nel programma oggetto un controllo sugli indici usati nei
riferimenti a elementi di arrays, con la possibilità di bloccare l’esecuzione in
caso di indici fuori dai limiti. Ma questo controllo renderebbe l’esecuzione
del programma molto lenta, e pertanto nessun compilatore lo prevede, salvo in alcuni casi prevedere un’opzione per attivarlo9 . In assenza di questo
controllo, il programma “fa finta di niente” e va alla locazione di memoria
citata nel riferimento come se effettivamente questa facesse parte dell’array.
Per esempio, nel programma precedente il riferimento a V(16) porterebbe ad
9
L’opzione per il compilatore gfortran è –fbounds–check, e può essere molto utile in
fase di messa a punto dei programmi. Se in un programma è presente un vettore X
di lunghezza 3, e si fa riferimento all’elemento X(4), il programma viene fermato con il
seguente messaggio di errore: “At line ... of file ... Fortran runtime error: Array reference
out of bounds for array ’x’, upper bound of dimension 1 exceeded (4 > 3)”.
21
agire sulla locazione di memoria consecutiva a quella riservata all’elemento
V(15), senza tener conto del fatto che essa non è riservata al vettore V. Le
conseguenze di questo modo di procedere sono diverse a seconda dell’uso che
la macchina sta facendo della locazione abusivamente occupata e di quello
che ne fa il programma. Spesso succede una delle tre cose seguenti:
– la locazione è inutilizzata ⇒ il programma va avanti e dà risultati corretti,
come se niente di strano fosse accaduto;
– la locazione è riservata a un’altra variabile usata nel programma stesso ⇒
il programma va avanti, ma dà sicuramente risultati sbagliati 10 ;
– la locazione è riservata a software diverso dal programma e protetta ⇒
l’esecuzione del programma viene bloccata, con una segnalazione di errore
del tipo segmentation fault.
7
Espressioni e istruzione di assegnazione
Un’istruzione di assegnazione ha la forma generale
v=e
(2)
dove v è un nome di variabile o di un elemento di array e e è un’espressione.
A seconda della natura delle grandezze coinvolte, si parla di assegnazione
numerica, logica o carattere. Noi ci occuperemo soltanto del caso numerico. Più in particolare, ci concentreremo su istruzioni di assegnazione che
coinvolgono costanti e variabili intere, reali e doppia precisione; per il caso
complesso rimandiamo ai testi in biliografia.
7.1
Espressioni aritmetiche
Cominciamo con lo specificare come si costruiscono le espressioni aritmetiche
e come ne viene calcolato il valore. Le espressioni aritmetiche si formano
legando fra loro costanti o variabili di tipo numerico tramite operatori aritmetici (per variabili, da qui in avanti intendiamo anche elementi di arrays).
Gli operatori aritmetici sono i seguenti:
+
∗
/
∗∗
addizione
sottrazione
moltiplicazione
divisione
elevamento a potenza
10
Questa situazione può rivelarsi particolarmente tragica quando il valore contenuto
nella locazione usata “abusivamente” viene cambiato: in questo caso infatti viene di fatto
cambiato, a nostra insaputa, il valore della variabile memorizzata in quella locazione.
22
istr_ass
Se necessario, si possono usare parentesi tonde e riferimenti a funzioni intrinseche
del FORTRAN. Un elenco completo di queste funzioni è reperibile
AGM
in [1]; qui riportiamo alcune delle più comunemente usate:
ABS
SQRT
EXP
LOG, LOG10
SIN, COS, TAN
ASIN, ACOS, ATAN
SINH, COSH, TANH
MAX, MIN
I,R,Dp
R,Dp
R,Dp
R,Dp
R,Dp
R,Dp
R,Dp
I, R,Dp
valore assoluto
radice quadrata
esponenziale
logaritmo naturale e logaritmo in base 10
seno, coseno e tangente
arcoseno, arcocoseno e arcotangente
seno , coseno e tangente iperbolici
massimo e minimo fra due o più valori
Nella colonna centrale indichiamo i tipi di argomenti che le funzioni accettano; il risultato è dello stesso tipo dell’argomento (o degli argomenti nel
caso di MIN e MAX). Esempi di espressioni valide sono i seguenti:
X+Y
1.25∗SQRT(X)
(1.+X(I))/(3∗M)
ABS(B∗∗2-4∗A∗C)
COS(A+B)
EXP(–0.1∗X)
2.∗SIN(X)∗∗2
MAX(A(1,1),B,2.∗X–1.)∗∗(–2)
x+y
√
1.25 x
1+xi
3m
|b2 −
4ac|
cos(a + b)
e−0.1x
2sin2 (x)
max(a11 , b, 2x − 1)−2
Per quanto riguarda l’ordine in cui le operazioni vengono eseguite, valgono
le regole standard dell’aritmetica.
L’esecuzione di un’operazione elementare (addizione, sottrazione, moltiplicazione e divisione) richiede che gli operandi siano dello stesso tipo. Il
risultato è anch’esso dello stesso tipo. Consideriamo ad esempio l’espressione N2∗N1; se N1 e N2 sono variabili intere, il risultato è intero e viene
memorizzato secondo le regole di memorizzazione dei numeri interi, mentre
se le due variabili sono reali il risultato viene memorizzato in forma floating point normalizzata. Cosı̀, se i valori di N1 e N2 fossero dell’ordine di
106 e 107 rispettivamente, la memorizzazione del risultato provocherebbe un
overflow nel primo caso (su una macchina a 32 bits), ma non nel secondo.
A proposito di operazioni fra numeri interi, è importante sottolineare
che la divisione fra interi dà come risultato il quoziente fra i due numeri,
nell’accezione del termine usata alle scuole elementari. Cosı̀ il risultato dell’espressione 3/4 è 0 e, se N è una variabile intera, il valore di N/2∗2 è uguale
a N se e solo se il valore di N è pari.
A dispetto del fatto che un’operazione avviene sempre fra operandi dello
stesso tipo, il FORTRAN consente di scrivere espressioni miste, in cui com23
paiono operazioni fra operandi di tipo diverso. In questo caso la macchina
opera una conversione implicita di uno dei due operandi da un tipo all’altro,
in modo da poter fare l’operazione fra numeri con la stessa rappresentazione.
Più precisamente, l’operando di tipo più “debole” viene convertito al tipo
più “forte”, secondo la gerarchia seguente:
1) Intero
2) Reale
3) Doppia precisione
più debole
↓
più forte
Abbiamo usato l’espressione “conversione implicita” perché la conversione
avviene a livello di registri aritmetici, senza cambiare il tipo dell’operando.
La conversione da intero a reale consiste nel trasformare il valore intero in
un numero equivalente in rappresentazione floating point; la conversione da
reale a doppia precisione consiste invece nel cambiare la rappresentazione
del numero da floating point precisione semplice a floating point doppia
precisione (le cifre in più di mantissa vengono tipicamente poste uguali a
zero).
Data un’espressione mista, si definisce tipo dell’espressione il tipo del
risultato, che coincide con il tipo più forte presente nell’espressione stessa.
Consideriamo alcuni esempi.
Sia R una variabile reale; allora 2+R è un’espressione mista reale e la
macchina esegue l’addizione convertendo il dato intero 2 a tipo reale, in
modo da calcolare la somma fra due numeri floating point. Il risultato ottenuto è un valore reale. Si potrebbe evitare il costo (in termini di tempo
di esecuzione) della conversione, scrivendo l’espressione nella forma 2.0+R,
dove la costante è in forma reale. Se R fosse una variabile in doppia precisione, il calcolo di 2+R richiederebbe una doppia conversione, da intero a
reale e poi da reale a doppia precisione; tutto questo viene evitato scrivendo
l’espressione nella forma 2.D0+R.
Se I e J sono due variabili intere con valori ≥ 1, l’espressione 1/(I+J)
dà come risultato il numero intero zero, perché viene eseguita in aritmetica
intera, mentre l’espressione mista 1.0/(I+J) viene calcolata in aritmetica
reale e dà pertanto un risultato reale diverso da zero.
Talvolta, i programmatori preferiscono evitare espressioni miste usando
le funzioni intrinseche di conversione, di cui riportiamo qui sotto l’elenco:
INT
NINT
REAL
DBLE
I,R,Dp
I,R,Dp
I,R,Dp
I,R,Dp
Conversione a intero per troncamento
Conversione a intero per arrotondamento
Conversione a reale
conversione a doppia precisione
Ad esempio, l’espressione 1.0/REAL(I+J) è un’alternativa a 1.0/(I+J) in
cui la conversione è esplicitata.
24
7.2
Assegnazione aritmetica
istr_ass
Possiamo ora tornare a considerare l’istruzione di assegnazione (2). L’esecuzione avviene in due fasi:
1) Viene valutata l’espressione e;
2) Il risultato ottenuto valutando e viene memorizzato in v.
Se v e e hanno lo stesso tipo, non c’è altro da dire. In caso contrario,
l’istruzione è un’assegnazione mista e la seconda fase coinvolge una conversione del risultato ottenuto nella prima fase al tipo di v. È importante
sottolineare che il tipo di v non influenza in alcun modo il calcolo del valore
di e, e che la conversione suddetta avviene solo dopo che questo valore è stato calcolato. Se v è di un tipo più forte di e, la conversione avviene secondo
le regole viste prima. D’altra parte, può succedere che v sia di un tipo più
debole di e, nel qual caso è coinvolta una conversione da un tipo più forte
a uno più debole. Consideriamo al solito alcuni esempi, supponendo che N
sia una variabile intera, A1 e A2 siano variabili reali, e D1 sia una variabile
doppia precisione.
Nell’istruzione A1=A2+EXP(D1) il risultato dell’espressione è doppia
precisione e A1 è invece reale; pertanto il risultato dell’espressione viene convertito da formato floating point doppia precisione a formato floating point
semplice precisione: la mantissa viene accorciata (per troncamento o arrotondamento) e la caratteristica viene memorizzata su un numero inferiore
di bits.
Nell’istruzione N=A1∗A2, l’espressione è reale e la variabile N è intera;
la conversione da reale a intero consiste nella memorizzazione della parte
intera nella locazione destinata a N.
In entrambi questi esempi c’è rischio di overflow nella fase di conversione:
nel primo caso la caratteristica potrebbe non rientrare nel range accettabile
per la precisione semplice, e nel secondo caso la parte intera del valore
dell’espressione potrebbe superare la soglia di overflow degli interi.
7.3
Fortran 90: operazioni fra arrays
A differenza del FORTRAN 77, secondo il quale le espressioni aritmetiche
e le istruzioni di assegnazione agiscono esclusivamente su grandezze scalari,
il Fortran 90 consente di eseguire operazioni aritmetiche e assegnazioni fra
arrays o porzioni di arrays. Queste operazioni sono sempre intese come
operazioni elemento a elemento, e non esistono operatori corrispondenti al
prodotto matriciale righe per colonne. Inoltre, lo standard non specifica
l’ordine in cui le operazioni scalari fra gli elementi coinvolti devono essere
eseguite, lasciando la decisione al compilatore che può creare un programma
oggetto efficiente sfruttando eventualmente la particolare architettura (vettoriale e/o parallela) della macchina per cui è progettato. Senza entrare nei
25
dettagli, facciamo alcuni esempi. Supponendo che A e B siano due matrici
reali 20×20 e V un vettore reale di lunghezza 10, le seguenti sono espressioni
valide in Fortran 90 (il risultato di ciascuna di esse è descritto accanto):
A∗B
matrice 20 × 20 con elementi
A(I,J)∗B(I,J) , per I=1,. . .,20 e J=1,. . .,20
3.∗V+1.
vettore di lunghezza 10 con elementi
3.∗V(I)+1. , per I=1,. . .,10
0.1/V+A(1:10,1)
vettore di lunghezza 10 con elementi
0.1/V(I)+A(I,1) , per I=1,. . .,10
ABS(V(2:8))
vettore di lunghezza 7 con elementi
ABS(V(I)) , per I=2,. . .,8
e le seguenti sono istruzioni di assegnazione aritmetica fra array valide:
A=A+2.
poni A(I,J) uguale a A(I,J)+2.
per I=1,. . .,20 e J=1,. . .,20
B(1:10,J) =V
poni B(I,J) uguale a V(I)
per I=1,. . .,10
V(2:5)=V(1:4)
poni V(I) uguale a V(I-1)
per I=2,. . .,5
A(1,11:20)=SQRT(V)
poni A(I,J) uguale a SQRT(V(J-10))
per J=11,. . .,20
8
Controllo del flusso di esecuzione
L’esecuzione di un’unità di programma avviene sequenzialmente a partire
dalla prima istruzione eseguibile fino a che non viene incontrata un’istruzione
di blocco dell’esecuzione (STOP o, come vedremo più avanti, RETURN) o
la END. D’altra parte molti algoritmi prevedono costrutti di scelta o di
ripetizione che intervengono sul flusso di esecuzione rendendolo da un certo punto di vista non più sequenziale.
Abbiamo già visto nei programmi
realizza
arrays
ERONE e MEDIE (paragrafo 5 e 6.5, rispettivamente) alcuni esempi di
queste situazioni realizzati direttamente in FORTRAN. Torniamo ora sulle
istruzioni usate in quei programmi per definirne la sintassi.
26
8.1
Istruzione GO TO
L’istruzione GO TO è chiamata istruzione di salto incondizionato e ha la
forma
GO TO l
dove GO TO (con o senza lo spazio fra le due parole) è la parola chiave e
l è l’etichetta di un’istruzione eseguibile nella stessa unità di programma.
L’effetto è di far saltare l’esecuzione all’istruzione di etichetta l, da cui poi
il flusso riprende sequenzialmente.
È un’istruzione da usarsi con moderazione, perché un suo uso eccessivo
può rendere il flusso dell’algoritmo cosı̀ contorto da risultare praticamente
incomprensibile11 . Come abbiamo a suo tempo osservato, nei due programmi campione ERONE e MEDIE abbiamo usato le istruzioni GO TO insieme
a istruzioni di scelta IF per simulare un ciclo while, costrutto inesistente in
FORTRAN 77. D’altra parte, nella maggior parte dei linguaggi di programmazione, compreso il Fortran 90, c’è la possibilità di tradurre esplicitamente
questo costrutto, e in linea di principio si può riuscire a scrivere qualunque
algoritmo senza bisogno di ricorrere a salti incondizionati. Ciò nonostante,
un’istruzione analoga al GO TO è presente in tutti i linguaggi maggiormente
usati12 perché in molte situazioni ricorrervi è un modo per semplificarsi la
vita.
8.2
Istruzioni IF
Le istruzioni IF sono tutte le istruzioni di scelta, in cui si controlla se una
o più condizioni sono verificate e in base all’esito del controllo si dirige il
flusso dell’esecuzione per una strada o un’altra. Le condizioni da verificare
sono espresse tramite espressioni relazionali o, più in generale, espressioni
logiche.
Un’espressione relazionale è costituita da due espressioni aritmetiche
legate da un operatore di relazione (< > ≤ ≥ = 6=). Questi operatori sono descritti in FORTRAN 77 mediante acronimi preceduti e seguiti
da un punto, come descritto nella tabella sottostante:
simbolo matematico
<
>
≤
≥
=
6=
FORTRAN 77
.LT.
.GT.
.LE.
.GE.
.EQ.
.NE.
11
Fortran 90
<
>
<=
>=
==
/=
In passato fu coniato il termine dispregiativo spaghetti code, ovvero programma
spaghetti per indicare un programma con tante istruzioni GO TO da renderne il flusso
intrecciato e indistricabile come un piatto di spaghetti.
12
Per quanto a nostra conoscenza, solo il MATLAB non la prevede.
27
Il Fortran 90 ha introdotto la possibilità di sostituire gli acronimi con simboli
matematici, e questa convenzione è riconosciuta da molti compilatori oggi
in uso. Alcuni esempi di espressioni relazionali sono i seguenti:
A(1)+B(1).LT.0.
B∗∗2 .LE. 4∗A∗C
N.EQ.M+1
X.NE.SQRT(Y)
ABS(X–Y).GE. 1E–6
a1 +b1 <0
b2 ≤ 4ac
n=m+1
√
x 6= y
|x-y| ≥ 10−6
Per descrivere una condizione può talvolta essere necessario usare anche gli
operatori logici AND, OR e NOT, nel qual caso la condizione è formalizzata come un’espressione logica. Gli operatori logici in FORTRAN sono
.AND., .OR. e .NOT. Senza entrare nei dettagli della valutazione del risultato di un’espressione logica, riportiamo alcuni esempi qui sotto; nella colonna
“oppure...” scriviamo l’espressione con delle parentesi che ne migliorano la
leggibilità, pur non essendo richieste dal linguaggio:
Condizione
Traduzione FORTRAN
oppure...
0 ≤ i ≤ 10
x < 1 o x>3.14
j 6= n,m
0.LE.I.AND. I.LE.10
X.LT.1.0.OR.X.GT.3.14
J .NE.N.AND.J.NE.M
(0.LE.I).AND.(I.LE.10)
(X.LT.1.0).OR.(X.GT.3.14)
(J .NE.N).AND.(J.NE.M)
8.2.1
IF logico
iflogico
La forma più semplice di istruzione IF è il cosiddetto IF logico, che ha la
forma
IF(condizione) istruzione eseguibile
e funziona nel seguente modo: se la condizione è vera si esegue l’istruzione
specificata, altrimenti questa istruzione viene ignorata e l’esecuzione del programma procede con l’istruzione che segue l’IF. Esempi di IF logico sono i
seguenti:
IF(X.NE.0.)P=P∗X
IF(ABS(A-B).LT.1.E-3)STOP
IF(X(I).GT.AMAX)AMAX=X(I)
IF(N.GE.MM)PRINT∗,’Attenzione’
8.2.2
IF–THEN–ENDIF.
L’evoluzione naturale dell’IF logico è l’istruzione IF–THEN–ENDIF, tramite
la quale si può saltare un intero blocco di istruzioni nel caso che la condizione
specificata non sia vera. La forma dell’istruzione è la seguente:
28
IF(condizione) THEN

Istruzione 1


Istruzione 2
blocco THEN

..

.
END IF
dove è obbligatorio andare a capo dopo la frase IF(condizione)THEN e scrivere su una riga a se stante la frase di chiusura END IF (che può essere scritta
con o senza lo spazio fra le due parole). Se la condizione è vera, il programma esegue le istruzioni comprese fra l’istruzione IF e l’istruzione END IF,
che tutte insieme formano il cosiddetto blocco THEN; altrimenti, il blocco
THEN viene saltato. In ogni caso, l’esecuzione prosegue dall’istruzione che
segue END IF. Ad esempio con le istruzioni seguenti
IF(I/2∗I.EQ.I)THEN
X(I)= X(I)+1
A(I,I)= A(I,I)+I
END IF
I=I+1
si modificano l’elemento X(I) del vettore X e l’elemento diagonale A(I,I)
della matrice A se l’indice I è pari; se I è dispari, gli elementi suddetti non
vengono modificati. In entrambi i casi, si prosegue incrementando di 1 il
valori di I.
Consideriamo ora il seguente programma ERONE2.
10
PROGRAM ERONE2
IMPLICIT DOUBLE PRECISION (A-H,O-Z)
NT=0
PRINT∗, ’immettere le lunghezze dei lati’
READ∗, A,B,C
IF((A.GT.0.).AND.(B.GT.0.).AND.(C.GT.0.))THEN
SP=(A+B+C)/2.
Q=SP∗(SP-A)∗(SP-B)∗(SP-C)
IF(Q.GT.0.)THEN
AREA=SQRT(Q)
PRINT∗,’area= ’, AREA
NT=NT+1
END IF
END IF
PRINT∗,’ancora? (1/0= si/no)’
READ∗,LEGGI
IF(LEGGI.EQ.1) GOTO 10
PRINT∗,’Numero totale di triangoli: ’,NT
END
realizza
Si tratta di una modifica del programma ERONE del paragrafo 5, ottenuta
29
inserendo dei controlli per verificare che i dati a, b, c rappresentino effettivamente le lunghezze dei lati di un triangolo. Più precisamente, se uno dei tre
dati non è positivo, ovviamente non si tratta di lunghezze e il programma
salta all’istruzione PRINT∗,’ancora? (1/0= si/no)’ per leggere eventualmente una nuova terna di dati. Se i dati sono tutti positivi, può ancora
succedere che non rappresentino i lati di un triangolo (basti pensare alla
terna di valori 1,3, e 5); di questo ci rendiamo conto dopo aver calcolato
q = s(s − a)(s − b)(s − c). Se infatti q non è positivo, deduciamo che non
esiste un triangolo che possa avere i lati lunghi a, b e c, e si procede di nuovo
all’eventuale lettura di nuovi dati; altrimenti, possiamo calcolare e stampare
l’area. Tramite la variabile NT, il programma conta i triangoli di cui è stata
calcolata l’area.
Il programma contiene un IF–THEN–ENDIF dentro l’altro (o, come
si dice, annidati). Il blocco THEN dell’IF più esterno va dall’istruzione
SP=(A+B+C)/2. al primo END IF compreso; quello dell’IF più interno è
costituito dalle tre istruzioni
AREA=SQRT(Q)
PRINT∗,’area= ’, AREA
NT=NT+1
Nello scrivere il programma non abbiamo fatto iniziare tutte le istruzioni
nello stesso punto della riga, sfruttando la regola che impone di scrivere le
istruzioni a partire dalla colonna 7 (e fino alla 72), ma non impedisce di iniziare a scriverle a una colonna successiva alla 7. Usare la cosiddetta indentazione (dall’inglese indentation), ovvero scrivere i blocchi THEN spostati a
destra di qualche carattere rispetto all’istruzione iniziale IF(condizione)THEN
e a quella finale END IF, è una buona abitudine perché rende molto più leggibili i programmi. Pur senza farlo notare, abbiamo già usato l’indentazione
in precedenza, e la useremo d’ora in avanti intensivamente13 .
ELSE
8.2.3
IF–THEN–ELSE–ENDIF
Una situazione che spesso si presenta negli algoritmi è la scelta fra due blocchi di istruzioni alternative in base al verificarsi o meno di una data condizione. Per tradurre in FORTRAN queste scelte si deve usare l’istruzione
IF–THEN–ELSE–ENDIF, la cui forma generale è:
13
L’utilità dell’indentazione è tale che molti editori di testo moderni usati in ambienti
di sviluppo integrati (come OPEN Watcom) forniscono una funzione di indentazione
automatica, che spesso viene applicata per default durante la scrittura dei programmi.
30
IF(condizione) THEN

Istruzione 1


Istruzione 2
blocco THEN

..

.
ELSE
Istruzione 1
Istruzione 2
..
.
END IF



blocco ELSE


Se la condizione è vera vengono eseguite le istruzioni del blocco THEN,
altrimenti quelle del blocco ELSE; in entrambi i casi l’esecuzione prosegue
con la prima istruzione dopo END IF. Usando questa istruzione, l’algoritmo
Se m > n, allora:
max = m
altrimenti:
max = n
Fine scelta
viene tradotto nel modo seguente:
IF(M.GT.N)THEN
MAX=M
ELSE
MAX=N
END IF
Usando l’istruzione IF–THEN–ELSE–ENDIF, possiamo inserire delle
istruzioni di stampa nel programma ERONE2 che avvertono se una terna
di dati non corrisponde ad un triangolo.
10
PROGRAM ERONE3
IMPLICIT DOUBLE PRECISION (A-H,O-Z)
NT=0
PRINT∗, ’immettere le lunghezze dei lati’
READ∗, A,B,C
IF((A.GT.0.).AND.(B.GT.0.).AND.(C.GT.0.))THEN
SP=(A+B+C)/2.
Q=SP∗(SP-A)∗(SP-B)∗(SP-C)
IF(Q.GT.0.)THEN
AREA=SQRT(Q)
PRINT∗,’area= ’, AREA
NT=NT+1
ELSE
PRINT∗,’Dati non corrispondenti a un triangolo’
END IF
31
ELSE
PRINT∗,’Dati non corrispondenti a un triangolo’
END IF
PRINT∗,’ancora? (1/0= si/no)’
READ∗,LEGGI
IF(LEGGI.EQ.1) GOTO 10
PRINT∗,’Numero totale di triangoli: ’,NT
END
8.2.4
IF concatenati.
Supponiamo di dover realizzare in FORTRAN una situazione in cui ci sono
più di due possibili strade da percorrere, e la scelta dipende da più condizioni
che si escludono a vicenda, come per esempio nel calcolo della funzione
 2
per a < 0
 a
f (a) =
a(a − 1) per 0 ≤ a ≤ 1

a + 10−3 per a > 1
Per effettuare questo calcolo si può arricchire l’istruzione IF–THEN–ELSE–
ENDIF con la clausola ELSE IF, ottenendo un costrutto in cui una serie di IF
dipendenti da condizioni diverse e mutuamente esclusive sono concatenati.
La sintassi di quella che rappresenta l’istruzione di scelta più complessa del
FORTRAN è la seguente:
IF(condizione 1) THEN

Istruzione 1


Istruzione 2
blocco 1

..

.
ELSE IF(condizione 2)THEN
Istruzione 1


Istruzione 2
blocco 2

..

.
·········
ELSE IF(condizione n)THEN
Istruzione 1


Istruzione 2
blocco n

..

.
ELSE

Istruzione 1


Istruzione 2
blocco n + 1 ≡ blocco ELSE

..

.
END IF
32
Se la condizione 1 è verificata, si eseguono le istruzioni del blocco 1; altrimenti si va a controllare se la condizione 2 è verificata; in caso positivo si
eseguono le istruzioni del blocco 2 e in caso negativo si va a controllare la
condizione 3, e cosı̀ via: se nessuna delle n condizioni è verificata, si vanno
ad eseguire le istruzioni del blocco ELSE. In ogni caso viene eseguito uno e
un solo blocco di istruzioni, dopo di che l’esecuzione procede con l’istruzione
che segue ENDIF. Non esiste limite al numero di clausole ELSEIF che si
possono concatenare; inoltre il blocco ELSE è opzionale e può non esserci
nel caso che le n condizioni prevedano già tutti i possibili casi.
Il calcolo della funzione f (a) può quindi essere tradotto in FORTRAN
nel seguente modo:
IF(A.LT.0.) THEN
F=A∗∗2
ELSE IF(A.LE.1.)THEN
F=A∗(A–1.)
ELSE
F=A+1.E–3
END IF
Osserviamo che nell’istruzione ELSEIF controlliamo soltanto se a ≤ 1 e non
se 0 ≤ a ≤ 1 come scritto nella definizione della funzione; questo è dovuto
al fatto che la condizione dell’ELSEIF viene controllata soltanto se la prima
condizione (a < 0) non è verificata, ovvero se a ≥ 0.
Un costrutto di IF concatenati rappresenta dal punto di vista del compilatore un’unica istruzione IF e quindi è prevista una sola istruzione ENDIF
di chiusura. Diversa è la situazione se gli IF sono annidati, nel qual caso
ognuno di essi deve avere la sua ENDIF. Osserviamo come il calcolo di f (a)
cambia se non usiamo la clausola ELSE IF, ma ricorriamo a IF annidati:
IF(A.LT.0.) THEN
F=A∗∗2
ELSE
IF(A.LE.1.)THEN
Y=A∗(A–1.)
ELSE
Y=A+1.E–3
END IF
END IF
N. B. Il passaggio da IF concatenati a IF annidati è stato effettuato semplicemente andando a capo dopo il primo ELSE!!
33
8.3
Fortran 90: istruzione SELECT CASE.
Le istruzioni di scelta previste dal FORTRAN 77 sono tutte e sole quelle descritte finora. Vogliamo in questo paragrafo accennare a un’altra istruzione
di scelta introdotta dal Fortran 90, che può offrire talvolta una valida alternativa agli IF concatenati, quando la scelta dipende dal valore assunto da
un’espressione (intera, logica o carattere). Si tratta dell’istruzione SELECT,
la cui forma generale è la seguente:
SELECT CASE(espressione)
CASE(caso 1)

Istruzione 1


Istruzione 2
blocco 1

..

.
·········
CASE(caso n)THEN

Istruzione 1


Istruzione 2
blocco n

..

.
CASE DEFAULT 
Istruzione 1


Istruzione 2
blocco DEFAULT

..

.
END SELECT
In ogni clausola CASE si specifica un possibile insieme di valori che l’espressione può assumere: se l’espressione assume un valore che rientra nell’i-esimo
caso, vengono eseguite le istruzioni del blocco i; se invece il valore non rientra in nessuno dei casi previsti, vengono eseguite le istruzioni del blocco
DEFAULT. Ovviamente, i casi specificati devono essere mutuamente esclusivi come negli IF concatenati e, se essi esauriscono i possibili valori, il blocco
DEFAULT è vuoto e può essere omesso. Ci sono diversi possibili modi per
specificare un caso. Supponendo ad esempio che l’espressione sia intera,
diamo sotto alcuni descrittori di casi con il corrispondente significato:
CASE(1)
CASE(1,3,5)
CASE(:-4)
CASE(0:)
CASE(10:100)
8.4
valore
valore
valore
valore
valore
uguale a 1
uguale a 1, 3 o 5
minore o uguale di -4
maggiore o uguale di 0
compreso fra 10 e 100, estremi inclusi
Istruzione DO
arrays
Nel programma MEDIE del paragrafo 6.5 abbiamo usato l’istruzione DO
per realizzare un ciclo, ovvero una struttura ripetitiva nella quale una o più
34
istruzioni devono essere eseguite più volte. Ricordiamo che dal punto di
vista algoritmico i cicli possono essere di diversi tipi. I primi sono quelli in
cui il numero di ripetizioni è stabilito a priori e l’esecuzione è controllata
da un contatore delle ripetizioni. I secondi sono quelli in cui non si conosce
a priori il numero di ripetizioni e l’inizio e fine sono controllati tramite il
verificarsi o meno di una determinata condizione; questi sono a loro volta
suddivisi in due tipi: i cicli while in cui le ripetizioni continuano fintanto che
la condizione specificata è vera e i cicli repeat in cui le ripetizioni continuano fintanto che la condizione specificata è falsa. Come già abbiamo avuto
modo di osservare, il FORTRAN 77 non prevede un’istruzione che realizza
i cicli while; aggiungiamo ora che non è prevista neanche un’istruzione che
realizza i cicli repeat. In sostanza, è prevista soltanto l’istruzione DO, che
corrisponde ai cicli del primo tipo. I cicli while e repeat possono essere simulati usando istruzioni IF e GO TO, come abbiamo fatto nel programma
ERONE.
Un ciclo DO in FORTRAN 77 ha la forma seguente:
DO n v = e1 , e2 , e3
Istruzione 1
Istruzione 2
..
.
n
CONTINUE



rango del DO


dove:
– n è un’etichetta che fa riferimento all’istruzione n
istruzione finale del DO;
CONTINUE, detta
– v è una variabile scalare, detta variabile del DO;
– e1 , e2 e e3 sono espressioni il cui valore rappresenta il valore iniziale che
v deve assumere, il valore finale e l’incremento (o decremento), rispettivamente. Se e3 è uguale a 1, può essere omessa.
Il Fortran 90 ha introdotto una forma leggermente diversa dell’istruzione
DO, nella quale non si deve scegliere un’etichetta per l’istruzione finale;
questa forma è
DO v = e1 , e2 , e3
Istruzione 1
Istruzione 2
..
.
END DO



rango del DO


ed è ormai accettata da molti compilatori.
L’istruzione finale. La parola chiave CONTINUE nell’istruzione finale
non produce alcuna azione particolare, ma dice semplicemente al programma
35
di “continuare” con quello che sta facendo. Il FORTRAN 77 prevede la
possibilità di chiudere un DO con istruzioni diverse da una CONTINUE
(in ogni caso etichettate con l’etichetta n). D’altra parte, ormai è prassi
consolidata usare questa istruzione, tanto che l’uso di istruzioni finali diverse
da CONTINUE e END DO è stato dichiarato obsoleto dagli estensori dello
standard Fortran 95, il che lo pone fra le caratteristiche del linguaggio da
eliminare in future versioni.
La variabile del DO. Per quanto riguarda la variabile v, lo standard del
FORTRAN 77 dice che v, e di conseguenza anche e1 , e2 e e3 , possono essere
intere, reali o doppia precisione. In realtà, ogni buon programmatore usa
soltanto variabili intere perché la gestione di un DO con variabile reale o
doppia precisione può creare problemi a causa degli errori di arrotondamento. Questo è talmente vero che in Fortran 90 la variabile del DO può essere
soltanto intera, e la possibilità di usare variabili reali o doppia precisione è
stata dichiarata obsoleta. D’ora in poi daremo quindi per scontato che v,
e1 , e2 e e3 sono intere.
Il rango del DO. Il rango di un DO può contenere qualsiasi istruzione
FORTRAN, compresi altri cicli DO (si parla in questo caso di cicli DO
annidati). A questo proposito, il FORTRAN impone due regole, entrambe
ovvie: DO annidati devono dipendere da variabili diverse e i loro ranghi non
si devono intersecare. È ad esempio sbagliata una sequenza del tipo:
DO 50 J=1,N1
..
.
DO 20 K=1,K2
..
.
50
CONTINUE
..
.
20 CONTINUE
Allo stesso modo, il rango di un DO può contenere un costrutto IF (e,
viceversa, un IF può contenere un ciclo DO); non è però possibile che i due
costrutti si intersechino, come nelle situazioni tratteggiate qui sotto:
DO 20 L=L1,1,-1
..
.
IF(condizione)THEN
..
.
DO 20 K=1,K2
..
.
ENDIF
..
.
20
IF(condizione)THEN
..
.
20
CONTINUE
..
.
ENDIF
CONTINUE
36
I seguenti due esempi realizzano il calcolo del vettore y = Ax, dove x
è un vettore di n componenti e A è una matrice n × n; nella sequenza di
istruzioniPdi sinistra si usa una variabile ausiliaria s per accumulare la sommatoria nj=1 aij xj , mentre in quella di destra si accumula la sommatoria
direttamente in yi :
DO 100 I=1,N
S=0.
DO 50 J=1,N
S=S+A(I,J)∗X(J)
50
CONTINUE
Y(I)=S
100 CONTINUE
DO 100 I=1,N
Y(I)=0.
DO 50 J=1,N
Y(I)=Y(I)+A(I,J)∗X(J)
50
CONTINUE
100 CONTINUE
Nel secondo caso, i ranghi dei due DO finiscono nello stesso punto; in questa
situazione si può usare una sola istruzione finale senza che l’esecuzione cambi
rispetto alla versione con due diverse istruzioni finali:
DO 100 I=1,N
Y(I)=0.
DO 100 J=1,N
Y(I)=Y(I)+A(I,J)∗X(J)
100 CONTINUE
Consideriamo ora la seguente sequenza di istruzioni, nella quale si legge un
certo numero (≤ 100) di dati reali e per ognuno di essi si calcola e stampa
il logaritmo naturale del suo valore assoluto; le ripetizioni si interrompono
prima di aver letto 100 dati, se viene immesso un dato uguale a 0.
DO 45 I=1,100
PRINT∗,’Immetti il prossimo dato (0 per interrompere)
READ∗,DATO
IF(DATO.EQ.0.)GO TO 58
Y=LOG(ABS(DATO))
PRINT∗,Y
45 CONTINUE
58 CONTINUE
Tramite l’istruzione GOTO 58 si esce dal rango del DO, di fatto fermandone
l’esecuzione prima che le ripetizioni previste siano esaurite. Ciò è perfettamente lecito, mentre non è lecito il contrario, cioè entrare con un GOTO
dentro il rango di un DO.
Esecuzione di un ciclo DO. L’esecuzione di un ciclo DO prevede le
seguenti fasi:
– Vengono calcolati i valori di e1 , e2 e e3 ;
37
– Viene calcolato il numero r di ripetizioni previste dal DO, dato da
e2 − e1 + e3
r = max 0,
e3
e inizializzato a r il contatore delle ripetizioni;
– se r è zero, il ciclo DO viene saltato, senza alcuna segnalazione.
– Se r 6= 0, le istruzioni che compongono il rango del DO vengono ripetute
r volte attribuendo a v i valori e1 , e1 + e3 , .... Diamo alcuni esempi di
istruzioni DO con e1 , e2 e e3 costanti, evidenziando il valore di r e i valori
assunti da v:
DO 10 I=1,10
r = 10
v = 1, 2, . . . , 10
DO 10 I=1,10,2
r=5
v = 1, 3, 5, 7, 9
DO 10 K=3,0
r=0
DO 10 K=10,1,-1
r = 10
v = 10, 9, . . . , 1
DO 10 J=16,0,-2
r=9
v = 16, 14, . . . , 0
Ad ogni ripetizione, ogni volta che viene incrementato il valore di v, viene
anche decrementato di un’unità il contatore delle ripetizioni. Il controllo del
contatore è realizzato in modo tale che, in uscita dal DO, la variabile v non
ha l’ultimo valore consentito, bensı̀ quello successivo. Per esempio, dopo
l’esecuzione del seguente segmento di programma
N=0
DO 110 I=1,10
J=I
DO 100 K=2,5,2
L=K
N=N+1
100
CONTINUE
110 CONTINUE
i valori delle variabili coinvolte sono: I=11, J=10, K=6, L=4, N=20.
8.5
DO impliciti
Quando nei programmi dei paragrafi precedenti abbiamo avuto bisogno di
leggere o scrivere gli elementi di un vettore, abbiamo usato scritture del tipo
(X(I), I=1,N)
all’interno delle istruzioni READ∗ e PRINT∗, intendendo con ciò: “leggi
o scrivi i valori di X(I) per I che va da 1 a N”. La suddetta scrittura è
chiamata DO implicito. Come i cicli DO, cosı̀ anche i DO impliciti possono
essere annidati, come si fa abitualmente quando si devono leggere o stampare
gli elementi di una matrice; in questi casi si può usare infatti la scrittura
((A(I,J), I=1,N), J=1,N)
38
che consiste in due DO impliciti annidati. Il DO implicito più esterno fa
riferimento alla variabile J, che rappresenta l’indice di colonna dell’elemento
A(I,J), mentre il DO implicito più interno fa riferimento all’indice di riga I.
Se idealmente “srotoliamo” i DO, vediamo che gli elementi della matrice A
vengono presi in considerazione nell’ordine seguente:
Si pone J=1 e si varia I da 1 a N: A(1,1)
Si pone J=2 e si varia I da 1 a N: A(1,2)
..
.
A(2,1)
A(2,2)
...
...
A(N,1)
A(N,2)
Si pone J=N e si varia I da 1 a N: A(1,N)
A(2,N)
...
A(N,N)
In breve, la matrice viene letta o scritta per colonne: prima tutta la colonna
1, poi tutta la colonna 2, e via dicendo fino alla colonna N. Se si vuole
leggere o scrivere la matrice per righe, si deve usare una scrittura diversa,
ad esempio
((A(I,J), J=1,N), I=1,N)
8.6
oppure
((A(J,I), I=1,N), J=1,N)
Fortran 90: DO illimitato e DO WHILE
Il Fortran 90 ha introdotto due forme alternative dell’istruzione DO per
realizzare cicli repeat e cicli while. Per i primi, si può usare il cosiddetto DO
illimitato combinato con l’istruzione EXIT, secondo la sintassi seguente:
DO
..
.
IF(condizione) EXIT
..
.
END DO




rango del DO illimitato



L’istruzione EXIT trasferisce il flusso all’istruzione che segue END DO. Le
ripetizioni continuano fintanto che la condizione espressa nell’IF è falsa,
e si interrompe appena questa diventa vera. Le istruzioni del rango che
precedono l’IF vengono sempre eseguite almeno una volta.
I cicli WHILE vengono realizzati tramite il cosiddetto DO WHILE, che
ha la struttura seguente:
DO WHILE(condizione)

Istruzione 1


Istruzione 2
rango del DO WHILE

..

.
END DO
In questo caso, le ripetizioni continuano finché la condizione espressa nell’istruzione DO WHILE è vera, e si interrompono appena diventa falsa. Se
la condizione è falsa prima dell’inizio del ciclo, le istruzioni del rango non
vengono eseguite neppure una volta.
39
9
riassunto
9.1
Esempi riassuntivi
Esempio 1
es1
Il primo programma, chiamato NORMA, calcola la norma euclidea di un
vettore x ∈ Rn , con n ≤ 20, mediante un algoritmo che permette di ridurre
l’impatto degli errori di arrotondamento.
PROGRAM NORMA
PARAMETER(NM=20)
DIMENSION X(NM)
PRINT∗, ’dare N minore o uguale di’, NM
READ∗, N
PRINT∗, ’dare X’
READ∗, (X(I), I=1,N)
C Calcolo XMAX= massimo valore assoluto degli elementi di X
XMAX=0
DO 15 I=1,N
IF(ABS(X(I)).GT.XMAX) XMAX=ABS(X(I))
15 CONTINUE
IF(XMAX.EQ.0.)THEN
C Se XMAX è uguale a zero, pongo RNORM=0
RNORM=0.
ELSE
C Altrimenti calcolo RNORM come somma dei quadrati di X(I)/XMAX
RNORM=0.
DO 20 I=1,N
RNORM=RNORM+ (X(I)/XMAX)∗∗2
20
CONTINUE
C ... e poi l’aggiusto
RNORM=XMAX∗SQRT(RNORM)
END IF
PRINT∗,’norma euclidea di X: ’,RNORM
END
Il primo ciclo nel programma (DO 15 I=1,N) serve a calcolare la quantità
r = maxi |xi |, memorizzandola in XMAX. Il DO è seguito da un IF–THEN–
ELSE–ENDIF nel quale si controlla se XMAX è zero o diverso da zero. Nel
primo caso, il vettore X è evidentemente il vettore nullo e quindi il risultato
è RNORM=0; nel secondo caso si procede al calcolo della norma: con il ciclo
DO 20 I=1,N si calcola il quadrato della norma del vettore con componenti
xi /r, memorizzandolo in RNORM. Il valore di RNORM viene poi aggiustato
per dare la norma di x desiderata.
40
9.2
Esempio 2
Il seguente programma ordina in senso crescente gli elementi di un vettore
x di Rn , con n ≤ 100.
PROGRAM ORDINA
∗ Programma che ordina in senso crescente N (<= 100) numeri reali
PARAMETER(NM=100)
DIMENSION X(NM)
10
PRINT∗, ’dare N minore o uguale di’,NM
READ∗, N
IF(N.GT.NM)THEN
PRINT∗,’N troppo grande. Leggi bene!’
GOTO 10
END IF
PRINT∗, ’dare X’
READ∗, (X(I), I=1,N)
∗ Per ogni I da 1 a N-1 si sistema l’i-esimo elemento nell’ordinamento
DO 50 I=1,N-1
∗ Per I compreso fra 1 e N-1, si cerca l’indice MIN del più
∗ piccolo fra X(I), ..., X(N)
MIN=I
DO 45 J=I+1,N
IF(X(J).LT.X(MIN)) MIN=J
45
CONTINUE
∗ Se MIN >I bisogna scambiare X(MIN) con X(I)
IF(MIN.GT.I))THEN
S=X(MIN)
X(MIN)=X(I)
X(MIN)=S
END IF
50
CONTINUE
∗ Stampa del vettore ordinato
PRINT∗,’vettore ordinato’,(X(I), I=1,N)
END
In questo programma abbiamo inserito un controllo sul valore di N in ingresso, dando la possibilità di tornare a leggere un nuovo valore nel caso che
il dato immesso sia troppo grande14 . Dopo le istruzioni di lettura dei dati,
il resto del programma si configura come un ciclo (DO 50 I=1,N-1) finaliz14
Siamo consapevoli di un pericolo insito nel programma: se una persona continuasse
indefinitamente a introdurre valori di N troppo grandi, l’esecuzione andrebbe avanti per
un tempo infinito. Per evitare questo richio, avremmo dovuto introdurre un numero
massimo di tentativi possibili, oltre il quale far bloccare il programma. Non l’abbiamo fatto
perché confidiamo nella non totale stupidità di qualsiasi persona. Lasciamo comunque agli
studenti come (utile) esercizio il perfezionamento del programma.
41
zato ad aggiustare l’I-esimo elemento nell’ordinamento richiesto; terminata
l’esecuzione del ciclo, l’ultimo elemento, l’N-esimo, è automaticamente già
nell’ordine richiesto rispetto ai precedenti. Il rango del DO principale comprende un altro ciclo DO e un IF. Il ciclo DO 45 J=I+1,N serve a individuare
l’indice MIN del più piccolo fra X(I), . . ., X(N): se MIN risulta uguale a I,
non occorre fare niente perché X(I) rispetta già l’ordinamento desiderato;
altrimenti occorre scambiare fra loro X(MIN) e X(I). Lo scambio avviene
servendosi di una variabile d’appoggio S.
9.3
Esempio 3
Il programma MATRICE calcola una matrice H quadrata di ordine n, con
n ≤ 20, i cui elementi sono dati da
hij = cos((j − 1)θ), con θ =
(2i − 1)π
.
2n
Una volta calcolata H, si calcola e si stampa la sua norma–1, definita da
kHk1 = maxj=1,...,n
n
X
i=1
|hij |.
Il programma usa la doppia precisione: infatti, tutte le variabili reali
sono esplicitamente dichiarate in doppia precisione e il nome simbolico PI
identifica la costante in doppia precisione 3.14159265358979D0. Se si volesse
trasformare il programma in precisione semplice, sarebbe sufficiente togliere
la dichiarazione DOUBLE PRECISION e modificare il valore della costante
nell’istruzione PARAMETER, sostituendo la lettera D con la E e riducendo
il numero di cifre dal momento che in precisione semplice non ha senso dare
costanti con più di 7 cifre.
Il corpo del programma è nettamente suddiviso in due parti: nella prima
si calcola la matrice H e nella seconda la norma–1 di H. Il calcolo di H avviene
tramite due DO annidati: il DO 10 I=1,N e il DO 5 J=1,N. Successivamente,
si calcola la norma–1 attraverso altri due DO annidati, nei quali si calcolano
le sommatorie previste dalla formula e, contemporaneamente, si porta avanti
il calcolo della massima sommatoria. Più precisamente, attraverso il DO più
esterno si fa variare J da 1 a N: per ogni J si calcola la sommatoria TMP
dei valori assoluti degli elementi della J–esima colonna di H; poi, si aggiorna
HNORM (inizializzato fuori dal DO con il valore zero) con il massimo fra il
precedente valore e TMP. In questo modo, quando l’esecuzione del DO 30
J=1,N è finita, il valore di HNORM è proprio la norma desiderata.
42
PROGRAM MATRICE
PARAMETER(PI=3.14159265358979D0)
DOUBLE PRECISION H, HNORM, THETA, TMP
DIMENSION H(20,20)
PRINT∗, ’dare N minore o uguale di 20’
READ∗, N
C Calcolo di H
DO 10 I=1,N
THETA=(2∗I-1)∗PI/(2∗N)
DO 5 J=1,N
H(I,J)=COS((J-1)∗THETA)
5
CONTINUE
10 CONTINUE
C Calcolo di norma–1 di H
HNORM=0.
DO 30 J=1,N
TMP=0.
DO 20 I=1,N
TMP=TMP+ABS(H(I,J))
20
CONTINUE
HNORM=MAX(TMP,HNORM)
30 CONTINUE
C Stampa della norma
PRINT∗,’norma–1 di H=’,RNORM
END
10
write
L’istruzione WRITE(u,f)
In molte situazioni si ha bisogno di far scrivere i risultati di un programma
su un file di testo, che può essere conservato in memoria ed eventualmente
stampato in un secondo momento; si può anche voler decidere il formato della stampa, nel senso di specificare quante cifre di mantissa scrivere
per i numeri reali, quando andare a capo, se e come incolonnare i risultati,
quanti spazi lasciare fra un numero e l’altro, e via dicendo. L’istruzione
PRINT∗ usata finora non permette di fare nessuna delle due cose: i risultati vengono stampati sul video e con un formato prestabilito. Analoghi
problemi possono nascere con l’istruzione di lettura READ∗, che prevede la
lettura esclusivamente da tastiera, impedendo ad esempio la lettura dei dati
da files precedentemente creati (cosa che spesso serve con programmi che
maneggiano grosse moli di dati).
Il FORTRAN prevede istruzioni alternative alla PRINT∗ e READ∗,
meno semplici da usare di queste, ma più flessibili. Il capitolo delle istruzioni
relative alle operazioni di ingresso/uscita è molto vasto, ma anche molto
43
AGM
tecnico (cfr [1]). Noi parleremo esclusivamente di istruzioni di uscita, limitandoci a presentare gli strumenti più comunemente utilizzati per scrivere i
risultati su file specificando il formato.
10.1
Scrivere su un file.
Per prima cosa si devono scegliere il nome del file su cui si vogliono scrivere i
risultati e un numero intero da associare ad esso. Sia per esempio ris.txt il
nome e 7 il numero. Una volta deciso, si deve inserire nella sezione esecutiva
del programma, in qualunque punto purché prima delle istruzioni di uscita
che coinvolgono il file, l’istruzione
OPEN (UNIT=7, FILE= ’ris.txt’)
che serve a due scopi:
a) associare il numero 7 al file ris.txt, in modo da poter usare il numero 7
al posto del nome ris.txt nelle successive istruzioni di uscita;
b) predisporre per la scrittura il file ris.txt. Se nella cartella di lavoro
non esiste un file con questo nome, ne viene automaticamente creato uno,
ovviamente vuoto per il momento; se invece esiste già un file ris.txt, la
macchina si predispone a sovrascrivere sul contenuto precedente15 . Al termine dell’esecuzione del programma, il file ris.txt sarà disponibile, con tutte
le informazioni che il programma stesso ha provveduto a scrivervi.
Se si vuole rendere il programma indipendente dal nome del file, si deve
prevedere che questo nome venga letto fra i dati in ingresso; a tale scopo si
può seguire il seguente schema:
PROGRAM PIPPO
CHARACTER∗10 FRIS
..
.
PRINT∗,’dare il nome del file per i risultati’
READ∗, FRIS
..
.
OPEN ( UNIT= 7, FILE= FRIS)
..
.
END
dove FRIS è una variabile di tipo carattere i cui valori sono stringhe di al
più 10 caratteri (ovviamente il 10 può essere sostituito da un altro numero).
Il valore a FRIS viene assegnato tramite l’istruzione READ∗, FRIS; al momento dell’esecuzione di questa istruzione, si dovrà scrivere da tastiera il
nome prescelto, ad esempio il nostro solito ris.txt, fra apici.
15
È possibile anche
AGMfare in modo che il contenuto nuovo venga accodato a quello vecchio;
per i dettagli cfr. [1].
44
10.2
Scegliere il formato di scrittura.
Una volta che l’istruzione OPEN è stata eseguita, il file è predisposto per
la scrittura. L’istruzione più generale per questa operazione è l’istruzione
WRITE, tramite la quale si può specificare non solo il nome del file ma
anche il formato delle stampe. La forma dell’istruzione è la seguente:
WRITE(u,f ) lista
dove:
– u è il numero associato al file nell’istruzione OPEN, oppure un asterisco;
nel secondo caso la scrittura avviene sul video.
– f è l’etichetta di un’istruzione FORMAT associata alla WRITE nella
quale si descrive il formato con cui si vogliono scrivere i dati contenuti nella
lista, oppure un asterisco; nel secondo caso il formato è quello deciso dalla
macchina.
– lista è l’elenco delle variabili (o, più in generale, delle espressioni) di cui si
vuole scrivere il valore.
Esempi di istruzioni WRITE sono i seguenti:
WRITE(7,100) A,B,N+M
WRITE(2,99) (X(I), I=1,N)
WRITE(7,1500) (J, X(J), J=2,N-1)
WRITE(7,∗)ABS(C),D
WRITE(∗,100)X1 (equivalente a PRINT 100, X1)
WRITE(∗,∗)BETA (equivalente a PRINT∗, BETA)
L’istruzione FORMAT ha la forma
f
FORMAT (descrizione del formato)
dove la descrizione del formato fra parentesi descrive il modo in cui si
vogliono comporre le righe di stampa. Questa descrizione si presenta come
un elenco di singoli descrittori separati da virgole, ognuno dei quali descrive
una “componente” della riga di stampa; i descrittori più utilizzati nelle
applicazioni numeriche sono:
nX
per lasciare n spazi
/
per andare a capo
’xxxxx’
per inserire la stringa di caratteri xxxxx
In
per scrivere un numero intero composto da un massimo di n
caratteri, comprensivi delle cifre che compongono il numero e
dell’eventuale segno.
45
Ew.d
per scrivere un numero reale come costante FORTRAN in
notazione scientifica con d cifre di mantissa. w rappresenta
il numero complessivo di caratteri occupati dal numero: oltre
alla mantissa vanno infatti considerati l’eventuale segno, il
punto decimale, la lettera E e l’esponente con eventuale segno.
Abitualmente questo codice viene usato nella forma E13.6 o
E14.7 per numeri reali in precisione semplice, per i quali è
logico visualizzare 6 o 7 cifre di mantissa.
Dw.d
analogo al precedente, viene si solito usato per dati in doppia
precisione (quindi D22.15 o D23.16); nella stampa compare la
lettera D, come nelle costanti FORTRAN doppia precisione.
Quelle che seguono sono alcune osservazioni sulle descrizioni di formato:
– Quando in una descrizione di formato è presente una /, questa funziona
anche da separatore fra descrittori e non occorre usare le virgole; ad esempio,
le due descrizioni di formato
I5,/,E13.6
e
I5/E13.6
sono equivalenti.
– Le stringhe di caratteri che si vogliono usare per “abbellire” le stampe
non vengono di solito inserite nella lista che accompagna la WRITE (come
abbiamo fatto finora con l’istruzione PRINT∗), bensı̀ nella descrizione di
formato, tramite il codice ’xxxxx’. Cosı̀, ad esempio, la coppia di istruzioni
WRITE(∗,100)
100 FORMAT(’Buongiorno!’)
è equivalente all’istruzione
PRINT∗,’Buongiorno!’
– Riguardo al codice In per i numeri interi, osserviamo che un numero composto da meno di n caratteri viene scritto allineato a destra nel campo di
n caratteri; se invece il numero richiede più di n caratteri, l’operazione di
scrittura non può essere eseguita e al posto del numero vengono scritti n
asterischi (o qualcosa di analogo, a seconda dei compilatori).
– Per quanto concerne i numeri reali, oltre ai codici Ew.d e Dw.d, si può
usare il codice Fw.d, con il quale il numero viene scritto come costante
FORTRAN senza esponente con d cifre dopo il punto decimale su un totale
di w caratteri. Se la parte intera del numero, compreso l’eventuale segno,
occupa più di w − d caratteri, il campo viene riempito di asterischi.
A titolo di esempio del funzionamento di un’istruzione WRITE con formato,
analizziamo l’effetto della coppia di istruzioni seguente:
46
WRITE(2,1000)ERR, N
1000 FORMAT(3X, ’errore=’, E13.6//3x,’n=’,I4)
3X
’errore=’
E13.6
/
/
3X
’n=’
I4
vengono lasciati 3 spazi all’inizio della riga
viene scritto il messaggio “errore=”
viene scritto il valore di ERR in forma esponenziale con 6
cifre di mantissa
si va a capo
si va nuovamente a capo
vengono lasciati 3 spazi all’inizio della nuova riga
viene scritto il messaggio “n=”
viene scritto il valore di N su un campo di 4 caratteri
Prima di dare altri esempi di istruzioni WRITE con formato, notiamo che
più istruzioni WRITE in un’unità di programma possono far riferimento alla
stessa istruzione FORMAT. Inoltre, una FORMAT può stare dovunque nella
sezione esecutiva di un’unità di programma; molti programmatori usano
raggruppare tutte le FORMAT immediatamente prima della END.
10.3
Alcuni esempi
– La coppia di istruzioni
WRITE(7,222)X,Y,Z
222 FORMAT(20X,’RISULTATI’/2X,D22.15,2X,D22.15,2X,D22.15)
permette di scrivere su una riga i valori di tre variabili in doppia precisione
X, Y, Z con formato D22.15, ognuno preceduto da 2 spazi. Questa riga è
preceduta da un’altra su cui compare la scritta RISULTATI preceduta da
20 spazi. La descrizione della seconda riga, in cui si ripete per 3 volte la
sequenza 2X,D22.15, può essere scritta in modo più compatto 3(2X,D22.15)
usando il contatore di ripetizione 3; pertanto l’istruzione FORMAT diventa:
222 FORMAT(20X,’RISULTATI’/3(2X,D22.15))
– Per stampare i primi N elementi di un vettore reale B incolonnati uno
sotto l’altro, possiamo usare la coppia di istruzioni
WRITE(7,111) (B(I), I=1,N)
111 FORMAT(3X,’Vettore B’/(2X,E14.7))
La parentesi che contiene la descrizione 2X,E14.7 serve a impedire che il
messaggio di intestazione “Vettore B” venga ripetuto prima di ogni riga
contenente il valore di un elemento di B. Infatti la macchina, quando esegue
un’operazione di scrittura con formato, scandisce di pari passo la lista e
la descrizione di formato. Se arriva alla fine della descrizione (ovvero alla
parentesi chiusa finale) e c’è ancora qualcosa da stampare, la macchina ri-
47
parte a scandire la descrizione dal cosiddetto punto di riscansione, costituito
dall’ultima parentesi aperta.
– Con la seguente coppia di istruzioni
WRITE(7,1000)(I, V(I), I=1,N)
1000 FORMAT(’V(’ , I3 ,’)=’ ,E14.7)
vengono generate N righe ognuna delle quali ha la forma “V(xxx)=yyyyyy”,
dove xxx è il valore di I e yyyyyy è il valore di V(I) scritto in forma
esponenziale con 7 cifre di mantissa.
– La sequenza di istruzioni
WRITE(7,121)
DO 300 I=1,N
WRITE(7,111) (A(I,J), J=1,M)
300 CONTINUE
111 FORMAT(3(2X,E13.6))
121 FORMAT(4X,’Matrice A per colonne’)
prevede N+1 operazioni di scrittura. Con la prima si crea una riga con l’intestazione “Matrice A per colonne”. Con ognuna delle successive si scrivono
gli elementi di una riga della matrice reale A di dimensione N×M; questi
elementi vengono scritti a tre a tre su una o più righe, a seconda di quanto
vale M.
11
sottoprog
Sottoprogrammi
compila
Come già accennato nel paragrafo 5.1, i programmi FORTRAN sono spesso
strutturati come un insieme di più unità di programma, una (e una sola)
delle quali è il programma principale e gli altri sono sottoprogrammi. Ogni
unità di programma viene compilata indipendentemente dalle altre, dando
luogo a un modulo oggetto; il linker crea il programma eseguibile collegando
fra loro i diversi moduli oggetto. Grazie a questa gestione autonoma dei
sottoprogrammi da parte del compilatore, è possibile creare e memorizzare
delle librerie di moduli oggetto corrispondenti a sottoprogrammi finalizzati
alla risoluzione di particolari classi di problemi: una libreria per problemi di
algebra lineare, una per risolvere equazioni differenziali, una per problemi
di ottimizzazione, per problemi statistici, e via dicendo. L’insieme delle funzioni intrinseche del FORTRAN costituisce una libreria che è già compresa
nel software che accompagna il compilatore. Esattamente come si fa con
queste funzioni, i sottoprogrammi di una qualsiasi libreria esterna possono
essere usati da qualunque programma FORTRAN, demandando al linker il
compito di attivare i dovuti collegamenti per creare il programma eseguibile.
48
Mentre il programma principale può non avere un nome, un sottoprogramma deve averlo perché è concepito per essere chiamato, cioè utilizzato,
dal programma principale o da altri sottoprogrammi. L’unità di programma
che utilizza un sottoprogramma è detta unità chiamante. L’organizzazione
di un programma strutturato in più unità di programma può essere anche
molto complessa perché non c’è limite al numero di sottoprogrammi che
un’unità di programma più chiamare o ai livelli di chiamate: il programma
principale chiama un certo numero di sottoprogrammi, ognuno dei quali ne
richiama altri, ognuno dei quali ne richiama altri, ognuno dei quali ne richiama altri, .... L’unico vincolo è che un sottoprogramma non può richiamare se
stesso, né direttamente né indirettamente attraverso una catena di chiamate
che alla fine torna a lui16 .
L’esecuzione di un programma inizia sempre dal programma principale
e procede sequenzialmente, finché non viene incontrato un riferimento a
un sottoprogramma. A questo punto, il sottoprogramma viene attivato e
le istruzioni della sua sezione esecutiva vengono eseguite sequenzialmente
finché non viene incontrata un’istruzione che rimanda al programma principale, la cui esecuzione riprende in sequenza. Il ritorno al programma principale può essere provocato dalla END oppure da un’istruzione RETURN,
che provoca appunto l’interruzione del sottoprogramma e il ritorno all’unità
chiamante.
In FORTRAN esistono due categorie di sottoprogrammi, che si differenziano per scopo e modalità di utilizzo. La prima categoria è costituita dalle
FUNCTION, che sono una generalizzazione delle funzioni intrinseche: possono dipendere da un numero qualsiasi di argomenti scalari o meno, ma il
risultato è uno solo e scalare. La seconda categoria è rappresentata dalle
SUBROUTINE, che realizzano algoritmi più generali, con un numero qualsiasi di dati e risultati, scalari o dimensionati. La forma dell’intestazione di
un sottoprogramma ne stabilisce l’appartenenza a una categoria o all’altra.
11.1
Sottoprogrammi FUNCTION
function
L’intestazione di una FUNCTION ha la forma
FUNCTION nome (m1 , m2 , . . . , mn )
dove nome e m1 , m2 , . . . , mn sono nomi simbolici: nome identifica contemporaneamente il sottoprogramma e il risultato; m1 , m2 , . . . , mn , detti argomenti muti o formali della FUNCTION, sono i nomi simbolici usati all’interno
della FUNCTION per identificare i dati su cui la funzione deve operare.
Mentre nome identifica sempre una grandezza scalare, il cui tipo è detto
16
Questo si configurerebbe complessivamente come un algoritmo ricorsivo. A differenza
di altri linguaggi di programmazione, il FORTRAN non supporta questo tipo di algoritmi.
49
tipo della FUNCTION, gli argomenti muti possono identificare grandezze
scalari o arrays. Nel secondo caso, la sezione dichiarativa della FUNCTION
deve contenere istruzioni dichiarative atte ad informare il compilatore della
natura vettoriale o matriciale di queste grandezze. Allo stesso modo, eventuali istruzioni di specificazione di tipo relative a nome o agli argomenti muti
devono necessariamente essere presenti nella FUNCTION. Ad esempio, nella
FUNCTION seguente
FUNCTION NF(N)
C Calcolo di n!
REAL NF
NF=1
DO 100 K=2,N
NF=NF∗K
100 CONTINUE
RETURN
END
che calcola n! per un dato valore di n, il risultato NF è uno scalare reale
perché il nome NF è dichiarato di tipo REAL. L’unico argomento muto N
è invece di tipo intero. Precisiamo che un’eventuale dichiarazione del tipo
della FUNCTION può essere inserita direttamente nell’intestazione prima
della parola chiave FUNCTION. Pertanto, le due istruzioni
FUNCTION NF(N)
REAL NF
nella FUNCTION NF possono essere sostituire dall’unica istruzione
REAL FUNCTION NF(N)
Tipicamente, nella sezione esecutiva di una FUNCTION non compaiono
istruzioni che possano modificare il valore degli argomenti muti; al contrario,
devono essere presenti una o più istruzioni che attribuiscano un valore al
risultato nome. Consideriamo ad esempio la FUNCTION NF. In essa sono
presenti due istruzioni di assegnazione che agiscono sul valore di NF: con la
prima il valore di NF viene inizializzato a 1; successivamente, il valore viene
cambiato tramite l’istruzione NF=NF∗K all’interno del DO, in modo tale
che al termine dell’esecuzione del sottoprogramma NF contiene il risultato
desiderato.
Se nella sezione esecutiva di una FUNCTION non compaiono istruzioni
che attribuiscano un valore a nome, il compilatore dovrebbe segnalare un
errore di sintassi e di conseguenza non creare il modulo oggetto. Purtroppo,
non tutti i compilatori si comportano in questo modo. Per fare un esempio,
supponiamo di cambiare il nome della FUNCTION di prima da NF a RNF
lasciando invariato tutto il resto e ottenendo quindi il sottoprogramma
50
FUNCTION RNF(N)
C Calcolo di n!
REAL NF
NF=1
DO 100 K=2,N
NF=NF∗K
100 CONTINUE
RETURN
END
che è chiaramente sbagliato perché il risultato RNF resta indefinito. Se sottoponiamo la FUNCTION ai nostri due compilatori di riferimento, gfortran
e Open WATCOM, non otteniamo messaggi di errore, bensı̀ di Warning.
Il compilatore gfortran, usato con opzione –Wall, dà un messaggio del
tipo: “Warning: Return value of function ... not set”; il compilatore Open
WATCOM dà un Warning più generico, avvertendo che la variabile RNF è
unreferenced, ovvero che il sottoprogramma non contiene alcun riferimento
ad essa. Trattandosi di Warnings e non errori, i compilatori creano il modulo
oggetto; nonostante questo, il programmatore attento che legge gli avvertimenti, può fare mente locale al problema e correggere l’errore, ad esempio
aggiungendo prima della RETURN l’istruzione di assegnazione RNF=NF.
Ma...attenzione! Se invece di RNF=NF scriviamo NF=RNF, sbagliando
il “verso” dell’assegnazione, nessuno dei due compilatori segnala più una
situazione anomala: peccato che il valore di RNF resti comunque indefinito!
Le FUNCTION si utilizzano allo stesso modo delle funzioni intrinseche,
facendo riferimento ad esse all’interno di espressioni. Un riferimento ha la
forma
nome(a1 , a2 , . . . , an )
dove a1 , a2 , . . . , an sono i cosiddetti argomenti attuali, costituiti da nomi simbolici (o, più in generale, espressioni) usati nell’unità chiamante per identificare i dati su cui effettivamente il sottoprogramma deve operare 17 . Gli
argomenti attuali sono in corrispondenza uno–a–uno con quelli muti: a1 corrisponde a m1 , a2 corrisponde a m2 , e cosı̀ via; ogni argomento attuale deve
essere dello stesso tipo del corrispondente argomento muto
e della stessa
assoc
natura (scalare, vettoriale,...). Vedremo nel paragrafo 11.3 il meccanismo
con cui gli argomenti attuali vengono resi disponibili per l’esecuzione del
sottoprogramma. Consideriamo a titolo di esempio le seguenti istruzioni:
17
L’aggettivo “attuali” è una brutta traduzione dell’inglese “actual”, ormai entrata
nell’uso corrente al posto di traduzioni migliori, come ad esempio “effettivi”.
51
PRINT∗, NF(K)
X=NF(N)
IF(NF(J+1).GT.A)THEN...
R=NF(N)/(NF(K)∗NF(N-K))
Tutti i riferimenti alla FUNCTION NF sono sintatticamente corretti; in
alcuni casi l’argomento attuale è un nome di variabile e in altri casi è un’espressione, ma in ogni caso (stando alla regola di default sui tipi intero
e reale) è di tipo intero, come l’argomento muto N. In fase di esecuzione,
questi riferimenti daranno il giusto risultato se e solo se nell’unità chiamante
il nome NF è dichiarato REAL come nella FUNCTION (ricordiamo che
l’unità chiamante e il sottoprogramma vengono compilati separatamente e
autonomamente, e quindi le istruzioni di specificazione presenti in uno non
servono per l’altro).
Il seguente è un programma principale che usa la FUNCTION NF per
definire gli elementi di una matrice simmetrica A secondo la formula

i!
 j!(i−j)! i > j
aij =
1
i=j

aji
i<j
C
C
Programma che definisce una matrice A simmetrica usando i
coefficienti binomiali
REAL NF
DIMENSION A(25,25)
PRINT∗, ’dare N minore o uguale di 25’
READ∗, N
DO 50 I=1,N
A(I,I)=1.
DO 50 J=1,I-1
A(I,J)=NF(I)/(NF(J)∗NF(I-J))
A(J,I)=A(I,J)
50 CONTINUE
PRINT∗,’Matrice A per righe’
DO 300 I=1,N
WRITE(∗,111) (A(I,J), J=1,N)
300 CONTINUE
111 FORMAT(3(2X,E13.6))
END
Consideriamo ora la seguente FUNCTION per il calcolo della norma
euclidea di un vettore.
52
FUNCTION ENORM(X,N)
DIMENSION X(N)
XMAX=0
DO 15 I=1,N
IF(ABS(X(I)).GT.XMAX) XMAX=ABS(X(I))
15 CONTINUE
IF(XMAX.EQ.0.)THEN
ENORM=0.
ELSE
XMAX=XMAX∗∗2
ENORM=0.
DO 20 I=1,N
ENORM=ENORM+ X(I)∗∗2/XMAX
20
CONTINUE
ENORM=XMAX∗SQRT(ENORM)
END IF
RETURN
END
La sezione dichiarativa contiene l’istruzione
DIMENSION X(N)
che specifica la natura vettoriale dell’argomento muto X, in modo che il compilatore possa tradurre in modo adeguato le istruzioni eseguibili18 . In assenza di questa dichiarazione, il compilatore interpreterebbe il simbolo X(I)
come il riferimento a una FUNCTION di nome X e in tal senso produrrebbe
il modulo oggetto. I problemi verrebbero fuori nella fase di collegamento nel
contesto di un qualsiasi programma: il linker cercherebbe infatti un modulo
oggetto di nome X, di fatto inesistente.
es1
Usando questa FUNCTION, il programma NORMA del paragrafo 9.1
può essere scomposto in due unità di programma: la FUNCTION stessa e
il programma principale a cui restano i compiti di leggere i dati, usare la
FUNCTION per fare i calcoli e stampare il risultato:
18
A differenza di quanto visto negli esempi di programmi principali dei paragrafi precedenti, qui non indichiamo la dimensione
dimvar di X tramite una costante, ma tramite una variabile; più avanti, nel paragrafo 11.5, spiegheremo perché questo è possibile e come funziona
in pratica.
53
PARAMETER(NM=20)
DIMENSION X(NM)
PRINT∗, ’dare N minore o uguale di’,NM
READ∗, N
IF (N.GT.NM)THEN
PRINT∗,’N troppo grande’
ELSE
PRINT∗, ’dare X’
READ∗, (X(I), I=1,N)
RNORMA=ENORM(X,N)
PRINT∗,’norma euclidea di X: ’,RNORMA
END IF
END
Può capitare, anche se raramente, che l’algoritmo realizzato da una
FUNCTION non preveda dati in ingresso. In questo caso l’intestazione
della FUNCTION non contiene una lista di argomenti muti, ma soltanto
una coppia di parentesi tonde (aperta e chiusa) vuota. A titolo di esempio, riportiamo una FUNCTION per il calcolo della precisione di macchina,
tipico algoritmo senza dati in ingresso:
10
FUNCTION EPSM( )
X=1.
X=0.5∗X
Y=1.+X
IF(Y.GT.1.) GO TO 10
EPSM=2. ∗X
END
e i seguenti sono esempi di utilizzo:
PRINT ∗, EPSM( )
IF(ABS(X).LT.EPSM( )) X=0.
IF(RCOND.LT.EPSM( ))PRINT∗,’Matrice singolare’
11.2
Sottoprogrammi SUBROUTINE
subroutine
L’intestazione di una SUBROUTINE ha la forma
SUBROUTINE nome(m1 , m2 , . . . , mn )
dove nome è il nome del sottoprogramma e m1 , m2 , . . . , mn sono gli argomenti muti. In questo caso nome non riveste un ruolo particolare come
nelle FUNCTION: esso serve solo per poter chiamare la SUBROUTINE da
un’altra unità di programma e non può essere usato come nome di variabile
all’interno della SUBROUTINE stessa o di un’unità che la chiami. Gli ar-
54
gomenti muti sono i nomi simbolici usati all’interno della SUBROUTINE
per identificare i dati su cui essa deve operare e i risultati che essa produce.
Tutti gli argomenti muti possono essere scalari o arrays.
La chiamata di una SUBROUTINE richiede un’istruzione speciale che
ha la forma:
CALL nome(a1 , a2 , . . . , an )
dove a1 , a2 , . . . , an sono gli argomenti attuali.
Supponiamo di voler trasformare la FUNCTION ENORM in un sottoprogramma di tipo SUBROUTINE. Se scegliamo per la SUBROUTINE il nome
NORMA, l’intestazione diventa:
SUBROUTINE NORMA(ENORM,X,N)
dove il risultato ENORM è stato inserito nella lista degli argomenti muti; il
resto del sottoprogramma non cambia. Per quanto riguarda il programma
principale, tutto resta come prima, salvo che l’istruzione di assegnazione
RNORMA=ENORM(X,N)
viene sostituita da:
CALL NORMA(RNORMA,X,N)
dove l’argomento attuale corrispondente a ENORM è RNORMA, nome
usato nel programma principale per il risultato.
Talvolta un argomento di una SUBROUTINE rappresenta contemporaneamente un dato e un risultato: quando la SUBROUTINE viene attivata,
il valore dell’argomento costituisce un dato in ingresso; poi la SUBROUTINE
ne cambia il valore, cosı̀ che al termine dell’esecuzione della SUBROUTINE,
quando il controllo torna all’unità chiamante, lo stesso argomento costituisce
un risultato. A titolo di esempio, consideriamo la seguente SUBROUTINE
ORDINA, per la quale il vettore X funge da dato (il vettore originale con gli
elementi non in ordine) e da risultato (il vettore con gli elementi riordinati).
SUBROUTINE ORDINA(N,X)
DIMENSION X(N)
C Sottoprogramma che ordina in senso crescente gli elementi di X
C N contiene in ingresso la lunghezza del vettore
C X contiene in ingresso il vettore da ordinare
C
e in uscita il vettore ordinato
DO 50 I=1,N-1
MIN=I
DO 45 J=I+1,N
IF(X(J).LT.X(MIN)) MIN=J
45
CONTINUE
55
50
IF(MIN.GT.I))THEN
S=X(MIN)
X(MIN)=X(I)
X(MIN)=S
END IF
CONTINUE
RETURN
END
Il seguente programma principale usa la SUBROUTINE ORDINA per ordinare gli elementi di un vettore e la FUNCTION ENORM per calcolarne
la norma euclidea:
10
20
30
11.3
PARAMETER(NM=100)
DIMENSION V(NM)
OPEN(UNIT=2,FILE=’ris.txt’)
PRINT∗, ’dare N minore o uguale di’, NM
READ∗, N
PRINT∗, ’dare V’
READ∗, (V(I), I=1,N)
WRITE(2,20)(V(I), I=1,N)
CALL ORDINA(N,V)
WRITE(2,10)(V(I), I=1,N)
WRITE(2,30)ENORM(V,N)
FORMAT(2X,’Vettore ordinato’/(2X,E13.6))
FORMAT(2X,’Vettore originale’/(2X,E13.6))
FORMAT(//2X,’Norma euclidea=’,E13.6)
END
Associazione fra argomenti muti e argomenti attuali
assoc
Nelle istruzioni che compongono la sezione esecutiva di un sottoprogramma, vengono utilizzati gli argomenti muti e anche altre variabili, i cui nomi
non fanno parte della lista degli argomenti muti perchè non identificano
grandezze i cui valori devono essere comunicati dall’unità chiamante al sottoprogramma o viceversa; esse sono le cosiddette variabili locali, usate all’interno del sottoprogramma per identificare grandezze utili alla scrittura
dell’algoritmo. Le variabili locali e gli argomenti muti sono trattati in modo diverso dal compilatore: le prime vengono inserite nella tavola dei simboli e associate a locazioni di memoria nel modo usuale; per i secondi, può
essere allocata memoria oppure no, a seconda delle modalità previste per
l’associazione fra essi e gli argomenti attuali che saranno specificati nelle
chiamate del sottoprogramma.
Fondamentalmente, i linguaggi di programmazione possono scegliere fra
due modalità per attuare questa associazione:
56
– per valore, nel qual caso il compilatore alloca memoria per gli argomenti muti. Al momento dell’attivazione del sottoprogramma il valore di ogni
argomento in ingresso viene copiato nella locazione del corrispondente argomento muto e, viceversa, al momento del rilascio il valore di ogni argomento
muto in uscita viene copiato nella locazione del corrispondente argomento
attuale.
– per indirizzo, nel qual caso il compilatore non alloca memoria per gli
argomenti muti. Al momento dell’attivazione, l’unità chiamante invia al
sottoprogramma gli indirizzi delle locazioni di memoria allocate per gli argomenti attuali, in modo che il sottoprogramma operi direttamente su queste
locazioni. In questo modo il sottoprogramma trova i dati in ingresso nelle locazioni degli argomenti attuali corrispondenti a argomenti muti in ingresso;
inoltre, siccome ogni modifica fatta dal sottoprogramma su un argomento
muto è di fatto un cambiamento del valore memorizzato nella locazione del
corrispondente argomento attuale, l’unità chiamante trova i risultati direttamente nelle locazioni degli argomenti in uscita. In pratica, l’associazione per
indirizzo può essere vista come un cambiamento temporaneo di nome per le
locazioni degli argomenti attuali: per la durata dell’esecuzione del sottoprogramma la locazione identificata con il nome ai dall’unità chiamante viene
identificata con il nome mi , e riacquisice il suo nome originale al momento
del rientro nell’unità chiamante.
Lo svantaggio ovvio dell’associazione per valore è il costo in termini di
occupazione di memoria; quello dell’associazione per indirizzo è che talvolta
occorre fare preventivamente una copia di un argomento attuale se non si
vuole che il sottoprogramma ne cambi il valore. Alcuni linguaggi usano
di default l’associazione per indirizzo, altri quella per valore, altri infine
permettono di scegliere la modalità argomento per argomento. Il FORTRAN
appartiene alla prima categoria.
Cerchiamo di capire meglio, alla luce di questa informazione, cosa si
intende quando si afferma che gli argomenti attuali devono essere in corrispondenza uno–a–uno con gli argomenti muti, anche alla luce del fatto che
abitualmente i compilatori non prevedono controlli sulla correttezza delle
liste di argomenti attuali rispetto alle corrispondenti liste di argomenti muti
(e pertanto, gli errori si manifestano in fase di esecuzione). Sia dato ad
esempio un sottoprogramma con l’intestazione
SUBROUTINE ESER(M,N,X,R)
dove M e N sono scalari interi e X e R scalari reali; consideriamo le seguenti
chiamate, supponendo che anche nell’unità chiamante sia rispettata la regola
di default per i nomi delle variabili intere e reali:
57
1)
2)
3)
4)
5)
6)
7)
CALL
CALL
CALL
CALL
CALL
CALL
CALL
ESER(M,M1,DATO,R1)
ESER(M,M1,V(2),ARS)
ESER(10,J,DATO,R(10))
ESER(N,M,DATO,RIS)
ESER(K(1),K(2),DATO, RR)
ESER(A,N,X,R)
ESER(M,N,RR)
–La prima e la quarta chiamata sono corrette.
–La seconda chiamata è corretta se nell’unità chiamante V è un vettore: in
fase di esecuzione l’indirizzo del secondo elemento di V viene mandato al
sottoprogramma, che lavora su questa locazione usando il nome X.
–La terza chiamata è corretta se nell’unità chiamante R è un vettore di
almeno 10 elementi e se l’argomento muto M rappresenta un dato per il
sottoprogramma che non viene modificato in corso di esecuzione (se cosı̀
non fosse, il sottoprogramma, operando sulla locazione in cui è memorizzata
la costante 10 e modificandone il contenuto, cambierebbe di fatto il valore
della costante !!).
–La quinta chiamata è corretta se nell’unità chiamante K è un vettore.
–La sesta chiamata è sbagliata perché il primo argomento attuale A è di
tipo reale mentre il corrispondente argomento muto M è di tipo intero. Cosa
succede in questo caso? Come già detto, il compilatore in generale non rileva
l’errore, e il problema si manifesta durante l’esecuzione: tenendo conto che
i nomi A e M si riferiscono di fatto alla stessa locazione di memoria, l’unità
chiamante interpreterà la sequenza di “0” e “1” contenuta nella locazione
come un numero reale floating point, e il sottoprogramma interpreterà la
stessa sequenza come un numero intero: è facilmente intuibile che i due
valori non avranno niente a che vedere fra loro. Lo stesso tipo difunction
errore si
verificherebbe se si utilizzasse la FUNCTION NF del paragrafo 11.1 senza
dichiarare il nome NF di tipo REAL nell’unità chiamante.
–L’ultima chiamata è sbagliata perché ci sono 3 argomenti attuali contro 4
argomenti muti. Anche questo errore si manifesterà in fase di esecuzione,
probabilmente come segmentation fault.
11.4
Argomenti muti dimensionati
Come funziona l’associazione per indirizzo se l’argomento muto è il nome di
una variabile dimensionata? In questa situazione l’argomento attuale deve
essere una variabile dimensionata a sua volta, oppure il nome di un elemento di variabile dimensionata: nel primo caso, l’indirizzo passato al sottoprogramma è quello della variabile dimensionata attuale, che coincide con
quello del suo primo elemento; nel secondo caso, l’indirizzo è quello dell’elemento specificato come argomento attuale. Per meglio puntualizzare questi
58
subroutine
concetti, consideriamo la SUBROUTINE ORDINA(N,X) del paragrafo 11.2
e supponiamo di chiamarla da un programma principale che contiene la
dichiarazione
DIMENSION Y(100), AM(10,10)
Allora le seguenti chiamate:
1)
2)
3)
4)
CALL
CALL
CALL
CALL
ORDINA(M,Y)
ORDINA(M,Y(11))
ORDINA(M,A)
ORDINA(M,A(1,2))
sono tutte corrette se M è una variabile intera. Esaminiamole ad una ad
una supponendo che il valore di M sia 10.
1) La chiamata è equivalente a CALL ORDINA(M,Y(1)). Viene trasmesso
al sottoprogramma l’indirizzo del vettore Y e a partire da questo indirizzo
il sottoprogramma “vede” il suo vettore X; lo schema di associazione fra i
singoli elementi del vettore attuale e del vettore muto è pertanto
Y(1)
X(1)
Y(2)
X(2)
Y(3)
X(3)
···
···
Y(10)
X(N)
essendo l’argomento muto N associato a M, che vale 10. Per effetto di questa
chiamata, i valori dei primi 10 elementi del vettore Y risulteranno ordinati
in senso crescente.
2) Viene trasmesso al sottoprogramma l’indirizzo di Y(11). L’associazione
è descritta dalla figura seguente
Y(11)
X(1)
Y(12)
X(2)
Y(13)
X(3)
···
···
Y(20)
X(N)
e l’effetto è l’ordinamento degli elementi di Y da Y(11) a Y(20).
3) Viene trasmesso al sottoprogramma l’indirizzo della matrice A, ovvero del
primo elemento A(1,1), e a partire da questa locazione il sottoprogramma
vede il suo vettore X; tenendo conto che le matrici sono memorizzate per
colonne, l’associazione è descritta dalla figura seguente
A(1,1)
X(1)
A(2,1)
X(2)
A(3,1)
X(3)
···
···
A(10,1)
X(N)
e l’effetto è il riordino degli elementi della prima colonna di A.
4) Questa chiamata è simile alla precedente, salvo il fatto che ora viene
trasmesso al sottoprogramma l’indirizzo dell’elemento di A di indici (1,2);
l’associazione è allora descritta dalla figura seguente
A(1,2)
X(1)
A(2,2)
X(2)
A(3,2)
X(3)
···
···
A(10,2)
X(N)
e l’effetto è l’ordinamento degli elementi della seconda colonna di A.
59
Il seguente è un programma che usa la FUNCTION ENORM per calcolare la norma euclidea di tutte le colonne di una matrice X di N righe e M
colonne.
DIMENSION X(20,30)
PRINT∗, ’dare N <= 20 e M <=30’
READ∗, N,M
PRINT∗, ’dare X per colonne’
READ∗, ((X(I,J), I=1,N), J=1,M)
DO 25 J=1,M
PRINT∗,’norma della colonna ’,J,: ’,ENORM(X(1,J),N)
25
CONTINUE
END
11.5
Dimensionamento variabile e indefinito per vettori
dimvar
Nei paragrafi precedenti abbiamo usato istruzioni DIMENSION riferite a
argomenti muti dei sottoprogrammi, in cui la lunghezza del vettore era descritta dalla variabile N (a sua volta, argomento muto). Questo rientra nella
norma del FORTRAN 77, secondo il quale è possibile specificare la lunghezza di un vettore argomento muto tramite un’espressione intera, nella quale
compaiano costanti e variabili intere, con il vincolo che queste variabili devono essere a loro
volta argomenti muti, o parte di un blocco COMMON
com
(cfr. paragrafo 15). Si parla in questo caso di dimensionamento variabile (in
inglese, adjustable size). Sono esempi corretti di uso del dimensionamento
variabile i seguenti:
FUNCTION ES1(X,IW,N,M,...)
DIMENSION X(N+M), IW(N)
SUBROUTINE PIPPO(VET,N,...)
DOUBLE PRECISION VET(N)
SUBROUTINE PLUTO(L,ALFA, WORK,...)
DIMENSION WORK(L+1), ALFA(2∗L)
Come funziona in pratica il dimensionamento variabile? Sia X il nome
dell’argomento muto dimensionato in modo variabile. L’istruzione di specificazione in cui si dichiara la dimensione di X serve per informare il compilatore della natura dimensionata di X, in modo tale che esso possa tradurre
correttamente le istruzioni eseguibili che coinvolgono elementi di X. La
lunghezza effettiva di X non serve ai fini della compilazione, ma viene determinata in fase di esecuzione, quando alle variabili che intervengono nell’espressione della dimensione vengono associati i corrispondenti argomenti
attuali e quindi il sottoprogramma può disporre del loro valore. Conside-
60
riamo la SUBROUTINE PLUTO dell’esempio precedente. Se si effettua la
chiamata
CALL PLUTO(5, A, W,...)
le lunghezze effettive di ALFA e WORK determinate durante l’esecuzione
della chiamata saranno rispettivamente 5+1=6 e 2×5=10.
La conoscenza della lunghezza effettiva del vettore non serve per allocare
memoria, perché le locazioni per l’argomento muto sono di fatto quelle del
corrispondente argomento attuale; essa viene sfruttata soltanto se nel sottoprogramma figurano istruzioni che fanno riferimento al vettore nella sua
totalità, come ad esempio PRINT∗, ALFA.
È importante ricordare che il dimensionamento variabile è consentito solo per argomenti muti, e non per variabili locali. Un compilatore che rispetti
rigorosamente lo standard FORTRAN 77 segnala errore se questa regola non
viene rispettata. Cosı̀ reagisce ad esempio l’Open WATCOM. Il compilatore gfortran prevede invece la possibilità di usare il dimensionamento
variabile all’interno di un sottoprogramma anche per variabili locali, purché
le variabili intere che compaiono nella dichiarazione di dimensione siano argomenti muti o facciano parte di un blocco COMMON. La memoria per
questi vettori viene allocata in fase di esecuzione, al momento del calcolo
della lunghezza effettiva, e rilasciata al termine dell’esecuzione del sottoprogramma. Questo è un piccolo esempio di uso dinamico della memoria.
Il problema, purtroppo, è che gfortran non gestisce sempre bene questa
allocazione dinamica, con effetti imprevedibili sul risultato dei programmi,
analoghi a quelli che si possono verificare quando si usa più memoria di
quella riservata a una variabile.
Quando in un sottoprogramma non sono presenti istruzioni che fanno
riferimento ad un vettore argomento muto nella sua totalità si può usare
una forma di dimensionamento ancora più semplice di quello variabile. Si
può infatti specificare la dimensione tramite un asterisco (∗), e si parla in
questo caso di dimensionamento indefinito (in inglese, assumed size). Ad
esempio, nella FUNCTION ENORM si potrebbe sostituire l’istruzione
DIMENSION X(N)
con
DIMENSION X(∗)
Dal punto di vista del compilatore, le due istruzioni sono equivalenti. L’unica
differenza è che nel secondo caso non viene mai determinata la lunghezza
effettiva del vettore19 .
19
Per questo motivo non è possibile, neanche usando gfortran, usare il dimensionamento indefinito per vettori che non facciano parte della lista degli argomenti muti. Se lo
si fa, viene segnalato l’errore “Assumed size array... must be a dummy argument”.
61
La scelta fra dimensionamento variabile o indefinito è una questione
di gusto personale del programmatore, che deve decidere come far sapere
all’utente del sottoprogramma quante locazioni di memoria devono essere
disponibili per l’array al momento dell’attivazione del sottoprogramma. Se
si opta per il dimensionamento variabile, questa informazione è contenuta
nell’istruzione di specificazione usata nel sottoprogramma per dichiarare l’array; se invece si opta per il dimensionamento indefinito, si deve corredare il
sottoprogramma di linee di commento in cui precisare la lunghezza dell’array.
11.6
Dimensionamento variabile e indefinito per matrici
dimvarmat
Il dimensionamento variabile può essere usato anche per matrici, ma in
questo caso occorre una certa accortezza. Per spiegare questa affermazione,
consideriamo il seguente sottoprogramma
10
SUBROUTINE MAT(A,N)
DIMENSION A(N,N)
DO 10 I=1,N
DO 10 J=1,N
A(I,J)=0.1∗(I+J)
CONTINUE
RETURN
END
il cui scopo è definire una matrice reale A quadrata di ordine n, con elementi
aij = 0.1(i + j). La matrice è un argomento muto e può essere dimensionata
in modo variabile; pertanto abbiamo usato l’istruzione dichiarativa
DIMENSION A(N,N).
Il seguente programma principale utilizza la SUBROUTINE MAT. L’argomento attuale B corrispondente a A è una matrice di ordine 4, e quindi il
sottoprogramma può essere usato per valori di n minori o uguali di 4.
PARAMETER(NM=4)
DIMENSION B(NM,NM)
PRINT ∗, ’dare N <=’,NM
READ ∗,N
CALL MAT(B,N)
WRITE( ∗,100) ((B(I,J), J=1,N), I=1,N)
100 FORMAT(4(2X,E13.6))
END
Se eseguiamo il programma con N uguale a N, il risultato è
0.200000E+00
0.300000E+00
0.400000E+00
0.500000E+00
0.300000E+00
0.400000E+00
0.500000E+00
0.600000E+00
0.400000E+00
0.500000E+00
0.600000E+00
0.700000E+00
62
0.500000E+00
0.600000E+00
0.700000E+00
0.800000E+00
e possiamo facilmente verificare che è quello giusto. Ma se N vale 2, il
risultato è 20 .
0.200000E+00
0.512685E-19
0.300000E+00
0.459163E-40
laddove il risultato corretto sarebbe
0.200000E+00
0.300000E+00
0.300000E+00
0.400000E+00
A cosa è dovuto questo risultato sbagliato, dal momento che tutte e
due le unità di programma sono perfettamente corrette dal punto di vista
sintattico? Per rispondere a questa domanda, dobbiamo ricordare che l’associazione fra B e A avviene per indirizzo, e che le matrici sono memorizzate
per colonne. Al momento dell’attivazione del sottoprogramma, il programma principale invia l’indirizzo di B (ovvero quello di B(1,1)) e a partire da
questa locazione di memoria il sottoprogramma “vede” la matrice A. Inoltre,
per effetto del dimensionamento variabile, il sottoprogramma determina le
dimensioni effettive di A durante l’esecuzione in base al valore attuale corrispondente a N. Pertanto, quando N vale 4, la matrice A viene considerata
di 4 righe e 4 colonne e l’associazione fra gli elementi di B e quelli di A è:
B(1,1)
A(1,1)
B(2,1)
A(2,1)
B(3,1)
A(3,1)
B(4,1)
A(4,1)
B(1,2)
A(1,2)
B(2,2)
A(2,2)
B(3,2)
A(3,2)
B(4,2)
A(4,2)
B(1,3)
A(1,3)
B(2,3)
A(2,3)
B(3,3)
A(3,3)
B(4,3)
A(4,3)
B(1,4)
A(1,4)
B(2,4)
A(2,4)
B(3,4)
A(3,4)
B(4,4)
A(4,4)
···
···
con una corrispondenza perfetta fra gli indici usati nel programma principale per B e quelli usati nel sottoprogramma per A: la locazione che il sottoprogramma identifica come A(I,J) è la stessa che il programma principale
identifica come B(I,J), qualunque sia la coppia di indici (I,J).
Osserviamo ora cosa succede quando N vale 2. In questo caso, A risulta
una matrice 2 × 2 e l’associazione con B segue lo schema seguente:
B(1,1)
A(1,1)
B(2,1)
A(2,1)
B(3,1)
A(1,2)
B(4,1)
A(2,2)
···
Questa volta non c’è più corrispondenza fra gli indici usati dal sottoprogramma e quelli usati dal programma principale: i primi due elementi di A (che
rappresentano la prima colonna) sono associati agli omonimi elementi di B;
poi però, il sottoprogramma “fa partire” la seconda colonna di A, mentre
ancora la prima colonna di B non è finita. Se vogliamo ottenere una perfetta
corrispondenza di indici anche in questo caso, dobbiamo fare in modo che il
sottoprogramma “salti” il resto della prima colonna di B e associ A(1,2) a
B(1,2), secondo uno schema del tipo
B(1,1)
A(1,1)
B(2,1)
A(2,1)
B(3,1)
B(4,1)
20
B(1,2)
A(1,2)
B(2,2)
A(2,2)
···
Teniamo conto che in virtù del formato di stampa scelto, troviamo 4 numeri su ogni
riga: per N=2, i primi due numeri formano la prima riga di B e gli altri la seconda riga.
63
Questo scopo può essere raggiunto soltanto rendendo noto al sottoprogramma il numero di righe di B, in modo che esso possa attribuire a A esattamente lo stesso numero di righe. Questa informazione può essere passata
al sottoprogramma in fase di esecuzione se inseriamo fra gli argomenti muti
una variabile NR a cui far corrispondere come argomento attuale il numero
di righe di B (qualunque sia il valore di N). La variabile NR deve essere
usata nel sottoprogramma esclusivamente per definire le dimensioni di A,
sostituendo l’istruzione
DIMENSION A(N,N)
con
DIMENSION A(NR,N)
Con questo cambiamento, ci sarà corrispondenza fra gli indici usati dall’unità
chiamante per B e quelli usati dal sottoprogramma per A, qualunque sia il
valore di N. Ad esempio, per N pari a 2, si ottiene l’associazione desiderata
fra gli elementi di B e quelli di A:
B(1,1)
A(1,1)
B(2,1)
A(2,1)
B(3,1)
A(3,1)
B(4,1)
A(4,1)
B(1,2)
A(1,2)
B(2,2)
A(2,2)
B(3,2)
A(3,2)
B(4,2)· · ·
A(4,2)
Gli elementi A(3,1), A(4,1), A(3,2) e A(4,2) non verrano mai usati nella sezione esecutiva. Infatti, in questa sezione del sottoprogramma, si devono continuare a usare soltanto le locazioni identificate dai nomi A(I,J) per
I,J=1,N. In conclusione, la SUBROUTINE MAT e il programma principale
diventano:
SUBROUTINE MAT(A,N,NR) !!!
DIMENSION A(NR,N)
!!!
DO 10 I=1,N
DO 10 J=1,N
A(I,J)=0.1∗(I+J)
10 CONTINUE
RETURN
END
PARAMETER(NM=4)
DIMENSION B(NM,NM)
PRINT ∗, ’dare N <=’,NM
READ ∗,N
CALL MAT(B,N,NM)
WRITE( ∗,100) ((B(I,J), J=1,N), I=1,N)
100 FORMAT(4(2X,E13.6))
END
!!!
dove abbiamo contrassegnato con !!! le istruzioni cambiate rispetto alla
64
versione precedente.
Data la sua importanza ai fini del corretto uso del sottoprogramma, il numero di righe di una matrice argomento muto è chiamato anche dimensione
principale (in inglese leading dimension) della matrice.
Anche per le matrici è possibile usare il dimensionamento indefinito, ma
soltanto per quanto riguarda il numero di colonne. Ad esempio, se H è un
argomento muto di un sottoprogramma, si può utilizzare una dichiarazione
del tipo
ma non
DIMENSION H(NR, ∗)
DIMENSION H(∗,∗)
Questa restrizione ha una sua logica: una volta che il sottoprogramma ha
l’informazione sul numero di righe della matrice, può assegnare gli indici ai
singoli elementi e identificare la locazione chiamata H(I,J) per qualunque
coppia di indici I e J. Se potesse essere lasciato indefinito anche il numero
di righe, il sottoprogramma non sarebbe in grado di assegnare indici dopo
l’elemento H(1,1).
12
Alcune regole di buona programmazione
Una volta scritto ed eventualmente compilato, un sottoprogramma è in linea
di principio utilizzabile nel contesto di qualsiasi programma FORTRAN in
cui sia necessario ricorrere all’algoritmo da esso realizzato. Tenendo a mente
questo fatto, ogni programmatore dovrebbe scrivere i sottoprogrammi in
modo da non limitarne le possibilità di utilizzo. Qui di seguito riportiamo
alcune regole da seguire per ottenere questo scopo.
12.1
Istruzioni di scrittura nei sottoprogrammi
Le istruzioni di scrittura sono in genere bandite dai sottoprogrammi. Il
programmatore può talvolta decidere di inserire nella lista degli argomenti
muti un argomento in ingresso tramite il quale l’utente esempi
può scegliere se
eseguire stampe oppure no (cfr. Esempio 2 del paragrafo 13). Fatta salva
questa situazione, le operazioni di scrittura sono riservate al programma
principale, al quale il sottoprogramma deve inviare tutte le informazioni
utili per poter decidere che cosa stampare.
Per fare un esempio, consideriamo la seguente SUBROUTINE ERONE,
che calcola l’area di un triangolo con la formula di Erone e prevede controlli
atti a individuare le situazioni
di errore, come nel programma (principale)
ELSE
ERONE3 del paragrafo 8.2.3. A differenza di quanto fatto in quel programma, nella SUBROUTINE non abbiamo inserito istruzioni di stampa, ma
65
abbiamo previsto fra gli argomenti muti un argomento in uscita che indica
all’unità chiamante come “sono andate a finire le cose“; più precisamente,
la SUBROUTINE prevede due risultati:
– IND è un intero che in uscita vale 1 se tutto è andato bene e l’area è stata
calcolata, 0 altrimenti;
– AREA è una variabile reale il cui valore è l’area del triangolo se IND vale
1 ed è 0 se IND vale 0.
La coppia di istruzioni
IND=0
AREA=0
prende il posto dell’istruzione di stampa
PRINT∗,’Dati non corrispondenti a un triangolo’
del programma ERONE3.
C
C
C
C
C
C
C
C
C
SUBROUTINE ERONE(A,B,C,IND,AREA)
Calcolo dell’area di un triangolo con la formula di Erone
A,B,C reali in ingresso
IND intero in uscita:
=1 se tutto va bene
=0 se A,B e C non sono le lunghezze dei lati di un triangolo
AREA reale in uscita:
area del triangolo se IND=1
0 se IND=0
IF((A.GT.0.).AND.(B.GT.0.).AND.(C.GT.0.))THEN
SP=(A+B+C)/2.
Q=SP∗(SP-A)∗(SP-B)∗(SP-C)
IF(Q.GT.0.)THEN
AREA=SQRT(Q)
IND=1
ELSE
IND=0
AREA=0.
END IF
ELSE
IND=0
AREA=0.
END IF
END
Un programma principale che usi questa SUBROUTINE deve contenere
un’istruzione IF dopo la chiamata per controllare il risultato attraverso il
66
valore di IND e eseguire le operazioni di stampa coerenti. Una sequenza di
istruzioni atta allo scopo potrebbe essere la seguente:
CALL ERONE(A,B,C,IND,AREA)
IF(IND.EQ.0)THEN
PRINT∗,’Dati non corrispondenti a un triangolo’
..
.
ELSE
PRINT∗,’area= ’, AREA
..
.
END IF
12.2
Istruzioni STOP e RETURN
Un sottoprogramma non contiene mai istruzioni STOP. È utile a questo
proposito puntualizzare la differenza fra le istruzioni RETURN e STOP.
L’istruzione RETURN, che può comparire soltanto in un sottoprogramma,
interrompe l’esecuzione del sottoprogramma e rimanda all’unità di programma chiamante, nella quale l’esecuzione riprende in sequenza. L’istruzione
STOP invece interrompe l’esecuzione dell’intero programma, in qualunque
unità (programma principale o sottoprogramma) venga incontrata. È ovvio
che un sottoprogramma non può “arrogarsi” il diritto di chiudere l’esecuzione
di qualsiasi programma in cui venga usato 21 .
12.3
Arrays di lavoro
lavoro
Quando un sottoprogramma è finalizzato alla risoluzione di un problema
di dimensione variabile (sistema lineare di n equazioni in n incognite, interpolazione polinomiale su n nodi, etc.), i vettori e le matrici coinvolti
nell’algoritmo devono essere dimensionati in modo variabile o indefinito,
a meno che la loro dimensione non sia indipendente dalla dimensione del
problema. Questo implica che talvolta si devono inserire nella lista degli
argomenti muti nomi di variabili dimensionate che non rappresentano dati
o risultati dell’algoritmo, ma sono semplicemente arrays di lavoro. Come
esempio di buona programmazione nel senso suddetto, mostriamo qui di seguito la parte iniziale del sottoprogramma SSTEV della libreria LAPACK,
reperibile in rete ad esempio all’indirizzo http://www.netlib.org, per il cal21
Questa appena enunciata e la precedente relativa alle istruzioni di scrittura, sono
regole di buona programmazione. Il fatto che comunque non sia vietato inserire istruzioni
di stampa e STOP in un sottoprogramma, permette di farne uso in fase di messa a punto
dei programmi. Infatti si usa spesso, quando un programma non dà risultati giusti, fare
una sorta di debug manuale, inserendo stampe in vari punti per capire dove si genera
l’errore, e eventualmente controlli che bloccano l’esecuzione del programma in determinate
circostanze.
67
colo di autovalori ed eventualmente autovettori di una matrice tridiagonale
simmetrica di ordine n.
SUBROUTINE SSTEV( JOBZ, N, D, E, Z, LDZ, WORK, INFO )
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
– LAPACK driver routine (version 3.2) –
– LAPACK is a software package provided by Univ. of Tennessee,
– Univ. of California Berkeley, Univ. of Colorado Denver and NAG Ltd.
– November 2006
.. Scalar Arguments ..
CHARACTER JOBZ
INTEGER INFO, LDZ, N
.. Array Arguments ..
REAL D( * ), E( * ), WORK( * ), Z( LDZ, * )
Purpose
=======
SSTEV computes all eigenvalues and, optionally, eigenvectors of a real
symmetric tridiagonal matrix A.
=========
Arguments
JOBZ (input) CHARACTER*1
= ’N’: Compute eigenvalues only;
= ’V’: Compute eigenvalues and eigenvectors.
N (input) INTEGER
The order of the matrix. N >= 0.
D (input/output) REAL array, dimension (N)
On entry, the n diagonal elements of the tridiagonal matrix A.
On exit, if INFO = 0, the eigenvalues in ascending order.
E (input/output) REAL array, dimension (N-1)
On entry, the (N-1) subdiagonal elements of the tridiagonal
matrix A, stored in elements 1 to N-1 of E.
On exit, the contents of E are destroyed.
Z (output) REAL array, dimension (LDZ, N)
If JOBZ = ’V’, then if INFO = 0, Z contains the orthonormal
eigenvectors of the matrix A, with the i-th column of Z
holding the eigenvector associated with D(i).
If JOBZ = ’N’, then Z is not referenced.
68
*
*
*
*
*
*
*
*
*
*
*
*
*
LDZ (input) INTEGER
The leading dimension of the array Z. LDZ >= 1, and if
JOBZ = ’V’, LDZ >= max(1,N).
WORK (workspace) REAL array, dimension (max(1,2*N-2))
If JOBZ = ’N’, WORK is not referenced.
INFO (output) INTEGER
= 0: successful exit
< 0: if INFO = -i, the i-th argument had an illegal value
> 0: if INFO = i, the algorithm failed to converge; i
off-diagonal elements of E did not converge to zero.
Dall’istruzione dichiarativa
REAL D(∗), E(∗), WORK(∗), Z(LDZ,∗)
apprendiamo che gli argomenti muti D, E, WORK, Z sono variabili dimensionate per le quali gli autori hanno utilizzato il dimensionamento indefinito.
Dalla successiva documentazione apprendiamo che:
– D e E sono argomenti di input/output: al momento dell’attivazione del
sottoprogramma identificano dei dati, poi vengono modificati in modo tale
che al momento del rientro nell’unità chiamante identificano dei risultati.
– Z è un argomento di output che contiene, se richiesti, gli autovettori. Se
il dato JOBZ vale ’N’, la matrice Z non viene usata; pertanto l’argomento
attuale corrispondente a Z potrà essere uno scalare e quello corrispondente
a LDZ potrà essere 1. Se invece JOBZ è ’Y’, la matrice Z viene usata e
l’argomento attuale corrispondente a LDZ dovrà essere maggiore o uguale
a N (noi sappiamo che dovrà essere uguale al numero di righe della matrice
attuale corrispondente a Z).
– Infine, WORK è un vettore di lavoro che viene utilizzato se JOBZ vale ’Y’.
Esso è presente nella lista degli argomenti muti perché la sua dimensione,
2N-2, dipende da N.
Un programma principale che utilizzi questo sottoprogramma deve contenere
un’istruzione dichiarativa tramite la quale si alloca memoria per il vettore
WORK: se W è il nome dell’argomento attuale che si intende associare a
WORK, il programma deve contenere ad esempio le istruzioni
PARAMETER(NM=20)
DIMENSION W(2∗NM-2)
A proposito di arrays di lavoro, è interessante la seguente osservazione. Sup-
69
poniamo di avere un programma costituito dal programma principale e due
sottoprogrammi, UNO e DUE: il programma principale chiama UNO, che a
sua volta chiama DUE. Supponiamo anche che DUE abbia bisogno di un vettore di lavoro W con dimensione variabile, ma che questo vettore non serva
a UNO. Ebbene, il vettore farà sicuramente parte della lista degli argomenti
muti di DUE, ma saremo costretti a inserirlo anche in quella di UNO perché
la memoria verrà allocata nel programma principale e, in fase di esecuzione,
UNO dovrà ricevere l’indirizzo del vettore attuale dal programma principale
e trasmetterlo a DUE.
13
esempi
13.1
Esempi di programmi con sottoprogrammi
Esempio 1.
In questo programma si approssima l’integrale
Z π
2
cos2 (x)esin(2x) dx
0
mediante le formule composite dei Trapezi e di Simpson su n sottointervalli.
Tutte le variabili reali sono in doppia precisione. Il programma è composto
da quattro unità di programma:
– Il programma principale, i cui compiti sono: leggere n; definire l’intervallo
di integrazione; calcolare i valori della funzione y0 , . . . , yn nei nodi usando la FUNCTION F; calcolare i valori approssimati dell’integrale usando i
sottoprogrammi SIMP e TRAP; stampare i risultati.
– La FUNCTION SIMP che realizza la formula di Simpson composita.
– La FUNCTION TRAP che realizza la formula dei Trapezi composita.
– La FUNCTION F che descrive la funzione integranda.
C =========================================
C Programma per l’approssimazione di un integrale mediante le formule
C composite di Simpson e dei Trapezi (Numero di sottointervalli <=20)
C
PARAMETER (M=20)
IMPLICIT DOUBLE PRECISION (A-H,O-Z)
DIMENSION Y(0:M)
PRINT ∗, ’Dare il numero N di sottointervalli’
PRINT ∗, ’N pari e <=’, M
READ*,N
A=0.D0
C Si calcola pi/2 come 2*arctg(1)
B = 2.D0*ATAN(1.D0)
H= (B-A)/N
70
DO 100 I = 0, N
Y(I) = F(A+I*H)
100
CONTINUE
S=SIMP(N,H,Y)
T=TRAP(N,H,Y)
PRINT*,’N=’, N,’ S=’, S, ’ T=’,T
END
C =========================================
FUNCTION F(X)
DOUBLE PRECISION F, X
F=COS(X)**2*EXP(SIN(2.D0*X))
END
C =========================================
FUNCTION SIMP(N,H,Y)
IMPLICIT DOUBLE PRECISION (A-H,O-Z)
C
C Subroutine per il calcolo di un integrale mediante la formula composita
C di Simpson con N sottointervalli di ampiezza H
C Il vettore Y contiene i valori della funzione integranda nei nodi
C
DIMENSION Y(0:N)
S1=0.
DO 150 I = 1, N-1, 2
S1 = S1 + Y(I)
150
CONTINUE
S2=0.
DO 200 I = 2, N-2, 2
S2 = S2 + Y(I)
200
CONTINUE
SIMP = H*(Y(0)+ 2.D0*S1+ 4.D0*S2 + Y(N))/3.D0
RETURN
END
C =========================================
FUNCTION TRAP(N,H,Y)
IMPLICIT DOUBLE PRECISION (A-H,O-Z)
C
C Subroutine per il calcolo di un integrale mediante la formula composita
C dei Trapezi con N sottointervalli di ampiezza H
C Il vettore Y contiene i valori della funzione integranda nei nodi
C
DIMENSION Y(0:N)
S=0.
DO 150 I = 1, N-1
S = S + Y(I)
150
CONTINUE
TRAP = H*(Y(0)+ 2.D0*S+ 71
Y(N))/2.D0
RETURN
END
C =========================================
13.2
Esempio 2
Il secondo esempio riguarda il metodo di Newton per risolvere un’equazione
non lineare f (x) = 0. Il programma è composto da cinque unità di programma:
– Il programma principale, nel quale si leggono i dati, si esegue il metodo di Newton utilizzando la SUBROUTINE NEWTON, e si stampano i
risultati. write
Questi vengono scritti su file usando l’istruzione WRITE (cfr.
paragrafo 10): il nome del file è identificato dalla variabile di tipo carattere NOMEF, il cui valore è un dato in ingresso, e l’identificatore numerico del file è espresso tramite una variabile intera IUNIT. L’istruzione
OPEN(UNIT=IUNIT,FILE=NOMEF) predispone il file per la scrittura.
Gli altri dati letti sono: il punto iniziale, il numero massimo di iterazioni e
la tolleranza per il criterio di arresto. Oltre a questi, si prevede il dato IP,
con valore 1 se si vuole che la SUBROUTINE stampi i risultati di tutte le
iterazioni e 0 altrimenti.
–La SUBROUTINE NEWTON, che realizza il metodo di Newton. Questo
sottoprogramma utilizza la FUNCTION F e la FUNCTION DER per il
calcolo dei valori di f e della derivata f ′ rispettivamente. Inoltre, esso
utilizza la FUNCTION EPSM per calcolare la precisione di macchina ǫm :
se a una certa iterazione la derivata è in valore assoluto minore di ǫm , il
sottoprogramma si arresta.
– La FUNCTION F che descrive la funzione f . Nell’esempio, si definisce in
particolare la funzione polinomiale f (x) = x3 − 100x2 − x + 100.
– La FUNCTION DER che descrive la derivata della funzione f .
– La FUNCTION EPSM che calcola la precisione di macchina.
C =========================================
PROGRAM EQNONLIN
CHARACTER*10 NOMEF
PRINT∗,’dare il nome del file per i risultati’
READ∗,NOMEF
IUNIT=3
OPEN(UNIT=IUNIT,FILE=NOMEF)
PRINT∗,’dare x (punto iniziale)’
READ∗,X
PRINT∗,’dare kmax (numero massimo di iterazioni)’
READ∗,KMAX
PRINT∗,’dare tol (tolleranza per il criterio di arresto)’
READ∗,TOL
PRINT∗,’stampe ad ogni iterazione (0 NO/1 SI)?’
READ∗,IP
CALL NEWTON(IUNIT,X,KMAX,TOL,IP,IND,FX,ITER)
72
IF(IND.EQ.1)THEN
WRITE(IUNIT,2000)
ELSE IF(IND.EQ.-1)THEN
WRITE(IUNIT,4000)
ELSE
WRITE(IUNIT,5000)
END IF
IF(IP.EQ.0)WRITE(IUNIT,6000)ITER,X,FX
STOP
2000 FORMAT(’soddisfatto il criterio di arresto’)
4000 FORMAT( ’derivata uguale a zero’)
5000 FORMAT( ’esaurite le iterazioni’)
6000 FORMAT(2X,’numero di iterazioni effettuate: ’,I3
1
2X,’ultima iterata : ’,E13.6
2
2X,’VALORE DI f : ’,E13.6)
END
C =========================================
SUBROUTINE NEWTON(IUNIT,X,KM,TOL,IP,IND,FX,ITER)
c
c Metodo di Newton per risolvere un’equazione non lineare f(x)=0
c
c in ingresso:
c IUNIT identificatore dell’unità di uscita
c X approssimazione iniziale
c KM numero massimo di iterazioni
c TOL tolleranza per il criterio di arresto
c IP stampe a tutte le iterazioni (1) o no (0)
c
c in uscita:
c IND indicatore del risultato:
c
=1 soddisfatto il criterio di arresto
c
=-1 |derivata| < della precisione di macchina
c
=-2 esaurite le iterazioni
c X ultima approssimazione calcolata
c FX valore di f in X
c ITER numero di iterazioni effettuate
c
c L’utente deve fornire un sottoprogramma FUNCTION F(X)
c e un sottoprogramma FUNCTION DER(X) che descrivono
c rispettivamente la funzione f e la sua derivata
c
EPS=EPSM( )
FX=F(X)
73
IF(IP.EQ.1)THEN
K=0
WRITE(IUNIT,1000)K,X,FX
END IF
DO 500 K=1,KM
ITER=K
FDX=DER(X)
c
valore assoluto della derivata < della precisione di macchina
IF(FDX.LE.EPS)THEN
IND=-1
RETURN
END IF
c
calcolo della successiva iterata
DX=FX/FDX
X=X-DX
FX=F(X)
IF(IP.EQ.1)THEN
WRITE(IUNIT,1000)K,X,FX
END IF
c
criterio di arresto
IF(ABS(FX).LE.TOLF) THEN
IND=1
RETURN
END IF
500
CONTINUE
c
esaurite le iterazioni
IND=-2
RETURN
1000 FORMAT(2X,’k=’,I3,2X,’ x=’, E13.6,2X,’f(x)=’,E13.6)
END
C =========================================
FUNCTION EPSM( )
X=1.
10 X=0.5∗X
Y=1.+X
IF(Y.GT.1.) GO TO 10
EPSM=2. ∗X
END
C =========================================
FUNCTION F(X)
F= X ∗∗3-100 ∗X ∗∗2-X+100
END
FUNCTION DER(X)
DER=3∗X∗∗2-200∗X-1
END
74
13.3
Esempio 3
In questo terzo esempio si vuole verificare se due matrici date A e B commutano. Il programma è organizzato su tre unità di programma:
– Il programma principale, nel quale si leggono le matrici, si calcolano i
due prodotti AB e BA usando la SUBROUTINE PMAT e si controlla la
commutatività mediante la FUNCTION ICOMM. Siccome il risultato della
SUBROUTINE PMAT viene sovrascritto alla seconda matrice, si devono
usare delle matrici di appoggio SA e SB. Inoltre, è necessario dichiarare
un vettore di lavoro W perché richiesto dalla stessa SUBROUTINE. Il programma prevede di leggere il valore di una tolleranza TOL utilizzata dalla
FUNCTION ICOMM.
– La SUBROUTINE PMAT che calcola il prodotto AB memorizzando il
risultato su B.
– La FUNCTION ICOMM, che verifica se due matrici reali date sono uguali
a meno di una tolleranza assegnata. Si tratta di una funzione logica, il cui
risultato è la costante logica .TRUE. oppure .FALSE.
C =========================================
C Programma per verificare se due matrici reali date commutano
C
PROGRAM COMMUTA
PARAMETER (NM=20)
DIMENSION A(NM,NM),B(NM,NM),SA(NM,NM),SB(NM,NM)
DIMENSION W(NM)
LOGICAL ICOMM
PRINT ∗, ’Dare TOL’
READ*,TOL
PRINT ∗, ’Dare N <=’,NM
READ*,N
PRINT ∗, ’Dare A per righe’
READ*,((A(I,J), J=1,N), I=1,N)
PRINT ∗, ’Dare B per righe’
READ*,((B(I,J), J=1,N), I=1,N)
C Faccio copie di A e B
DO 100 J=1,N
DO 100 I=1,N
SA(I,J)=A(I,J)
SB(I,J)=B(I,J)
100 CONTINUE
C Memorizzo il prodotto AB in SB e il prodotto BA in SA
CALL PMAT(N,NM,A,SB,W)
CALL PMAT(N,NM,B,SA,W)
75
C Verifico la commutativita’
IF(ICOMM(N,NM,SA,SB,TOL))THEN
PRINT*,’Le due matrici commutano’
ELSE
PRINT*,’Le due matrici non commutano’
END IF
END
C
C =========================================
C
SUBROUTINE PMAT(N,NR,A,B,W)
DIMENSION A(NR,N), B(NR,N), W(N)
c
C Sottoprogramma che calcola il prodotto fra A e B, memorizzandolo in B
C W vettore di lavoro
C
DO 50 J=1,N
DO 30 I=1,N
W(I)=0.
DO 30 K=1,N
W(I)=W(I)+A(I,K)∗B(K,J)
30
CONTINUE
DO 40 I=1,N
B(I,J)=W(I)
40
CONTINUE
50 CONTINUE
RETURN
END
C =========================================
C
LOGICAL FUNCTION ICOMM(N,NR,A,B,TOL)
DIMENSION A(NR,N), B(NR,N)
C Sottoprogramma che verifica se A e B sono uguali a meno di TOL
C In uscita: ICOMM=.TRUE. se A e B sono uguali
C
=.FALSE. altrimenti
DO 200 J=1,N
DO 200 I=1,N
IF(ABS(A(I,J)-B(I,J)).GT.TOL∗ABS(A(I,J))+TOL)THEN
ICOMM=.FALSE.
RETURN
END IF
200 CONTINUE
ICOMM=.TRUE.
END
76
13.4
Esempio 4
L’ultimo esempio è dedicato al metodo di Gauss con pivoting per la risoluzione
di un sistema lineare Ax = b. Il programma consiste di cinque unità di
programma, tutte in doppia precisione:
– Il programma principale, in cuiP
la matrice A è prevista in lettura, mentre
il termine noto è definito da bi = nj=1 aij , in modo che sia nota la soluzione
esatta x̃ = (1, 1, . . . , 1)T . Una volta risolto il sistema, il programma calcola
∞
(usando la FUNCTION VETNINF) e stampa l’errore kx− x̃k∞ e krk
kbk∞ , dove
r = Ax − b è il vettore residuo.
– La SUBROUTINE GAUSSPIV, il cui scopo è triangolarizzare A usando il
metodo di Gauss con pivoting; se nel corso dell’algoritmo viene trovato un
pivot uguale a zero, la SUBROUTINE “comunica” al programma principale
che A è singolare.
– La SUBROUTINE RISOLVI che risolve il sistema usando i risultati della
SUBROUTINE GAUSSPIV.
– La SUBROUTINE RESIDUO che calcola il vettore residuo.
– La FUNCTION VETNINF per il calcolo della norma infinito di un vettore.
C =========================================
C Soluzione di un sistema lineare con il metodo di Gauss con pivoting
C
PROGRAM DGAUSS
IMPLICIT DOUBLE PRECISION (A-H,O-Z)
PARAMETER(NM=10)
DIMENSION A(NM,NM),B(NM),X(NM),IP(NM-1),R(NM),E(NM)
DIMENSION CA(NM,NM), CB(NM)
CHARACTER*10 RIS
PRINT∗,’dare il nome del file risultati’
READ*,RIS
OPEN(UNIT=2,FILE=RIS)
PRINT∗,’dare n <=’,NM
READ∗,N
PRINT∗,’dare A per colonne’
READ∗,((A(I,J), I=1,N), J=1,N)
C Si calcola B e si fanno copie di A e B, da usarsi per il calcolo
C del vettore residuo (GAUSSPIV e RISOLVI modificano A e B)
DO 10 I=1,N
B(I)=0.
DO 5 J=1,N
B(I)=B(I)+ A(I,J)
CA(I,J)=A(I,J)
77
5
CONTINUE
CB(I)=B(I)
10
CONTINUE
C Si triangolarizza A. Se risulta singolare (IFLAG=1),ci si ferma.
CALL GAUSSPIV(N,A,NM,IPIV,IFLAG)
IF(IFLAG.EQ.1)THEN
WRITE(2,1000)
STOP
END IF
C Si procede alla risoluzione del sistema triangolare
CALL RISOLVI(N,NM,A,IP,B,X)
C Si calcolano vettore residuo e errore e relative norme
CALL RESIDUO(N,NM,CA,CB,X,R)
DO 50 I=1,N
E(I)=X(I)-1.D0
50 CONTINUE
RNORM= VETNINF(R,N)
BNORM= VETNINF(B,N)
ENORM= VETNINF(E,N)
WRITE(2,2000)
WRITE(2,3000) (X(I),IPIV(I),I=1,N)
WRITE(2,3100) ENORM, RNORM/BNORM
1000 FORMAT(/2X,’ matrice singolare’)
2000 FORMAT(10X,’soluzione’, 6X ,’ipiv’//)
3000 FORMAT(2X,D22.15,2X,I3)
3100 FORMAT(/10X,’errore: ’, D13.6/10X,’residuo relativo: ’, D13.6)
END
C =========================================
SUBROUTINE GAUSSPIV(N,A,NMAX,IPIV,IFLAG)
IMPLICIT DOUBLE PRECISION (A-H,O-Z)
DIMENSION A(NMAX,N), IPIV(N-1)
C
C Metodo di Gauss con pivoting, fase 1: triangolarizzazione di A
C
Argomenti in ingresso:
C N, dimensione del sistema
C A, matrice dei coefficienti
C NMAX, dimensione principale di A
C Argomenti in ingresso:
C Argomenti in uscita:
C A, nel triangolo superiore contiene la matrice triangolare
C
nel triangolo strettamente inferiore contiene i moltiplicatori
C IPIV, vettore degli indici delle righe pivotali
C IFLAG, indicatore del risultato:
C
=0 tutto OK
78
C
=1 matrice singolare
C
IFLAG=0
DO 1000 K=1,N-1
IPIV(K)=K
AMAX= ABS(A(K,K))
DO 10 I=K+1,N
IF(AMAX.LT.ABS(A(I,K))THEN
AMAX=ABS(A(I,K))
IPIV(K)=I
END IF
10
CONTINUE
IF(AMAX.EQ.0)THEN
IFLAG=1
RETURN
END IF
IF(IPIV(K).NE.K)THEN
DO 20 J=K,N
S=A(IPIV(K),J)
A(IPIV(K),J)=A(K,J)
A(K,J)=S
20
CONTINUE
END IF
DO 40 I=K+1,N
A(I,K)=A(I,K)/A(K,K)
DO 40 J=K+1,N
A(I,J)=A(I,J)-A(I,K)∗A(K,J)
40
CONTINUE
1000 CONTINUE
IF(A(N,N).EQ.0.)IFLAG=1
RETURN
END
C =========================================
SUBROUTINE RISOLVI(N,NMAX,A,IPIV,B,X)
IMPLICIT DOUBLE PRECISION (A-H,O-Z)
DIMENSION A(NMAX,N), B(N), X(N),IPIV(N-1)
C
C Metodo di Gauss con pivoting, fase 2: modifica di b e risoluzione del
C sistema triangolare con il metodo di sostituzione all’indietro
C
C Argomenti in ingresso:
C N, dimensione del sistema
C NMAX, dimensione principale di A
C A, IPIV come usciti dalla SUBROUTINE GAUSSPIV
C B, vettore dei termini noti
C Argomenti in uscita:
C B, modificato con le trasformazioni di Gauss
C X, soluzione del sistema
79
C Trasformazione di B
DO 1000 K=1,N-1
IF(IPIV(K).NE.K)THEN
S=B(K)
B(K)=B(IPIV(K))
B(IPIV(K))=S
END IF
DO 100 I=K+1,N
B(I)=B(I)-A(I,K)∗B(K)
100
CONTINUE
1000 CONTINUE
C Sostituzione all’indietro
X(N)=B(N)/A(N,N)
DO 200 I=N-1,1,-1
S=0.
DO 150 J=I+1,N
S=S+A(I,J)∗X(J)
150
CONTINUE
X(I)=(B(I)-S)/A(I,I)
200 CONTINUE
END
C =========================================
SUBROUTINE RESIDUO(N,NR,A,B,X,R)
IMPLICIT DOUBLE PRECISION (A-H,O-Z)
DIMENSION A(NR,N), B(N), X(N),R(N)
C Calcolo del vettore residuo r=Ax-b
DO 200 I=1,N
S=-B(I)
DO 99 J=1,N
S=S+A(I,J)∗X(J)
99
CONTINUE
R(I)=S
200 CONTINUE
RETURN
END
C =========================================
FUNCTION VETNINF(V,N)
IMPLICIT DOUBLE PRECISION (A-H,O-Z)
DIMENSION V(N)
VETNINF=0.
DO 100 I=1,N
IF(ABS(V(I)).GT.VETNINF) VETNINF=ABS(V(I))
100 CONTINUE
END
80
14
Istruzione EXTERNAL
ext
Consideriamo la SUBROUTINE NEWTON dell’Esempio 2. Un utente che
voglia utilizzarla deve scrivere due FUNCTION per descrivano la funzione
di cui si vuole calcolare uno zero e la sua derivata; questi sottoprogrammi
devono necessariamente chiamarsi F e DER, perché il linker ha bisogno di
un modulo oggetto di nome F e uno di nome DER per creare il programma
eseguibile. In alcune situazioni questo vincolo sui nomi può essere restrittivo. Ad esempio, l’utente può avere già due FUNCTION atte allo scopo ma
con nomi diversi da F e DER, oppure può voler scrivere un programma per
risolvere due equazioni corrispondenti a due diverse funzioni (è ovvio che
le FUNCTION corrispondenti ai due problemi non possono chiamarsi nello stesso modo). Per questo motivo, converrebbe modificare l’intestazione
della SUBROUTINE NEWTON inserendo i nomi F e DER nell’elenco degli
argomenti muti, in modo tale i due nomi diventino formali; in questo modo, gli argomenti attuali corrispondenti, ovvero i nomi dei sottoprogrammi
realmente esistenti, potranno essere diversi da F e DER, esattamente come
succede per tutti gli argomenti di un sottoprogramma. In questa ottica, la
lista degli argomenti muti della SUBROUTINE diventerebbe:
(F,DER,IUNIT,X,KM,TOLF,TOLX,IP,IND,FX,K)
e la parte di documentazione che recita
c
c
c
L’utente deve fornire un sottoprogramma FUNCTION F(X)
e un sottoprogramma FUNCTION DER(X) che descrivono
rispettivamente la funzione f e la sua derivata
sarebbe da intendersi vincolante per quanto concerne gli argomenti dei sottoprogrammi F e DER, ma non per quanto riguarda i nomi. Con questa
nuova intestazione, si potrebbe concepire la seguente chiamata:
CALL NEWTON(F1,D1,IUNIT,X,KM,TOLF,TOLX,IP,IND,FX,K)
purché siano disponibili una FUNCTION F1 e una FUNCTION D1, entrambe dipendenti da un unico argomento reale.
Affinché il linker sappia che i nomi dei moduli oggetto da cercare sono
F1 e D1, e l’associazione fra i nomi formali F e DER e i nomi attuali F1
e D1 possa essere correttamente realizzata in fase di esecuzione, occorre
informare il compilatore che questi ultimi sono nomi di sottoprogrammi,
e non semplicemente nomi di variabili. Questa informazione viene fornita
tramite l’istruzione dichiarativa
EXTERNAL F1, D1
che ovviamente deve comparire nell’unità di programma chiamante. In
questo modo sarebbe possibile concepire una sequenza di istruzioni come
quella tratteggiata qui sotto, in cui si risolvono più equazioni:
81
EXTERNAL F1,D1, F2,D2, F3, D3
..
.
CALL NEWTON(F1,D1,IUNIT,X,KM,TOLF,TOLX,IP,IND,FX,K)
..
.
CALL NEWTON(F2,D2,IUNIT,X,KM,TOLF,TOLX,IP,IND,FX,K)
..
.
CALL NEWTON(F3,D3,IUNIT,X,KM,TOLF,TOLX,IP,IND,FX,K)
Il programma in questione sarebbe composto da otto unità di programma: il
programma principale, la SUBROUTINE NEWTON, e le function F1, F2,
F3, D1, D2 e D3.
Alcuni programmatori, tipicamente quelli che hanno l’abitudine di specificare il tipo di tutte le variabili anche quando non è necessario, sono soliti
inserire un’istruzione EXTERNAL anche nel sottoprogramma fra i cui argomenti muti compare un nome di sottoprogramma. Ad esempio, questi
programmatori inserirebbero nella SUBROUTINE NEWTON con la nuova
intestazione l’istruzione
EXTERNAL F, DER
È importante ribadire che questa dichiarazione non è assolutamente necessaria per la corretta compilazione del sottoprogramma e tanto meno per la
corretta esecuzione, esattamente come non lo è una dichiarazione del tipo
INTEGER N. Dichiarazioni superflue come queste vanno intese in generale
come nient’altro che informazioni per l’utilizzatore del sottoprogramma.
15
Istruzione COMMON
com
Per quanto abbiamo visto finora, lo scambio di informazioni fra unità di programma avviene esclusivamente attraverso il meccanismo di associazione fra
argomenti muti e argomenti attuali (o, come si dice abitualmente, attraverso
la lista degli argomenti). Ci sono situazioni in cui questo può provocare un
appesantimento del lavoro del programmatore.
lavoro
Per esempio, abbiamo già osservato nel paragrafo 12.3 cosa può succedere
in presenza di una catena di chiamate del tipo
P. principale ⇒ Sottoprog. 1 ⇒ Sottoprog. 2 ⇒ Sottoprog. 3
Se il sottoprogramma 3 ha bisogno di un vettore di lavoro dimensionato in
modo variabile, questo vettore deve far parte della lista degli argomenti muti
dei sottoprogrammi 1 e 2 perchè l’indirizzo del vettore attuale deve arrivare
dal programma principale attraverso tutti gli anelli della catena. Questo
vincolo non riguarda ovviamente solo le variabili di lavoro, ma in generale
82
tutte le variabili con dimensionamento variabile o indefinito, e può talvolta
costringere i programmatori a scrivere liste degli argomenti molto lunghe 22 .
Esaminiamo ora un’altra situazione in cui l’uso esclusivo delle liste di
argomenti come strumento per la trasmissione delle informazioni fra unità
di programma può diventare molto oneroso. A questo scopo consideriamo
ancora il sottoprogramma NEWTON. Qualunque sia il nome che si utilizza
per il sottoprogramma che descrive la funzione f , questo deve prevedere
un solo argomento muto, reale e scalare; l’obbligo deriva dal modo in cui
la funzione F viene usata nella SUBROUTINE NEWTON. Supponiamo
ora di voler scrivere un programma in cui si usa la SUBROUTINE per
risolvere un’equazione dipendente da un parametro, per diversi valori di
questo parametro. Ad esempio, l’equazione potrebbe essere:
f (x; α) = x2 − αx + cos(3x + α) = 0,
dove α è il parametro, a cui vogliamo assegnare i valori 0.5, 1.5 e 2. A causa
dell’impossibilità di usare la lista degli argomenti muti per trasmettere alla
FUNCTION il valore di α, siamo costretti a scrivere tre diverse FUNCTION,
una per ogni valore di α:
FUNCTION F1(X)
ALPHA=0.5
F1=X∗∗2-ALPHA∗X+COS(3.∗X+ALPHA)
END
FUNCTION F2(X)
ALPHA=1.5
F2=X∗∗2-ALPHA∗X+COS(3.∗X+ALPHA)
END
FUNCTION F3(X)
ALPHA=2.
F4=X∗∗2-ALPHA∗X+COS(3.∗X+ALPHA)
END
e altrettante FUNCTION da far corrispondere alla DER. Dovremmo a questo
punto fare tre chiamate alla SUBROUTINE NEWTON, secondo uno schema
analogo a quello tracciato nell’esempio precedente. In questa ottica, se volessimo usare α = 0.5k con k = 1, 2, . . . , 20, dovremmo scrivere 20 “copie”
di F e 20 di DER.
In realtà esiste un’alternativa. Ogni volta che, per un motivo qualsiasi,
non si può o non si vuole utilizzare la lista degli argomenti come mezzo
di comunicazione fra unità di programma, si può ricorrere ad un diverso
meccanismo di comunicazione offerto dal FORTRAN: i blocchi COMMON.
22
Anche se un programmatore esperto sa come fare per evitarlo; ma questo è un discorso
che esula dallo scopo di queste dispense.
83
Un blocco COMMON è un insieme di locazioni di memoria consecutive
alle quali hanno accesso più unità di programma, che vi fanno riferimento
con nomi locali. Ad esempio, un blocco COMMON costituito da 3 locazioni
per numeri floating point in precisione semplice potrebbe essere visto come
un unico vettore reale di 3 elementi da una unità di programma e come
3 variabili reali scalari da un altro. Per definire un blocco COMMON si
usa l’istruzione dichiarativa caratterizzata dalla parola chiave COMMON.
Se X,Y e Z sono tre variabili reali scalari, l’istruzione
COMMON X,Y,Z
definisce un blocco COMMON formato da tre locazioni consecutive. Nell’unità di programma in cui questa istruzione compare, le tre locazioni sono
identificate dai nomi X, Y e Z. Se in un’altra unità dello stesso programma
compare l’istruzione
COMMON A,B,C
le stesse locazioni sono identificate in quella unità dai nomi A, B e C.
L’istruzione COMMON può essere usata anche per dichiarare i vettori, alla
stregua delle istruzioni di specificazione di tipo. Cosı̀, l’istruzione
COMMON X(3)
dice al compilatore che X è un vettore di 3 elementi, le cui locazioni costituiscono un blocco COMMON da spartire con altre unità di programma.
Con questo nuovo strumento si può risolvere brillantemente la situazione
precedente delle equazioni dipendenti da un parametro. Possiamo infatti
scrivere un programma principale del tipo:
PROGRAM EQALFA
COMMON ALPHA
EXTERNAL F,DER
..
.
DO 100 K=1,20
ALPHA=0.5∗K
CALL NEWTON(F,DER,IUNIT,X,KM,TOLF,TOLX,IP,IND,FX,K)
..
.
100 CONTINUE
END
dove le funzioni F e DER acquisiscono il valore del parametro accedendo al
blocco COMMON:
FUNCTION F(X)
COMMON ALPHA
FALFA=X∗∗2-ALPHA∗X+COS(3.∗X+ALPHA)
END
84
FUNCTION DER(X)
COMMON ALPHA
DALFA=2.∗X-ALPHA-3.∗SIN(3.∗X+ALPHA)
END
È importante sottolineare che l’istruzione COMMON compare soltanto nelle
unità di programma in cui la variabile coinvolta (ALPHA) viene utilizzata:
nel programma principale, dove le viene assegnato il valore, e nelle due
FUNCTION F e DER che usano questo valore. Non è invece assolutamente
necessario inserire l’istruzione anche nella SUBROUTINE NEWTON perché
in questa SUBROUTINE la variabile ALPHA non interviene. Di fatto, possiamo dire che in questo modo abbiamo scavalcato un anello della catena di
chiamate, mettendo in comunicazione direttamente il programma principale
con le FUNCTION F e DER.
L’istruzione COMMON può essere usata per definire più di un blocco
COMMON all’interno di un programma FORTRAN. In questo caso i blocchi
vengono contraddistinti da un nome e vengono chiamati blocchi COMMON
etichettati, mentre quello che abbiamo usato prima è un blocco non etichettato, detto anche blank COMMON. Per definire un blocco etichettato si usa
l’istruzione COMMON nella forma
COMMON/nome/ lista
Ad esempio, l’istruzione
COMMON/C1/ A(1000)
definisce un blocco COMMON di nome C1 formato da 1000 locazioni di
memoria per dati reali, identificate come elementi di un vettore di lunghezza 1000. Un’altra unità di programma potrebbe avere accesso alle stesse
locazioni di memoria identificandole come elementi di una matrice di 10
righe e 100 colonne (ordinati per colonna) usando l’istruzione
COMMON/C1/ A(10,100)
AGM
Per ulteriori dettagli sull’istruzione COMMON rimandiamo a [1].
Riferimenti bibliografici
AGM
MR
Overton
[1] G.Aguzzi, M.G. Gasparo, M. Macconi, FORTRAN 77 uno strumento
per il calcolo scientifico, Pitagora ed. 1987.
[2] M. Metcalf, J. Reid, Fortran 90/95 explained,
Publications, 1996.
Oxford Science
[3] M.L. Overton dal, Numerical computing with IEEE floating point
arithmetic, SIAM, 2001.
85