Per Input/Output (I/O) si intende l'insieme delle operazioni di ingresso
ed uscita, cioè di scambio di informazioni tra il programma e le
unità periferiche del calcolatore (video, tastiera, dischi, etc.).
Dal punto di vista del supporto dato dal linguaggio di programmazione alla
gestione dello I/O, va sottolineato che il C non comprende alcuna istruzione
rivolta alla lettura dalle periferiche né alla scrittura su di esse.
In C l'I/O è interamente implementato mediante funzioni di libreria,
in coerenza con la filosofia che sta alla base del C stesso, cioè
di un linguaggio il più possibile svincolato dall'ambiente in cui
il programma deve operare, e pertanto portabile.
Ciononostante, la gestione delle operazioni di I/O, in C, è piuttosto
standardizzata, in quanto sono state sviluppate funzioni dedicate che,
nel tempo, sono entrate a far parte della dotazione standard di libreria
che accompagna quasi tutti i compilatori[1].
Le prime versioni di dette librerie sono state sviluppate, inizialmente,
in ambiente Unix, sistema operativo in cui le periferiche sono trattate,
a livello software, come file. Il linguaggio C consente di sfruttare tale
impostazione, mediante il concetto di stream, cioè di flusso
di byte da o verso una periferica. Alla luce delle considerazioni espresse,
sembra di poter azzardare che leggere dati da un file non sia diverso che
leggerli dalla tastiera e scrivere in un file sia del tutto analogo a scrivere
sul video. Ebbene, in effetti è proprio così: associando
ad ogni periferica uno stream, esse possono essere gestite, ad alto livello,
nello stesso modo.
Gli stream
Dal punto di vista tecnico, uno stream è una implementazione software
in grado di gestire le informazioni relative all'interazione a basso livello
con la periferica associata, in modo che il programma possa trascurarne
del tutto la natura. Lo stream rappresenta, per il programmatore, una interfaccia
per la lettura e l'invio di dati tra il software e la periferica: non riveste
alcuna importanza come il collegamento tra dati e periferiche sia realizzato;
l'isolamento tra l'algoritmo e la "ferraglia" è forse
il vantaggio più interessante offerto dagli streams.
Stream standard
Il DOS rende disponibili ad ogni programma 5 stream che possono essere
utilizzati per leggere e scrivere dalla o sulla periferica associata. Essi
sono i cosiddetti streams standard: la tabella che segue li elenca e li
descrive.
STREAM STANDARD
Nome DOS
Nome C
Periferica associata per default
flusso
Descrizione
CON:
stdin
tastiera
In
Standard Input. Consente di ricevere input dalla tastiera.
Può essere rediretto su altre periferiche.
CON:
stdout
video
Out
Standard Output. Consente di scrivere sul video della
macchina. Può essere rediretto su altre periferiche.
stderr
video
Out
Standard Error. Consente di scrivere sul video della
macchina. Non può essere rediretto. E' generalmente utilizzato per
i messaggi d'errore.
COM1:
stdaux
prima porta seriale
In/Out
Standard Auxiliary. Consente di inviare o ricevere
dati attraverso la porta di comunicazione asincrona. Può essere
rediretto.
LPT1:
stdprn
prima porta parallela
Out
Standard Printer. Consente di inviare dati attraverso
la porta parallela. Può essere rediretto. E' generalmente utilizzato
per gestire una stampante.
Per maggiori approfondimenti si rimanda alla documentazione che accompagna
il sistema operativo DOS. Qui preme sottolineare che ogni programma C ha
la possibilità di sfruttare il supporto offerto dal sistema attraverso
l'implementazione degli streams offerta dal linguaggio; va tuttavia tenuto
presente che i nomi ad essi associati differiscono da quelli validi in
DOS, come evidenziato dalla tabella.
Gli stream in C
Un programma C può servirsi degli stream standard senza alcuna operazione
preliminare: è sufficiente che nel sorgente compaia la direttiva
#include <stdio.h>
Di fatto, molte funzioni standard di libreria che gestiscono l'I/O li utilizzano
in modo del tutto trasparente: ad esempio printf() non scrive
a video, ma sullo stream stdout. L'output di printf()
compare perciò a video solo in assenza di operazioni di redirezione
DOS[2] dello stesso su altre periferiche, o meglio su altri streams.
La funzione fgetchar(), invece, legge un carattere dallo standard
input, cioè da stdin: con un'operazione di redirezione
DOS è possibile forzarla a leggere il carattere da un altro stream.
Esistono, in C, funzioni di libreria che richiedono di specificare esplicitamente
qual è lo stream su cui devono operare[3].
Si può citare, ad esempio, la fprintf(), che è del
tutto analoga alla printf() ma richiede un parametro aggiuntivo:
prima del puntatore alla stringa di formato deve essere indicato lo stream
su cui effettuare l'output. Analogamente, la fgetc() può
essere considerata analoga alla getchar(), ma richiede che le
sia passato come parametro lo stream dal quale effettuare la lettura del
carattere.
Nell'esempio, la chiamata a printf() e la prima delle due chiamate
a fprintf() sono assolutamente equivalenti e producono output
perfettamente identici sotto tutti i punti di vista[4].
La seconda chiamata a fprintf(), invece, scrive ancora la medesima
stringa, ma la scrive su stderr, cioè sullo standard error:
a prima vista può risultare un po' difficile cogliere la differenza,
perché il DOS associa al video sia stdout che stderr,
perciò, per default, tutte le tre stringhe (identiche) sono scritte
a video. La diversità di comportamento degli stream appare però
evidente quando sulla riga di comando si effettui una redirezione dell'output
su un altro stream, ad esempio un file: in esso è scritto l'ouptut
prodotto da printf() e dalla prima chiamata a fprintf(),
mentre la stringa scritta dalla seconda chiamata a fprintf() continua
a comparire a video, in quanto, come si è detto poco fa, il DOS
consente la redirezione dello standard output, ma non quella dello standard
error.
E' immediato trarre un'indicazione utile nella realizzazione di programmi
che producono un output destinato a successive postelaborazioni: se
esso è scritto su stdout, ad eccezione dei messaggi di
copyright, di errore o di controllo, inviati allo standard error, con una
semplice redirezione sulla riga di comando è possibile memorizzare
in un file tutto e solo l'output destinato ad elaborazioni successive.
Il programma, inoltre, potrebbe leggere l'input da stdin per poterlo
ricevere, anche in questo caso con un'operazione di redirezione, da un
file o da altre periferiche.
Il C non limita l'uso degli stream solamente in corrispondenza con quelli
standard resi disponibili dal DOS; al contrario, via stream può
essere gestito qualunque file. Vediamo come:
#include <stdio.h>
....
FILE *outStream;
char *string;
int var;
....
if(!(outStream = fopen("C:\PROVE\PIPPO","wt")))
fprintf(stderr,"Errore nell'apertura del file.\n");
else {
if(fprintf(outStream,"Stringa: %s\nIntero: %d\n",string,var) == EOF)
fprintf(stderr,"Errore di scrittura nel file.\n");
fclose(outStream);
}
....
Quelle appena riportate sono righe di codice ricche di novità. In
primo luogo, la dichiarazione
FILE *outStream;
appare piuttosto particolare. L'asterisco indica che outStream
è un puntatore, ma a quale tipo di dato? Il tipo FILE non
esiste tra i tipi intrinseci... Ebbene, FILE è un tipo
di dato generato mediante lo specificatore typedef, che consente
di creare sinonimi per i tipi di dato. Non pare il caso di approfondirne
sintassi e modalità di utilizzo; in questa sede basta sottolineare
che quella presentata è una semplice dichiarazione di stream. Infatti,
il dichiaratore FILE cela una struct ed evita una dichiarazione
più complessa[5], del tipo struct...; outStream
è quindi, in realtà, un puntatore alla struttura utilizzata
per implementare il meccanismo dello stream, perciò possiamo riferirci
direttamente ad esso proprio come ad uno stream. Ora è tutto più
chiaro (insomma...): la prima operazione da effettuare per poter utilizzare
uno stream è dichiararlo, con la sintassi che abbiamo appena descritto.
L'associazione dello stream al file avviene mediante la funzione di libreria
fopen(), che riceve quali parametri due stringhe, contenenti,
rispettivamente, il nome del file (eventualmente completo di path) e l'indicazione
della modalità di apertura del medesimo. Aprire un file significa
rendere disponibile un "canale" di accesso al medesimo, attraverso
il quale leggere e scrivere i dati; il nome del file deve essere valido
secondo le regole del sistema operativo (il DOS, nel nostro caso), mentre
le modalità possibili di apertura sono le seguenti:
MODALITA' DI APERTURA DEL FILE CON fopen()
MODO
Significato
"r"
sul file sono possibili solo operazioni di lettura; il file
deve esistere.
"w"
sul file sono possibili solo operazioni di scrittura; il
file, se non esistente, viene creato; se esiste la sua lunghezza è
troncata a 0 byte.
"a"
sul file sono possibili solo operazioni di scrittura, ma
a partire dalla fine del file (append mode); in pratica il file
può essere solo "allungato", ma non sovrascritto. Il file,
se non esistente, viene creato.
"r+"
sul file sono possibili operazioni di lettura e di scrittura.
Il file deve esistere.
"w+"
sul file sono possibili operazioni di lettura e di scrittura.
Il file, se non esistente, viene creato; se esiste la sua lunghezza è
troncata a 0 byte.
"a+"
sul file sono possibili operazioni di lettura e di scrittura,
queste ultime a partire dalla fine del file (append mode); in pratica
il file può essere solo "allungato", ma non sovrascritto.
Il file, se non esistente, viene creato.
La fopen() restituisce un valore che deve essere assegnato
allo stream[6], perché questo possa essere in seguito utilizzato
per le desiderate operazioni sul file; in caso di errore viene restituito
NULL. Abbiamo ora le conoscenze che servono per interpretare correttamente
le righe di codice
if(!(outStream = fopen("C:\PROVE\PIPPO","wt")))
fprintf(stderr,"Errore nell'apertura del file.\n");
Con la chiamata a fopen() viene aperto il file PIPPO
(se non esiste viene creato), per operazioni di sola scrittura, con traslazione
automatica CR/CRLF. Il file aperto è associato
allo stream outStream; in caso di errore (fopen() restituisce
NULL) viene visualizzato un opportuno messaggio (scritto sullo
standard error).
La scrittura nel file è effettuata da fprintf(), in modo
del tutto analogo a quello già sperimentato con stdout
e stderr; la sola differenza è che questa volta lo stream
si chiama outStream. La fprintf() restituisce il numero
di caratteri scritti; la restituzione del valore associato alla costante
manifesta EOF (definita in STDIO.H) significa che si
è verificato un errore. In tal caso viene ancora una volta usata
fprintf() per scrivere un messaggio di avvertimento su standard
error.
Al termine delle operazioni sul file è opportuno "chiuderlo",
cioè rilasciare le risorse di sistema che il DOS dedica alla sua
gestione. La funzione fclose(), inoltre, rilascia anche lo stream
precedentemente allocato[7] da fopen(), che non può
più essere utilizzato, salvo, naturalmente, il caso in cui gli sia
assegnato un nuovo valore restituito da un'altra chiamata alla fopen().
Vi sono altre funzioni di libreria operanti su stream: ecco un esempio.
#include <stdio.h>
....
int iArray[100], iBuffer[50];
FILE *stream;
....
if(!(fstream = fopen("C:\PROVE\PIPPO","w+b")))
fprintf(stderr,"Errore nell'apertura del file.\n");
....
if(fwrite(iArray,sizeof(int),50,fstream) < 50*sizeof(int))
fprintf(stderr,"Errore di scrittura nel file.\n");
....
if(fseek(fstream,-(long)(10*sizeof(int)),SEEK_END)
fprintf(stderr,"Errore di posizionamento nel file.\n");
....
if(fread(iBuffer,sizeof(int),10,fstream) < 10)
fprintf(stderr,"Errore di lettura dal file.\n");
....
if(fseek(fstream,0L,SEEK_CUR)
fprintf(stderr,"Errore di posizionamento nel file.\n");
....
if(fwrite(iArray+50,sizeof(int),50,fstream) < 50)
fprintf(stderr,"Errore di scrittura sul file.\n");
....
fclose(fstream);
....
Con la fopen() il file viene aperto (per lettura/scrittura in
modalità binaria) ed associato allo stream fstream: sin
qui nulla di nuovo. Successivamente la fwrite() scrive su fstream
50 interi "prelevandoli" da iArray: dal momento che
la modalità di apertura "w" implica la distruzione
del contenuto del file se questo esiste, o la creazione di un nuovo file
(se non esiste), i 100 byte costituiscono, dopo l'operazione, l'intero
contenuto del file. La fwrite(), in contrapposizione alla fprintf(),
che scrive sullo stream secondo le specifiche di una stringa di formato,
è una funzione dedicata alla scrittura di output non formattato
e proprio per questa caratteristica essa è particolarmente utile
alla gestione di dati binari (come gli interi del nostro esempio). La sintassi
è facilmente deducibile, ma vale la pena di dare un'occhiata al
prototipo della funzione:
int fwrite(void *buffer,int size,int count,FILE *stream);
Il primo parametro è il puntatore al buffer contenente i dati (o
meglio, puntatore al primo dei dati da scrivere). E' un puntatore void,
in quanto in sede di definizione della funzione non ha senso indicare a
priori quale tipo di dato deve essere gestito: di fatto, in tal modo tutti
i tipi sono ammissibili. Il secondo parametro esprime la dimensione di
ogni singolo dato, e si rende necessario per le medesime ragioni poc'anzi
espresse; infatti la fwrite() consente di scrivere direttamente
ogni tipo di "oggetto", anche strutture o unioni; è sufficiente
specificarne la dimensione, magari con l'aiuto dell'operatore sizeof(),
come nell'esempio. Il terzo parametro è il numero di dati da scrivere:
fwrite() calcola il numero di byte che deve essere scritto con
il prodotto di count per size. L'ultimo parametro, evidentemente,
è lo stream. La fwrite() restituisce il numero di oggetti
(gruppi di byte di dimensione pari al valore del secondo parametro) realmente
scritti: tale valore risulta inferiore al terzo parametro solo in caso
di errore (disco pieno, etc.): ciò chiarisce il significato della
if in cui sono collocate le chiamate alla funzione.
La seconda novità dell'esempio è la fseek(), che
consente di riposizionare il puntatore al file, cioè di muoversi
avanti e indietro lungo il medesimo per stabilire il nuovo punto di partenza
delle successive operazioni di lettura o scrittura.
Il primo parametro della fseek() è lo stream, mentre il
secondo è un long che esprime il numero di byte dello spostamento
desiderato; il valore è negativo se lo spostamento procede dal punto
di partenza verso l'inizio del file, positivo se avviene in direzione opposta.
Il punto di partenza è rappresentato dal terzo parametro (un intero),
per il quale è comodo utilizzare le tre costanti manifeste appositamente
definite in STDIO.H:
MODALITA' OPERATIVE DI fseek()
Costante
Significato
SEEK_SET
lo spostamento avviene a partire dall'inizio del file.
SEEK_CUR
lo spostamento avviene a partire dall'attuale posizione.
SEEK_END
lo spostamento avviene a partire dalla fine del file.
La fseek() restituisce 0 se l'operazione riesce; in caso
di errore è restituito un valore diverso da 0.
Il codice dell'esempio, pertanto, con la prima delle due chiamate ad fseek()
sposta indietro di 20 byte, a partire dalla fine del file, il puntatore
allo stream, preparando il terreno alla fread(), che legge gli
ultimi 10 interi del file.
La fread() è evidentemente complementare alla fwrite():
legge da uno stream dati non formattati. Anche i parametri omologhi delle
due funzioni corrispondono nel tipo e nel significato, con la sola differenza
che buffer esprime l'indirizzo al quale i dati letti dal file
vengono memorizzati. Il valore restituito da fread(), ancora una
volta di tipo int, esprime il numero di oggetti effettivamente
letti, minore del terzo parametro qualora si verifichi un errore.
L'esempio necessita ancora un chiarimento, cioè il ruolo della seconda
chiamata a fseek(): il secondo parametro, che come si è
detto esprime "l'entità", cioè il numero di byte,
dello spostamento, è nullo. La conseguenza immediata è che,
in questo caso, la fseek() non effettua alcun riposizionamento;
tuttavia la chiamata è indispensabile, in quanto, per caratteristiche
strutturali del sistema DOS, tra una operazione di lettura ed una di scrittura
(o viceversa) su stream ne deve essere effettuata una di seek, anche
fittizia[8].
Il frammento di codice riportato si chiude con una seconda chiamata a fwrite(),
che scrive altri 50 interi "allungando" il file (ogni operazione
di lettura o di scrittura avviene, in assenza di chiamate ad fseek(),
a partire dalla posizione in cui è terminata l'operazione precedente).
Infine, il file è chiuso dalla fclose().
Vale ancora la pena di soffermarsi su un'altra funzione, che può
essere considerata complementare della fprintf(): si tratta della
fscanf(), dedicata alla lettura da stream di input formattato.
Come fprintf(), anche fscanf() richiede che i primi due
parametri siano, rispettivamente, lo stream e una stringa di formato ed
accetta un numero variabile di parametri. Tuttavia vi è tra le due
una sostanziale differenza: i parametri di fscanf() che seguono
la stringa di formato sono puntatori alle variabili che dovranno contenere
i dati letti dallo stream. L'uso dei puntatori è indispensabile,
perché fscanf() deve restituire alla funzione chiamante
un certo numero di valori, cioè modificare il contenuto di un certo
numero di variabili: dal momento che in C le funzioni possono restituire
un solo valore e, comunque, il passaggio dei parametri avviene mediante
una copia del dato originario, l'unico metodo
possibile per modificare effettivamente quelle variabili è utilizzare
puntatori che le indirizzino.
E' ovvio che lo stream passato a fscanf() è quello da cui
leggere i dati, e la stringa di formato descrive l'aspetto di ciò
che la funzione legge da quello stream. In particolare, per ogni carattere
diverso da spazio, tabulazione, a capo ("\n") e percentuale
fscanf() si aspetta in arrivo dallo stream proprio quel carattere;
in corrispondenza di uno spazio, tabulazione o ritorno a capo la funzione
continua a leggere dallo stream in attesa del primo carattere diverso da
uno dei tre e trascura tutti gli spazi, tabulazioni e ritorni a capo; il
carattere "%", invece, introduce una specifica di formato
che indica a fscanf() come convertire i dati provenienti dallo
stream. E' evidente che deve esserci una corrispondenza tra le direttive
di formato e i puntatori passati alla funzione: ad esempio, ad una direttiva
"%d", che indica un intero, deve corrispondere un puntatore
ad intero. Il carattere "*" posto tra il carattere "%"
e quello che indica il tipo di conversione indica a fscanf() di
ignorare quel campo.
La fscanf() restituisce il numero di campi ai quali ha effettivamente
assegnato un valore.
La stringa di formato passata a fscanf() ne determina il seguente
comportamento: il primo carattere letto dallo stream è memorizzato
nella variabile cVar; quindi sono ignorati tutti i caratteri spazio,
tabulazione, etc. (blank) incontrati, sino al primo carattere (cifra decimale)
facente parte di un intero, che viene memorizzato in iVar. Tutti
gli spazi incontrati dopo l'intero sono trascurati e il primo nonblank
segna l'inizio della stringa da memorizzare in string, la quale
è chiusa dal primo blank incontrato successivamente. Ancora una
volta, tutti i blank sono scartati fino al primo carattere (cifra decimale)
del numero in virgola mobile, memorizzato in fVar. Il primo blank
successivamente letto determina il ritorno di fscanf() alla funzione
chiamante. E' importante notare che a fscanf() sono passati gli
indirizzi delle variabili mediante l'operatore "&"
(address of); esso non è necessario
per la sola string, in quanto il nome di un array
ne rappresenta l'indirizzo.
E' importante sottolineare che per fscanf() ciò che proviene
dallo stream è una sequenza continua di caratteri, che viene interrotta
solo dal terminatore (blank) che chiude l'ultimo campo specificato nella
stringa di formato. Se, ad esempio, il file contiene
x 123 ciao 456.789 1
fscanf() assegna x a cVar, 123 a iVar,
"ciao" a string e 456.789 a fVar,
come del resto ci si aspetta, e la cifra 1 non viene letta: può
esserlo in una successiva operazione di input dal medesimo stream. Ma se
il contenuto del file è
x123ciao456.789 1
'x' è correttamente assegnato a cVar, poiché
lo specificatore %c implica comunque la lettura di un solo carattere.
Anche il numero 123 è assegnato correttamente a iVar,
perché fscanf() "capisce" che il carattere 'c'
non può far parte di un intero. Ma la mancanza di un blank tra ciao
e 456.789 fa sì che fscanf() assegni a string
la sequenza "ciao456.789" e il numero 1.00000
a fVar. Inoltre, lo specificatore %c considera i nonblanks
equivalenti a qualsiasi altro carattere: se il file contiene la sequenza
\n123 ciao 456.789 1
alla variabile cVar è comunque assegnato il primo carattere
letto, cioè il '\n' (a capo). La fscanf(), inoltre,
riconosce comunque i blanks come separatori di campo, a meno che non le
sia indicato esplicitamente di leggerli: se la stringa di formato è
"%c%d%s%f", il comportamento della funzione con i dati
degli esempi appena visti risulta invariato.
Vediamo ora un esempio relativo all'utilizzo del carattere di soppressione
'*', che forza fscanf() ad ignorare un campo: nella chiamata
fscanf(fstream,"%d %*d %f",&iVar,&fVar);
è immediato notare che i puntatori passati alla funzione sono due,
benché la stringa di formato contenga tre specificatori. Il carattere
'*' inserito tra il '%' e la 'd' del secondo
campo forza fscanf() ad ignorare (e quindi a non assegnare alla
variabile indirizzata da alcun puntatore) i dati corrispondenti a quel
campo. Perciò, se il file contiene
123 45 67.89
l'intero 123 è assegnato a iVar, l'intero 45
è ignorato e il numero in virgola mobile 67.89 è
assegnato a fVar.
Con gli specificatori di formato è possibile indicare l'ampiezza
di campo, quando questa è costante[9].
Consideriamo la seguente chiamata:
fscanf(fstream,"%3d%*2d%5f",&iVar,&fVar);
Se i dati letti sono
1234567.89
il risultato è assolutamente identico a quello dell'esempio precedente:
le costanti inserite nelle specifiche di formato indicano a fscanf()
di quanti caratteri si compone ogni campo e quindi essa è in grado
di operare correttamente anche in assenza di blank.
Vediamo ancora un esempio: supponendo di effettuare due volte la chiamata
fscanf(fstream,"%c%3d%*2d%5f",&cVar,&iVar,&fVar);
senza operazioni di seek sullo stream e nell'ipotesi che i dati presenti
nel file siano
01234567.89\n01234567.89
ci si aspetta, presumibilmente, di memorizzare in entrambi i casi, in cVar,
iVar e fVar, rispettivamente, '0', 123
e 67.89. Invece accade qualcosa di leggermente diverso: con la
prima chiamata il risultato è effettivamente quello atteso, mentre
con la seconda i valori assunti da cVar, iVar e fVar
sono, nell'ordine, '\n', 12 e 567.8. Il motivo
di tale comportamento, anomalo solo in apparenza, è che, come accennato,
lo stream è per fscanf() semplicemente una sequenza di
caratteri in ingresso, pertanto nessuno di essi, neppure il ritorno a capo,
può essere scartato se ciò non è esplicitamente richiesto
dal programmatore. Per leggere correttamente il file è necessaria
una stringa di formato che scarti il carattere incontrato dopo il float,
oppure indichi la presenza, dopo il medesimo, di un blank: "%c%3d%*2d%5f%*c"
e "%c%3d%2*d%5f " raggiungono entrambe l'obiettivo.
Le considerazioni espresse sin qui non esauriscono la gestione degli streams
in C: le librerie standard dispongono di altre funzioni dedicate; tuttavia
quelle presentate sono di utilizzo comune. Tutti i dettagli sintattici
possono essere approfonditi sulla manualistica del compilatore utilizzato;
inoltre, un'occhiatina a STDIO.H è sicuramente fonte di
notizie e particolari interessanti.
Il caching
La gestione dei file implica la necessità di effettuare accessi,
in lettura e scrittura, ai dischi, che, come qualsiasi periferica hardware,
hanno tempi di risposta più lenti[10]
della capacità elaborativa del microprocessore. L'efficienza delle
operazioni di I/O su file può essere incrementata mediante l'utilizzo
di uno o più buffer, gestiti mediante algoritmi di lettura ridondante
e scrittura ritardata, in modo da limitare il numero di accessi
fisici al disco[11]. La libreria C comprende alcune funzioni atte all'implementazione
di capacità di caching nei programmi: nonostante la massima efficienza
sia raggiungibile solo con algoritmi sofisicati[12],
vale la pena di citare la
int setvbuf(FILE *stream,char *buf,int mode,int size);
che consente di associare un buffer di caching ad uno stream. La gestione
del buffer è automatica e trasparente al programmatore, che deve
unicamente preoccuparsi di chiamare setvbuf() dopo avere aperto
lo stream con la solita fopen(): del resto il primo parametro
richiesto da setvbuf() è proprio lo stream sul quale operare
il caching. Il secondo parametro è il puntatore al buffer: è
possibile passare alla funzione l'indirizzo di un'area di memoria precedentemente
allocata, la cui dimensione è indicata dal quarto parametro, size
(il cui massimo valore è limitato a 32767); tuttavia, se il secondo
parametro attuale è la costante manifesta NULL, setvbuf()
provvede essa stessa ad allocare un buffer di dimensione size.
Il terzo parametro indica la modalità di gestione del buffer: allo
scopo sono definite (in STDIO.H) alcune costanti manifeste:
MODALITA' DI CACHING CON setvbuf()
Costante
Significato
_IOFBF
Attiva il caching completo del file (full buffering):
in input, qualora il buffer sia vuoto la successiva operazione di lettura
lo riempie (se il file ha dimensione sufficiente); nelle operazioni di
output i dati sono scritti nel file solo quando il buffer è pieno.
_IOLBF
Attiva il caching a livello di riga (line buffering):
le operazioni di input sono gestite come nel caso precedente, mentre in
output i dati sono scritti nel file (con conseguente svuotamento del buffer)
ogni volta che nello stream transita un carattere di fine riga.
_IONBF
Disattiva il caching (no buffering).
La funzione setvbuf() restituisce 0 in assenza di errori,
altrimenti è restituito un valore diverso da 0.
Dove si nasconde l'errore? L'array cbuf è allocato come
variabile automatica e, pertanto, cessa di
esistere in uscita dalla funzione; tuttavia fopenWithCache(),
se non si è verificato alcun errore, restituisce il puntatore allo
stream aperto, dopo avervi associato proprio cbuf come buffer di caching.
E' evidente che tale comportamento è errato, perché forza
tutte le operazioni di buffering a svolgersi in un'area di memoria riutilizzata,
assai probabilmente, per altri scopi. In casi analoghi a quello descritto,
è opportuno utilizzare malloc(); meglio ancora è,
comunque, lasciare fare a setvbuf() (passandole NULL
quale puntatore al buffer): ciò comporta, tra l'altro, il vantaggio
della sua deallocazione automatica al momento della chiusura dello stream.
Per un esempio pratico di utilizzo di setvbuf() vedere la utility
SELSTR.
Altri strumenti di gestione dei file
Gli stream costituiscono un'implementazione software di alto livello, piuttosto
distante dalla reale tecnica di gestione dei file a livello di sistema
operativo DOS, il quale, per identificare i file aperti, si serve di proprie
strutture interne di dati e, per quanto riguarda l'interfacciamento con
i programmi, di descrittori numerici detti handle. Questi altro
non sono che numeri, ciascuno associato ad un file aperto, che il programma
utilizza per effettuare le operazioni di scrittura, lettura e posizionamento.
Poiché il DOS non possiede routine di manipolazione diretta degli
stream, questi, internamente, sono a loro volta basati sugli handle[13],
ma ne nascondono l'esistenza e mettono a disposizione funzionalità
aggiuntive, quali la possibilità di gestire input e output formattati
nonché oggetti diversi dalla semplice sequenza di byte.
Le librerie standard del C includono funzioni di gestione dei file basate
sugli handle, i cui prototipi sono dichiarati in IO.H, tra le
quali vale la pena di citare:
int open(char *path,int operation,unsigned mode);
int _open(char *path,int oflags);
int write(int handle,void *buffer,unsigned len);
int read(int handle,void *buffer,unsigned len);
int lseek(int handle,long offset,int origin);
int close(int handle);
L'analogia con fopen(), fwrite(), fread(), fseek()
e fclose() è immediato: in effetti le prime possono essere
considerate le omolghe di queste. Non esistono, però, funzioni omologhe
di altre molto utili, quali la fprintf() e la fscanf().
Non sembra necessario dilungarsi sulla sintassi delle funzioni basate su
handle: d'altra parte qualsiasi file può sempre essere manipolato
via stream (ed è questa, tra l'altro, l'implementazione grandemente
curata e sviluppata dal C++); è forse il caso di commentare brevemente
la funzione open().
Il parametro path equivale al primo parametro della fopen()
ed indica il file che si desidera aprire.
Il secondo parametro (operation) è un intero che specifica
la modalità di apertura del file (ed ha significato analogo al secondo
parametro di fopen()), il cui valore risulta da un'operazione
di OR su bit tra le costanti manifeste elencate
di seguito, definite in FCNTL.H.
MODALITA' DI APERTURA DEL FILE CON open(): PARTE 1
COSTANTE
SIGNIFICATO
O_RDONLY
Apre il file in sola lettura.
O_WRONLY
Apre il file in sola scrittura.
O_RDWR
Apre il file il lettura e scrittura.
Le tre costanti sopra elencate sono reciprocamente esclusive. Per specificare
tutte le caratteristiche desiderate per la modalità di apertura
del file, la costante prescelta tra esse può essere posta in or
su bit con una o più delle seguenti:
MODALITA' DI APERTURA DEL FILE CON open(): PARTE 2
COSTANTE
SIGNIFICATO
O_APPEND
Le operazioni dn scrittura sul file possono esclusivamente
aggiungere byte al medesimo (modo append).
O_CREAT
Se il file non esiste viene creato e i permessi di accesso
al medesimo sono impostati in base al terzo parametro di open(),
mode. Se il file non esiste, O_CREAT è ignorata.
O_TRUNC
Se il file esiste, la sua lunghezza è troncata a 0.
O_EXCL
E' utilizzato solo con O_CREAT: se il file esiste,
open() fallisce e restituisce un errore.
O_BINARY
Richiede l'apertura del file in modo binario (è alternativa
a O_TEXT).
O_TEXT
Richiede l'apertura del file in modo testo (è alternativa
a O_BINARY).
Se né O_BINARY né O_TEXT sono specificate,
il file è aperto nella modalità impostata dalla variabile
globale _fmode, come del resto avviene con fopen() in
assenza degli specificatori "t" e "b".
Il terzo parametro di open(), mode, è un intero
senza segno che può assumere uno dei valori seguenti (le costanti
manifeste utilizzate sono definite in SYS\STAT.H):
PERMESSI DI ACCESSO AL FILE CON open()
COSTANTE
SIGNIFICATO
S_IWRITE
Permesso di accesso al file in scrittura.
S_IREAD
Permesso di accesso al file in sola lettura.
S_IREAD|S_IWRITE
Permesso di accesso al file in lettura e scrittura.
La libreria Borland comprende una variante di open() particolarmente
adatta alla gestione della condivisione di file nel networking[14]:
si tratta della
int _open(char *path,int oflags);
Il secondo parametro, oflags, determina la modalità di
apertura ed accesso condiviso al file, secondo il valore risultante da
un'operazione di or su bit di alcune costanti manifeste. In particolare,
deve essere utilizzata una sola tra le costanti O_RDONLY, O_WRONLY,
O_RDWR (proprio come in open()); possono poi essere usate,
a partire dalla versione 3.0 del DOS, le seguenti:
MODALITA' DI CONDIVISIONE DEL FILE CON _open()
COSTANTE
SIGNIFICATO
DEFINITA IN
O_NOINHERIT
Il file non è accessibile ai child process.
FCNTL.H
SH_COMPAT
Il file può essere aperto in condivisione da altre
applicazioni solo se anche queste specificano SH_COMPAT nella
modalità di apertura.
SHARE.H
SH_DENYRW
Il file non può essere aperto in condivisione da altre
applicazioni.
SHARE.H
SH_DENYWR
Il file può essere aperto in condivisione da altre
applicazioni, ma solo per operazioni di lettura.
SHARE.H
SH_DENYRD
Il file può essere aperto in condivisione da altre
applicazioni, ma solo per operazioni di scrittura.
SHARE.H
SH_DENYNO
Il file può essere aperto in condivisione da altre
applicazioni per lettura e scrittura, purché esse non specifichino
la modalità SH_COMPAT.
SHARE.H
Sia open() che _open() restituiscono un intero positivo
che rappresenta lo handle del file (da utilizzare con write(),
read(), close(), etc.); in caso di errore è restituito
1.