- Tipo di dato da leggere è constituito da numeri double.
- Tipo di contenitore di dati è un array (dinamico) del C.
- Operazioni sui dati vengono svolte mediante funzioni.
Prima di incominciare a scrivere il codice è utile ripassare rapidamente alcuni elementi base del linguaggio :
Passaggio di
input da linea di comando
È possibile passare al programma degli input direttamente da
riga di comando, ad esempio:
./programma <input1> <input2>
per fare questo nella dichiarazione del main bisogna aggiungere due
argomenti:
main(int argc, char **argv)
argc è il numero di argomenti presenti sulla riga di
comando. Il valore di argc è sempre maggiore di 0 poiché il primo argomento
è il nome del programma.
argv è un array di argc elementi che contiene le stringhe di
caratteri passate da riga di comando. Quindi argv[0] è
il nome
del programma, argv[1] il primo argomento, ecc..
Se da riga di comando passiamo un numero, esso verrà
passato tramite argv come una stringa di caratteri, per convertire una
stringa di caratteri in un numero intero si usa la funzione
atoi(char*) (che è contenuta in <cstdlib>):
int N;
N = atoi(argv[1]);
Per completezza di informazione, la funzione corrispondente per convertire una
stringa di caratteri in un numero reale è atof(char*),
anch'essa disponibile in <cstdlib>.
|
cin e cout
L'output
su schermo e l'input da tastiera sono gestiti in C++ usando gli oggetticin
e cout, che sono definiti
nella
libreria iostream.h (che si include con #include <iostream> senza
il .h!).
Il principale vantaggio di questi oggetti rispetto a scanf e printf (usati nel C) è che non è
necessario specificare il tipo (double, string, etc) che si sta
passando all'oggetto.
Uso di cout:
cout <<"A = " <<a <<endl;
<< serve a passare le variabili allo stream di output;
<<"A = " stampa "A = " (senza apici) a schermo;
<<a stampa il valore della variabile a a schermo,
qualsiasi sia il tipo di a;
<<endl (end line) stampa la fine della riga e svuota il
buffer. In generale, senza endl nessuna scritta appare a video.
Uso di cin:
cin >> a;
>> serve a prendere le variabili dallo stream di input;
>>a legge da video un contenuto appropriato e lo salva nella variabile a.
ATTENZIONE se a è una variabile int e voi a schermo digitate
2.34, il valore di a sarà convertito a 2. Se digitate a schermo
"pippo", non sarà possibile convertirlo in un numero, ed il valore di a rimarrà inalterato.
|
Allocazione dinamica della memoria
L'allocazione
dinamica della memoria consente di decidere al momento dell'esecuzion, runtime (e non a livello di
compilazione) quanta memoria il programma deve allocare.
In C++ l'allocazione (e la deallocazione) dinamica della memoria viene
effettuata con gli operatori new
e delete:
Il comando
double *x = new double[N];
crea un puntatore x a una zona
di memoria di N double (cioè a
un array di double con N
elementi)
Il comando
delete[] x;
dealloca la memoria. Ciò vuol dire che un tentativo di accedere agli
elementi di x dopo il comando delete risulterà in un errore
di segmentation violation.
È estremamente importante ricordarsi di deallocare la memoria. Infatti
in programmi complessi che utilizzano molta memoria (o in cicli che
continuano ad allocare memoria), l'assenza della deallocazione può
portare a consumare progressivamente tutta la memoria RAM della macchina
(memory leak),
causando un blocco del sistema.
Nel caso si allochino vettori (come nel nostro caso), la presenza delle
parantesi [] dopo delete indica che bisogna deallocare tutta la zona
di memoria. Il comando
delete x;
crea un memory leak, perché dealloca solo lo spazio della prima componente del vettore, non di tutto il vettore.
|
fstream
L'input e
l'output da files è gestito in C++ dalla libreria fstream.h.
I principali oggetti sono ifstream (input file stream) e ofstream
(output file stream).
Gli stream vengono dichiarati e inizializati come:
#include <fstream>
using namespace std;
ifstream inputFile("nomeInput.txt") ofstream outputFile("nomeOutput.txt")
Per controllare che il file sia stato aperto con successo si può usare
il seguente codice
if(!inputFile){ cout <<"Error ...." <<endl; //stampa un messaggio return -1; //ritorna un valore diverso da quello usuale }
L'utilizzo della stream per scrivere su un file di output o per caricare
da un file di input è uguale all'uso di cin e cout
inputFile >>a; outputFile <<"pippo " <<a <<endl;
Un metodo estremamente utile di ifstream è
inputFile.eof();
che restituisce vero se si è raggiunta la fine del file e falso
altrimenti.
Dopo l'utilizzo del file e' buona norma chiuderlo con il metodo close()
inputFile.close(); outputFile.close();
|
ESERCIZIO 1.0 - Primo codice per analisi :
Proviamo a scrivere un unico codice che legga dati da file, li immagazzini in un array dinamico, calcoli
la media, la varianza e la mediana dei dati raccolti. Scriviamo su un file di output i dati riordinati
in ordine crescente. Il numero di elementi da caricare e il nome del file in cui trovare i dati sono
passati da tastiera nel momento in cui il programma viene eseguito.
Cerchiamo di costruire il codice passo passo.
Struttura del programma
Per questo primo esercizio ripassiamo la struttura generale di un programma:
|
Provate ad implementare le parti mancanti. Se non ci riuscite sbirciate pure sotto.
1) Caricamento elementi da file
In questo frammento di codice apriamo un file utilizzando un ifstream e carichiamo ndata elementi:
|
2) Calcolo della media e della varianza
In questo frammento di codice calcoliamo la media degli elementi immagazzinati nell'array data. Costruite voi usando lo stesso schema il frammento di codice per il calcolo della varianza.
|
3) Riordino elementi di un array
In questo frammento di codice riordiniamo gli elementi dell'array data in ordine crescente. Utilizziamo un semplice algoritmo di riordinamento che dovreste già conoscere il selection sort
|
4) Calcolo della mediana
In questo frammento di codice calcoliamo la mediana lavorando sull'array ordinato vcopy:
|
5) Scrittura elementi su file
Infine scriviamo il vettore ordinato su un file output_file.txt:
|
- Compiliamo il programma invocando al solito g++:
g++ main.cpp -o main
- Eseguiamo il programma :
./main 1000000 data.dat
ESERCIZIO
1.1 - Codice di analisi con funzioni: :
Vogliamo ora riorganizzare il codice precedente per renderlo più modulare e facilmente
riutilizzabile. Per capirci meglio: il calcolo della media e' una operazione generale che
può essere immaginata come un blocco di codice che accetta in input un array di dati
e una dimensione e restituisce un valore ( la media appunto ). Se in uno stesso codice principale
dobbiamo calcolare più volte la media di array di dati diversi non vogliamo ripetere
più volte il frammento di codice relativo. Lo stesso vale per la lettura di un set
di dati da un file o per il calcolo della mediana. Il codice dovrebbe avere quindi una
struttura del tipo
- Dichiarazione di tutte le funzioni che verranno utilizzate.
- Programma vero e proprio int main() {....} in cui le funzioni vengono utilizzate.
- Al termine del programma principale l'implementazione di tutte le funzioni dichiarate.
Dal momento che abbiamo deciso di spezzare il codice in funzioni proviamo a fare uso di una funzione dedicata che scambi
tra loro due elementi di un array. In questo caso ripassiamo prima rapidamente come funziona il passaggio di dati
in una funzione.
Funzioni con argomenti
by reference e by value (e by pointer)
Il
passaggio di valori a una funzione può avvenire by value, by reference
o by pointer.
Ad esempio vogliamo scrivere una funzione che incrementi di uno il
valore di una variabile intera, abbiamo tre possibilità:
Tipo
|
by value (C and C++)
|
by
reference (C++ only)
|
by
pointer (C and C++)
|
Implementazione
|
void incrementa(int a){ a++; }
|
void incrementa(int &a){ a++; }
|
void incrementa(int *a){ (*a)++; }
|
Chiamata
|
int a = 0; incrementa(a);
|
int a = 0; incrementa(a);
|
int a = 0; incrementa(&a);
|
Effetto
|
a NON viene incrementato
|
a viene
incrementato
|
a viene
incrementato
|
Il passaggio dei parametri by value non funziona poiché alla funzione
vengono passate copie dei parametri.
La funzione chiamata opera su queste copie dei parametri. Qualunque
cambiamento apportato alle copie non ha
alcun effetto sui valori originali dei parametri presenti nella
funzione chiamante.
Le chiamate by pointer e by reference passano alla funzione l'indirizzo
di memoria in cui il programma ha memorizzato la variabile a. Per cui
la funzione agisce direttamente sulla variabile a e non su una copia.
|
Vediamo passo passo come fare :
Struttura del programma
Ecco come potrebbe diventare il vostro codice dopo la cura:
Il main è ora decisamente più compatto e leggibile. Quasi tutte le principali funzionalità del codice sono state scorporate in un opportuno set di funzioni.
|
- Come nel caso dell'esercizio precedente compiliamo il programma invocando al solito g++:
g++ main.cpp -o main
- Eseguiamo il programma :
./main 1000000 data.dat
ESERCIZIO
1.2 - Codice di analisi con funzioni e Makefile.:
In questo esercizio terminiamo il processo di riorganizzazione dell'esercizio 1.0. Procederemo in questo modo:
- Tutte le dichiarazioni di variabili che abbiamo messo in testa al programma le spostiamo in un file separato funzioni.h.
- Tutte le implementazioni delle funzioni in coda al programma le spostiamo in un file separato funzioni.cpp.
- Ricordiamoci di includere il file funzioni.h sia in main.cpp sia in funzioni.cpp tramite il solito #include "funzioni.h"
- Compiliamo separatamente main.cpp e funzioni.cpp utilizzando un Makefile
Prima di incominciare rivediamo rapidamente come si scrive un Makefile:
Il Makefile
Vogliamo
creare un Makefile (Makefile)
che ci permetta di compilare il nostro programma quando questo è
composto/spezzato in diversi
file sorgenti. Supponiamo di avere un codice spezzato in
main.cpp
funzioni.cpp
funzioni.h
Ovviamente possiamo compilare il tutto con
>
g++ main.cpp funzioni.cpp -o main
ma possiamo farlo in maniera molto più efficace.
La struttura/sintassi del Makefile è la seguente:
target: dipendenze
[tab] system command
Nel nostro caso
main: funzioni.cpp main.cpp
g++ funzioni.cpp main.cpp -o main
lanciando il comando make tutto viene compilato.
Possiamo scriverlo anche esplicitando le dipendenze in modo che anche
quando cambiamo il .h il tutto venga propriamente ricompilato
In questo caso
il Makefile diventa
main: main.o funzioni.o
g++ main.o funzioni.o -o main
main.o: main.cpp funzioni.h
g++ -c main.cpp -o main.o
funzioni.o : funzioni.cpp funzioni.h
g++ -c funzioni.cpp -o funzioni.o
|
ESERCIZIO
1.3 - Overloading di funzione (da
consegnare):
Aggiungete alla vostra libreria di funzioni una funzione void Print(double *, int) che permetta di scrivere gli elementi di un array a video. Questo è possibile grazie all'overloading (funzioni con stesso nome, ma con argomenti differenti).
Overloading
di
funzioni
L'overloading
delle funzioni è una funzionalità specifica del C++ che non è presente
in C. Questa funzionalità permette di poter utilizzare lo stesso nome
per funzioni diverse (cioè che compiono operazioni
diverse) all'interno dello stesso programma, a patto però che gli
argomenti forniti alla funzione siano differenti. In maniera
automatica, il compilatore sceglierà la funzione appropriata a seconda
del tipo di argomenti passati. In pratica:
void Print(double * data int ndata) {...}
void Print(const char * filename, double * data , int ndata) {...}
Le due funzioni hanno lo stesso nome, ma ovviamente
il codice al
loro interno dovrà essere differente!
Si noti che per poter fare l'overloading di una
funzione non basta che soltanto il tipo restituito dalla funzione sia
differente, ma occorre
che siano diversi i tipi e/o il numero dei parametri passati alla funzione.
|
Ulteriori suggerimenti :
Formattazione
dell'output
La C++ Standard
Library permette di manipolare la formattazione dell'output utilizzando
i manipolatori, alcuni dei quali sono dichiarati nell'header
<iomanip>.
In generale i manipolatori modificano lo stato di uno stream (cout,
cin, ofstream, ifstream...).
I manipolatori che ci serviranno per modificare l'output di numeri
floating-point sono:
fixed: stampa i
numeri senza l'uso di esponenti, ove possibile
scientific: stampa
i numeri utilizzando gli esponenti
setprecision(int n): stampa n cifre dopo la virgola
Esempio:
cout << "double number: " << setprecision(4) << double_number;
Utili per stampare i dati in una tabella sono
setw(int n): imposta la
larghezza di un campo ad n
setfill(char c): usa c come carattere di riempimento
(quello di default è lo spazio)
Ad esempio
cout <<setw(5) <<"0.132" <<setw(5) <<"234" <<endl <<setw(5) <<"10" <<setw(5) <<"12" <<endl
stampa i numeri in due colonne allineate
|
Questione di stile
Proviamo a vedere alcune possibili varianti per le funzioni relative al calcolo della media e della varianza:
La prima funzione implementa il calcolo in modo intuitivo. La seconda è meno ovvia ma se ci pensate ha lo stesso effetto con il grosso vantaggio di non conservare la somma di tutti i valori che potrebbe diventare troppo grande.
Nel caso della varianza la prima implementazione richiede una chiamata alla funzione CalcolaMedia() mentre la seconda no. La terza infine implementa il calcolo nello stesso modo visto per la media, ovvero evitando di immagazzinare somme troppo elevate.
|
|