15 Dateien

Die Verwendung von Dateien auf Massenspeichern ist im eigentlichen Sprachkonzept von C überhaupt nicht vorgesehen; vielmehr wird die gesamte Anbindung an die Welt außerhalb des Arbeitsspeichers des Rechners über Aufrufe von Funktionen realisiert.

In allen älteren C-Versionen (K&R) gibt es dabei zwei verschiedene Dateikonzepte:

Im weiteren wird nur noch die gepufferte Ein- und Ausgabe verfolgt.

Die Funktionen werden hier nicht detailliert vorgestellt, sondern nur das eigentliche Konzept erklärt.

In der Datei stdio.h ist ein Datentyp FILE vereinbart (mittels typedef). FILE ist eine Struktur und enthält alle Informationen, welche die Bibliotheksfunktionen zu einer geöffneten Datei verwalten müssen, wie etwa Dateiname, Kennummer der Datei für das Betriebssystem (file handle), Lage, Größe und Gültigkeit des aktuellen Puffers, eventuell aufgetretene Fehler und so weiter. Diese Informationen brauchen den Benutzer nicht zu interessieren; es ist nicht legal, auf die einzelnen Elemente einer solchen Struktur zuzugreifen. Selbst wenn der Zugriff formal möglich ist, kann man ein solches Programm nicht mehr portieren.

Neben der Struktur FILE existiert noch ein spezieller Datentyp für die Position in Dateien, nämlich fpos_t. Dies ist ein ganzzahliger Datentyp (meist unsigned long int bzw. size_t), der eine Dateiposition speichern kann. Dieser Datentyp wird nur für fgetpos() und fsetpos() benötigt.

Die gesamte Ein- und Ausgabe basiert auf der Vorstellung von Datenströmen (stream, also auf irgendwelchen dunklen Kanälen, in denen die Daten im Gänsemarsch entlangdüsen. Dabei hat jede offene Datei einen solchen Kanal zur Verfügung.

Mit Strom ist dabei gemeint, daß in der Regel alle Daten einer Datei oder eines Geräts hintereinander geschrieben oder gelesen werden. Man kann sich also einen Zeiger in die Datei denken, der nach dem Öffnen meist auf dem ersten Zeichen der Datei steht und bei jeder Schreib- oder Leseoperation über die transferierten Zeichen hinwegbewegt wird, so daß die nächste Operation hinter der letzten stattfindet. Es gibt aber Funktionen, welche die normale Reihenfolge (sequentiell) aufbrechen und diesen gedachten Zeiger frei in der Datei positionieren lassen (wahlfreier Zugriff). Dies sind insbesondere die Funktionen fsetpos(), fseek() und rewind().

Ein solcher Kanal ist dabei aber nicht an eine Datei auf Diskette oder Magnetplatte gebunden; ebenfalls mit Kanälen werden nämlich die Ausgabe auf den Bildschirm, das Lesen von der Tastatur, die Druckerausgabe und eventuell anderes abgewickelt.

Will man einen Datenstrom verwenden, muß man ihn in der Regel mit der Funktion fopen() öffnen. Dabei erhält man einen Zeiger auf eine Struktur vom Typ FILE zurück. Diesen Zeiger braucht man nur beim Aufruf anderer Funktionen aus stdio.h29. fopen() erhält zwei Parameter. Der erste der beiden ist ein String, der den Namen der zu öffnenden Datei enthält. Je nach Rechnersystem sind die Regeln zur Bildung von Dateinamen unterschiedlich.

Der zweite Parameter (Dateimodus) ist ebenfalls ein String, der angibt, wie die Datei zu öffnen ist. Mögliche Werte sind"r", "w", "a", "rb", "wb", "ab", "r+", "w+", "a+", "r+b", "w+b", "a+b", "rb+", "wb+" und "ab+". Ein r im zweiten Parameter besagt, daß die Datei nur gelesen werden soll und deshalb schon existieren muß. Mit einem w gibt man an, daß die Datei nur beschrieben werden soll. Existiert sie schon, dann wird sie überschrieben, also der alte Inhalt gelöscht. Ein a ist wie w, aber die Datei wird anfangs nicht auf den Dateianfang, sondern auf das Dateiende positioniert und nicht gelöscht, wenn sie schon existiert. Mit einem r+ sagt man, daß die Datei beschrieben und gelesen werden soll (update). w+ ist fast identisch; aber wenn die Datei schon existiert, dann wird der alte Inhalt gelöscht. a+ ist wie r+, aber der Dateizeiger steht anfangs am Ende.

Ein b besagt, daß die Datei als Binärdatei behandelt werden soll. Ansonsten ist es eine Textdatei. Zum Unterschied siehe fopen().

Wenn eine Datei zum Schreiben und Lesen geöffnet wird, dann muß jeder Wechsel zwischen Lesen und Schreiben oder umgekehrt von fflush() oder einer Dateipositionierung begleitet werden, wie etwa fseek() oder rewind(). Sonst kann die Pufferung nämlich nicht klappen.

Für die am häufigsten benötigten Ströme sind allerdings bei Programmstart schon Kanäle geöffnet: stdin zum Lesen von der Standardeingabe (das ist in der Regel die Tastatur), stdout zum Schreiben auf die Standardausgabe (in der Regel der Bildschirm) und stderr als Kanal für Fehlermeldungen (meistens ebenfalls der Bildschirm). Manche Systeme bieten weitere bei Programmstart geöffnete Kanäle an, wie stdprn zum Schreiben auf den Drucker und stdaux als zusätzlicher Kanal, der meistens mit einer seriellen Schnittstelle wie einem Modem verbunden ist; diese sind aber nicht standardisiert. Die vorgöffneten Dateien std... sind globale Variablen vom Typ FILE *.

Nur stdin, stdout und stderr sind im ANSI-Standard definiert.

Will man einen Strom, also einen Ein- oder Ausgabekanal, öffnen, der noch nicht vordefiniert ist, dann muß man eine Variable vom Typ FILE * vereinbaren und fopen() aufrufen. Der Rückgabewert von fopen() wird der Variablen zugewiesen. Wenn die Datei aus irgendwelchen Gründen nicht wunschgemäß geöffnet werden kann, dann liefert fopen() den Wert NULL zurück; ansonsten einen Zeiger auf eine Variable vom Typ FILE. Diesen Zeiger muß man an eine eigene Variable vom Typ FILE * zuweisen und für die nachfolgenden Aufrufe der Dateifunktionen aufbewahren. (Mehr als Aufbewahren und an andere Funktionen übergeben darf man damit auch nicht machen.)

Auf vielen Systemen kann man nur wenige Dateien gleichzeitig geöffnet halten (meist etwa 10 oder 20). Etwas Bescheidenheit ist hier von Nutzen.

Alle weiteren Funktionen beziehen sich auf einen solchen Zeiger, um auf die geöffnete Datei zugreifen zu können.

fclose() schließt eine Datei und ist damit das Gegenstück zu fopen(). Nach dem Aufruf von fclose() darf auf die Datei nicht mehr zugegriffen werden. Beim Programmende werden alle noch offenen Dateien automatisch geschlossen; aber da die Anzahl der gleichzeitig offenen Dateien meist begrenzt ist, sollte man Dateien schließen, wenn sie nicht mehr benötigt werden.

fgetc() liest ein Zeichen von einem Datenstrom, fputc() gibt ein Zeichen auf einen Datenstrom aus.

fread() und fwrite() lesen und schreiben gleich einen ganzen Block in den Speicher oder in die Datei.

fgets() und fputs() lesen beziehungsweise schreiben einen String.

fprintf() und fscanf() führen formatierte Ein- und Ausgabe durch, wie printf() und scanf(), nur eben mit Dateien anstatt der Standardein- und -ausgabe.

Die ansonsten mit fprintf() und fscanf() identischen Funktionen sprintf() und sscanf() übertragen nicht nach und von Dateien, sondern in einen String oder lesen von dort. Sie haben außer der Ähnlichkeit mit printf() beziehungsweise scanf() nichts mit Dateien zu tun, sind aber trotzdem in stdio.h vereinbart.

Mit den Paaren ftell() und fseek() beziehungsweise fgetpos() und fsetpos() kann man die aktuelle Dateiposition erfragen und ändern.

rewind() positioniert eine Datei wieder an den Anfang.

Mit setbuf() und setvbuf() kann man auf die Pufferung Einfluß nehmen.

feof() prüft, ob ein Dateizeiger schon am Ende der Datei angelangt ist; ferror() prüft, ob der Fehlerindikator für die betreffende Datei gesetzt ist.

clearerr() löscht einen eventuell gesetzten Fehlerindikator.

Einige Funktionen liefern im Fehlerfall den Wert EOF (üblicherweise als -1 in stdio.h definiert).

Für einige Funktionen existieren Varianten für die Standardeingabe stdin oder die Standardausgabe stdout. So ist printf( ... ) identisch mit fprintf( stdout, ... ).

Das folgende kleine Beispiel schreibt einige int-Werte in eine Textdatei (und zwar einen Wert je Zeile) und liest die Werte nach dem Zurückspulen wieder aus:

/* sttxt.c 30. 7.95 kw
 */

#include <stdio.h>
#include <stdlib.h>

int main()
{
  FILE   *f;
  int     wert;

  /* Die Datei oeffnen:
   */
  f = fopen( "sttxt.dat", "w+" );
  if( f==NULL )  /* Fehler? */
  {
    printf( "Ich kann die Datei nicht oeffnen!\n" );
    return EXIT_FAILURE;
  }

  /* Einige Werte schreiben:
   */
  fprintf( f, "%d\n%d\n%d\n", 123, 456, 789 );
  fprintf( f, "%d\n%d\n%d\n", 14, 25, 36 );

  /* Die Datei an den Anfang zurueckspulen:
   */
  rewind( f );

  /* Die Werte lesen, bis Dateiende auftritt:
   */
  while( fscanf( f, "%d", &wert )==1 )
  {
    /* Kontrollausgabe auf den Bildschirm:
     */
    printf( "gelesener Wert: %d\n", wert );
  }

  /* Datei wieder schliessen:
   */
  fclose( f );

  /* Programm beenden:
   */
  return EXIT_SUCCESS;
}

Nach dem Lauf dieses Programms existiert eine Textdatei mit dem Namen sttxt.dat und folgendem Inhalt:

123
456
789
14
25
36

Will man keine Textdatei haben, dann ist eine entsprechende unformatierte Binärdatei meist platzsparender und vor allem schneller zu schreiben und lesen (vom Programm aus), da die Umwandlung der programminternen Binärdarstellung in Textzeichen entfällt.

Das Beispielprogramm würde dann so aussehen:

/* stbin.c 30. 7.95 kw
 */

#include <stdio.h>
#include <stdlib.h>

int main()
{
  FILE   *f;
  int     wert;

  /* Die Datei oeffnen:
   */
  f = fopen( "test.dat", "wb+" ); /* b==Binaerdatei!    */
  if( NULL==f )                   /* Fehler?            */
  {
    printf( "Ich kann die Datei nicht oeffnen!\n" );
    return EXIT_FAILURE;
  }

  /* Einige Werte schreiben:
   */
  wert = 123;
  fwrite( &wert, sizeof(wert), 1, f );
  wert = 456;
  fwrite( &wert, sizeof(wert), 1, f );
  wert = 789;
  fwrite( &wert, sizeof(wert), 1, f );
  wert = 14;
  fwrite( &wert, sizeof(wert), 1, f );
  wert = 25;
  fwrite( &wert, sizeof(wert), 1, f );
  wert = 36;
  fwrite( &wert, sizeof(wert), 1, f );

  /* Die Datei an den Anfang zurueckspulen:
   */
  rewind( f );

  /* Die Werte lesen, bis Dateiende auftritt:
   */
  while( fread( &wert, sizeof(wert), 1, f )==1 )
  {
    /* Kontrollausgabe auf den Bildschirm:
     */
    printf( "gelesener Wert: %d\n", wert );
  }

  /* Datei wieder schliessen:
   */
  fclose( f );

  /* Programm beenden:
   */
  return EXIT_SUCCESS;
}

AnyWare@Wachtler.de