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
Documenti analoghi
Introduzione alla programmazione in FORTRAN
✚ L’editor per scrivere il codice sorgente Per scrivere il codice sorgente,
occorre avere a disposizione un text editor cioè un editore di testo - quali, ad
esempio, Notepad su Windows, o emacs su...