Im ANSI-C-Standard gibt es einige Definitionen zum Thema "Dateien", die unabhängig von der jeweiligen Maschine sind. Schauen wir uns einmal den Dateibegriff an und danach die Definitionen des Standards.
Der Text oder die Daten werden in einer Datei gespeichert. Die Programme, die man zum Bearbeiten benötigt, liegen ebenfalls in Dateien. Dateien sind allgegenwärtig. Alle Programme im PC liegen in Dateien auf der Festplatte oder der verwendeten Diskette. Es gibt von dieser Regel eigentlich nur bei den (original) IBM-PCs eine Ausnahme. Sie besitzen ein BASIC, das in ROMs abgelegt ist. Ansonsten laden wir bei Bedarf alle Programme von der Festplatte. Am Anfang ist das der Kommandointerpreter "command.com" bei DOS, eine Shell bei UNIX, und später ist es dann der Editor, ein "C"-Compiler oder ein beliebiges anderes Programm.
Man kann Dateien als Behälter für Daten auffassen. Normalerweise können Dateien gelesen und beschrieben werden. Dateien im engeren Sinne haben normalerweise die Eigenschaft, daß sie technisch auf einem Massenspeicher (Festplatte, Diskette, CD-ROM u. a.) liegen und somit das Abschalten des Rechners überleben. Wie nennen sie Plattendateien. Dateien im weiteren Sinne können auch andere Eigenschaften besitzen. Denken Sie nur an Dateien in einer RAM-Disk oder an die Gerätedateien bei UNIX. Hier sind auf der Platte nur Namen abgelegt und die Information, welcher Treiber verwendet werden soll, wenn jemand auf diesen Namen schreibt oder davon liest.
Bild 2-1: Dateiaufbau aus C-Sicht
Bei jedem Schreib- oder Lesevorgang wird automatisch der Dateiindex mitgeführt, soweit es sich um eine positionierbare Datei handelt.
Im ANSI-C-Standard hat man versucht, die unterschiedlichen Realisierungen von Dateien auf verschiedenen Systemen im Begriff der streams (byteorientierte Verbindung zwischen Programm und Datei) zu normieren. Dieses Modell gibt es auf DOS-Compilern genauso wie auf einer UNIX-Maschine. Der Programmierer kann daher C-Programme nach dem Standard schreiben, ohne wissen zu müssen, auf welcher Maschine seine Programme laufen.
Bild 2-2: Aufbau einer FILE-Struktur unter UNIX (Beispiel)
Er wird in der Informationsdatei stdio.h definiert. Am besten sehen Sie sich hier einmal Ihre eigene stdio.h an und drucken sie auch aus.
Auf jeder Maschine und sogar bei jedem Compilerhersteller kann die Struktur einen völlig unterschiedlichen Aufbau haben. Sicher ist nur, daß sie FILE heißt. (Im DOS-Beispiel wurde der Name für die Struktur weggelassen und nur der Typname definiert.)
Bild 2-3: Aufbau einer FILE-Struktur unter DOS (Beispiel)
extern FILE _streams [FOPEN_MAX];
Dieses Feld wird außerhalb des eigentlichen C-Programms angelegt. Unter UNIX wird die Shell das Feld bereitstellen, unter DOS bindet man zu jedem C-Programm ein spezielles Startmodul dazu, das dann dieses Feld beinhaltet.
Bild 2-4: Aufbau der Dateiverwaltungstabelle
Einen Eintrag in diesem Feld kann man auch als logischen Dateisteuerblock betrachten (FCB - file control block). Natürlich muß es daneben noch eine weitere Verwaltung der Datei im Betriebssystem geben. Die "FILE"-Variable im Feld enthält nur die Informationen, die für "C" wichtig sind. Im Betriebssystem liegen dann die technischen Beschreibungen.
Eine ganze Reihe von Funktionen arbeitet nur mit bestimmten Verbindungen. Dann erübrigt sich die Übergabe der Adresse.
Schließlich gibt es noch einige Funktionen, die aus historischen Gründen vorhanden sind. Normalerweise wird man dann mit Hilfe von Makros einfach den alten Namen gegen einen neuen austauschen. Aber das sehen wir uns noch im einzelnen an.
Nur mit einer geöffneten Datei können wir Daten austauschen. Bei Plattendateien kann man darüberhinaus noch positionieren, d. h. festlegen, an welcher Stelle der nächste Schreib- oder Lesevorgang stattfinden soll.
Man sagt auch, daß das Arbeiten mit Dateien verbindungsorientiert abläuft. Als besondere Dienstleistung kann auch die Umgebung eines Programms Dateien schließen. Vergißt der Programmierer eine selbst geöffnete Datei wieder zu schließen, erledigt dies die Umgebung am Programmende für ihn.
Für jede Dateiverbindung benötigen wir eine Variable, die die Verbindung beschreibt. Da alle wesentlichen Informationen in einer FILE-Variablen gespeichert werden, müssen wir uns in unserem Programm nur merken, welche FILE-Variable gerade benutzt wird. Dazu genügt eine Variable vom Typ Zeiger auf FILE.
FILE * dateiverbindung_1;
Sehen Sie sich dazu einmal mit der eingebauten Hilfestellung den Eintrag zu fopen() an. Die Hilfestellung aktivieren Sie bei TurboC dadurch, daß Sie die Taste [Shift] und [F1] drücken. Danach sind Sie im Hilfeindex und können einfach den gewünschten Namen langsam tippen, bis der richtige Eintrag ausgewählt ist. RETURN zeigt dann den Eintrag an. Ein solcher Hilfe-Eintrag heißt englisch manual page. Unter UNIX geben Sie daher das Kommando man fopen oder benutzen das xman-System unter X-Windows.
#include <stdio.h>
FILE * fopen (const char * dateiname, const
char * modus);
fopen() erwartet zwei Argumente: den Dateinamen mit absolutem oder relativem Pfad sowie die Angabe der gewünschten Bearbeitungsart. Die Funktion prüft nun, ob es diese Datei unter dem angegebenen Pfad gibt und ob sie mit der gewünschten Betriebsart bearbeitet werden kann. Der Zugriff ist auch dann nicht immer möglich, wenn die Datei existiert. Hängt der Rechner in einem Netzwerk, greift möglicherweise ein anderes Programm gerade exklusiv zu. Oder denken Sie an ein Window-System. Hier kann man mehrere Programme in verschiedenen Fenstern starten, die möglicherweise auf die gleiche Datei zugreifen möchten. Das sogenannte file locking (Datei sperren) ist aber nicht Teil des ANSI-C-Standards.
Bild 2-5: Einfaches Schreiben in eine Datei
Wurde die gewünschte Datei gefunden und ist sie bearbeitbar, dann sucht fopen() nach dem nächsten freien Eintrag in der Dateiverwaltungstabelle und liefert dessen Startadresse zurück. In den Fällen, in denen die Datei nicht gefunden wird, die Bearbeitung nicht möglich ist oder keine freie FILE-Variable mehr vorhanden ist, liefert fopen() den Wert NULL. NULL in "C" entspricht dem NIL anderer Sprachen und bedeutet "ungültige Adresse". NULL ist zumeist als die Zahl 0 definiert, die auf einen typlosen Zeiger gewandelt wird.
#define NULL ( (void *) 0)
Der Rückgabewert kann dann in der bereits angelegten Zeigervariablen gespeichert werden. Diese Zeigervariable werden wir bei allen weiteren Funktionsaufrufen übergeben, damit die Funktion weiß, welche der möglichen Dateien gerade zu bearbeiten ist.
Der Dateiname wird kann einen absoluten oder relativen Pfad beinhalten. Ein absoluter Pfadname beginnt immer an der Wurzel ("/" unter UNIX oder "\" unter DOS), ein relativer im aktuellen Verzeichnis. Der Pfad kann unter DOS auch einen Laufwerkskennbuchstaben enthalten. Bitte beachten Sie, daß auch unter DOS der normale Schrägstrich "/" zur Trennung der Namensbestandteile verwendet wird. Sollten Sie trotzdem den umgekehrten Schrägstrich unter DOS verwenden wollen, müssen Sie ihn doppelt angeben, da dieses Zeichen ein Fluchtsymbol ist.
Der Name darf dabei nicht länger werden, als in der symbolischen Konstante FILENAME_MAX beschrieben.
Als erstes Beispiel diente uns ein einfacher Schreibvorgang in eine Datei, die im aktuellen Verzeichnis liegt. Das Schreiben macht eine aus allen "C"-Einführungen gut bekannte Funktion: printf(). Hier verwenden wir die allgemeine Version fprintf(). Der Unterschied liegt nur im ersten Parameter. Hier erwartet fprint() die Angabe der Dateiverbindung.
fopen() legt für eine Dateiverbindung Ein- und Ausgabepuffer an, falls es sich nicht um ein interaktives Gerät handelt.
Die Rückgabe von NULL ist nur der Fehlerindikator. Im Abschnitt über die Fehlerbehandlungen werden wir dann den Fehler genau bestimmen.
Bild 2-6: Einfaches Lesen aus einer Datei
Zum Lesen eignen sich alle Eingabefunktionen, unabhängig davon, ob sie einzelne Zeichen, Zeilen oder andere Einheiten lesen. Im ersten Beispiel wollen wir das Gegenstück zu fprintf(), die fscanf()-Funktion verwenden.
Im ersten Lesen-Beispiel können wir nur ein Wort lesen. Für das Lesen von Zeichen oder Zeilen kennt "C" weitere Funktionen, die wir zusammenhängend diskutieren werden.
Bild 2-7: Anhängen an eine Datei
Ein typischer Vorgang für updates wäre eine Veränderung eines Kundensatzes in einer Datei. Dazu würde man den Dateiindex auf den gewünschten Kunden positionieren, den Satz lesen und verändern und nach einem neuerlichen Positionieren zurückschreiben. Die Positionierung besprechen wir bei den Dateiarten.
"update" beinhaltet also immer eine Schreib- und Leseerlaubnis. Nach dem Schreiben ist der Dateiindex weitergesetzt worden. Wenn wir einen gerade geschriebenen Satz wieder lesen wollen, dann müssen wir zuerst den Dateiindex neu setzen. Im Beispiel wurde die einfachste Funktion verwendet, die den Dateiindex auf den Beginn der Datei zurücksetzt: rewind().
Bild 2-8: Datei mit "update"-Funktion
Beim updaten, (der Duden verzeihe mir) gibt es eine Randbedingung. Nach einer Schreibfunktion darf nicht sofort anschließend eine Lesefunktion gerufen werden. Dazwischen muß eine Positionierung des Dateiindex erfolgen oder aber der Dateipuffer mit fflush() auf die Platte geschrieben werden. Die Pufferung wird gleich nach dem Eröffnen besprochen.
Der ANSI-Standard unterscheidet daher zwischen uninterpretierten (binären) Dateien und interpretierten Textdateien. Binäre Dateien werden Byte für Byte ohne Veränderung nach "C" transportiert. Bei Textdateien werden beim Lesen die Wagenrücklaufzeichen entfernt und beim Schreiben hinzugefügt. Außerdem bricht der Lesevorgang ab, wenn er ein "\x1a" (Ctl-Z) findet. (Im Listing der Textdatei ist "\x1a" durch ein "?" dargestellt.)
Bild 2-9: Textdatei mit eingestreutem Dateiendezeichen
Zur Unterscheidung, ob eine Datei mit oder ohne Interpretation der Wagenrücklaufzeichen und des Dateiendezeichens von DOS bearbeitet werden soll, dienen die beiden Buchstaben "t" und "b", die zusätzlich zur Betriebsart angegeben werden. Im folgenden Beispiel können Leser auf DOS-Maschinen einmal die Datei als binäre Datei öffnen oder einmal als Textdatei. Hier bricht die Ausgabe früher als im Binärmodus ab, da in der Datei ein Endezeichen eingesetzt wurde.
Bild 2-10: Lesen einer Textdatei mit Dateiendezeichen
Leser, die das Programm unter UNIX laufen lassen, sollten keinen Unterschied bemerken. Die beiden Kennbuchstaben "b" und "t" sind in UNIX überflüssig und unter älteren UNIX-Systemen unbekannt. Hier kann es eventuell zu Problemen kommen. Lassen Sie hier die Unterscheidung Text-Binär einfach weg.
Puffern bedeutet auf der einen Seite einen erheblichen Zeitgewinn. Andererseits sind aber nach einem Schreibvorgang die Daten noch nicht unbedingt auf der Platte gespeichert oder am Bildschirm angezeigt. Bei Stromausfall gehen Daten in Puffern verloren.
Es gibt daher die Funktion fflush(), die den momentanen Pufferinhalt sicher schreibt.
#include <stdio.h>
int fflush ( FILE
* stream);
int setvbuf(FILE
* stream, char *buf, int mode, size_t size);
void setbuf (FILE
* stream, char *buf);
An fflush() kann man statt einer Dateiverbindung auch NULL übergeben. Dies bedeutet "alle Puffer". fflush() wird dann die Dtaeiverwaltungstabelle durchsuchen und alle Puffer schreiben. Im Fehlerfall liefert fflush() EOF, sonst "0".
Bild 2-11: Vergrößern des Dateipuffers
Für die Pufferung stellt uns die Standardbibliothek verschiedene Funktionen und Namen bereit. Der erste Name ist BUFSIZ. Er gibt die Anzahl von Zeichen an, die pro Datei zwischengepuffert werden müssen, um die Umwandlung von zeichenweisem Lesen und Schreiben auf den sektorweisen Transfer von Daten von und zur Platte zu ermöglichen.
Dieser Puffer kann beträchtlich vergrößert werden. Wir können damit einen erheblichen Geschwindigkeitsgewinn erzielen, da die Zugriffe auf die Platte reduziert werden.
Die ANSI-Funktion zum Festlegen eines Puffers heißt setvbuf(). Um kompatibel zu älteren Systemen zu bleiben, gibt es nach wie vor die Funktion setbuf().
Die Pufferungsfunktionen setvbuf() und setbuf() dürfen nur sofort nach fopen() aufgerufen werden.
Für eine bestimmte Dateiverbindung kann man einen beliebig großen Puffer zuordnen. buf gibt dabei die Startadresse und size die Größe in char an. Die Betriebsart der Pufferung läßt sich mit mode festlegen. Dazu gibt es vordefinierte Namen: _IOFBF steht für volle Pufferung, _IOLBF für eine zeilenweise Pufferung und _IONBF für keine Pufferung. In diesem Fall kann buf auch NULL sein.
Die Namen dieser symbolischen Konstanten beginnen mit einem Unterstrich. In Zukunft sollen alle Namen, die im Sprachsystem definiert werden, eine solche Kennzeichnung bekommen. Bei den historisch gewachsenen Namen wie BUFSIZ ist das allerdings nicht möglich.
Der Programmierer soll daher für seine Namen weder am Anfang noch am Ende einen Unterstrich benutzen.
Die zweite Funktion setbuf() entspricht dem Aufruf von setvbuf() mit der Betriebsart _IOFBF und dem Wert BUFSIZ für size. Setzt man buf auf NULL, dann entspricht dies der Betriebsaart _IONBF.
Man sollte für neue Programme nur noch setvbuf() verwenden.
#include <stdio.h>
int fclose (FILE
* stream);
Mit dem Schließen der Verbindung wird zuerst ein möglicherweise vorhandener Puffer geleert und danach wird der Verwaltungsblock freigegeben. Bei Programmen, die die mögliche Anzahl gleichzeitig offener Verbindungen erreichen, kann es passieren, daß eine Datei zwischendurch geschlossen werden muß, um einen Verwaltungseintrag frei zu machen.
Der Rückgabewert ist 0 für Erfolg, sonst EOF. Bei Bedarf kann dies als boolesches Ergebnis in einer Abfrage verwendet werden.
fopen() sucht sich immer den ersten freien Eintrag in der Dateiverwaltungstabelle. Bei einer leeren Tabelle ist das der Eintrag mit dem Index 0. Da sofort danach die Standardausgabe stdout eingerichtet wird, erhält sie den Index 1. Bei dem Standardfehlerkanal stderr unterscheiden sich die UNIX- und die DOS-Welt. Bei UNIX kann der Fehlerkanal umgelenkt werden, bei DOS nicht. In jedem Fall wird der Eintrag mit dem Index 2 benutzt.
Der Dateizeiger, den wir für eine geöffnete Verbindung benötigen, ist ein Zeiger auf FILE. Daher werden die drei Namen stdin, stdout, und stderr in der Datei stdio.h als Adreßkonstante definiert. Da die Standardausgabe immer im ersten Eintrag der Dateiverwaltungstabelle verwaltet wird, ist stdin auch die Adresse des ganzen Feldes aus FILE's.
#define stdin &_streams[0]
#define stdout &_streams[1]
#define stderr &_streams[2]
FILE* freopen(const
char* nam,const char* mode,FILE* stream);
Ein "C"-Programm weiß also nicht, mit welchem Gerät oder welcher Datei der jeweilige Standardkanal verbunden ist, es weiß nur, welcher Eintrag in der Dateiverwaltungstabelle für einen bestimmten Kanal zuständig ist.
Im Bild 4 wurden die ersten Einträge in die Dateiverwaltungstabelle sowohl mit dem Index als auch mit den symbolischen Namen "stdin" etc. dargestellt.
Eine wichtige Funktion für Programme, die die Standardverbindungen benutzen, ist freopen().
Die Funktion freopen() schließt eine im letzten Parameter angegebene Verbindung und öffnet mit Hilfe der ersten beiden Parameter ähnlich fopen() eine neue Datei. Die neue Dateiverbindung benutzt den gleichen Verwaltungsblock wie die bisherige. Mit freopen() werden daher häufig Standardverbindungen neu belegt, ohne daß der Rest des Programmes davon Kenntnis haben muß.
Das zugehörige Beispiel finden Sie im Bild 12. Bild 2-12: Umlenken einer Standardverbing
#include <stdio.h>
int putchar (int
c);
int getchar (void);
int fputc (int c,
FILE * stream);
int putc (int c,
FILE * stream);
int fgetc(FILE *
stream);
int getc (FILE *
stream);
int ungetc (int
c, FILE * stream);
Die Funktionen putchar() und "getchar() sind historische Namen. Sie verwenden ausschließlich die Standardausgabe bzw. Eingabe. In vielen stdio.h-Dateien nimmt man Makros und setzt diese Namen in Funktionsaufrufe der neueren Funktionen fputc() und fgetc() um (vgl. folgenden Abschnitt).
#define getchar() fgetc(stdin)
#define putchar(x)
fputc((x),stdout)
Sehen wir uns nun einige Details der Funktionen an. Es mag auffallen, daß die Funktionen mit int-Parametern arbeiten. Da es sich um einzelne Zeichen handelt, wäre sicher char naheliegend. Man verwendet aber int aus mehreren guten Gründen.
Bild 2-13: Werte der "char"-Werte und EOF
int ist die Größe, die der Architektur der Hardware am nächsten kommt. Alle Prozessoren können am schnellsten auf Variablen zugreifen, die der Maschinenbreite entsprechen und richtig ausgerichtet im Speicher liegen. Ausrichtung bedeutet, daß beispielsweise ein 4-Byte große Variable an einer Adresse liegt, die ohne Rest durch 4 teilbar ist. Aber verlassen wir lieber den Bereich der Hardware und sehen uns noch ein Argument aus der Software an.
getchar() liefert entweder ein gelesenes Zeichen oder einen speziellen Statuswert EOF, der in stdio.h definiert wird. In den meisten Fällen ist EOF auf "-1" gesetzt. Mit Hilfe der int-Rückgabe kann nun sowohl ein Zeichen als auch eine Statusinformation geliefert werden. Dies gilt auch für fgetc() oder getc(). Mit der Konstanten EOF wird angezeigt, daß es nicht möglich war, die Operation korrekt durchzuführen. Der häufigste Fall ist das Erreichen des Dateiendes. Daher hat die Konstante auch ihren Namen: EOF ( end of file / Dateiende).
Bei den Ausgabe-Funktionen putchar(), fputc() oder putc() wird ebenfalls ein Wert zurückgegeben. Wieder dient EOF als Fehleranzeige. Bei den Ausgabefunktionen erhalten wir als Ergebnis entweder das gerade ausgegebene Zeichen oder die Fehleranzeige zurück.
Bei Ausgaben kann eine volle Diskette oder auch ein abgeschalteter Drucker die Ausgabe verhindern.
Im Abschnitt über Fehlerbehandlungen werden wir mit einer detailierteren Auswertung der möglichen Fehler beginnen.
Makros sind Textersetzungen, die durch den Präprozessor vor dem eigentlichen Compilerlauf durchgeführt werden. Sie werden ausführlich im Kapitel über den Präprozessor besprochen.
Letzen Endes bleibt es dem einzelnen Programmierer überlassen, welche Version er wählt. Da aber der Vorgang, ein Zeichen aus einem Puffer zu lesen, nicht ganz trivial ist, bevorzuge ich hier die Funktionen. Makros benötigen bei häufiger Verwendung relativ viel Speicherplatz, Funktionen existieren nur einmal.
Ein Scanner liest einen Text Zeichen für Zeichen und stellt an Hand der Art des gelesenen Zeichens fest, ob ein Wort begonnen hat oder ob es beendet wurde. In einem mathematischen Ausdruck kann ein Variablenname durch ein Operatorsymbol (+,-,*,/...) beendet werden. Also liest der Wortsucher, bis er ein Zeichen findet, das nicht in einem Wort vorkommen darf. Dieses Zeichen kann er dann mit ungetc() wieder zurückstellen, damit der nächste Programmteil dieses Zeichen erneut auswerten kann.
Man kann nur ein einzelnes Zeichen (also nicht EOF) zurückstellen. Das Zeichen kann auch nur dann weiterverwendet werden, wenn nach dem Rückstellen wieder ein Leseaufruf erfolgt. Ein Setzen des Dateiindex würde ein zurückgestelltes Zeichen löschen (fseek(), rewind(), fsetpos()). Die Funktionen zur Positionierung werden wir im Abschnitt über direkte Dateien besprechen.
Direkte Dateien erlauben eine Positionierung innerhalb der Datei. Die Voraussetzung dazu ist natürlich ein Datenträger, der dies erlaubt. Auf allen Disketten, Plattenlaufwerken oder CD-ROMs ist das kein Problem.
"C" kennt zur Positionierung mehrere Funktionsaufrufe. Beginnen wir mit der klassischen Funktion, die auch in den meisten Systemen vorhanden ist, die noch nicht dem ANSI-Standard entsprechen.
#include <stdio.h>
int fseek ( FILE
*stream, long int offset, int whence);
void rewind (FILE
* stream);
int fsetpos (FILE
* stream, const fpos_t *poszeig);
int fgetpos (FILE
* stream, fpos_t *poszeig);
fseek() positioniert den Dateiindex (bitte nicht mit der Variablen für den Dateizeiger verwechseln!). offset gibt dabei den gewünschten Abstand an und whence die Position, ab der gerechnet werden soll. Dazu werden in der stdio.h drei symbolische Konstante definiert:
Bei binären Dateien muß man nicht vom Ende aus positionieren können. Bei Textdateien soll bei der Positionierung entweder der Abstand 0 sein, oder aber der Abstand sollte vom Anfang an berechnet werden, wobei dieser zusätzlich durch einen vorhergegangenen Aufruf der Funktion ftell() ermittelt werden sollte. ftell() liest den Dateiindex.
Bild 2-14: Positionierung mit fseek()
Diese Beschränkungen müssen nicht immer eingehalten werden. Sie sind eine Vorsichtsmaßnahme für Dateisysteme, die innerhalb der Textdateien einen eigenen Datensatzaufbau haben.
Eine Variante von fseek() ist rewind(). Diese Funktion setzt den Dateiindex auf den Anfang und löscht zusätzlich das Fehlerbit in der Dateiverwaltung.
Mit fseek() kann man rewind() wie folgt ausdrücken:
fseek (stream, 0L, SEEK_SET); clearerr (stream);
Die Funktion "clearerr()" löscht das Fehlerbit und auch das EOF-Bit in der zugehörigen FILE-Struktur für die angegebene Verbindung. "rewind()" allein würde das EOF-Bit nicht beeinflussen. (siehe den folgenden Abschnitt über Fehlerbehandlung im gleichen Kapitel)
Will man im Programm wissen, wie groß eine bestimmte Datei ist, kann man an das Ende einer Datei positionieren und mit ftell() die Dateilänge erfragen.
Dieser Datentyp wird in den Funktionen fsetpos() und fgetpos() verwendet. Dazu legt man zuerst eine Variable vom Typ fpos_t an. An fgetpos() übergibt man die Adresse dieser Variablen. Die Funktion wird dann in der Variablen die aktuelle Position ablegen. Danach kann dann die unveränderte Variable in einem fsetpos() Aufruf benutzt werden.
fgetpos() liefert bei Erfolg eine "0", bei Mißerfolg einen Wert ungleich "0" (also ein boolesches Ergebnis) und zusätzlich eine Fehlernummer in der globalen Variablen errno.
Der englische Begriff random access läßt sich mit "wahlfreier Zugriff" wiedergeben. Gemeint ist die notwendige freie Positionierbarkeit. Die Funktionen zum Lesen oder Schreiben von Datensätzen sind die beiden folgenden.
#include <stdio.h>
size_t fread (void*
p, size_t s, size_t no, FILE* stream);
size_t fwrite(const
void* p,size_T s,size_t no,FILE* stream);
Der Datentyp size_t wird u. a. in stdio.h definiert. Es ist der Datentyp, der vom Schlüsselwort sizeof zurückgeliefert wird. Die Grundeinheit von sizeof ist char, sodaß sizeof (char) immer "1" liefert. Mit der Einführung solcher Datentypen versucht man, möglichst maschinenunabhängig zu sein.
Legen wir dazu ein Telefonbuch an, wie es in einem Kommunikationsprogramm existieren könnte, um einen Teilnehmer leichter anwählen können. Zuerst benötigen wir eine Typdefinition.
Mit Hilfe der Typdefinition gehen wir wieder in drei Stufen vor. Zuerst legen wir in der Rolle des Spezialisten eine Informationsdatei an. Danach schreiben wir die zugehörigen Operatorfunktionen, wieder als Spezialist. Und schließlich wechseln wir die Fronten und schreiben als Anwender ein Hauptprogramm.
Bild 2-15: Typdefinition für einen Telefonbucheintrag
Die beiden Operatorfunktionen sind hier sehr einfach. Bitte bedenken Sie das iterative ( = sich in einer Schleife wiederholende) Vorgehen. Es ist sehr wichtig, daß wir auf die richtige Art und Weise anfangen und die Trennung Spezialist - Anwender beachten. Es ist momentan noch nicht unbedingt notwendig, jede Feinheit der Operatorfunktionen zu berücksichtigen.
Bild 2-16: Erste Operatorfunktionen für das Telefonbuch
Im Hauptprogramm legen wir dann eine Datei an. Die Eröffnungsart muß binär sein, da in den Datensätzen andere Grunddatentypen außer char und char-Feldern vorkommen. Nach dem Eröffnen (mit automatischem Anlegen) steht dem Programm eine leere Datei zur Verfügung. In einer Schleife schreibt das Programm dann eine Anzahl teilweise initialisierter Datensätze in die Datei. Nur durch die Verwendung innerhalb eines Programms erhält die Datei einen Aufbau. Weder "C" noch das Betriebssystem sollten einen bestimmten Aufbau erwarten oder voraussetzen.
Mit Hilfe der Numerierungsfunktion erhält jeder Datensatz eine eindeutige Nummer.
Bild 2-17: Erzeugen einer Telefondatei
Diese Datei wollen wir anschließend in einem einzigen Datensatz ändern, ohne den Rest der Datei zu beeinflussen.
Bild 2-18: Gezielter Zugriff auf einen Datensatz
Ein Sicherungsprogramm muß an Hand einer Fehlermeldung des Betriebssystems erkennen, daß der Datenträger voll ist und den nächsten beim Benutzer anfordern. Vielleicht wäre Mitteilung besser als Fehler.
Es gibt bei den Funktionen der Standardbibliothek verschiedene Arten, dem Programm Erfolg oder Mißerfolg mitzuteilen.
Im einfachsten Fall ist der Rückgabewert eine boolesche Aussage: Erfolg Ja/Nein. Beispiele für solche Funktionen sind fgetpos() oder fsetpos(). Hier kann man die Funktion direkt in einer "if"-Abfrage benutzen.
if (fsetpos (dateizeiger, &fpos_variable) )
Bild 2-19: Fehlermeldung mit perror()
Falls Sie die Möglichkeit haben, untersuchen Sie doch einmal "C"-Quellen auf die korrekte Abfrage der Dateiendebedingung. Manchmal kann man dabei nur staunen, wie oft eine Rückgabe von EOF automatisch als Dateiende gewertet wird, ohne mit den erwähnten Makros die wahre Endebedingung zu prüfen.
Falls uns eine zeichenweise arbeitende Funktion mit EOF eine Ausnahme gemeldet hat und auch ferror() eine Fehlerbedingung anzeigt, dann bleibt uns noch eine dritte Informationsquelle. Es gibt eine globale Variable errno vom Typ int, die im Falle eines aufgetretenen Fehlers eine Fehlernummer enthält. Bei erfolgreichen Operationen wird errno nicht gelöscht. Daher muß vor der Benutzung die Existenz des Fehlers auf eine andere Art festgestellt worden sein.
Im ANSI-C-Standard wird erwähnt, daß errno keine Variable sein muß, sondern auch anders realisiert werden kann. In jedem Fall muß aber errno einen int-Wert liefern. In allen (mir bekannten) Compilern ist errno eine Variable.
Für diese Fehlernummern sind in jedem System ein Reihe von symbolischen Namen vereinbart worden. Diese Namen finden Sie in der Datei errno.h. Die Inhalte der Informationsdateien sind auch im Anhang zusammengefaßt.
Möchten Sie mit Hilfe dieser Nummer einen Text ausgeben, dann gibt es eine Funktion perror(), die die Fehlervariable liest, damit aus einer internen Tabelle den passenden (englischen) Fehlertext holt und ihn an den Standardfehlerkanal ausgibt. Der Benutzer kann zusätzlich einen eigenen Fehlertext mit angeben.
Im folgenden Beispiel soll einmal ein Aufruf einer Systemfunktion ausführlich getestet werden. Falls EOF gemeldet wurde, wird zuerst kontrolliert, ob tatsächlich ein Fehler vorliegt (Zeile 17 / ferror() ). Wurde der Systemaufruf unterbrochen ohne daß ein sonstiger Fehler vorlag, dann befindet sich in errno der Wert EINTR. In diesem Fall darf der Systemaufruf einfach wiederholt werden. DOS-Programmierer benötigen diese Abfrage nicht. Nur wenn kein Fehler gemeldet wurde, sendet putchar() das Zeichen zur Ausgabe.
Bild 2-20: Fehlerabfragen nach Systemaufruf
Bild 2-21: Anlegen und Entfernen einer temporären Datei
Der ANSI-Standard kennt dazu folgende Konstanten und Funktionen:
L_tmpnam: maximale Länge des temporären Dateinamens
TMP_MAX: maximale Anzahl temporärer Dateien
FILE *tmpfile (void); # Anlegen einer temp. Datei
char *tmpnam (char * s); # Ermitteln eines temp. Namens
Die Funktion tmpnam() generiert einen Dateinamen, der sich von vorhandenen Dateinamen unterscheidet. Die maximal erzeugte Länge ist dabei L_tmpnam. Übergibt man an tmpnam() die Adresse eines char-Feldes, dann muß dieses Feld mindestens L_tmpnam Elemente besitzen. Man kann auch NULL übergeben. Dann legt tmpnam() den erzeugten Namen intern ab und liefert die Adresse des internen Puffers.
Der erzeugte Dateiname kann danach vom Programmierer in einem normalen fopen()-Aufruf weiterverwendet werden.
Die Konstante TMP_MAX gibt an, wieviele verschiedene Dateinamen hintereinander generiert werden können. Als unterste Grenze gilt "25".
In keinem Fall werden temporäre Dateien beim einem Warmstart mit Ctl-Alt-Del unter DOS oder einem Kaltstart mit der RESET-Taste entfernt.
Die Konstanten können mit dem Programm zeigkons.c ausgegeben werden. Da unter den verschiedenen Entwicklungsumgebungen die einzelnen Definitionen in der stdio.h unterschiedlich sein können, kann man sich durch die Ausgaben einen Überblick über die jeweilige Implementierung verschaffen.
Im folgenden Bild finden Sie das Listing und danach eine Ausgabe, die unter DOS erzeugt wurde.
Bild 2-22: Ausgabe der Konstanten in "stdio.h"
Das Ergebnis ist:
Ausgabe der stdio.h Konstanten. Puffergröße (BUFSIZ): 512 Wert für Fehlerkennung (EOF): -1 Größe von FILE (sizeof (FILE)): 16 Max. Dateinamen (FILENAME_MAX): 80 Max. offene Dateien (FOPEN_MAX): 20 Größe Dateiindex (sizeof (fpos_t)): 4 Größe von size_t (sizeof (size_t)): 2 Max. temp. Dateiname (L_tmpnam): 13 Wert für NULL: 0000 Max. temp. Dateien (TMP_MAX): 65535
Um einen einheitlichen Zugriff auf Verzeichnisse möglich zu machen, wurde ein kleine Bibliothek geschrieben, die inzwischen auch ihren Weg in DOS-Compiler gefunden hat. (TurboC++ 3.0)
#include <dirent.h>
DIR * opendir (char * verzeichnisname);
void closedir (DIR * vz);
struct dirent * readdir (DIR * vz);
int dirclose (DIR * vz);
void rewinddir (DIR * vz);
Analog der normalen Dateibearbeitung wird eine Verbindung zu einem Verzeichnis hergestellt. Fall es dieses Verzeichnis nicht gibt, oder der Name einer anderen Dateiart verwendet wurde, gibt die Eröffnungsfunktion NULL zurück. Die normale Rückgabe ist ein Zeiger auf eine Variable vom Typ DIR.
Bild 2-24: Arbeiten mit Verzeichnissen (1)
So wie man bei einer Dateiverarbeitung einen Zeiger auf FILE anlegt, legt man bei Inhaltsverzeichnissen einen Zeiger auf DIR an und legt dort das Ergebnis von opendir() ab. Die einzelnen Einträge können mit readdir() ermittelt werden. Es werden dabei alle Einträge geliefert, unabhängig von ihren Zugriffsbits. Die Rückgabe von readdir() ist ein Zeiger auf eine Struktur dirent (directory entry / Verzeichniseintrag).Warum allerdings dafür kein Datentyp vergeben wurde, kann vermutlich nur noch historisch erklärt werden. Mit jedem Lesen eines Eintrages wird der interne Lesezeiger auf den nächsten Eintrag gesetzt. Am Ende des Verzeichnisses liefert readdir() NULL zurück.
Bild 2-25: Arbeiten mit Verzeichnissen (2)
Die Funktion closedir() schließt die Verbindung wie üblich und liefert eine "0" für eine erfolgreiche Beendigung zurück.
Es bleibt nur noch eine Funktion zu erwähnen: die rewinddir()-Funktion. Die einzige Positionierung, die möglich ist, ist der Neubeginn am Anfang. Dies kann sinnvoll sein, wenn die Verbindung schon längere Zeit besteht und man sicher gehen will, daß auch neu angelegte Dateien mit ausgegeben werden. rewinddir() setzt neu auf und liest das Verzeichnis erneut ein.
Bild 2-26: Arbeiten mit Verzeichnissen (3)
rewinddir() hat keine Rückgabe. Ein Tip am Schluß: nicht alle Compilerunterlagen haben immer die richtigen Beschreibungen der Funktionen. Im Zweifel funktionieren die hier angegebenen.