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).
Esempi: cout << "Scrive una stringa\n"; cout << Variabile_intera; cout << Variabile_float; ecc..... 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.
Esempio: cout << dato1 << dato2 << dato3; equivale a:
cout << dato1; cout << dato2; cout << dato3; 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.
Per esempio, l'istruzione: cout << 'A' << " ha codice ascii: " << (int)'A' << "\n"; visualizza la frase: A ha codice ascii 65
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.
Esempio: cin >> dato1 >> dato2 >> dato3; (i dati dato1, dato2, dato3 devono essere forniti nello stesso ordine).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).