int main() {
/* All'interno della funzione deve essere inserito il codice appropriato */
return 0; /* Bisogna fornire un valore di ritorno */
}
Il preprocessore di fatto effettua delle sostituzioni
letterali
all'interno del programma, definite da alcune direttive, riconoscibili
perche' 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 iostream
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
puo' 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 e' una direttiva molto utile, ma anche subdola: FUNC(x+y)
da' 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 puo' essere utile terminare la compilazione dopo questa
fase,
utilizzando l'opzione -c del compilatore:
g++ -c -o nomefileoggetto nomefilesorgente
Se il compilatore non riesce a capire che istruzioni generare
perche'
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
e' molto esplicativo. In piu' il compilatore dice sempre a
quale riga dell file sorgente e' presente un errore. Tranne nel
caso
in cui ci si sia dimenticati un ; al termine di una riga, nel qual caso
l'informazione puo' 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 g++ senza
l'opzione
-c:
g++ -o nomeeseguibile [lista file sorgente]
[lista file oggetto] -lfilelibreria1 -lfilelibreria2...
Il comando g++
prevede molte opzioni, che si possono mettere tutte prima del -o. Una particolarmente consigliata
e' 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.
Per leggere e scrivere dati attraverso la finestra terminale, si utilizzano gli oggetti cin e cout. Questi oggetti sono dichiarati nel file iostream che quindi dovrà essere incluso nel file sorgente del programma.
Se a fosse una variabile del programma, per stampare il suo valore su terminale si usa l'istruzione:
cout << a ;se invece vogliamo leggere il suo valore da terminale, usiamo l'istruzione analoga
cin >> a ;
Il C++ non inserisce automaticamente spazi tra due oggetti che vengono stampati, e neppure un carattere di a capo al termine di una riga di stampa: questi devono essere forniti esplicitamente dal programmatore. In particolare, quando si terminano le operazioni di stampa, e' utile inserire sempre il carattere di fine riga. Questo carattere in C++ e' automaticamente definito nella costante endl.
Ora siamo in grado di scrivere quello che in quasi tutti i libri di programmazione e' il primo programma: "HelloWorld".
Questo programma semplicemente stampa le parole "Hello World!" sul terminale. Aprite con nedit un nuovo file HelloWorld.cxx:
nedit HelloWorld.cxxed inserite il testo seguente:
#include <iostream>
using namespace std;
int main() {
cout << "Hello world!" << endl ;
return 0;
}
Tutto dovrebbe essere chiaro, eccetto l'istruzione using namespace std. Questa viene utilizzata perche' molti oggetti e definizioni standard del C++ hanno il loro vero nome preceduto da std::, per cui, ad esempio, il nome completo di cin e` in realta` std::cin. In termini del linguaggio questo fatto si esprime dicendo che cin si trova nel namespace std. L'istruzione cha abbiamo aggiunto informa il compilatore che intendiamo usare oggetti definiti nel namespace std attraverso il loro nome abbreviato. Se non avessimo messo questa istruzione, il programma avrebbe dovuto esplicitare i nomi completi degli oggetti, con conseguente perdita di leggibilita`:
#include <iostream>
int main() {
std::cout << "Hello world!" << std::endl ;
return 0;
}
A questo punto siamo pronti per compilare il programma:
g++ -o HelloWorld HelloWorld.cxxed eseguirlo:
./HelloWorldCome piccolo esercizio finale, potete provare a compilare ed eseguire il programma senza includere il carattere di fine riga, e vedere cosa succede.
Fortunatamente la maggior parte dei compilatori ora effettuano automaticamente questa inclusione e non e' necessaria nessuna azione esplicita da parte dell'utente.
L'elenco delle funzioni delle librerie matematiche si trova nelle pagine
54-57 del libro di testo. In particolare, vale la pena di ricordare
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
e' abs
per le espressioni integer e fabs per le
espressioni float
(ed e' importantissimo usare le espressioni giuste al posto
giusto!)
Scrivere un programma che chieda all'utente di immettere da terminale un numero reale x e restituisca il valore |x1/3|.

#include <iostream> /* per avere le definizioni di cin e cout */
int main() {
/* inserire le dichiarazioni delle variabili da usare */
/* leggere da terminale x0, xN e N */
h=(xN-x0)/N;
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 puo' semplificare il tutto usando un'opportuna 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). e' 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 puo' 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 granularita' dell'intervallo fino ad ottenere una precisione prefissata.
Mantenendo lo stesso numero di valutazioni della funzione
sull'intervallo, la formula di Simpson migliora la precisione
dell'integrale tenendo in conto anche la convessita' della
funzione integranda. La sua applicazione richiede che N sia pari e la
formula da usare e':

La complicazione rispetto al programma precedente e' 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.
A questo proposito, il C++ fornisce un utile operatore: l'operatore % "resto della divisione per". Ad esempio il valore dell'espressione N%2 sara' 0 se N e' pari o 1 se N e' dispari.
Si verifichi su alcune funzioni di prova che la formula di Simpson e' infatti piu' precisa di quella dei trapezoidi.
Per migliorare la precisione dell'integrazione, una possibile soluzione e' 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 e' necessario ricalcolare tutto l'integrale, ma basta aggiungere alla sommatoria dei valori della funzione la valutazione nei nuovi punti aggiunti a meta' tra i punti precedentemente calcolati.
Questa possibilita' 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 e' noto a priori, concettualmente il processo di raddoppio degli intervalli puo' 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 e' 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 e' preferito
separarla dalla variabile contentente l'integrale perche'
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, e' opportuno far stampare il numero di passi necessari per raggungere la precisione voluta.