|
Appunti informatica |
|
Visite: 1872 | Gradito: | [ Grande appunti ] |
Leggi anche appunti:Allocazione dinamica della memoriaAllocazione dinamica della memoria Quando è dichiarata una variabile, il compilatore L'i/o e la gestione dei fileL'I/O e la gestione dei file Per Input/Output (I/O) si intende l'insieme delle |
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
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.
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.
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.
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 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 . 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: %snIntero: %dn',string,var);
fprintf(stdout,'Stringa: %snIntero: %dn',string,var);
fprintf(stderr,'Stringa: %snIntero: %dn',string,var);
.
Nell'esempio, la chiamata a printf() e la prima delle due chiamate a fprintf() sono assolutamente equivalenti e producono outputs perfettamente identici sotto tutti i punti di vista . 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:PROVEPIPPO','wt')))
fprintf(stderr,'Errore nell'apertura del file.n');
else
.
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, 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:
Modalità 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 , 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:PROVEPIPPO','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 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:PROVEPIPPO','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
Modalità 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 se l'operazione riesce; in caso di errore è restituito un valore diverso da
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
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 (pag. ), 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 %fn',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 '&' (pag. ); esso non è necessario per la sola string, in quanto il nome di un array ne rappresenta l'indirizzo (pag. e seguenti).
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 a iVar ciao' a string e a fVar, come del resto ci si aspetta, e la cifra 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 è 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 fa sì che fscanf() assegni a string la sequenza 'ciao456.789' e il numero 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
l'intero è assegnato a iVar, l'intero è ignorato e il numero in virgola mobile è assegnato a fVar
Con gli specificatori di formato è possibile indicare l'ampiezza di campo, quando questa è costante . Consideriamo la seguente chiamata:
fscanf(fstream,'%3d%*2d%5f',&iVar,&fVar);
Se i dati letti sono
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.89n01234567.89
ci si aspetta, presumibilmente, di memorizzare in entrambi i casi, in cVar iVar e fVar, rispettivamente, e . 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' e . 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.
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 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 . 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 , 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:
Modalità 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 in assenza di errori, altrimenti è restituito un valore diverso da
Attenzione al listato che segue:
#include <stdio.h>
FILE *fopenWithCache(char *name,char *mode)
return(stream);
Dove si nasconde l'errore? L'array cbuf è allocato come variabile automatica e, pertanto, cessa di esistere in uscita dalla funzione (vedere pag. ); 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 pag.
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 , 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 (vedere pag. ) tra le costanti manifeste elencate di seguito, definite in FCNTL.H
Modalità 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:
Modalità 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 SYSSTAT.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 : 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:
Modalità 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
In particolare, la _open() sfrutta a fondo le funzionalità offerte dal servizio 3Dh dell'int 21h, descritto a pag.
Ciò non significa, purtroppo, che le implementazioni del C siano identiche in tutti gli ambienti. Le caratteristiche di alcuni tipi di macchina, nonché quelle di certi sistemi operativi, possono rendere piuutosto difficile realizzare ciò che in altri appare invece naturale. Ne consegue che, in ambienti diversi, non sempre la libreria standard contiene le stesse funzioni; inoltre, una stessa funzione può avere in diverse librerie caratteristiche lievemente differenti (vedere anche pag. e seguenti).
Con redirezione DOS si intende un'operazione effettuata al di fuori del codice del programma, direttamente sulla riga di comando che lo lancia, con i simboli '> >>' o '<
Non è escluso che nelle librerie di qualche compilatore C la printf() sia implementata semplicemente come un 'guscio' che chiama fprintf() passandole tutti i parametri ricevuti, ma preceduti da stdout
E' evidente che quel valore è l'indirizzo della struttura di tipo FILE: non ha molta importanza; per noi rappresenta lo stream. Circa la fopen() va ancora precisato che ciascuna delle stringhe di indicazione della modalità di apertura può essere modificata aggiungendo il carattere 'b' o, in alternativa, 't'. La 't' indica che ogni 'n', prima di essere scritto in un file, deve essere trasformato in una coppia 'rn' e che in lettura deve essere effettuata la trasformazione opposta: tale impostazione si rivela comoda per molte operazioni su file ASCII. La 'b' invece indica che il file deve essere considerato 'binario', e non deve essere effettuata alcuna trasformazione. La modalità di default è, di norma, 't'; nel C Borland è definita la variabile external _fmode, che consente di specificare il default desiderato per il programma.
La fclose() richiede che le sia passato come parametro lo stream da chiudere e non restituisce alcun valore. Per chiudere tutti gli stream aperti dal programma è disponibile la fcloseall(), che non richiede alcun parametro.
Ancora una particolarità su fseek(): uno spostamento di byte è il solo affidabile su stream associati a file aperti in modo text ('t'). Le funzioni di libreria dedicate agli stream non gestiscono correttamente il computo dei caratteri scritti o letti quando alcuni di essi siano eliminati dal vero flusso di dati (o aggiunti ad esso).
A dire il vero, ciò vale anche per printf() e fprintf(). L'analogia tra queste funzioni è tale da fa pensare che esista anche una scanf() ed è proprio così. La scanf() equivale ad una fscanf() chiamata passandole stdin come parametro stream.
Proviamo a chiarire: per lettura ridondante (read forwarding) si intende la lettura dal file di una quantità di byte superiore al richiesto e la loro memorizzazione in un buffer, nella 'speranza' che eventuali successive operazioni di lettura possano essere limitate alla ricerca dei dati nel buffer medesimo, senza effettuare ulteriori accessi fisici al disco. La scrittura ritardata (write staging) consiste nel copiare in un buffer i dati da scrivere di volta in volta, per poi trasferirne sul disco la massima quantità possibile col minimo numero di accessi fisici.
Esistono, del resto, programmi dedicati al caching dei dischi, generalmente operanti a livello di sistema operativo (ad esempio SMARTDRV.EXE, fornito come parte integrante del DOS). Inoltre non è raro il caso di dischi incorporanti veri e propri sistemi di caching a livello hardware.
Lo dimostra il fatto che uno dei campi della struttura di tipo FILE è un intero contenente proprio lo handle associato al file aperto. Detto campo (che nell'implementazione C della Borland ha nome fd) può essere utilizzato come handle del file con tutte le funzioni che richiedono proprio lo handle quale parametro in luogo dello stream.
Quando più personal computer sono collegati in rete (net), alcuni di essi mettono le proprie risorse (dischi, stampanti, etc.) a disposizione degli altri. Ciò consente di utilizzare in comune, cioè in modo condiviso, periferiche, dati e programmi (che in assenza della rete dovrebbero essere duplicati su ogni macchina interessata). In questi casi è possibile, per non dire probabile, che siano effettuati accessi concorrenti, da più parti, ai file memorizzati sui dischi condivisi tra computer: è evidente che si tratta di situazioni delicate, che possono facilmente causare, se non gestite in modo opportuno, problemi di una certa gravità. Si pensi, tanto per fare un semplice esempio, ad un database condiviso: un primo programma accede in lettura ad un record del medesimo; mentre i dati sono visualizzati, un secondo programma accede al medesimo record per modificarne il contenuto determinando così una palese incongruenza tra il risultato dell'interrogazione della base dati e la reale consistenza della medesima. E' evidente che occorre stabilire regole di accesso alle risorse condivise tali da evitare il rischio di conflitti analoghi a quello descritto.
Appunti su: |
|