Laboratorio di Calcolo 2
proff. A. Andreazza, D. Galli, E. Spoletini, G. Tiana, A.Vairo
Università degli Studi di Milano
Anno Accademico 2004/2005
Lezione 1
Introduzione
Scopo della prima sessione di laboratorio e' di impratichirsi con i
comandi
necessari per la compilazione e lo sviluppo di programmi in C in
ambiente
Linux.
Questa sessione prevede:
- ripasso dei comandi fondamentali del sistema operativo;
- include files;
- input/output da terminale;
- cicli for;
- librerie matematiche;
- cicli while.
L'esercizio
L'esercizio consiste nello scrivere un programma per l'integrazione di
una funzione con il metodo dei trapezoidi. Il programma richiede come
input
da terminale gli estremi a e b dell'intervallo di
integrazione ed il numero n
di intervalli in cui questo viene diviso per effettuare l'integrazione
numerica. Come output, il programma stampa il valore stimato
dell'integrale.
Una volta che il programma è stato scritto e compilato con
successo, le sue funzionalità dovranno essere controllate con
funzioni il cui integrale è noto.
Qui di seguito ci sono dei suggerimenti su come impostare la
struttura
del programma e le differenze tra il FORTRAN che avete studiato nel
Laboratorio di Calcolo 1 ed il C.
main
In C non esistono programmi o subroutine, ma solo funzioni. Il
programma
eseguibile è in effetti una funzione speciale che si chiama main
ed ha un valore di ritorni di tipo int. Quindi
scrivere un programma di
fatto significa scrivere una funzione:
int main() {
/* All'interno della funzione deve essere inserito il codice appropriato */
return 0; /* Bisogna fornire un valore di ritorno */
}
La compilazione
Il compilatore C di fatto consta di tre parti fondamentali:
- il preprocessore,
- il compilatore vero e proprio,
- il linker.
I tre passi possono venire effettuati con un comando solo. La maniera
più
semplice di invocare il compilatore C è con:
gcc -o nomeeseguibile nomefilesorgente
Se non viene indicato il nome dell'eseguibile, questo viene creato
con il nome a.out.
IMPORTANTE: ricordarsi di far sempre
seguire -o dal nome del file di uscita,
dimenticarselo e scrivere gcc -o nomefilesorgente, risulta nella cancellazione del file
sorgente!
Il preprocessore di fatto effettua delle sostituzioni
letterali
all'interno del programma, definite da alcune direttive, riconoscibili
perché iniziano con un #.
La direttiva #include inserisce nel file in corso di
compilazione il testo di un altro file, di solito un file che contiene
le dichiarazioni delle funzioni da usare (come stdio.h
per esempio). Invece una direttiva
#define PIGRECO 3.1415196
sostituisce tutte le sequenze di caratteri P,I,G,R,E,C,O
incontrate nel testo con la sequenza di caratteri 3,.,1,4,1,5,1,9,6,
e risulta un modo pratico di inserire delle costanti.
Il #define
può anche venire usato per definire delle sostituzioni contenti
delle parti variabili, ad esempio
#define
FUNC(A) sin(A)/A
sostituisce espressioni come FUNC(xyx)
con sin(xyz)/xyz.
Questa è una direttiva molto utile, ma anche subdola: FUNC(x+y)
dà il risultato che vi aspettate? Come fare per avere il
risultato corretto?
Il compilatore trasforma il codice che abbiamo scritto in
una
sequenza di istruzioni comprensibili per la macchina (file oggetto). A
volte può essere utile terminare la compilazione dopo questa
fase,
utilizzando l'opzione -c del compilatore:
gcc -c -o nomefileoggetto nomefilesorgente
Se il compilatore non riesce a capire che istruzioni generare
perché
il file sorgente non rispetta le regole del C, esso fornisce un errore
di sintassi e non crea il file oggetto. Il testo degli errori di
sintassi
è molto esplicativo. In più il compilatore dice sempre a
quale riga dell file sorgente è presente un errore. Tranne nel
caso
in cui ci si sia dimenticati un ; al termine di una riga, nel qual caso
l'informazione può anche risultare piuttosto criptica.
Il linker si preoccupa di aggiungere l'informazione di tutte
le funzioni utilizzate dal file oggetto e non implementate
esplicitamente
al suo interno. Il linker viene chiamato se si invoca il gcc senza
l'opzione
-c:
gcc -o nomeeseguibile [lista file sorgente]
[lista file oggetto] -lfilelibreria1 -lfilelibreria2...
Il comando gcc
prevede molte opzioni, che si possono mettere tutte prima del -o. Una particolarmente consigliata
è l'opzione -Wall che scrive un sacco di
messaggi
su tutte le cose che, sebbene perfettamente legali in C, suonano un po'
strane al compilatore. Questi messaggi chiamati warnings sono
molto
utili per mettere in evidenza al momento della compilazione molti errori
di logica.
Funzioni di input/output
Per leggere e scrivere dati attraverso la finestra terminale, le
funzioni
da usare sono scanf e printf. Il
loro utilizzo
è stato introdotto a lezione. Però se avete dubbi, potete
sembre guardare la descrizione dettagliata di queste funzioni con il
comando
man
scanf o man printf. Ricordatevi che
scanf
ha bisogno dell'indirizzo della variabile per potervi immagazzinare il
valore letto dal terminale, quindi attenti ad usare correttamente
l'operatore
&
(=indirizzo di). Per utilizzare queste funzioni.
Librerie matematiche
Per valutare la vostra funzione, probabilmente avrete bisogno di
funzioni matematiche elementari. In FORTRAN molte funzioni matematiche
e l'elevamento
a potenza sono funzioni intrinseche del linguaggio. In C non
è
cosí semplice. Le funzioni matematiche hanno quasi tutte lo
stesso
nome delle analoghe in FORTRAN (la radice quadrata sarà sqrt, per
esempio), ma sono contenute in una libreria separata. Quindi per
poterle
utilizzare è necessario fare due cose:
- a livello di scrittura del codice, ricordarsi di includere il file
header contenente la dichiarazione delle librerie matematiche, di
modo
che il compilatore sappia di che cosa stiamo parlando. Questo si fa,
inserendo
all'inizio del file la direttiva:
#include <math.h>
che viene interpretata dal preprocessore, che inserisce nel programma
il contenuto del file math.h, che si trova tra i file di sistema; - al
momento della compilazione, bisogna dire al linker che le
funzioni matematiche
si trovano precompilate in un opportuno file di libreria. Poiché
le librerie matematiche sono una delle componenti maggiormente usate
del
C, l'opzione da fornire al compilatore è molto semplice: basta
aggiungere -lm
all'istruzione di compilazione.
Due funzioni di largo uso, ma dal nome diverso dal FORTRAN, sono
l'elevamento a potenza, pow(x,a) (=xa ), ed il valore assoluto che
è abs
per le espressioni integer e fabs per le
espressioni float
(ed è importantissimo usare le espressioni giuste al posto
giusto!)
Schema del programma
Data una funzione f(x),
definita su un certo intervallo [x0,xN],
suddiviso in N
sottointervalli di uguale dimensione, il suo integrale numerico con il
metodo dei trapezoidi è dato dalla formula:
dove

Quindi un approccio per il programma potrebbe essere:
#include <stdio.h> /* per avere le definizioni di scanf e printf */
#include <math.h> /* per avere le definizioni delle funzioni matematiche */
int main() {
/* inserire le dichiarazioni delle variabili da usare */
/* leggere da terminale x0, xN e N */
h=(xN-x0);
integrale = 0.5*h*(f(x0)+f(xN));
for ( i=1; i<N; i++ ) {
x = /* calcolare la coordinata x del punto i-esimo */
integrale += h*f(x);
}
/* stampa dell'integrale */
return 0; /* bisogna fornire un valore di ritorno */
}
dove spetta a voi completare questo schema introducendo il codice
appropriato al posto dei commenti e trasformando lo pseudocodice in un
effettivo programma.
Si noti che l'espressione esplicita della funzione f(x) viene
ripetuta almeno tre volte e, se vogliamo cambiare la funzione
integranda, dobbiamo cambiare il codice in tre punti diversi. Non si
può semplificare il tutto usando un'opporuna direttiva #define?
Dopo che siete riusciti ad ottenere un programma compilabile, provate a
vedere se fornisce il risultato corretto su alcune funzioni di prova
(ad esempio sin(x) o la radice quadrata). È istruttivo prendere
un intervallo in cui la funzione varia molto e, partendo con pochi
intervalli (N=2,4...), vedere
quando si ottiene un risultato a vostro avviso accettabile.
La realizzazione del programma ed il suo controllo costituiscono la
parte obbligatoria dell'esercizio di oggi. Chi ha tempo può
proseguire con due sviluppi del programma: la scrittura di un programma
che utilizza la formula di Simpson per la stima dell'integrale o la
scrittura di un programma che, invece di calcolare l'integrale con un
numero fisso di passi, aumenta gradualmente la granularità
dell'intervallo fino ad ottenere una precisione prefissata.
Integrazione alla Simpson
Mantenendo lo stesso numero di valutazioni della funzione
sull'intervallo, la formula di Simpson migliora la precisione
dell'integrale tenendo in conto anche la convessità della
funzione integranda. La sua applicazione richiede che N sia pari e la
formula da usare è:

La complicazione rispetto al programma precedente è che nella
lettura da terminale di N, bisogna
controllare che questo sia pari. Inoltre all'interno del ciclo for bisogna
usare pesi differenti per i termini pari (2/3) e quelli dispari (4/3)
della sommatoria.
Il C fornisce un utile operatore a proposito: l'operatore % "resto
della divisione per". Ad esempio il valore dell'espressione N%2
sarà 0 se N è
pari o 1 se N
è dispari.
Si verifichi su alcune funzioni di prova che la formula di Simpson
è infatti più precisa di quella dei trapezoidi.
Integrazione a precisione prefissata
Per migliorare la precisione dell'integrazione, una possibile soluzione
è aumentare il numero di punti di integrazione. La formula dei
trapezoidi ha una caratteristica attraente per fare questa operazione:
se si raddoppia il numero dei sottointervalli, non è necessario
ricalcolare tutto l'integrale, ma basta aggiungere alla sommatoria dei
valori della funzione la valutazione nei nuovi punti aggiunti a
metà tra i punti precedentemente calcolati.
Questa possibilità offre lo spunto per un programma che abbia in
input solo gli estremi dell'intervallo e poi proceda a valutare
l'integrale raddoppiando ogni volta il numero di intervalli fin tanto
che l'errore relativo sull'integrale non raggiunge la precisione
desiderata. Per valutare l'errore, possiamo considerare la differenza
tra due valutazioni successive dell'integrale.
Siccome il numero di passi non è noto a priori, concettualmente
il processo di raddoppio degli intervalli può stare bene
all'interno di un ciclo while:
while ( fabs( (newint-oldint)/oldint ) > EPS ) {
oldint=newint;
N*=2;
h/=2;
for ( /* costruite il for in modo che si cicli solo sui nuovi punti */ ) {
x=...
sum+=f(x);
}
newint = h*sum;
}
Nello scrivere questo frammento del codice, si è supposto che EPS sia una
costante definita mediante una direttiva #define. La
variabile sum
contiene la somma delle valutazioni delle funzioni (la parte tra
parentesi nella formula dei trapezoidi) e si è preferito
separarla dalla variabile contentente l'integrale perché
le due sono legate dal fattore h
che varia variando il numero di intervalli.
IMPORTANTE: in questo programma
bisogna fare molta attenzione all'inizializzazione corretta di tutte le
variabili!
Al termine del programma, oltre a farsi stampare il valore
dell'integrale, è opportuno far stampare il numero di
passi necessari per raggungere la precisione voluta.
Relazione
Prima di lasciare l'aula, siete pregati di riempire un piccolo formulario
con domande relative allo svolgimento dell'esercizio.