ZurückZum InhaltVorwärts

Dateien

Die wahrscheinlich häufigste Anwendung des Konzeptes von Spezialisten und Anwendern finden wir bei der Dateiverarbeitung realisiert. Auf jedem PC oder anderen allgemeinen Rechnern finden wir Dateien. Jedes Betriebssystem hat aber seine eigene Vorstellung, was genau nun eine Datei ist und wie sie unter diesem Betriebssystem technisch realisiert wird.

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 Dateibegriff

Vielleicht kennen Sie auch die böse Erfahrung: da hat man lange vor dem Rechner gesessen und einen Text oder ein Programm geschrieben, und dann passiert es auf einmal. Man kommt aus Versehen an den Netzschalter, oder jemand stolpert über das Netzkabel des PCs. Die ganze Arbeit ist umsonst, denn man wollte ja gerade noch den Satz fertig schreiben und dann sichern, d. h. den Inhalt in eine Datei schreiben.

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.

Allgemeine Dateien

Dateien sind beliebige Bestände von Daten, die zusammen unter einem Namen angesprochen werden können. Unter UNIX und "C" besteht eine (Platten-) Datei einfach aus einem (fast) beliebig großen Feld aus einzelnen Bytes zusammen mit einem Dateiindex. Sie hat keinerlei internen Satzaufbau. Der Dateiindex gibt bei Plattendateien die Postion für den nächsten Schreib- oder Lesevorgang an.

Dateiaufbau aus C-Sicht

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.

Realisierung des Dateibegriffs

Die Realisierung des Dateibegriffs geschieht innerhalb der verwendeten Betriebssysteme. Sie legen z. B. fest, wie groß eine Datei werden kann. Weiter legen sie fest, ob die Bearbeitung abhängig vom Inhalt ist. Die häufigste Unterscheidung macht man zwischen Text- und Programmdateien. Zur Realisierung des Dateibegriffs gehört auch die Art der Organisation für den Fall, daß es viele Dateien gibt. Häufig verwendet man dann eine Organisation mit verschachtelten Inhaltsverzeichnissen.

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.

Typdefinition

Um den Begriff der "Datei" in C einzuführen, definiert der Hersteller des Compilers eine Struktur und einen Datentyp. Der Datentyp heißt stets FILE.

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)

Datendeklaration

Neben der Typdefinition finden wir noch weitere wichtige Informationen in der stdio.h. Zuerst einmal gibt es immer die Deklaration eines Feldes aus FILE. Die Größe dieses Feldes bestimmt die Anzahl der gleichzeitig benutzbaren Dateiverbindungen. In vielen Fällen wird die Größe "20" sein. Genau erfahren Sie es, wenn Sie die vordefinierte symbolische Konstante FOPEN_MAX betrachten.

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.

Aufbau der Dateiverwaltungstabelle

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.

Deklaration der Operatorfunktionen

Zu der Typdefinition FILE brauchen wir noch einen Satz von Operatorfunktionen. Wie im Kapitel über das Arbeiten mit Strukturen schon erwähnt, erwarten Operatorfunktionen normalerweise die Adresse der zu bearbeitenden Strukturvariablen. In unserem Fall ist das dann ein Zeiger auf eine Variable aus dem Feld aus FILE's.

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.

Arbeiten mit Dateien

Die Arbeit mit Dateien geschieht grundsätzlich in zwei Schritten. Zuerst bauen wir eine Verbindung mit einer Datei auf, und danach erst dürfen wir Daten schreiben oder lesen. Den Verbindungsaufbau nennen wir eröffnen / open, den Verbindungsabbau schließen / close.

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.

Programmvorbereitungen

Zuerst wird in einem Programm, das mit Dateien arbeiten will, die Informationsdatei stdio.h eingelesen. Damit steht uns der Datentyp FILE zur Verfügung.

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;

Öffnen der Verbindung

Nun müssen wir beim Betriebssystem anfragen, ob es die gewünschte Datei gibt und ob wir sie in der von uns gewünschten Art benutzen können. Dazu versuchen wir, eine Verbindung zur Datei zu eröffnen. Die dazu benötigte Operatorfunktion heißt: fopen().

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.

Arten der Dateieröffnung

Prinzipiell gibt es drei Arten der Dateieröffnung: Und es gibt zwei verschiedene Dateiarten:

Schreiben (write)

Eine Datei, die zum Schreiben geöffnet wird, kann vorhanden sein, muß aber nicht. Ist sie vorhanden, wird der Inhalt gelöscht, und das Schreiben beginnt am Anfang. Sollte die Datei nicht existieren, wird eine leere Datei angelegt. Ist es nicht möglich, die Datei anzulegen, dann meldet fopen() NULL zurück.

Die Rückgabe von NULL ist nur der Fehlerindikator. Im Abschnitt über die Fehlerbehandlungen werden wir dann den Fehler genau bestimmen.

Lesen (read)

Eine Datei, die zum Lesen geöffnet werden soll, muß vorhanden sein. Falls sie unter dem angegebenen Pfad nicht vorhanden ist, liefert fopen() den Wert NULL zurück.

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.

Anhängen (append)

Die dritte Variante ist das Anhängen. Alle Schreibvorgänge vergrößern die Datei, da immer am jeweiligen Dateiende weitergeschrieben wird. Die Datei wächst dabei. Dies gilt auch dann, wenn die Dateiposition willkürlich gesetzt wurde.

Bild 2-7: Anhängen an eine Datei

Mischen von Schreiben und Lesen (update)

In allen drei Eröffnungsarten darf auch ein "+"-Zeichen nach dem Kennbuchstaben angefügt werden, um anzugeben, daß die jeweils andere Grundoperation ebenfalls gewünscht wird. Diese Betriebsart nennt man update (auf den neuesten Stand bringen).

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.

Arbeiten mit Text- und Binärdateien

Es gibt unter Betriebssystemen wie DOS das Problem, daß Texte nicht so dargestellt werden, wie es "C" erwartet. In "C" haben wir nur "\n" (Neue Zeile) als Zeilenendezeichen. In den meisten Textdateien bei DOS finden wir aber sowohl "\r" (Wagenrücklauf) als auch "\n" als Zeilenabschluß. Textdateien müssen also speziell behandelt werden. Zudem gibt es ein Dateiendezeichen unter DOS in Textdateien: "\x1a" (Ctl-Z).

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.

Pufferung der Dateiverbindungen

Die Funktionen der stdio.h sind in der Lage, einzelne Zeichen zu bearbeiten. Disketten oder Festplatten sind aber stets sektorweise aufgebaut. Um auch auf Platten einzelne Zeichen schreiben zu können, müssen wir einen Puffer dazwischenschalten. Die Ausgabe eines Zeichens wird in den Puffer eingetragen und erst zur Platte geschickt, wenn der Puffer gefüllt ist.

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.

Das Schließen einer Verbindung

Jeder Programmierer sollte dafür sorgen, daß die von ihm geöffneten Dateien auch wieder geschlossen werden. Der Funktionsaufruf ist fclose().

#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.

Die Standardverbindungen

An Hand der stdio.h kann man auch sehr einfach die Standardverbindungen erkennen. Vor dem Start eines "C"-Programms bei der Funktion main() werden von der jeweiligen Umgebung, also der Shell bei UNIX oder dem Startmodul bei DOS, drei fopen()-Aufrufe durchgeführt. Der erste Aufruf legt die Verbindung fest, die später stdin heißen wird. Dazu wird die Kommandozeile durchsucht, ob darin eine Dateiumlenkung für die Standardeingabe angegeben wurde, z. B. ">>Datei1.txt". Falls ja, wird der Name der angegebenen Datei beim ersten fopen() verwendet, falls nicht, wird ein fopen() mit dem Tastaturtreiber durchgeführt. Dies ist möglich, da Geräte und Plattendateien gleich behandelt 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

Zeichenweise Zugriffe

Nach der Eröffnung einer Datei können wir nun mit dem Lesen und Schreiben beginnen. Sehen wir uns dazu zuerst die zeichenweise arbeitenden Funktionen an. Die folgende Tabelle enthält die entsprechenden Deklarationen.

#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.

[Zeichenmenge]

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 oder Funktionen

Die Ein- und Ausgabefunktionen existieren doppelt, zumindest scheint es so. In Wirklichkeit gibt es einmal eine Implementierung als Funktion ("f...()") und eine Implementierung als Makro (getc / putc).

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.

Rückstellung eines Zeichens

Eine im ersten Moment ungewöhnliche Funktion ist ungetc(). Ein Zeichen, das von einer Eingabeverbindung, z. B. der Tastatur gelesen wurde, kann wieder in die Verbindung zurückgestellt werden. Diese Funktion wird beim Überprüfen von Eingabedaten häufig gebraucht. Programme, die Daten auf ihren Aufbau hin überprüfen, heißen Scanner und bilden die Grundlage für das Auswerten der Kommandozeile oder auch für Compiler.

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.

Dateiarten: sequentiell - direkt

In der EDV gibt es zwei grundlegende Dateiarten: sequentielle Dateien und direkte Dateien (im Englischen: random access files / Dateien mit wahlfreiem Zugriff). Alle Dateien auf Magnetbändern sind sequentielle Dateien. Hier muß man von Anfang bis Ende lesen, um einen bestimmten Datensatz zu finden.

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:

Damit kann man nun in einer Datei an jede beliebige Stelle positionieren. Es gibt aber einige Randbedingungen.

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)

Ermitteln der momentanen Position

Will ein Programmierer den momentanen Stand des Dateiindex ermitteln, kann er die Funktion ftell() benutzen. ftell() liefert für eine Verbindung den Wert des Dateiindex im Datentyp long. Ein mit ftell() ermittelter Wert darf in jedem Fall bei fseek() verwendet werden.

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.

ANSI-Positionierungsfunktionen

ANSI-C hat zusätzlich Funktionen eingeführt, die den Dateiindex positionieren können, ohne den Datentyp long zu verwenden. Dazu wird in der stdio.h ein eigener Datentyp für den Dateiindex eingeführt: fpos_t.

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.

Direkter Zugriff auf Dateien

Auf direkte Dateien kann man den Dateiindex beliebig positionieren. Damit wird es möglich, auf einen bestimmten Datensatz zuzugreifen. Alle Dateien auf Festplatten oder in einer RAM-Disk sind direkte Dateien. Besonders sinnvoll sind direkte Dateien für alle Daten, die den festen Aufbau einer Struktur besitzen. Beispiele sind Kunden- oder Artikelinformationen. In der Organisationslehre spricht man hier auch von Stammsätzen.

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.

Telefondatei als Beispiel für direkten Zugriff

Wir wollen einmal eine Testdatei anlegen und dann in einem zweiten Schritt einen speziellen Datensatz verändern.

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

Fehlerbehandlungen

Das Wort "Fehler" hat leider im Deutschen einen sehr unangenehmen Beigeschmack. Man erinnert sich unwillkürlich an so manche unangenehme Reaktion anderer auf eigene Fehler. In einem Programm ist ein error (Fehler) oder eine exception (Ausnahme) allerdings etwas absolut Natürliches. Eine gute Behandlung von gemeldeten Fehlern macht viele Programme erst benutzbar.

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) )

Eine andere Möglichkeit haben wir bei zeichenweise arbeitenden Funktionen gefunden. Hier wird entweder ein Zeichen oder der Wert EOF zurückgeliefert. Allerdings müssen wir noch unterscheiden, welche Art von Meldung vorliegt. Dazu gibt es zwei Makros: feof() und ferror(). Diese Makros werten die Statusinformation in der Verwaltung der Verbindung aus und liefern einen booleschen Wert als Ergebnis.

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

Arbeiten mit temporären Dateien

Für viele Aufgaben benötigen wir temporäre Dateien. Ein Editor, der beliebig große Dateien bearbeiten will, muß in der Lage sein, in eigenen Hilfsdateien Teile der Originaldatei zu halten und nur mit einem "Fenster" (einem Ausschnitt) im Speicher zu arbeiten. Die Compiler legen meist Zwischendateien an, die automatisch wieder gelöscht werden, wenn sie nicht mehr gebraucht werden.

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".

Anlegen einer verwalteten temporären Datei

Ganz anders arbeitet die Funktion tmpfile(). Sie legt eine temporäre Datei an und liefert den Dateizeiger zurück. Die Betriebsart ist dabei "wb+" (Schreiben und Lesen, binär). Eine mit tmpfile() angelegte Datei wird bei einem fclose()- Aufruf oder bei einer normalen Beendigung des Programmes automatisch gelöscht. Brechen Sie das Programm unkontrolliert ab, dann ist allerdings nicht garantiert, daß die temporäre Datei verschwindet. Wir werden im Lauf des Buches noch einen wichtigen Mechanismus kennenlernen, die Signale. Damit kann man dann zumindest ein Ctl-C abfangen und die notwendigen Aufräumungsarbeiten, wie das Entfernen temporärer Dateien, durchführen.

In keinem Fall werden temporäre Dateien beim einem Warmstart mit Ctl-Alt-Del unter DOS oder einem Kaltstart mit der RESET-Taste entfernt.

Übersicht über Konstante der "stdio.h"

In diesem Kapitel haben wir immer wieder auf Konstante und Typen aus der stdio.h-Datei verwiesen.

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

Arbeiten mit Inhaltsverzeichnissen

Der ANSI-Standard hat bei seiner Normierungsarbeit hauptsächlich die bisherigen Standardwerke über C verwendet. In allen C Unterlagen blieb aber ein Kapitel weitestgehend ausgespart: die Inhaltsverzeichnisse. Hier greift man besser auf die POSIX-Normierungen zurück (IEEE 1003.1), die ihrerseits wieder auf Arbeiten an der Universität von Berkeley basieren.

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.


Zum Dateianfang