C-Programmierung: Dateien

Aus Wikibooks

In diesem Kapitel geht es um das Thema Dateien. Aufgrund der einfachen API stellen wir zunächst die Funktionen rund um Streams vor, mit deren Hilfe Dateien geschrieben und gelesen werden können. Anschließend folgt eine kurze Beschreibung der Funktionen rund um Dateideskriptoren.

Streams[Bearbeiten]

Die Funktion fopen dient dazu, einen Datenstrom (Stream) zu öffnen. Datenströme sind Verallgemeinerungen von Dateien. Die Syntax dieser Funktion lautet:

 FILE *fopen (const char *Pfad, const char *Modus);

Der Pfad ist der Dateiname, der Modus darf wie folgt gesetzt werden:

  • r - Datei nur zum Lesen öffnen (READ)
  • w - Datei nur zum Schreiben öffnen (WRITE), löscht den Inhalt der Datei, wenn sie bereits existiert
  • a - Daten an das Ende der Datei anhängen (APPEND), die Datei wird nötigenfalls angelegt
  • r+ - Datei zum Lesen und Schreiben öffnen, die Datei muss bereits existieren
  • w+ - Datei zum Lesen und Schreiben öffnen, die Datei wird nötigenfalls angelegt
  • a+ - Datei zum Lesen und Schreiben öffnen, um Daten an das Ende der Datei anzuhängen, die Datei wird nötigenfalls angelegt

Es gibt noch einen weiteren Modus():

  • b - Binärmodus (anzuhängen an die obigen Modi, z.B. "rb" oder "w+b").

Ohne die Angabe von b werden die Daten im sog. Textmodus gelesen und geschrieben, was dazu führt, dass unter bestimmten Systemen bestimmte Zeichen bzw. Zeichenfolgen interpretiert werden. Unter Windows z.B. wird die Zeichenfolge "\r\n" als Zeilenumbruch übersetzt. Um dieses zu verhindern, muss die Datei im Binärmodus geöffnet werden. Unter Systemen, die keinen Unterschied zwischen Text- und Binärmodus machen (wie zum Beispiel bei Unix, GNU/Linux), hat das b keine Auswirkungen.

Die Funktion fopen gibt NULL zurück, wenn der Datenstrom nicht geöffnet werden konnte, ansonsten einen Zeiger vom Typ FILE auf den Datenstrom.


Die Funktion fclose dient dazu, die mit der Funktion fopen geöffneten Datenströme wieder zu schließen. Die Syntax dieser Funktion lautet:

int fclose (FILE *datei);

Alle nicht geschriebenen Daten des Stromes *datei werden gespeichert, alle ungelesenen Eingabepuffer geleert, der automatisch zugewiesene Puffer wird befreit und der Datenstrom *datei geschlossen. Der Rückgabewert der Funktion ist EOF, falls Fehler aufgetreten sind, ansonsten ist er 0 (Null).

Dateien zum Schreiben öffnen[Bearbeiten]

#include <stdio.h>
int main (void)
{
  FILE *datei;
  datei = fopen ("testdatei.txt", "w");
  if (datei == NULL)
  {
    printf("Fehler beim oeffnen der Datei.");
    return 1;
  }
  fprintf (datei, "Hallo, Welt\n");
  fclose (datei);
  return 0;
}

Der Inhalt der Datei testdatei.txt ist nun:

Hallo, Welt

Die Funktion fprintf funktioniert genauso, wie die schon bekannte Funktion printf. Lediglich das erste Argument muss ein Zeiger auf den Dateistrom sein.

Dateien zum Lesen öffnen[Bearbeiten]

Nachdem wir nun etwas in eine Datei hineingeschrieben haben, versuchen wir in unserem zweiten Programm dieses einmal wieder herauszulesen:

#include <stdio.h>

int main()
{
  FILE *datei;
  char text[100+1];

  datei = fopen("testdatei.txt", "r");
  if (datei != NULL) {
    fscanf(datei, "%s", text);  // %c: einzelnes Zeichen %s: Zeichenkette
    // String muss mit Nullbyte abgeschlossen sein
    text[100] = '\0';
    printf("%s\n", text);
    fclose(datei);
  }
  return 0;
}

Die Ausgabe des Programmes ist wie erwartet

Hallo, Welt

fscanf ist das Pendant zu scanf.

Positionen innerhalb von Dateien[Bearbeiten]

Stellen wir uns einmal eine Datei vor, die viele Datensätze eines bestimmten Types beinhaltet, z.B. eine Adressdatei. Wollen wir nun die 4. Adresse ausgeben, so ist es praktisch, an den Ort der 4. Adresse innerhalb der Datei zu springen und diesen auszulesen. Um das folgende Beispiel nicht zu lang werden zu lassen, beschränken wir uns auf Name und Postleitzahl.

#include <stdio.h>
#include <string.h>

/* Die Adressen-Datenstruktur */
typedef struct _adresse
{
  char name[100];
  int plz; /* Postleitzahl */
} adresse;

/* Erzeuge ein Adressen-Record */
void mache_adresse (adresse *a, const char *name, const int plz)
{
  sprintf(a->name, "%.99s", name);
  a->plz = plz;
}

int main (void)
{
  FILE *datei;
  adresse addr;
 
  /* Datei erzeugen im Binärmodus, ansonsten kann es Probleme 
     unter Windows geben, siehe Anmerkungen bei '''fopen()''' */
  datei = fopen ("testdatei.dat", "wb");
  if (datei != NULL)
    {
      mache_adresse (&addr, "Erika Mustermann", 12345);
      fwrite (&addr, sizeof (adresse), 1, datei);
      mache_adresse (&addr, "Hans Müller", 54321);
      fwrite (&addr, sizeof (adresse), 1, datei);
      mache_adresse (&addr, "Secret Services", 700);
      fwrite (&addr, sizeof (adresse), 1, datei);
      mache_adresse (&addr, "Peter Mustermann", 12345);
      fwrite (&addr, sizeof (adresse), 1, datei);
      mache_adresse (&addr, "Wikibook Nutzer", 99999);
      fwrite (&addr, sizeof (adresse), 1, datei);
      fclose (datei);
    }

  /* Datei zum Lesen öffnen - Binärmodus */
  datei = fopen ("testdatei.dat", "rb");
  if (datei != NULL)
    {
      /* Hole den 4. Datensatz */
      fseek(datei, 3 * sizeof (adresse), SEEK_SET);
      fread (&addr, sizeof (adresse), 1, datei);
      printf ("Name: %s (%d)\n", addr.name, addr.plz);
      fclose (datei);
    }
  return 0;
}

Um einen Datensatz zu speichern bzw. zu lesen, bedienen wir uns der Funktionen fwrite und fread, welche die folgende Syntax haben:

size_t fread  (void *daten, size_t groesse, size_t anzahl, FILE *datei);
size_t fwrite (const void *daten, size_t groesse, size_t anzahl, FILE *datei);

Beide Funktionen geben die Anzahl der geschriebenen / gelesenen Zeichen zurück. Die groesse ist jeweils die Größe eines einzelnen Datensatzes. Es können anzahl Datensätze auf einmal geschrieben werden. Beachten Sie, dass sich der Zeiger auf den Dateistrom bei beiden Funktionen am Ende der Argumentenliste befindet.

Um nun an den 4. Datensatz zu gelangen, benutzen wir die Funktion fseek:

int fseek (FILE *datei, long offset, int von_wo);

Diese Funktion gibt 0 zurück, wenn es zu keinem Fehler kommt. Der Offset ist der Ort, dessen Position angefahren werden soll. Diese Position kann mit dem Parameter von_wo beeinflusst werden:

  • SEEK_SET - Positioniere relativ zum Dateianfang,
  • SEEK_CUR - Positioniere relativ zur aktuellen Dateiposition und
  • SEEK_END - Positioniere relativ zum Dateiende.

Man sollte jedoch beachten: wenn man mit dieser Funktion eine Position in einem Textstrom anfahren will, so muss man als Offset 0 oder einen Rückgabewert der Funktion ftell angeben (in diesem Fall muss der Wert von von_wo SEEK_SET sein).

Besondere Streams[Bearbeiten]

Neben den Streams, die Sie selbst erzeugen können, gibt es schon vordefinierte:

  • stdin - Die Standardeingabe (typischerweise die Tastatur)
  • stdout - Standardausgabe (typischerweise der Bildschirm)
  • stderr - Standardfehlerkanal (typischerweise ebenfalls Bildschirm)

Diese Streams brauchen nicht geöffnet oder geschlossen zu werden. Sie sind "einfach schon da".

...
fprintf (stderr, "Fehler: Etwas schlimmes ist passiert\n");
...

Wir hätten also auch unsere obigen Beispiele statt mit printf mit fprintf schreiben können.

Echte Dateien[Bearbeiten]

Mit "echten Dateien" bezeichnen wir die API rund um Dateideskriptoren. Hier passiert ein physischer Zugriff auf Geräte. Diese API eignet sich auch dazu, Informationen über angeschlossene Netzwerke zu übermitteln.

Dateiausdruck[Bearbeiten]

Das folgende Beispiel erzeugt eine Datei und gibt anschließend den Dateiinhalt oktal, dezimal, hexadezimal und als Zeichen wieder aus. Es soll Ihnen einen Überblick verschaffen über die typischen Dateioperationen: öffnen, lesen, schreiben und schließen.

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
 
int main (void)
{
  int fd;
  char ret;
  const char *s = "Test-Text 0123\n"; 

  /* Zum Schreiben öffnen */
  fd = open ("testfile.txt", O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR);
  if (fd == -1)
    exit (-1);
  write (fd, s, strlen (s));
  close (fd); 

  /* Zum Lesen öffnen */
  fd = open ("testfile.txt", O_RDONLY);
  if (fd == -1)
     exit (-1);
 
  printf ("Oktal\tDezimal\tHexadezimal\tZeichen\n");
  while (read (fd, &ret, sizeof (char)) > 0)
    printf ("%o\t%u\t%x\t\t%c\n", ret, ret, ret, ret);
  close (fd);

 return 0;
}

Die Ausgabe des Programms ist wie folgt:

Oktal   Dezimal Hexadezimal     Zeichen
124     84      54                    T
145     101     65                    e
163     115     73                    s
164     116     74                    t
55      45      2d                    -
124     84      54                    T
145     101     65                    e
170     120     78                    x
164     116     74                    t
40      32      20
60      48      30                    0
61      49      31                    1
62      50      32                    2
63      51      33                    3
12      10      a

Mit open erzeugen (O_CREAT) wir zuerst eine Datei zum Schreiben (O_WRONLY). Wenn diese Datei schon existiert, so soll sie geleert werden (O_TRUNC). Derjenige Benutzer, der diese Datei anlegt, soll sie lesen (S_IRUSR) und beschreiben (S_IWUSR) dürfen. Der Rückgabewert dieser Funktion ist der Dateideskriptor, eine positive ganze Zahl, wenn das Öffnen erfolgreich war. Sonst ist der Rückgabewert -1.

In diese so erzeugte Datei können wir schreiben:

ssize_t write (int dateideskriptor, const void *buffer, size_t groesse);

Diese Funktion gibt die Anzahl der geschriebenen Zeichen zurück. Sie erwartet den Dateideskriptor, einen Zeiger auf einen zu schreibenden Speicherbereich und die Anzahl der zu schreibenden Zeichen.

Der zweite Aufruf von open öffnet die Datei zum Lesen (O_RDONLY). Bitte beachten Sie, dass der dritte Parameter der open-Funktion hier weggelassen werden darf.

Die Funktion read erledigt für uns das Lesen:

ssize_t read (int dateideskriptor, void *buffer, size_t groesse);

Die Parameter sind dieselben wie bei der Funktion write. read gibt die Anzahl der gelesenen Zeichen zurück.

Streams und Dateien[Bearbeiten]

In einigen Fällen kommt es vor, dass man - was im allgemeinen keine gute Idee ist - die API der Dateideskriptoren mit der von Streams mischen muss. Hierzu dient die Funktion:

FILE *fdopen (int dateideskriptor, const char * Modus);

fdopen öffnet eine Datei als Stream, sofern ihr Dateideskriptor vorliegt und der Modus zu den bei open angegebenen Modi kompatibel ist.