Introduzione all'I/O sui dispositivi standard
In questa lezione introdurremo le caratteristiche principali dell'I/O in C++, limitandoci per il momento all'I/O in free-format sui dispositivi standard di input e di output.
Precisiamo che useremo una libreria (dichiarata nell'header-file: <iostream.h>) che è ormai "superata" dalla Libreria Standard (alcuni compilatori danno un messaggio di warning, avvisando che si sta usando una "deprecated" (?!) library). Tuttavia questa libreria è ancora integrata nello standard e ci sembra un buon approccio per introdurre l'argomento.
Dispositivi standard di
I/O
In C++ (come in
C) sono definiti i seguenti dispositivi
standard di I/O (elenchiamo i tre principali):
stdout standard output
(di default associato al video)
stderr standard output
per i messaggi (associato al video)
stdin standard
input (di default associato alla tastiera)
stdin e
stdout sono reindirizzabili a
files nella linea di comando quando si lancia il programma
eseguibile.
Oggetti globali di
I/O
In C++ i dispositivi
standard di I/O stdout,
stderr e
stdin sono "collegati"
rispettivamente agli oggetti globali
cout,
cerr e
cin.
Oggetto (definizione
temporanea): variabile appartenente a un
tipo astratto, non
nativo del linguaggio.
Globale: visibile sempre
e dappertutto.
Un oggetto globale è
creato appena si lancia il programma, prima che venga eseguita la
prima istruzione del main.
Per definire gli oggetti
globali di I/O bisogna includere
l'header-file:
<iostream.h>.
Operatori di flusso di
I/O
In C++ sono definiti gli
operatori di
flusso di I/O
<<
(inserimento)
e
>>
(estrazione)
i cui left-operand sono rispettivamente
cout (oppure
cerr, che non menzioneremo più, in
quanto le sue proprietà sono identiche a quelle di
cout) e
cin.
Il compilatore distingue gli
operatori di
flusso da quelli di
shift dei bit (identificati dagli stessi
simboli) in base al contesto, cioè in base al
tipo degli
operandi.
Output tramite l'operatore di
inserimento
In C++ un'operazione di
output si identifica con un'operazione di
inserimento
nell'oggetto
cout:
cout
<<
dato;
dove dato è una qualsiasi
variabile o espressione di tipo
nativo (oppure una
stringa). L'istruzione significa: il
"dato" viene
"inserito"
nell'oggetto
cout (e da questo automaticamente trasferito
su stdout).
A differenza dalla funzione printf
non è necessario usare specificatori di
formato, in quanto il tipo delle
variabili è riconosciuto automaticamente (in realtà, come vedremo
più avanti, esistono anche qui degli
specificatori, detti
"manipolatori di formato", ma servono soltanto
quando la scrittura deve essere non in
free-format).
In ogni operazione viene trasferito un solo dato per volta; per cui,
se si devono scrivere più dati (specie se di
tipo diverso), vanno fatte altrettante
operazioni di inserimento, con istruzioni
separate. Alternativamente, in una stessa istruzione si possono
"impilare" più operazioni di
inserimento una di seguito all'altra.
equivale a:
questo è possibile grazie al fatto che
l'operatore
<< restituisce lo stesso
oggetto del
left-operand (cioè
cout) e che
l'associatività dell'operazione
procede da sinistra a destra.
Una variabile di tipo
char è scritta come carattere;
per scriverla come numero occorre fare il
casting.
Input tramite l'operatore di
estrazione
In C++ un'operazione di
input si identifica con un'operazione di
estrazione
dall'oggetto
cin:
cin
>>
dato;
dove
dato è un
l-value di qualsiasi
tipo nativo (oppure una variabile
stringa). L'istruzione significa: il valore
immesso da stdin (automaticamente trasferito
in cin) viene
"estratto" dall'oggetto
cin e memorizzato nella variabile
"dato".
Come le operazioni di
inserimento, anche quelle di
estrazione possono essere
"impilate" una di seguito all'altra in un'unica istruzione.
Il programma interpreta la lettura di un dato come
terminata se incontra un
blank, un carattere
di tabulazione o un
ritorno a capo. Ne consegue che, se
l'input è una
stringa, non deve contenere
blanks (né
tabs) e non può essere spezzata
in due righe. D'altra parte l'esistenza dei
terminatori
(blank,
tab o
CR) consente di immettere più
dati nella stessa riga.
Casi particolari:
i terminatori inseriti
ripetutamente o prima del dato da leggere sono ignorati
se il dato da leggere è di tipo
numerico, la lettura è
terminata quando incontra un carattere
non valido (compreso il punto decimale se
il numero è intero, cioè
non esegue conversioni di
tipo)
se il dato da leggere è di
tipo
char, legge un solo carattere
Memorizzazione dei dati introdotti da
tastiera
Se stdin è associato, come di
default, alla tastiera, la memorizzazione dei dati segue
delle regole generali, che sono le stesse sia in
C++ (lettura tramite
l'oggetto
cin) che in
C (lettura tramite le funzioni di
libreria):
la lettura non avviene direttamente, ma tramite un'area di memoria,
detta buffer di input;
il programma, appena incontra un'istruzione di lettura, si appresta
a memorizzare i dati (che distingue l'uno dall'altro riconoscendo i
terminatori) trasferendoli dal
buffer di input, finchè questo non
resta vuoto;
se il buffer di input si svuota
prima che la lettura sia terminata (oppure se il
buffer è già vuoto all'inizio
della lettura, come dovrebbe succedere sempre), il programma si ferma
in attesa di input e il controllo passa all'operatore,
che viene abilitato a introdurre dati da tastiera fino a quando non invia
un enter (indipendentemente dal numero di dati da leggere);
l'intera riga digitata dall'operatore viene poi trasferita nel
buffer di input, al quale il programma
riaccede per completare l'operazione di lettura;
se nel buffer di input restano ancora
dati dopo che l'operazione di lettura è finita, questi verranno
memorizzati durante la lettura successiva.
Come si può notare, la presenza del buffer
di input (molto utile peraltro per migliorare l'efficienza del
programma) crea una specie di "asincronismo" fra operatore
e programma, che può essere facilmente causa di errore: bisogna fare
attenzione a fornire ogni volta esattamente il numero di dati richiesti.
Comportamento in caso di errore in
lettura
Le operazioni di estrazione non
restituiscono mai espliciti messaggi di errore, tuttavia,
se il primo carattere letto non è valido (per esempio
una lettera se vuole leggere un
numero), il programma non memorizza
il dato e imposta una condizione di errore interna che inibisce anche
le successive operazioni di lettura (nel senso che tutte le istruzioni
di lettura, dal punto dell'errore in poi, vengono "saltate");
se invece il carattere non valido non è il primo, il programma
accetta il dato letto fino a quel momento, ma il carattere invalido resta
nel buffer, disponibile per le operazioni di lettura successive.
Per accorgersi di un errore (e per porvi rimedio) bisogna utilizzare
alcune proprietà
dell'oggetto
cin (di cui parleremo più
avanti).
Esempi:
cout
<< "Scrive
una
stringa\n";
cout
<<
Variabile_intera;
cout
<<
Variabile_float;
ecc.....
Esempio:
cout
<<
dato1
<<
dato2
<<
dato3;
cout
<<
dato1;
cout
<<
dato2;
cout
<<
dato3;
Per esempio, l'istruzione:
cout
<< 'A'
<<
" ha codice ascii: "
<<
(int)'A'
<<
"\n";
visualizza la frase:
A ha codice ascii 65
Esempio: cin
>>
dato1
>>
dato2
>>
dato3;
(i dati dato1,
dato2,
dato3 devono essere forniti nello stesso
ordine).