34. Dateien I

Dateien definieren, öffnen und schließen

Die Schnittstelle zu Dateien ist in stdio.h deklariert. Dateien können über Variablen vom Typ FILE verwaltet werden: FILE *in;.

In Python konnten wir Dateiobjekte nur erzeugen, indem wir sie gleich mit einer Datei auf der Platte verbanden (allerdings konnten quasi ungebundene Datei-Objekte entstehen, indem wir die close-Methode eines Datei-Objekts riefen). In C ist das ganz ähnlich (nur werden Datei-Objekte durch die Funktion fopen erzeugt); da wir aber Referenzen deklarieren müssen, bevor wir sie verwenden, haben wir nach der Definition zunächst eine ungültige Referenz, die eben nicht auf ein FILE-Objekt verweist. Alle Versuche, vor der Zuweisung mit fopen etwas damit zu machen, werden mit Segmentation Faults und ähnlichem bestraft.

Ein FILE ist übrigens in der Regel als struct realisiert.

Die Verbindung einer FILE*-Variablen mit einer Datei auf der Platte geht durch

FILE *fopen(char *path, char *mode);

path ist dabei der Name der Datei, ggf. mit einem Pfad dorthin (ansonsten wird die Datei im aktuellen Verzeichnis gesucht), mode ist ein string, in dem z.B. w (Schreibzugiff, Datei wird ggf. neu angelegt, sonst überschrieben), r (Lesezugriff, wenn die Datei nicht existiert, kommt NULL zurück) oder a (Daten an bestehende Datei anhängen) stehen kann.

Unter CP/M und seinen Folgesystemen (MS-DOS, Windows) gibt es dazu noch den Modus b (“binary”). Wird kein b im Modestring angegeben, so übersetzt die C-Bibliothek die Bytefolge 13 10 (das entspricht CR LF, also etwas wie “Wagenrücklauf, Zeilenvorschub”) beim Lesen durch eine einfache 10, während beim Schreiben aus einer 10 wieder ein 13 10 wird. Grund dafür ist, dass unter diesen Betriebssystemen die Zeilen in Textdateien üblicherweise eben durch CR LF getrennt sind (unter Unix ist nur LF üblich), was einem das Leben bei der Stringverarbeitung etwas schwer macht.

Diese Ersetzung ist aber natürlich tödlich bei Binärdateien (z.B. Bilder oder Audiodateien), in denen gerne auch mal rein zufällig die Bytefolge 13 10 vorkommen kann. Will man Dateien dieser Art verarbeiten, so ist sowohl beim Lesen als auch beim Schreiben das b mit in den Modestring aufzunehmen. Auf anderen Systemen stört das b nicht weiter – unter Python sind die Verhältnisse übrigens analog.

Eine offene Datei sollte mit fclose(FILE *f) geschlossen werden, sobald sie nicht mehr gebraucht wird.

Das ist aus ein paar Gründen empfehlenswert: C hält intern Puffer, so dass Material, das in offene Dateien geschrieben wurde, eventuell noch gar nicht auf der Platte angekommen ist, bevor man fclose aufruft. Außerdem können Betriebssysteme üblicherweise nicht beliebig viele Dateien offen halten. Sollte auf der Platte ankommen, was ihr geschrieben habt, ohne dass ihr ein fclose gemacht habt, liegt das am netten Service der C-Laufzeitumgebung, alle offenen Dateien zu schließen, wenn main beendet wird. Sollte euer Programm abstürzen, wird das natürlich unterbleiben und eure Dateien sind dann vielleicht leer.

Textdateien

fprintf

und fscanf gehen für Dateien wie von printf und scanf bekannt, nur ist das erste Argument dann ein FILE*. Tatsächlich öffnet die C-Bibliothek immer schon drei Dateien und exportiert sie unter den Namen stdin, stdout und stderrprintf ist nur eine Abkürzung für fprintf(stdout, ...). Außerdem:

  • fgets(string, maxchars, file) liest eine Zeile, maximal aber maxchars-1 Zeichen in einen String
  • fputc(ch, file) schreibt ein Zeichen in eine Datei
  • fgetc(file) liest ein Zeichen aus einer Datei
  • fputs(str, file) schreibt eine Zeile in eine Datei

Ein ganz schlichtes Beispiel: Eine Funktion, die eine Datei öffnet und ausgibt

#include <stdio.h>

int catFileChar(char *fName)
{
  FILE *inF=fopen(fName, "r");
  int c;

  if (!inF) {
    return -1;
  }
  while ((c=fgetc(inF))!=EOF) {
    fputc(c, stdout);
  }
  return 0;
}

int main(void)
{
  if (catFileChar("schlicht.c")) {
    fprintf(stderr, "schlicht.c gibt es nicht\n");
  }
  return 0;
}

Zur Erinnerung: c ist als int definiert, weil sonst die while-Schleife je nach Maschine entweder nie (unsigned char) oder z.B. für das schöne niederländische Zeichen ý (signed char, iso-8859-1, EOF ist -1 – macht euch anhand der Zweierkomplementdarstellung klar, warum das so ist) terminiert.

Ganz ähnlich geht sowas mit den eher zeilenorientierten Funktionen. Dabei besteht natürlich wieder das Problem mit den Arrays fester Größe, bei denen man höllisch aufpassen muss, das man nicht “hinten raus” schreibt. Eine mögliche Anwendung könnte das Parsen einer einfachen Konfigurationsdatei sein. Zeilen in dieser Konfigurationsdatei können entweder das Format # Kommentar oder das Format key=value haben – dabei wollen wir uns hier um whitespace nicht kümmern, es geht uns ja vor allem um die Dateien.

#include <stdio.h>

#define MAX_LN_LEN 80

void doSomething(char *buf, char *cp)
{
  printf("Key: %s, Value: %s\n", buf, cp);
}

char *skipToEqual(char *cp)
{
  while (*cp && *cp!='=') {
    cp++;
  }
  return cp;
}

void removeLastChar(char *cp)
{
  char *start=cp;

  while (*cp) {
    cp++;
  }
  if (start!=cp) {
    *--cp = 0;
  }
}

void parseConfig(FILE *cfgFile)
{
  char buf[MAX_LN_LEN], *cp, *val;

  while (fgets(buf, MAX_LN_LEN, cfgFile)) {
    if (buf[0]=='#') {
      continue;         /* Ignore Comments */
    }
    cp = skipToEqual(buf);
    if (*cp!='=') {
      fprintf(stderr, "Syntax Error: %s\n", buf);
      continue;
    }
    *cp++ = 0;
    val = cp;
    removeLastChar(cp);
    doSomething(buf, val);
  }
}

int main(int argc, char **argv)
{
  FILE *cfgFile=fopen(argv[1], "r");

  if (!cfgFile) {
    fprintf(stderr, "Config file %s not found.\n", argv[1]);
    return 1;
  }
  parseConfig(cfgFile);
  return 0;
}

(Man sollte natürlich besser prüfen, ob man tatsächlich ein Kommandozeilenargument bekommen hat)

Das Programm könnte etwa so laufen:

examples> cat example.cfg
# Ein albernes Beispiel
Kurs=Programmieren II
Dateien=vorl.tex fig_pointer0.tex fig_pointers.tex
Syntaxfehler;
# Ein Kommentar
komisch=dies ist != syntaxfehler (sollte es einer sein?)
tucana:home/msdemlei/coli/lehre/2prog/examples> parsecfg example.cfg
Key: Kurs, Value: Programmieren II
Key: Dateien, Value: vorl.tex fig_pointer0.tex fig_pointers.tex
Syntax Error: Syntaxfehler;

Key: komisch, Value: dies ist != syntaxfehler (sollte es einer sein?)

Dies ist übrigens ein ganz einfaches Beispiel für einen (handgestrickten) Parser, ein Programm also, das eine Zeichenfolge in eine strukturierte Repräsentation überführt.

Für komplexere Grammatiken wird man sich Parser generieren lassen – etwa von Programmen wie yacc/bison – oder gut untersuchte Parsingalgorithmen mit extern spezifizierten Grammatiken verwenden.


Markus Demleitner

Copyright Notice