L'I/O e la gestione dei file 

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. 

Vediamole al lavoro: 
    ....
    int var;
    char *string;

    printf("Stringa: %s\nIntero: %d\n",string,var);
    fprintf(stdout,"Stringa: %s\nIntero: %d\n",string,var);
    fprintf(stderr,"Stringa: %s\nIntero: %d\n",string,var);
    ....
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 post­elaborazioni: 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/CR­LF. 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. 

Ed ecco alcuni esempi: 
#include <stdio.h>

    ....
    FILE *fstream;
    int iVar;
    char cVar, string[80];
    float fVar;

    ....
    fscanf(fstream,"%c %d %s %f",&cVar,&iVar,string,&fVar);
    printf("%c %d %s %f\n",cVar,iVar,string,fVar);
    ....
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 non­blank 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 non­blanks 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

Attenzione al listato che segue: 
#include <stdio.h>

FILE *fopenWithCache(char *name,char *mode)
{
    FILE *stream;
    char cbuf[1024];

    if(!(stream = fopen(name,mode)))
        return(NULL);
    if(setvbuf(stream,cbuf,_IOFBF,1024)) {
        fclose(stream);
        return(NULL);
    }
    return(stream);
}
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_BINARYO_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

In particolare, la _open() sfrutta a fondo le funzionalità offerte dal   servizio3Dh dell'int21h

OK, andiamo avanti a leggere il libro... 

Non ci ho capito niente! Ricominciamo...