Capitolo 10. - Le funzioni di Input/Output

 

 

 

 

            10.1 - Definizione del problema

 

 

                Ogni programma presuppone l’esistenza di due risorse della macchina: memoria centrale, o di lavoro, e microprocessore, rispettivamente per immagazzinare i dati e le istruzioni del programma e per eseguirne le operazioni; non è però minore l’importanza delle comunicazioni con i dispositivi periferici, o esterni alla memoria stessa: tastiera, video, file su disco, stampante ed eventualmente altri più specializzati come mouse, porte seriali o parallele, ecc. E’ difficile pensare ad un programma di qualche utilità che non utilizzi queste comunicazioni: ammesso che esso sia stato caricato in memoria ed eseguito con successo, l’utente non avrebbe nessuna indicazione e nessun grado di controllo su ciò che é avvenuto.

 

                Tutto ciò che riguarda gli scambi di informazioni tra memoria centrale e dispositivi periferici  rientra nella etichetta generale di Input/Output, o in breve I/O. I linguaggi di programmazione più ‘vecchi’, Fortran, Cobol e Basic definiscono nella stessa sintassi alcune istruzioni speciali dedicate allo scopo, la logica delle quali é però in genere palesemente diversa da quella delle altre istruzioni.

 

                In una concezione più ‘moderna’, come é accaduto per Algol, Pascal e per lo stesso C, si é preferito scorporare la parte che riguarda lo I/O dalla sintassi del linguaggio, delegandola ad una classe specifica di funzioni della libreria.

 

                Ciò permette una trattazione più flessibile, che il C ha realizzato dividendo i problemi in classi, per ognuna delle quali esistono funzioni specifiche; si tratta di considerare:

 

  (5.1.1)                   (a) - Funzioni I/O di base per la trasmissione di un singolo carattere tra la memoria ed un

                               dispositivo esterno, in cui l’operazione non é filtrata da alcuna interpretazione e ciò che

                               viene trasmesso é in ogni caso un semplice codice ASCII.

 

                               (b) - Funzioni di I/O filtrate o formattate, in cui lo scambio interessa tipi di dati organizzati,

                               che operano per gruppi di byte, con l’intervento di un criterio di traduzione tra diverse forme

                               della stessa informazione.

 

                               (c) - Funzioni di I/O per blocchi in cui il trasferimento di informazioni avviene per blocchi

                               di byte di lunghezza fissa o variabile, indipendentemente dal loro contenuto.

 

 

·         Categoria (a): è probabilmente la più importante, permettendo il controllo completo su qualunque tipo di operazione I/O; in essa rientra la funzione getch da noi incontrata in diversi esempi, nei quali il flusso di byte dalla tastiera al programma ha dovuto essere interpretato scrivendo istruzioni addizionali. Queste funzioni sono utilizzate raramente nella programmazione ‘ordinaria’.

 

·         Categoria (b): si tratta di casi come printf e scanf, in cui il flusso di byte viene decodificato secondo criteri automatici, riducendo al minimo la descrizione delle operazioni da parte dell’utente. Le funzioni di questo tipo sono quelle utilizzate più spesso in un programma C, inizialmente per l’acquisizione di dati dalla tastiera (scanf, gets) e per la loro rappresentazione a video (printf). Ne esistono varianti per lo scambio di dati tra la memoria ed un file su disco, o in stampa, ad esempio fprintf e fscansf, ed anche per operazioni di scrittura su stringhe e lettura da stringhe, sprintf e sscansf, che sono invece conversioni di dati tra diverse forme di rappresentazione in memoria.

 

·         Categoria (c): si tratta di funzioni progettate esigenze di programmazione non elementare; ad esempio il trasferimento di un array dalla memoria al disco, mantenendo per l’informazione la forma binaria della classificazione in tipi, oppure l’organizzazione di data-base ad ordinamenti multipli, problemi per i quali è necessario fare ricorso a forme speciali di organizzazione di file.

 


 

            10.2 - Funzioni orientate al carattere

 

 

                Presentiamo per prime le funzioni della classe (a), limitandoci a quelle per tastiera e video, indicando esplicitamente i tipi di dato che debbono essere forniti come argomento ed il file di testata cui esse fanno riferimento.

 

                Ricordiamo ancora che poiché la libreria standard é costruita ‘su misura’ per ogni compilatore, possono esservi variazioni anche consistenti da un sistema all’altro, ed anche da una versione all’altra, ed é quindi essenziale consultare la documentazione che accompagna il compilatore.

 

                Nel seguito ci riferiamo, come in altre occasioni, al compilatore Microsoft C, rel. 6, nella versione disponibile per il sistema operativo MS-DOS.

 

 

  (5.2.1)                   getchar (  )                           <stdio.h>

                               putchar ( char )   <stdio.h>

                               kbhit (  )                                <conio.h>

                               getch ( )                                <conio.h>

                               getche ( )                              <conio.h>

                               putch ( char )                       <conio.h>

 

                Le prime due utilizzano solo i riferimenti standard contenuti in <stdio.h>, mentre per le successive sono necessarie ulteriori specificazioni; <conio.h> deriva da console, termine con cui si indica tradizionalmente il ‘posto di controllo’ della macchina.

 

                L’informazione che viene trasmessa é in ogni caso un singolo byte, acquisito dalla tastiera e da utilizzarsi in una espressione del linguaggio, oppure generato da una espressione e destinato alla rappresentazione a video; é sufficiente fare qualche osservazione su casi particolari.

 

·         In una lettura da tastiera con getch (o una sua variante) non ci di deve attendere che i tasti che normalmente svolgono certe funzioni, come cancellazione di un carattere, tabulazione, ritorno a capo, frecce di direzione, ecc. continuino ad avere lo stesso effetto. Il codice ASCII del carattere viene invece normalmente trasmesso al programma, e gli effetti grafici a video si possono osservare (se ce ne sono) solo con varianti come getche.

 

·         la funzione kbhit, da keyboard (tastiera) ed hit (colpite), permette di controllare se il sistema operativo segnala che é stato premuto un tasto: si noti che questo non significa acquisire il valore del tasto, cosa che deve essere fatta ad esempio con una chiamata di getch.

 

·         Il numero di tasti di controllo presenti sulle tastiere attuali é molto maggiore dei codici di controllo previsti dalla tabella ASCII, che prevede, ad esempio, solo quattro spostamenti sullo schermo: tabulazione orizzontale e verticale, ritorno a capo e ritorno indietro di una posizione. Ne deriva che per la maggior parte dei tasti funzionali sono necessarie convenzioni speciali, o trattazioni estese dei codici, naturalmente variabili da sistema a sistema; in ambiente MS-DOS la convenzione adottata prevede la generazione di due byte, dei quali il primo sempre nullo.

 

·         Per la scrittura a video, putch e putchar accettano come argomento un carattere, ma ‘tollerano’ anche un intero, che viene però ridotto modulo 256; la maggior parte dei codici comporta la rappresentazione fisica del carattere grafico corrispondente (alfabetico, numerico o altro). Se però il codice é di controllo, cioè tra ASCII 0..31, l’effetto a video é di nuovo variabile con il sistema.

 

·         In genere non é possibile ottenere effetti di controllo complessi sullo schermo con putch; per questo scopo possono invece essere utilizzate le sequenze di escape ANSI; ‘possono’ in quanto il sistema deve essere predisposto per accettarle.

 

·         Tali sequenze richiedono funzioni più complesse, come printf; esse sono state definite per ottenere l’indipendenza dal sistema operativo e dallo hardware, ma é sempre necessario controllare che (a) lo schermo possa accettarle; (b) le relative opzioni siano state attivate. Anche per questo é necessario consultare la documentazione.


 

            10.3 - Funzioni per I/O formattato

 

 

                Un programma ‘vede’ il video e la tastiera come semplici sequenze lineari di caratteri; dal momento della prima operazione esiste un primo carattere, seguito da un secondo, ecc.. Il termine file sequenziale descrive appunto una simile struttura di dati e viene utilizzato in genere in riferimento al disco; ad esempio, il testo di un programma in C appartiene appunto a questa categoria.

 

                Dal punto di vista del programma, video e tastiera non sono altro che particolari file sequenziali; che il supporto fisico per la rappresentazione di dati sia fisicamente distinto non ha alcuna importanza, se considerato in rapporto con la memoria centrale.

 

                In ogni caso, si può immaginare che esista una ‘porta’ attraverso la quale può transitare solo un byte per volta, e sempre procedendo nella stessa direzione. Gli effetti fisici che questa operazione può provocare sul video, o il modo in cui i byte provenienti dalla tastiera sono organizzati in dati più complessi non hanno interesse per la modalità sequenziale della trasmissione

 

                Per fissare le idee, consideriamo in particolare le scritture a video. In origine vi sono informazioni in memoria che hanno in generale una forma binaria codificata secondo il loro tipo: int, double, ecc.; generarne una equivalente leggibile significa sottoporre il dato ad una fase di traduzione, in modo da trasformarlo in una sequenza di caratteri adeguata alla rappresentazione richiesta; ad esempio, la sequenza di cifre decimali di un valore numerico.

 

                Qualche esempio di cosa ciò significhi in dettaglio é stato visto considerando la funzione getch per il transito di dati in senso opposto, cioè dalla tastiera.

 

                Le sequenze di eventi da controllare in queste trasmissioni sono quindi piuttosto complesse; le funzioni per I/O formattato, cioè con organizzazione strutturate, permettono di risolvere la maggior parte dei problemi di filtraggio e traduzione dell’informazione in semplici schemi operativi;  esse richiedono solo <stdio.h> e sono:

 

  (10.3.1) printf ( formato, [ esp_1] ... [, esp_n ] ] ) ,

                               scanf ( formato, par_1 [, par_2] ... [, par_n] ] ) ,

                               gets ( stringa ) ,

                               puts ( stringa ) .

 

                Le prime due sono già in parte familiari; entrambe hanno come primo parametro obbligatorio una stringa di formato, che può essere costante o variabile; nella prima le espressioni esp_1, .., esp_n sono parametri passati per valore, tutti facoltativi, mentre nella seconda par_1, .., par_n sono sempre parametri passati per riferimento, poiché lo scopo essenziale di scanf é assegnare loro un valore.

 

                La stringa di formato deve essere vista come un minilinguaggio nel linguaggio ed ha regole sue proprie, finalizzate alle operazioni di filtro e traduzione automatica; essa può contenere informazione di tre tipi:

 

·         caratteri ‘neutri’, che svolgono il ruolo di ‘scenario’ per la scrittura da generare;

·         sequenze di escape, aperte dal simbolo di backslash ‘\’;

·         opzioni di formato, aperte dal simbolo ‘%’.

 

                Le opzioni disponibili sono comuni a printf e scanf, ma nel caso della seconda esse si riducono in genere ad una lista di quelle del terzo tipo.

 

                Le sequenze aperte da ‘\’ sono state presentate e commentate in (4.2.7); esse non sono altro che un modo conveniente di descrivere caratteri costanti, e potrebbero quindi essere assimilate ai caratteri ‘di sfondo’, ma é opportuno evidenziarne il ruolo di controllo.

 

                Le sole parti realmente ‘variabili’ sono quindi le opzioni ‘%’, che operano appunto come filtri, che dettano i criteri di traduzione tra le forme interne e quelle esterne dei dati. La loro modalità operativa é:

 

  (10.3.2) Nella esecuzione di una scrittura con printf, le opzioni ‘%’ presenti nella stringa di

                               formato vengono utilizzate in corrispondenza ordinata con la lista delle espressioni

                               di cui si richiede la scrittura.


 

  (10.3.4) Se una opzione ‘%’ non contiene una descrizione di formato valida, viene ignorata

                               nel corso dell’esecuzione; se invece la corrispondenza tra opzioni  ‘%’ e lista delle

                               espressioni non é biunivoca in numero e tipo, gli effetti dell’esecuzione dipendono

                               dal compilatore e dal sistema e non sono prevedibili.

 

 

                Le opzioni di formato più frequenti sono:

 

  (10.3.5) opzione                 tipo                        descrizione

 

                               c                             char                       carattere senza traduzione

                               d (oppure i)           int                          numero intero decimale (segno solo se negativo)

                               u                             unsigned int         numero intero privo di segno

                               f                              float                       numero non intero decimale

                               o                             solo interi             numero ottale

                               x (o X)                    tutti                        numero esadecimale con lettere minuscole / maiuscole

                               s                             char *                    stringhe in formato ASCIZ

                               p                             tipo *                     puntatore in notazione appropriata al sistema

 

                A tali opzioni possono essere premessi dei modificatori coerenti con i tipi corrispondenti; in genere viene utilizzato solo ‘l’, per long, nelle combinazioni ‘%ld”, “%lu”, “%lf”, che debbono essere utilizzate per i rispettivi tipi, pena effetti imprevedibili.

 

                Tra il simbolo ‘%’ ed il descrittore può inoltre essere inserita una costante, ad esempio “%3d”, “%20s”, che indica sempre la occupazione minima in numero di colonne.

 

                Per molte combinazioni é ammessa la presenza di due costanti separate da un punto, come in “8.2f”, “7.5d”, “6.6x”; il primo valore descrive ancora la occupazione minima in colonne, mentre il secondo ha significato diverso per interi e non interi: per i secondi specifica il numero di cifre richieste per la parte non intera (precisione), mentre per i primi impone la effettiva scrittura di un certo numero di cifre, quindi l’eventuale aggiunta di zeri non significativi.

 

                Vi sono anche altre possibilità, ma per esse rimandiamo alla documentazione del compilatore.

 

 

                Commentiamo ancora brevemente il caso di scanf; si é già detto che nei relativi formati compaiono in genere solo opzioni ‘%’ e si deve aggiungere che é opportuno che esse compaiano in semplice sequenza, senza spaziature, né altri caratteri addizionali.

 

                In questo caso, infatti, le regole di interpretazione del flusso di caratteri dalla tastiera sono solo quelle predefinite, che prevedono l’utilizzazione di uno o più spazi come separatore degli elementi presenti nella lista di lettura ed assimilano il cambio di riga ad un solo spazio; questa regola é semplice e sicura e l’introduzione di ‘variazioni sul tema’ può avere effetti diversi, variabili con il sistema ed il compilatore.

 

 

                Concludiamo considerando gets e puts, che hanno entrambe per argomento una stringa; la prima é la funzione di lettura e la seconda quella di scrittura.

 

                Nella stringa rilevata in lettura gli spazi vengono trattati come caratteri ordinari, e la stringa si considera conclusa colo quando viene premuto il tasto di Invio (ASCII 13); nel caso della scrittura, la differenza rispetto a printf sta nell’assenza di stringa di formato.

 

                Delle due la più utile é gets, che deve essere utilizzata quando ci si attende dalla tastiera una riga di dati, presumibilmente articolati in forma variabile. Ciò pone però il problema, non ovvio, di decodificarne i singoli componenti con istruzioni addizionali del programma.

 

                Per la gestione del valore di ritorno delle funzioni presenti in (10.3.1), che é in genere un intero, rinviamo ancora una volta alla documentazione sul compilatore.