11.3. Wahlfreier Dateizugriff
Bestimmend für die Art, wie auf eine Datei sinnvoll zugegriffen werden kann, ist deren innere Organisation. Unstrukturierte Dateien wie z.B. Textdateien sind typische Kandidaten für eine sequentielle Bearbeitung. Dabei werden neue Daten entweder am Ende der Datei angehängt oder vom Datei-Anfang beginnend über den bestehenden Inhalt geschrieben. Eine solche Datei wird genau in der Reihenfolge ausgelesen, in der sie geschrieben wurde.
Wird eine Datei für "Lesen" oder "Schreiben" geöffnet, dann befindet sich der Positionszeiger am Dateianfang; beim Öffnen für "Anfügen" (engl. "append") steht er am Dateiende. Jeder anschließende Lese- oder Schreibvorgang bewegt ihn automatisch um die Anzahl an Byte nach vorne, die gelesen oder geschrieben wurden. Sequentielle Zugriffe schieben den Positionszeiger auf diese Art so lange vorwärts, bis das Dateiende erreicht ist oder keine Daten mehr geschrieben werden sollen.
Selbst Textverarbeitungsprogramme, die Ihnen auf Anhieb als Gegenbeispiel für einen derartigen Umgang mit Textdateien einfallen könnten, benutzen in der Regel sequentielle Dateizugriffe: Die Einfüge-Operationen in den Text finden im Arbeitsspeicher statt, erst beim Speichern wird der Text auf einmal in eine Datei geschrieben.
Im Gegensatz dazu ist es bei Datenbanken mit fixen Größen für Datensätze und Felder nicht wünschenswert, beim Ansteuern eines bestimmten Felds die ganze Datei immer wieder vom Anfang her zu durchlaufen, bis der Positionszeiger auf dem gewünschten Eintrag steht. Vielmehr soll es dort möglich sein, beliebige Datenfelder direkt anzusteuern, indem der Positionszeiger bei Bedarf neu ausgerichtet wird. Für diesen Vorgang muß natürlich die genaue Position des gewünschten Datenfelds bekannt sein, was sich bei solchen Dateien jedoch durch relativ einfache Arithmetik erreichen läßt: Angenommen eine Datenbank verfügt über die Attribute "Vorname", "Name
"
und "Wohnort", wobei alle drei eine feste Länge von 25 Byte aufweisen. Dann berechnet sich die Position des Felds "Name"
im dritten Datensatz offensichtlich mit 2 * (3 * 25) + 25 Byte vom Dateianfang (siehe weiter unten Beispiel fseek.c).Für den wahlfreien Zugriff auf Dateien (engl. "random access") existieren Bibliotheks-Funktionen, die eine beliebige Manipulation des Positionszeigers zulassen. Eine der einfachsten davon ist rewind(). Ihre Aufgabe besteht darin, den Positionszeiger auf den Dateianfang zu richten. Ihr Prototyp ist in stdio.h wie folgt definiert:
void rewind( FILE *fp)
Das folgende kurze Programm nimmt Eingaben von stdin entgegen und schreibt sie in die Datei user.dat. Anschließend setzt rewind() den Positionszeiger wieder an den Dateianfang, und alle Daten aus user.dat werden mit printf() auf stdout ausgeben:
/* rewind.c */
#include <stdio.h>
#include <stdlib.h>
int main(void){
int iCharIn;
char szBuffer[80];
FILE *DatFile;
/* user.dat für Lesen und Schreiben öffnen */
DatFile = fopen("user.dat", "w+");
if(DatFile == NULL){
fprintf(stderr,
"Fehler beim Öffnen von user.dat\n");
exit(EXIT_FAILURE);
}
puts("Bitte Daten eingeben, beenden mit EOF:");
while( (iCharIn = getchar()) != EOF)
fputc( iCharIn, DatFile );
/* Positionszeiger auf den Datei-Anfang setzen.
* Ohne rewind() würde fgets() am Datei-Ende zu
* lesen beginnen, weil der Positionszeiger
* nach dem Schreibvorgang dort steht. Da fgets()
* beim Auftreten von EOF sofort abbricht, würden
* keine Daten eingelesen.
*/
rewind(DatFile);
puts("Inhalt von user.dat:");
while( fgets( szBuffer, sizeof(szBuffer), DatFile) != 0)
printf("%s", szBuffer);
fclose(DatFile);
return 0;
}
Die flexibelste unter den Funktionen zur Manipulation des Positionszeigers ist fseek(). Mit ihr kann er an jede beliebige Stelle der Datei gesetzt werden. Die Position, an die der Positionzeiger bewegt werden soll, errechnet sich aus "Ursprung" plus "Offset"; für den "Ursprung" sind in stdio.h folgende symbolische Konstanten definiert:
Symbolische Konstante | Bedeutung |
---|---|
SEEK_SET | Dateianfang |
SEEK_CUR | Aktuelle Position des Positionszeigers |
SEEK_END | Dateiende |
Der Prototyp von fseek(), der ebenfalls in stdio.h enthalten ist, lautet:
fseek( FILE *fp, long lOffset, int iUrsprung);
Um beispielsweise den Positionszeiger um 5 Byte hinter den Dateianfang zu setzen, müßten Sie fseek() so aufrufen:
fseek( DatFile, 5L, SEEK_SET );
Das folgende Beispiel fseek.c erzeugt eine kleine Datenbank-Datei mit den drei Feldern "Name", "Vorname" und "Wohnort" und fügt dort drei Datensätze ein. Anschließend wird im dritten Datensatz der Wert des Felds "Name" von "Taler" in "Mayer" geändert:
/* fseek.c */
#include <stdio.h>
#include <stdlib.h>
#define NR_REC 3 /* Anzahl Datensätze */
#define FLD_LEN 25 /* Feldlänge */
int main(void){
int iCount;
FILE *DatFile;
char szBuffer[80];
char *szName[NR_REC] ={"Jahn",
"Müller", "Taler"};
char *szVorName[NR_REC] ={"Petra",
"Inge", "Peter"};
char *szWohnOrt[NR_REC] ={"München",
"Stuttgart", "Bremen"};
DatFile = fopen("db.dat", "w+");
if(DatFile == NULL){
fprintf(stderr,
"Fehler beim Öffnen von db.dat\n");
exit(EXIT_FAILURE);
}
/* Daten aus den Arrays in die Datei db.dat
* schreiben
*/
for( iCount = 0; iCount < NR_REC; iCount ++ )
fprintf( DatFile, "%25s%25s%25s",
szVorName[iCount],
szName[iCount],
szWohnOrt[iCount]);
/* Positionszeiger vom Dateiende um die
* Länge von 3 Feldbreiten zurücksetzen
*/
fseek( DatFile, (long) -3 * FLD_LEN, SEEK_END);
fgets( szBuffer, sizeof(szBuffer), DatFile);
printf("3.Datensatz vor Update:\n%s\n", szBuffer);
/* Positionszeiger vom Anfang um die Länge
* von 2*3+1 Feldern verschieben (auf das
* Feld "Name" des 3. Datensatzes).
*/
fseek( DatFile,
(long) 2 * (3*FLD_LEN) + FLD_LEN,
SEEK_SET);
fprintf(DatFile, "%25s", "Mayer");
/* Positionszeiger ausgehend von der aktuellen
* Position um 2 Feldlängen zurücksetzen
*/
fseek( DatFile, (long)-2 * FLD_LEN, SEEK_CUR);
fgets( szBuffer, sizeof(szBuffer), DatFile);
printf("3.Datensatz nach Update:\n%s\n", szBuffer);
fclose(DatFile);
return 0;
}
Problematisch ist die Verwendung von fseek(), wenn eine Datei im Textmodus geöffnet wurde. In diesem Fall stimmt bekanntlich die Anzahl der gelesenen bzw. geschriebenen Zeichen nicht immer mit der Anzahl der in einer Datei tatsächlich gespeicherten Zeichen überein. Wegen der Übersetzung einzelner Zeichen ist der Abstand zwischen Datenblöcken nicht so ohne weiteres auszumachen; als Ausweg empfiehlt sich dann, die Datei entweder im Binärmodus zu öffnen oder eine Indexdatei unter Verwendung der Funktionen fgetpos() bzw. ftell() zu erstellen. Beide nachfolgend besprochenen Funktionen liefern nämlich korrekte Informationen über die Position innerhalb einer Datei, da sie die Übersetzung von bestimmten Zeichen berücksichtigen (siehe dazu weiter unten das Beispiel gotoline.c).
Wollen Sie die aktuelle Position des Zeigers speichern, um später wieder dorthin zurückkehren zu können, dann steht Ihnen dafür das Funktionspaar fgetpos() und fsetpos() zur Verfügung. Das erste speichert den aktuellen Wert des Positionszeigers in einer Variable vom Typ fpos_t. Dieser ist in stdio.h definiert und meist identisch mit long. Den Wert dieser Variablen können Sie dann an fsetpos() übergeben, um den Positionszeiger zu restaurieren. Die Verwendung der beiden Funktionen zeigt das folgende Beispiel:
/* fsetpos.c */
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv){
fpos_t OldPos;
FILE *DatFile;
char szBuffer[80];
if(argc != 2){
fprintf(stderr,
"Aufruf: fsetpos [Dateiname]\n");
exit(EXIT_FAILURE);
}
DatFile = fopen( argv[1], "r");
if(DatFile == NULL){
fprintf(stderr,
"Fehler beim Öffnen von %s\n", argv[1]);
exit(EXIT_FAILURE);
}
fgets( szBuffer, sizeof(szBuffer), DatFile);
printf("%s\n", szBuffer);
/* An fgetpos() und fsetpos() muß die Adresse
* der Variablen übergeben werden, in der der Wert
* des Positionszeigers abgepeichert wird.
*/
if( fgetpos( DatFile, &OldPos ) != 0 ){
fprintf(stderr, "Fehler bei fgetpos()\n");
exit(EXIT_FAILURE);
}
/* Verschiedene Lese- und Schreibzugriffe */
/* Positionszeiger wieder auf den alten Wert
* zurücksetzen
*/
if( fsetpos( DatFile, &OldPos ) != 0 ){
fprintf(stderr, "Fehler bei fsetpos()\n");
exit(EXIT_FAILURE);
}
fgets( szBuffer, sizeof(szBuffer), DatFile);
printf("%s\n", szBuffer);
fclose(DatFile);
return 0;
}
Schließlich ist es bei häufigen Manipulationen des Positionszeigers sehr nützlich, wenn seine aktuelle Position jederzeit ermittelt werden kann. Dafür steht die Funktion ftell() zur Verfügung. Sie gibt die Anzahl der Byte zurück, die der Positionszeiger vom Dateianfang entfernt ist. Beim Auftreten eines Fehlers ist ihr Rückgabewert -1L. Durch ftell() gelieferte Informationen stimmen nur bei binären Streams mit der Anzahl der tatsächlich gespeicherten Byte überein, bei Text-Streams berücksichtigt ftell() die Übersetzungen einzelner Zeichen. Der Prototyp für ftell() lautet:
long ftell(FILE *fp);
Das folgende Beispiel benutzt ftell(), um einen Zeilen-Index für Textdateien zu erstellen. Dabei wird die betreffende Datei zuerst mit fgetc() auf Zeilenvorschübe durchsucht und bei jedem Auftreten eines solchen der Wert des Positionszeigers in einer Variablen gespeichert. Nach der anfänglichen Index-Erstellung ist dann ein blitzschneller Zugriff auf eine bestimmte Zeile möglich:
/* gotoline.c
* erstellt für die angegebene Datei mit
* Hilfe von ftell() einen Zeilen-Index,
* der einen schnellen Zugriff auf eine
* beliebige Textzeile erlaubt
*/
#include <stdio.h>
#include <stdlib.h>
/* Prototyp für Funktion, die die Text-
* darstellung übernimmt */
void PrintLines( long *lLineIndex,
long NoLines,
long lGotoLine );
/* FILE-Pointer als globale Variable */
FILE *TextFile;
int main(int argc, char **argv){
/* lLineIndex nimmt alle Indexwerte auf */
long *lLineIndex, lNoLines = 250;
int iChar;
long lCount = 0, lUserInp;
if(argc != 2){
fprintf(stderr,
"Aufruf: gotoline [Dateiname]\n");
exit(EXIT_FAILURE);
}
TextFile = fopen( argv[1], "r");
if(TextFile == NULL){
fprintf(stderr,
"Fehler beim Öffnen von %s\n", argv[1]);
exit(EXIT_FAILURE);
}
lLineIndex = (long *) malloc(lNoLines * sizeof(long));
if( lLineIndex == NULL ){
fputs("Kein Speicher mehr frei!", stderr);
exit(EXIT_FAILURE);
}
/* Der Offset für die erste Zeile beträgt 0 */
lLineIndex[0] = 0L;
/* Datei mit fgetc() auf '\n' durchsuchen */
while( (iChar = fgetc(TextFile)) != EOF ){
/* Wenn notwendig, für lLineIndex mehr
* Speicher anfordern */
if( lCount >= lNoLines){
lNoLines += 250;
lLineIndex = (long *) realloc( lLineIndex,
lNoLines * sizeof(long));
}
if( lLineIndex == NULL ){
fputs("Kein Speicher mehr frei!", stderr);
exit(EXIT_FAILURE);
}
}
/* Wert des Positionszeigers speichern */
if( iChar == '\n')
lLineIndex[++lCount] = ftell(TextFile);
}
/* lNoLines enthält nun die Gesamtzahl aller
* vorhandenen Zeilen */
lNoLines = lCount - 1;
/* Darstellung der ersten 24 Zeilen */
PrintLines(lLineIndex, lNoLines, 0L);
/* Bis ein nicht-numerischer Wert eingegeben
* wird, Prompt darstellen und Printlines mit
* der gewünschten Zeilennummer aufrufen
*/
while(scanf("%lu", &lUserInp) != 0)
PrintLines(lLineIndex, lNoLines, lUserInp —
1);
free(lLineIndex);
fclose(TextFile);
return 0;
}
/***********************************************
*
* Funktion PrintLines()
* nimmt nach der Benutzereingabe die Bild-
* schirmdarstellung vor.
* Argumente:
* lLineIndex: Pointer auf den Zeilen-Index
* lNoLines: Gesamtzahl der Zeilen
* lGotoLine: Zeile, die angezeigt werden soll
*
* Rückgabewert:
* keiner
*
***********************************************/
void PrintLines( long *lLineIndex,
long lNoLines,
long lGotoLine ){
char szBuffer[80]; /* für 80 Spalten */
long lCount;
/* Falls zu große Zahl eingegeben wurde */
if( lNoLines —
lGotoLine <= 24 )
lGotoLine = lNoLines - 24;
/* Positionzeiger am Indexwert ausrichten */
fseek( TextFile, lLineIndex[lGotoLine], SEEK_SET);
for(lCount = 0; lCount <= 23; lCount++){
if( feof(TextFile) != 0)
break;
fgets( szBuffer, sizeof(szBuffer), TextFile);
printf("%s", szBuffer);
}
/* Prompt ausgeben. Anstelle von 'q' könnte
* natürlich jeder andere nicht-numerische
* Wert angegeben werden
*/
printf("Gehe zu Zeile (1 —
%lu), Ende mit 'q'> ",
lNoLines );
}