Programmieren in C

| ### Programmieren in C von Prof. Jürgen Plate

# 5 Dateien

Dateien sind ein allgemeines Konzept für die permanente Speicherung bzw. Ein-/Ausgabe. Dateien bestehen aus beliebig vielen Komponenten eines bestimmten Datentyps. Je nach Dateiart können die einzelnen Komponenten sequentiell oder wahlfrei gelesen werden. Am Ende einer Datei können weitere Komponenten hinzugefügt werden. Die Abbildung der abstrakten Dateistruktur auf reale Speichergeräte (Platte, Band, Drucker, etc.) erfolgt durch das Betriebssystem.

Eine Datei besitzt also lauter Komponenten gleichen Typs. Zusammen mit der Dateivariable wird implizit auch ein Pufferbereich im Arbeitsspeicher definiert, der mindestens eine Dateikomponente (--> aktueller Wert) aufnehmen kann. Auf diesen Datenwert wird dann über die Dateivariable zugegriffen. Zusammen mit dem Typ "Datei" müssen einige Standardoperationen definiert sein, die den Zugriff auf die Datei erlauben:

- Open
  Eröffnen einer Datei zur Inspektion des Inhalts (Lesen) oder zum Anfügen neuer Komponenten am Ende (Schreiben). Mit dieser Prozedur wird nicht nur der Zugriffsmodus festgelegt, sondern gegebenenfalls auch eine leere Datei generiert (beim Modus "Schreiben" bei einer noch nicht vorhandenen Datei).
- Close
  Beenden des Zugriffs auf eine Datei. Das Betriebssystem wird angewiesen, die Datei zu schließen. Damit verbunden sind Verwaltungsvorgänge des Betriebssystems, z. B. Wegschreiben des Inhalts des Pufferbereichs.
- Read, Write
  Lesen und Schreiben von Komponenten einer Datei. Die Arbeitsweise dieser Operationen hängt von der Art der Datei ab. Auf diese Arbeitsweise wird in einem folgenden Kapitel noch näher eingegangen.

## 5.1 Zugriffsart

Nach Art des Zugriffs auf die Komponenten wird unterschieden in sequentielle Dateien und Dateien mit wahlfreiem Zugriff.

- Sequentielle Dateien (sequential files)
  In den Anfängen der Computertechnik gab es nur sequentielle Dateien, da das Lesen/Schreiben bei Magnetbändern und Lochstreifen nur streng sequentiell möglich war. Auch der Drucker wird über sequentielle Dateivariablen angesprochen. Beim Öffnen einer Datei muß zwischen Lesen und Schreiben unterschieden werden:
  - Lesen
    Der Dateizeiger wird auf die erste Komponente positioniert. Nach dem Zugriff auf eine Komponente wird auf die nächstfolgende Komponente positioniert.
    Das Ende einer Datei wird durch eine spezielle Standardfunktion (`feof()` oder einen speziellen Wert(`EOF` = End Of File) zurückgemeldet.
  - Schreiben
    Der Dateizeiger wird hinter der letzten Komponente positioniert; durch das Schreiben wird die Datei um eine Komponente erweitert. Danach wird wieder hinter die Datei positioniert.
- Dateien mit wahlfreiem Zugriff (random files)
  Bei Plattenspeicher kann wahlfrei auf jeden Datenblock der Platte zugegriffen werden. Mit der Verbreitung solcher Speicher lag es nahe, den Datentyp "Datei" um diese Möglichkeit zu erweitern. Bei sequentiellen Dateien wird die Positionierung auf eine Komponente implizit vorgenommen und unterliegt nicht dem Einfluß des Programms. Bei Dateien mit wahlfreiem Zugriff werden die Lese- und Schreiboperationen um die Angabe eines Komponentenindex ergänzt. Der Komponentenindex wird dabei in der Regel von 1 ab aufwärts gezählt. Damit besitzt die Datei mit wahlfreiem Zugriff eine starke Ähnlichkeit mit dem Datentyp "Array". Der Versuch, eine Komponente zu lesen, die "hinter" dem Dateiende liegt, führt zu einem Fehler. Beim Versuch, eine Komponente zu schreiben, deren Index I größer als die Anzahl N der aktuell vorhandenen Komponenten ist, können zwei Fälle unterschieden werden:
  - I = N + 1: Die Datei wird um eine Komponente erweitert (wie bei der sequentiellen Datei).
  - I > N + 1: Bei einigen Systemen führt der Versuch zu einem Fehler. Bei anderen Implementierungen wird der "Zwischenraum" mit leeren Komponenten aufgefüllt (gefährlich!).

## 5.2 Grundlegende Abläufe auf Dateien

Der Zugriff auf Laufwerke bzw. allg. auf die Peripherie erfolgt in C ebenfalls über Zeiger (sog. Streams oder Filepointer). Diesbezüglich gilt alles als "Datei"; prinzipiell muß eine Datei vor dem Zugriff auf sie geöffnet, und hinterher wieder geschlossen werden. Verwendet werden Funktionen, Datentypen, etc. aus der Standardbibliothek `<stdio.h>`.
Wenn in einem Programm nichts weiter vereinbart wird, so wirken die Schreib-und Lesebefehle auf sogenannte Standarddateien. Diese sind durch das Betriebssystem vordefiniert.(z. B. Tastatur als Eingabe und Bildschirm als Ausgabe). Will man das ändern, so kann man die Standard-E/A z. B. durch UNIX-Kommandos umlenken.

Programmname < Dateiname
Dieses Kommando bewirkt, daß das Programm von der angegebenen Datei liest.

Programmname > Dateiname
Dieses Kommando bewirkt, daß das Programm auf die angegebenen Datei ausgibt.

Die sogenannten Standard-Filepointer sind immer initialisiert, die zugehörigen Dateien immer geöffnet:

`stdin` | Standardeingabe | (Tastatur)
`stdout` | Standardausgabe | (Bildschirm)
`stderr` | Fehlerausgabe | (Bildschirm!)

Die Unterscheidung zwischen `stdout` und `stderr` ist beispielsweise dann relevant, wenn man die Bildschirmausgabe (`stdout`) in irgendwelche Files umleitet (z. B. mit '>' unter DOS und UNIX), aber verhindert werden soll, daß auch etwaige Fehlermeldungen dorthin "verschwinden".

Ein einfaches Beispiel für Zugriff auf die Standarddateien `stdin` und `stdout`:

    /* Programm 'cat' */

    #include <stdio.h>

    int main(void)
      {
      int ch;
      while ((ch = getchar()) != EOF)
        putchar(ch);
       return 0;
      }

Solange Eingabedaten von der Standardeingabe kommen werden sie auf der Standardausgabe wieder ausgegeben. Das Programm "echot" also die Standardeingabe. Mit der Ausgabeumleitung kann man damit Tastatureingaben in eine Datei schreiben, z. B. durch den Programmaufruf: `cat > mein.text`. Mittels Eingabeumleitung kann man Dateiinhalte auf den Bildschirm bringen, z. B.: `cat < mein.text`.

Eine Unschönheit von C kann man an diesem Programm gut erkennen. Die Variable `ch` ist nicht - wie man es erwarten sollte - als `char` definiert, sondern als int. Das liegt einzig und alleine daran, daß die vordefinierte Konstante `EOF` den Wert -1 besitzt und daher bei der Verwendung einer `char`-Variablen das Datei- bzw. Eingabeende nicht erkannt würde.

Das Eingabeende unter UNIX wird durch das Zeichen `Ctrl-D` (bzw. auf der deutschen Tastatur `Strg-D`) repräsentiert. Unter DOS/Windows ist es das Zeichen `Ctrl-Z` (bzw. auf der deutschen Tastatur `Strg-Z`).

Beispiel: Kopieren der Standardeingabe zur Standardausgabe; Zählen der Byteanzahl.

    #include <stdio.h>

    int copy(void);

    void main()
      {
      int bytes;
      int blocks;
      bytes = copy();
      fprintf (stderr, "%d Bytes kopiert.\n", bytes);
      }

    int copy(void)
      {
      int c;
      int bytes;
      bytes = 0;
      while ((c = getchar()) != EOF)
        {
        putchar(c);
        bytes = bytes + 1;
        }
      return (bytes);
      }

Beispiel: Kopieren der Standardeingabe zur Standardausgabe; Numerieren aller Zeilen.

    #include <stdio.h>

    #include <stdio.h>
    #define YES 1
    #define NO 0
    main()
      {
      short nl_anz;
      int c, zeile_nr;
      zeile_nr = 1;
      nl_anz = YES;
      while ((c = getchar()) != EOF)
        {
        if(nl_anz)
          {
          printf("%6d ", zeile_nr);
          zeile_nr = zeile_nr + 1;
          }
    	if(c != '\n')
    	  nl_anz = NO;
    	else
    	  nl_anz = YES;
    	putchar(c);
        }
      }

Beispiel: Worte, Zeichen und Zeilen zählen.

    #include <stdio.h>

    #define IMWORT	1
    #define AUSSEN	0

    main()
      {
      int c,wo;
      int nc, nw, nl;		    /* Zeichen, Worte, Zeilen */

      wo = AUSSEN;              /* Anfangswerte */
      nc = nw = nl = 0;
      while((c = getchar())!= EOF)
        {
        ++nc;
        if(c == '\n') ++nl;
        if( (c == '\n') || (c == '\t') || (c == ' ') ) wo = AUSSEN;
        else
          if(wo == AUSSEN)
            {
            wo=IMWORT;
            ++nw;
            }
        }
    	printf("\nDas waren %d Zeichen, %d Worte und %d Zeilen.\n",nc,nw,nl);
      }

Beispiel: Wortlängen im Eingabestrom ermitteln
Es wird die länge eines jeden Wortes bestimmt und die Längen zwischen 1 und 25 gezählt. Alle Wörter, die länger als 25 Zeichen sind, werden in einem 26. Zähler registriert. Als Ausgabe erhält man die Anzahlen der der Worte:

    #include <stdio.h>

    #define MAXWORDLEN 26               /* maximale Wort-Laenge + 1*/

    int main(void)
      {
      int c;                            /* gelesenes Zeichen */
      int wl,zl;                        /* Wortlaenge, Zeilenlaenge */
      int i;                            /* Schleifenzaehler */
      int words,lines;		            /* Wortzaehler, Zeilenzaehler */
      int maxline,maxword;              /* Maximallaengen */
      int wordlen[MAXWORDLEN];          /* speichert die Wortlaengen */

      zl = words = maxline = maxword = lines = 0;
      for ( i=0; i < MAXWORDLEN; i++ )
        wordlen[i] = 0;
      c = getchar();                     /* 1. Zeichen lesen */
      while (c != EOF)
        {
        while (!(('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'))
    	   && (c != '\n') && (c != EOF))
          {                               /* Wortanfang suchen */
          c = getchar();                  /* Zeichen lesen */
          zl++;                           /* Zeilenlaenge erhoehen */
          }
        wl = 0; 			              /* Wortlaenge auf 0 setzen */
        while ((c != EOF)
    	   && ((('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'))
    	   || ('0' <= c && c <= '9') || (c == '_')))
          {                               /* naechstes Wort lesen */
          c = getchar();                  /* Zeichen lesen */
          zl++;                           /* Zeilenlaenge erhoehen */
          wl++;                           /* Wortlaenge erhoehen */
          }
        if (wl > 0) words++;              /* Wortzaehler erhoehen */
        if (wl > maxword ) maxword = wl;  /* max. Wortlaenge */
        if (wl >= MAXWORDLEN)             /* groesser als MAXWORDLEN? */
          wordlen[MAXWORDLEN-1]++;        /* ja - ins letzte Element  */
        else
          wordlen[wl-1]++;                /* passenden Zaehler erhoehen */

        if ((c == '\n') || (c == EOF))    /* neue Zeile? */
          {
          lines++;                        /* Zeilenzaehler erhoehen */
          if (zl > maxline)
    	maxline = zl;                     /* max. Zeilenlaenge */
          zl = 0;
          c = getchar();
          }
        }

      for ( i=0; i < MAXWORDLEN; i++ )    /* Maximalzahl der Worte */
        printf("%10d | %10d\n", i, wordlen[i]);

      return 0;
      }

### Sequentielle Dateien

Will man zusätzlich zu den Standard-Dateien noch weitere Dateien auf der Platte benutzen, so muß man diese Dateien an das Programm anbinden. Alle Befehle zur Dateibearbeitung sind nicht Teil von C. Die Standardbibliotheken enthalten aber viele Funktionen zum Arbeiten mit Dateien.

Im folgenden Beispiel wird eine Datei sequentiell beschrieben und wieder gelesen. An diesem Beispiel werden einige grundlegende Befehle erklärt, weshalb der Quelltext mit Zeilennummern in eckugen Klammern versehen wurde. Die Zeilennummern finden Sie in den Erklärungen unten wieder:

    [ 1] #include <stdio.h>
    [ 2]
    [ 3] int main(void)
    [ 4]   {
    [ 5]   FILE *fp;
    [ 6]   int i,xi;
    [ 7]   static char dateiname[]="daten.datei";
    [ 8]   char text[80];
    [ 9]
    [10]   /* Beschreiben der Datei */
    [11]   fp = fopen(dateiname,"w");
    [12]   if ( fp == NULL)
    [13]     {
    [14]     fprintf(stderr,"Datei %s kann nicht zum Schreiben\
    [15]                     geoeffnet werden\n",dateiname);
    [16]     exit(1);
    [17]     }
    [18]   for ( i=0; i<10; i=i+2)
    [19]     fprintf(fp,"%d\n",i);
    [20]   fclose(fp);
    [21]
    [22]   /* Lesen der Datei */
    [23]   fp = fopen("meine.dat","r");
    [24]   if (fp == NULL)
    [25]      {
    [26]      fprintf(stderr,"Datei %s kann nicht zum Lesen\
    [27]                      geoeffnet werden\n",dateiname);
    [28]      exit(2);
    [29]      }
    [30]   while(feof(fp) == 0)
    [31]      {
    [32]      fscanf(fp,"%d",&xi);
    [33]      printf("%d",xi);
    [34]      }
    [35]   fclose(fp);
    [36]   exit(0);
    [37]   }

Im Programm finden wir folgende Dateianweisungen:

1. [ 5] Definieren einer Datei:
   `FILE *<Dateizeiger>`
   `FILE` ist eine spezielle Stream-Struktur, der Datentyp für Dateien. Er ist in der Standardbibliothek <stdio.h> als Struktur festgelegt, die Informationen über die Datei enthält (z. B. Pufferadresse, Zugriffsrechte u.s.w. ).
   Mit obiger Anweisung wird ein Zeiger auf den Datentyp FILE definiert.

2. [11], [23] Öffnen einer Datei:
   `<Dateizeiger> = fopen(<Dateiname>,<Zugriffsmodus>);`
   Die Funktion fopen verbindet den externen Namen der Datei mit dem Programm und liefert als Ergebnis den Zeiger auf die Beschreibung der Datei. Im Fehlerfall wird der NULL-Zeiger zurückgeliefert. Die Funktion ist definiert als

   `FILE *fopen(const char *filename, const char *modus)`

   als Zugriffsmodus steht zur Verfügung eine Kombination von "a", "r", "w" und "+":

   - '`r`' (Lesen (read))
   - '`w`' (Schreiben (write))
   - '`a`' (Anhängen (append))
   - '`r+`' (Lesen und Schreiben)
   - '`w+`' (Schreiben und Lesen)
   - '`a+`' (Lesen an bel. Position, Schreiben am Dateiende)
   Durch anhängen eines Zusatzes kann festgelegt werden, ob es sich bei der zu bearbeitenden Datei um eine Binär- oder Textdatei handelt:
   - '`t`' (für text)
   - '`b`' (für binary)

   Die Funktion fopen reagiert folgendermaßen:

   - Beim Öffnen einer existierenden Datei
     - zum Lesen: keine Probleme
     - zum Anhängen: keine Probleme
     - zum Schreiben: Inhalt der Datei geht verloren

   - Beim Öffnen einer nicht existierenden Datei
     - zum Lesen: Fehler, Ergebnis ist NULL-Zeiger
     - zum Anhängen: neue Datei wird angelegt
     - zum Schreiben: neue Datei wird angelegt
   Maximal `FOPEN_MAX` Dateien können gleichzeitig geöffnet werden, maximale Dateinamenlänge: `FILENAME_MAX`.
   Zwischen Lesen und Schreiben ist ein Aufruf von `fflush()` oder ein Positionierungsvorgang nötig.

3. [14], [19], [26] formatierte Ausgabe auf Datei:
   `fprintf(<Dateizeiger>, "<Format>",<Werte>);`
   Entspricht der Funktion `printf` und schreibt die angegebenen Werte im angegebenen Format auf die Datei. Dateizeiger verweist auf die Datei, auf die geschrieben wird. `fprintf` ist definiert als

   `int fprintf(FILE*, const char *format, ...)`

4. [32] formatierte Eingabe von Datei:
   `fscanf(<Dateizeiger>, "<Format>",<Werte>);`
   Entspricht der Funktion `scanf` und liest die angegebenen Werte im vereinbarten Format der Datei. `fscanf` ist definiert als

   `int fscanf(FILE*, const char *format, ...)`

5. [30] Dateiende abfragen:
   `feof(<dateizeiger>);`
   Die Funktion `feof` liefert den Integerwert 1, wenn das Dateiende gelesen wurde, sonst 0.(`int feof(FILE*)`)

6. [20], [35] Schließen einer Datei:
   `fclose(<Dateizeiger>);` Die Datei wird geschlossen, vom Programm abgehängt und der Platz für den Filebeschreibungsblock wieder freigegeben. Beim Schreiben auf Dateien sollte die Datei geschlossen werden, sobald alle Schreiboperationen abgschlossen sind, da erst beim Schließen die Dateipuffer auf die Platte geschrieben und die Informationen über die Datei in der Dateiverwaltung aktualisert werden. `fclose` ist definiert als `int fclose(FILE*)`.

Im 2. Programmbeispiel werden von der Datei "daten.dat" Datenzeilen gelesen. Jede Zeile enthält einen Integerwert, einen Double-Wert und einen Textstring. Diese Daten werden dann mit einer vorangestellten Zeilennummer in die Datei "tabelle.txt" geschrieben.

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

    int  main(void)
      {
      char  msg[100];
      int  n, i;
      double  d;
      FILE  *fpin, *fpout;
      /* Dateien oeffnen */
      fpin  = fopen("daten.dat", "rt");
      fpout = fopen("tabelle.txt", "wt");
      if((fpin == NULL) || (fpout == NULL))
        { /* ... hat nicht funktioniert */
        fprintf(stderr, "Fehler: I/O");
        exit(1);
        }
      /* jetzt neue Datei mit anlegen, mit den Wertesaetzen zeilenweise
         und mit Zeilennummern versehen */
      i = 1;
      for(;;) /* Endlosschleife */
        {
        fscanf(fpin, "%d%lf%s", &n, &d, msg);
        if(feof(fpin)) break; /* Dateiende gelesen, sofort aufhoeren! */
        fprintf(fpout, "%d>  %s  %d  %f\n", i++, msg, n, d);
      }
      /* Dateien schliessen */
      fclose(fpout);
      fclose(fpin);

      return 0;
      }

Ein weiteres Beispiel: Programm zum Kopieren einer Datei. Das Programm hat zwei Kommandozeilen-Parameter, die Namen von Quell- und Zieldatei. In diesem Beispiel wird besonders auf die Fehlerbehandlung eingegangen. Wenn etwas schief geht, interessiert uns die Ursache, die bei vielen Funktionen als Nummer in `errno` und als englischer Text in `strerror(errno)` hinterlegt sind. Deshalb wird die Headerdatei `errno.h` eingebunden.

Sämtliche Funktionen, deren Ergebniswerte nicht mehr interessieren, sind mit einem Cast nach `void` versehen:

- Wenn `fprintf(stderr,...)` nicht mehr funktioniert, hilft sowieso nichts mehr.
- Wenn es schon einen anderen Fehler gegeben hat, interessiert es nicht mehr, ob die Dateien noch geschlossen werden können.

Sie sehen, in C sollte man sehr, sehr sorgfältig alle Ergebniswerte korrigieren, dies ist häufig viel aufwendiger als der Teil, der die gewünschten Aktionen durchführt.

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

    int main(int argc, const char *argv[])
      {
      FILE *infile,*outfile;
      int c,i;

      if(argc != 3)
        {
        (void)fprintf(stderr,
           "Aufruf: %s quelldatei zieldatei\n", argv[0]);
        return(EXIT_FAILURE);
        }
      infile = fopen(argv[1],"rb");
      if(infile == NULL)
        {
        (void)fprintf(stderr,
           "Fehler beim öffnen der Datei %s: %s\n",
           argv[1],strerror(errno));
        return(EXIT_FAILURE);
        }
      outfile = fopen(argv[2],"wb");
      if(outfile == NULL)
        {
        (void)fprintf(stderr,
          "Fehler beim Erzeugen der Datei %s: %s\n",
          argv[2], strerror(errno));
        (void)fclose(infile);
        return(EXIT_FAILURE);
        }
      while((c = getc(infile)) != EOF)
        if(putc(c,outfile) == EOF)
          {
          (void)fprintf(stderr,
            "Fehler beim Schreiben der Datei %s: %s\n",
            argv[2], strerror(errno));
          (void)fclose(infile);
          (void)fclose(outfile);
          return(EXIT_FAILURE);
          }
      if(ferror(infile))
        {
        (void)fprintf(stderr,
          "Fehler beim Lesen der Datei %s: %s\n",
          argv[1], strerror(errno));
        (void)fclose(infile);
        (void)fclose(outfile);
        return(EXIT_FAILURE);
        }
      if(fclose(infile) == EOF)
        {
        (void)fprintf(stderr,
          "Fehler beim Schließen der Datei %s\n", argv[1]);
        (void)fclose(outfile);
        return(EXIT_FAILURE);
        }
      if(fclose(outfile) == EOF)
        {
        (void)fprintf(stderr,
          "Fehler beim Schließen der Datei %s\n", argv[2]);
        return(EXIT_FAILURE);
        }
      return(EXIT_SUCCESS);
      }

## 5.3 Textdateien

Dies sind Dateien, deren Komponenten Schriftzeichen sind (Typ char). Sie nehmen eine Schlüsselrolle ein, da die Eingabe- und Ausgabedaten der meisten Computerprogramme Textfiles sind (darunter fällt beispielsweise auch die Druckausgabe). Ein Programm kann vielfach allgemein als eine Datentransformation von einer Textdatei in eine andere aufgefaßt werden.

#### Das Zeilenende-Problem

Nun sind Texte i. a. in Zeilen unterteilt, und es stellt sich die Frage, wie diese Zeilenstruktur auszudrücken ist. In der Regel enthält der in einem DVS verwendete Zeichencode spezielle Steuerzeichen, von denen eines als Zeilenende-Zeichen verwendet werden kann. Bedauerlicherweise verhindert die Realisierung von Textdateien auf Betriebssystemebene eine einfache Realisierung der Textdatei.

- Bei einigen Betriebssystemen wird ein einziges Steuerzeichen als Zeilenende interpretiert (z. B. bei UNIX: Linefeed, Mac: Carriage Return). Das verwendete Zeichen ist jedoch nicht einheitlich festgelegt.
- Bei anderen Systemen besteht das Zeilenende aus zwei Zeichen (in der Regel Carriage Return und Linefeed, z. B. bei MS-DOS).
- Es sind auch Systeme bekannt, die keine Steuerzeichen verwenden.

Diese Unterschiede machen es oft notwendig, Textdateien bei der Übertragung zwischen verschiedenen Rechner- bzw. Betriebssystemen zu konvertieren.

#### Gepufferte E/A vs. ungepufferte E/A

Wie bei den meisten Betriebssystemen wird auch bei DOS oder UNIX die Ausgabe gepuffert. Das heißt, es wird erst etwas ausgegeben, wenn ein Zeilenende an das Ausgabegerät gesendet wird, oder gar erst, wenn das Programm zuende ist. Falls Sie das nicht möchten, müssen also dafür sorgen, daß nach jedem Zeichen wirklich auch eine Bildschirmausgabe erfolgt. Sie erreichen dies durch den Funktionsaufruf `fflush(stdout);` nach jeder Ausgabe.
Die Funktion `fflush (FILE *datei)` sorgt für sofortiges Wegschreiben des internen Dateipuffers.

Auch die Eingabe wird wie die Ausgabe gepuffert. Das heißt, es wird beim Aufruf von `getchar()` gewartet, bis nach dem Zeichen die Enter-Taste gedrückt wird. Um das zu vermeiden (wenn z. B. nur ein einziger Tastendruck erfolgen soll), muß das System in den ungepufferten Betrieb geschaltet werden. Sie erreichen dies unter UNIX durch Umschalten des Terminals in den ungepufferten (raw-)Modus. Das kann durch den Aufruf des Systemkommandos `stty` erreicht werden:

`system ("stty raw");`

Will man zusätzlich verhindern, daß das eingegebene Zeichen vom Betriebssystem geechot wird, kann man den Aufruf erweitern:

`system ("stty raw -echo");`

Danach wird das eingegebene Zeichen auch nicht mehr automatisch auf dem Bildschirm geechot, sondern erst durch die Ausgabe in Ihrem Programm. Auf diese Weise lassen sich beispielsweise auch Passworteingaben realisieren oder es kann verhindert werden, daß die Eingabe eine Bildschirmmaske verunstaltet. Der Funktionsaufruf fflush(stdout); nach jeder Ausgabe kann dann auch entfallen. Bevor das Programm beendet wird, müssen Sie jedoch wieder in den "Normalmodus" zurückschalten:

`system ("stty -raw");`
bzw.
`system ("stty -raw echo");`

#### Weitere Dateifunktionen für Textdateien

- `int putc(int c, FILE *f)` und `int fputc(int c, FILE *f)`
  Schreiben ein Zeichen in die Datei. Sie arbeiten wie `putchar()`. `putc()` kann ein Makro sein.

- `int getc(FILE *f)` und `int fgetc(FILE *f)`
  Lesen ein Zeichen aus der Datei. Sie arbeiten wie `getchar()`. (`getc()` kann Makro sein)

- `int puts(const char *s)` und `int fputs(const char *s, FILE *f)`
  Schreiben eine Zeichenkette in die Datei. `s` ist ein Zeiger (Verweis) auf die Zeichenkette. Die Funktionen liefern ok (0) oder `EOF`. `puts()` ergänzt die Zeichenkette um ein Zeilenende-Zeichen (newline).

- `char *gets(char *s)`
  Liest eine Zeichenkette aus der Datei. Die Funktion liefert entweder die Zeichenkette `s` oder `NULL`, falls das Dateiende erreicht wurde. Bei einem Lesefehler wird ebenfalls `NULL` zurückgegeben. `gets()` legt statt des Newline-Zeichens (\n) ein Nullbyte am Stringende ab.

- `char *fgets(char *s, int n, FILE *f)`
  Liest eine Zeichenkette aus der Datei. Die Funktion liefert entweder die Zeichenkette `s` oder `NULL`, falls das Dateiende erreicht wurde. Es werden jedoch maximal n-1 Zeichen gelesen. `fgets()` legt ein Nullbyte am Stringende ab.

- `int ungetc(int c, FILE *f)`
  Diese Funktion stellt das Zeichen `c` in den Eingabepuffer zurück. Es kann dann mit dem nächsten `getchar()`, `getc()` oder `fgetc()` wieder gelesen werden. `ungetc())` liefert entweder das Zeichen c oder EOF als Status zurück.

Beispiel: Das folgende Programm macht in Textdateien solche Zeichen sichtbar, die normalerweise nicht nicht sichtbar sind, nälich Steuerzeichen mit den ASCII-Codes 0 bis 31. Diese Zeichen werden Hexadezimal ausgegeben und als Markierung ein '\' davor, so würde das Formfeed-Zeichen als '\0C' ausgegeben. Die Zeichen "Tabulator", "Newline" und das Leerzeichen bleiben unverändert, die Zeilenstruktur der Datei bleibt also erhalten.
Auf der Kommandozeile können beliebig viele Dateinamen angegeben werden. Bleibt die Kommandozeile leer, wird automatisch von der Standardeingabe gelesen.

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

    void vis(FILE *fp);

    main(int argc, char *argv[])
      {
      int i;     /* Zaehler fuer Parameter */
      FILE *fp;  /* Eingabedatei */

      if (argc == 1) /* keine Datei angegeben */
        vis(stdin);
      else           /* Dateiliste abarbeiten */
        {
        for (i = 1; i < argc; i++)
        if ((fp=fopen(argv[i], "r")) == NULL)
          {
          fprintf(stderr, "Can't open %s\n", argv[1]);
          exit(1);
          }
        else
          {
          vis(fp);
          fclose(fp);
          }
        }
        exit(0);
      }

    void vis(FILE *fp)
      {
      int c;
      while ((c = getc(fp)) != EOF)
        if (isascii(c) &&
            (isprint(c) || c=='\n' || c=='\t' || c==' '))
          putchar(c);
        else
          printf("\\%02x", c);
        exit(0);
    }

Als nächstes schreiben wir ein Programm, das den Mittelwert aus den eingelesenen Zahlen bildet:

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

    main(int argc, char *argv[])
      {
      double x;  /* Eingabe-Werte */
      int r;     /* Rueckgabewert von fscanf  */
      int n;     /* Anzahl der Werte */
      double sx; /* in sx werden die eingelesenen Werte aufsummiert */
      double mx; /* Mittelwert der x-Werte */
      int i;     /* Zaehler fuer Parameter */
      FILE *fp;  /* Eingabedatei */

      if (argc == 1) /* keine Datei angegeben */
        {
        fprintf(stderr, "Keine Datei angegeben!\n");
        exit(1);
        }
      else           /* Dateiliste abarbeiten */
        {
        for (i = 1; i < argc; i++)
        if ((fp=fopen(argv[i], "r")) == NULL)
          {
          fprintf(stderr, "Can't open %s\n", argv[i]);
          exit(1);
          }
        else  /* Datei bearbeiten */
          {
          n  = 0; /* Zaehler und Summe auf Null setzen */
          sx = 0;
          for(;;)
            {
            r = fscanf (stdin,"%F",&x); /* Wert einlesen */
            if (r == EOF) break; /* Dateiende -- fertig */
            n = n + 1;
            sx = sx + x;
            }
          mx = sx/n;
          fprintf(stdout,"Datei:  %s\n", argv[i]);
          fprintf(stdout,"   Anzahl der verarbeiteten Werte:  %d\n",n);
          fprintf(stdout,"   Mittelwert: %f\n",mx);
          fclose(fp);
          }
        }
        exit(0);
      }

Aufgabe: Erweitern Sie das Mittelwert-Programm so, daß gleichzeitig das Minimum und das Maximum der eingelesenen Zahlen ermittelt und ausgegeben wird.

Oft braucht man ein Programm, daß verschiedene Dateien nach einer Zeichenfolge durchsucht. In UNIX gibt es dazu das recht mächtige Tool `grep` und seine "Verwandten" `egrep` und `fgrep`. Hier soll eine einfache Variante vorgestellt werden, die `mgrep` heiße. Das Programm kann diverse Optionen verarbeiten:

- i Groß-/Kleinschreibung nicht beachten
- l nur Dateinamen ausgeben
- c Zeilen zählen
- h kein Dateinamen/Zeilennummer ausgeben
- n Zeilennummer ausgeben
- v alle Zeilen ausgeben, die nicht übereinstimmen

Die Optionen werden durch ein '-'-Zeichen eingeleitet, um sie im Programm vom Suchbegriff und Dateinamen unterscheiden zu können, also z. B. "-i" oder "-i -c".
Die Kommandozeile hat folgenden Aufbau:

`mgrep <Optionen> <Suchbegriff> <Dateiname(n)>`

wobei die Optionen auch fehlen dürfen, es werden dann Voreinstellungen verwendet. Der Teil des Programms, der die Kommandozeile auswertet, ist etwa genau so lang, wie der eigentliche Suchteil.

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

    #define MAXLINE 1024                 /* max. Zeilenlänge */
    #define TRUE  1
    #define FALSE 0

    char upper   = FALSE;                /* Option i */
    char count   = FALSE;                /* Option c */
    char fnonly  = FALSE;                /* Option l */
    char linen   = FALSE;                /* Option n */
    char header  = TRUE;                 /* Option h */
    char nomatch = FALSE;                /* Option v */

    void usage(void)
    {
      printf("Aufruf: mgrep [<Optionen>] <Suchmuster> <Datei(en)> \n\n");
      printf("Optionen: i Groß-/Kleinschreibung nicht beachten\n");
      printf("          l nur Dateinamen ausgeben\n");
      printf("          c Zeilen zählen\n");
      printf("          h kein Dateinamen/Zeilennummer ausgeben\n");
      printf("          n Zeilennummer ausgeben\n");
      printf("          v alle Zeilen ausgeben, die nicht übereinstimmen\n\n");
      exit(1);
    }

    void delete(char *str,int pos, int n)
      /* n Zeichen aus str ab pos löschen */
      {
      int i = pos + n;
      while ((str[pos++] = str[i++]) != '\0');
      }

    void del_spaces(char *line)
      /* Führende Leerzeichen (Space und Tab) entfernen */
      {
      while (line[0]==' ' || line[0]==0x09) delete(line, 0, 1);
      }

    FILE *open_file(char *name)
      /* Datei öffnen */
      {
      FILE *fp;
      if ((fp=fopen(name,"r")) == 0)
        {
        fprintf(stderr,"Datei %s kann nicht geöffnet werden",name);
        exit(1);
        }
      return fp;
      }

    char strstrc(char *str1, char *str2)
      /* Feststellen, ob str2 in str1 enthalten ist           */
      /* In Abhängigkeit von upper Groß-/Kleinschreibung      */
      /* beachten bzw. nicht beachten                         */
      {
      if (upper)
        {
        int i, n, len;
        n = strlen(str2);
        len = strlen(str1) - n;
        for (i = 0; i <= len; i++)
          if (strnicmp(&str1[i], str2, n) == NULL) return TRUE;
        }
      return strstr(str1, str2) != NULL;
      }

    int main(int argc, char *argv[])
      {
      char line[MAXLINE], search[MAXLINE], name[120], match;
      int i, lineno, cnt = 0;
      FILE *fp;

      if (argc < 3) usage();       /* falsche Parameteranzahl */

      /* zuerst die Optionen bearbeiten */
      while (argc > 2 && argv[1][0] == '-')
        {
        printf("Argument: %s\n",argv[1]);
        switch (argv[1][1])
            {
            case 'i':
            case 'I': upper = TRUE; break;
            case 'n':
            case 'N': linen = TRUE; break;
            case 'c':
            case 'C': count = TRUE; break;
            case 'l':
            case 'L': fnonly = TRUE; break;
            case 'h':
            case 'H': header = FALSE; break;
            case 'v':
            case 'V': nomatch = TRUE; break;
            default:  usage(); exit(1);
            }
            argc--;
            argv++;
        }

      /* jetzt sollte noch Suchbegriff und Dateiname da sein */
      if (argc < 2)
        {
        usage();
        exit(1);
        }

      /* suchbegriff uebernehmen */
      strcpy(search, argv[1]);
      argc--;
      argv++;

      /* Text in allen Dateien suchen */
      for (i = 1; i < argc; i++)
        {
        lineno=0;
        strcpy(name, argv[i]);
        printf("Durchsuchen: %s\n",name);
        fp = open_file(name);
        while (fgets(line, MAXLINE, fp) != NULL)
          {
          if (((match = strstrc(line, search)) == TRUE
                && !nomatch) || (!match && nomatch))
            {
            cnt++;
            if (!count)
              {
              if (header)
                {
                printf("%s ", name);
                if (fnonly)
                  {
                  putchar('\n');
                  break;
                  }
                if (linen) printf("%04d", lineno);
                putchar(':');
                }
              del_spaces(line);
              printf("%s", line);
              }
            }
          lineno++;
          }
        fclose(fp);
        }
      if (count) printf("%d Zeilen gefunden",cnt);
      return 0;
      }

Wenn ASCII-Zeichen auf eine Zeichenkette eingelesen werden, muß gegebenenfalls eine Umwandlung in eine Zahl erfolgen. Die folgende Funktion atof() leistet das Gewünschte:

    double atof(char s[])
      /* Zeichenkette s nach double wandeln */
      {
      double val, power;
      int i, sign;
      for (i = 0; s[i] == ' ' || s[i] == '\n' || s[i] == '\t'; i++)
        ; /* Zwischenraum uebergehen */
      sign = 1;
      if (s[i] == '+' || s[i] == '-') /* Vorzeichen */
        if (s[i++] == '-') sign = -1;
      for (val = 0; s[i] >= '0' && s[i] <= '9'; i++)
        val = 10 * val + (s[i] - (int)'0');
      if s[i] == '.') i++;
      for (power = 1, s[i] >= '0' && s[i] <= '9'; i++)
        {
        val = 10*val + (s[i] - (int)'0');
        power = power*10;
        }
      return (sign*val/power);
      }

## 5.4 Binärdateien

Bei binärer Ein- und Ausgabe auf Dateien werden die Daten nicht in "lesbarer" Form abgelegt, sondern die Interndarstellung der Speicherinhalte wird direkt (byteweise) in die Datei übertragen. Binäres Schreiben einer long-Variablen benötigt also stets vier Byte Speicherplatz, wogegen der erforderliche Speicherplatz bei formatierter Ausgabe von der Größe der Zahl bzw. vom Format abhängt.

Die Funktion `fwrite` schreibt eine angegebene Anzahl von Datenelementen gleicher Größe in eine Datei. übergeben werden muss:

- Die Adresse des ersten Datenelements. Da nicht von vorne herein klar ist, welche Daten geschrieben werden sollen, wird hier ein Zeiger auf void übergeben. Der Zeiger auf die aktuell zu schreibenden Daten kann problemlos in einen solchen gewandelt werden.
- Die Größe eines einzelnen Datenelements. Hierzu gibt es den vordefinierten Typ `size_t`, der normalerweise ein vorzeichenloser Ganzzahltyp ist. Die aktuelle Größe wird normalerweise mit dem `sizeof`-Operator ermittelt.
- Die Anzahl der zu schreibenden Datenelemente
- Die Ausgabedatei

`fwrite` wird also definiert als:

`size_t fwrite(const void *pt, size_t size, size_t n, FILE *f)`

Die auszugebenden Daten müssen zusammenhängend im Speicher stehen. Dies ist bei Vektoren stets der Fall, ebenso wie bei Speicherplatz, der durch einen einzelnen `malloc()`-Aufruf (siehe Zeiger) zur Verfügung gestellt wurde.

Beispielprogramm für binäres Schreiben:

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

    int main(void)
      {
      int Feld[] = {3, 91, 2134, 6, 33, 267, 9123, -5, 22, 0};
      FILE *pf;
      char dateiname[] = "daten.bin"; /* Dateiname */
      pf = fopen(dateiname, "wb");
      if (pf == NULL)
        {
        printf("Fehler beim Oeffnen der Datei\n");
        exit(1);
        }
      fwrite( (void *)Feld, sizeof(int), 10, pf);
      /* Das Feld wird komplett auf einmal geschrieben */
      fclose(pf);
      }

Die Größe der Datei `daten.bin` ist also 10 * sizeof(int).

Die Funktion `fread` ist die zu `fwrite` gehörige analoge Einlesefunktion. Ihr Rückgabewert ist die Anzahl der tatsächlich gelesenen Bytes. Diese Zahl kann kleiner sein, als die Zahl der zu lesenden Bytes, wenn das Dateiende vorzeitig erreicht wurde. SIe ist definiert als:

`int fread(void *ptr, size_t size, size_t n, FILE *f)`

Das folgende Programm kopiert eine Datei unter Verwendung der beiden Funktionen fread() und fwrite(). Dabei wird ein char-Array als Puffer verwendet. Diese Kopiermethode ist wesentlich schneller als zeichenweises Kopieren. Das Dateiende wird dadurch erkannt, daß fread() den Wert 0 zurückliefert. fwrite() schreibt genau soviele Bytes, wie von fread() gelesen wurden.

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

    #define MAXBUF 32768 /* Puffergroesse */

    int main(void)
      {
      char buffer[MAXBUF];            /* Kopierpuffer */
      FILE *rf, *wf;                  /* Lesedatei, Schreibdatei */
      int numread;                    /* Anzahl gelesener Zeichen */
      char readfrom[] = "daten.bin";  /* Dateinamen */
      char writeto[] = "daten.bak";

      rf = fopen(readfrom, "rb");
      if (rf == NULL)
        {
        printf("Fehler beim Oeffnen der Datei\n");
        exit(1);
        }
      wf = fopen(writeto, "wb");
      if (wf == NULL)
        {
        printf("Fehler beim Oeffnen der Datei\n");
        exit(1);
        }
      do
        {
        numread = fread(buffer, sizeof(char), MAXBUF, rf);
        fwrite( buffer, sizeof(char), numread, wf);
        } while (numread > 0);
      fclose(wf);
      fclose(rf);
      return 0;
      }

Beispiel: Zum Speichern von Daten für das Telefonverzeichnis des Fachbereichs wird die folgende Struktur definiert:

    struct Adresse
      {
      char Name[30];
      char Vorname[30];
      int Raumnummer;
      int Telefon;
      };

Gegeben ist eine Binärdatei namens "teldat", die Datensätze vom Struktur-Typ "Adresse" enthält. Gesucht wird eine Funktion `tabelle`, welche die Binärdatei zum Lesen öffnet, und alle Datensätze nacheinander als formatierte Tabelle ausdruckt:

    int tabelle(char dateiname[])
      {
      FILE * fp;
      struct Adresse Data;

      fp = fopen(dateiname,"rb");
      if (fp == NULL) return(1);  /* Fehler */
      printf("+------------------------------------------+------+------+\n");
      printf("| Name                                     | Raum | Tel. |\n");
      printf("+------------------------------------------+------+------+\n");
      while (!feof(fp))
        {
        fread(&Data,sizeof(Data),1,fp);
        printf("| %-10s %-30s | %4d | %4d |\n",
               Data.Vorname, Data.Name, Data.Raumnummer, Data.Telefon);
        }
      printf("+------------------------------------------+------+------+\n");
      fclose(fp);
      return(0);
      }

Beachten Sie, daß im Gegensatz zum Feld oben, nun der Adressoperator '&' bei `fread` verwendet werden muß.

Die Daten der Datei "teldat" sollen in eine zweite Binärdatei umkopiert werden. Dafür wird die Funktion `kopiere` verwendet, welche die Binärdatei mit den Adressen (also die Quelldatei) zum Lesen und gleichzeitig eine weitere Binärdatei, die Zieldatei, zum Schreiben öffnet. Die zweite Binärdatei soll Datensätze enthalten, die folgender Struktur genügen:

    struct NeueAdresse
      {
      char Name[30];
      int Telefon;
      };

Aufgabe der Funktion ist es, jeweils einen Datensatz aus der Quelldatei zu lesen, die Inhalte in einen Datensatz der Zieldatei zu übertragen und diesen dann in die Zieldatei zu schreiben:

    int kopiere(char quelldateiname[], char zieldateiname[])
      {
      FILE * qfp;
      FILE * zfp;
      struct Adresse Data;
      struct NeueAdresse NData;

      qfp = fopen(quelldateiname,"rb");
      if (qfp == NULL) return(1);
      zfp = fopen(zieldateiname,"wb");
      if (zfp == NULL) return(1);
      while (!feof(qfp))
        {
        fread(&Data,sizeof(Data),1,qfp);
        strcpy(NData.Name,Data.Name);
        NData.Telefon = Data.Telefon;
        fwrite(&NData,sizeof(NData),1,zfp);
        }
      fclose(qfp);
      fclose(zfp);
      return(0);
      }

## 5.5 Dateien mit wahlfreiem Zugriff

Auf die einzelnen Datensätze einer Datei kann direkt zugegriffen werden. Dazu stehen folgende Befehle zur Verfügung:

`int fseek(FILE *f, long offset, int origin)`

Mit dieser Funktion kann man einen Zeiger auf eine bestimmte Position innerhalb einer Datei setzen
Die Funktion positioniert um einen `offset`, der in Bytes gezählt wird. Der Wert `origin` legt fest, worauf sich `offset` bezieht:

- `SEEK_SET` oder 0:
  Die neue Position ergibt sich aus Dateianfang + Offset.
- `SEEK_END` oder 1:
  Die neue Position ergibt sich aus der aktuellen Position + Offset.
- `SEEK_CUR` oder 2:
  Die neue Position ergibt sich aus Dateiende - Offset.

Der Rückgabewert ist 0, wenn die Positionierung erfolgreich war, sonst -1.

Die Funktion `long ftell(FILE *f)` gibt die aktuelle Position in der Datei an, auf die der Dateizeiger weist. Im Fehlerfall liefert `ftell` den Wert -1.

Die Funktion `void rewind(FILE *f)` positioniert auf den Dateianfang und löscht den Fehlerstatus.

Beispiel: Programm mit wahlfreiem Zugriff auf eine Datei

    #include <stdio.h>

    int main(void)
      {
      long pos;
      int count;
      FILE *fp;
      int mode = 0;
      char c;

      fp = fopen("daten.bin","w+");
      if (fp == NULL)
        {
        printf("Fehler beim Oeffnen der Datei\n");
        exit(1);
        }

      /* Datei beschreiben */
      fputs("abcdefghijklmnopqrstuvwxyz",fp);
      puts("abcdefghijklmnopqrstuvwxyz");
      printf("\n");

      /* Wahlfreier Zugriff auf Datei */
      printf("Eingabe der Position im File (0 bis 25):\n");
      scanf("%ld",&pos);
      fseek(fp,pos,mode);
      pos = ftell(fp);
      printf("Dateiposition ist %ld\n",pos);
      fread(&c,1,1,fp);
      printf("\nBuchstabe an dieser Position: %c\n\n",c);
      fclose(fp);
      return 0;
      }

Beispiel: Hexdump
Der Inhalt einer Datei soll Hexadezimal und in ASCII ausgegeben werden. Für die Bildschirmausgabe soll das Programm die Datei blockweise anzeigen, jeweils 16 Zeilen. Der folgende Bildschirmabzug zeigt die Ausgabe:

    Adresse      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F    0123456789ABCDEF

    00000000 :  2F 2A 20 48 65 78 64 75 6D 70 20 65 69 6E 65 72    /* Hexdump einer
    00000010 :  20 44 61 74 65 69 20 2A 2F 0D 0A 0D 0A 23 69 6E     Datei */....#in
    00000020 :  63 6C 75 64 65 20 3C 73 74 64 69 6F 2E 68 3E 0D    clude <stdio.h>.
    00000030 :  0A 23 69 6E 63 6C 75 64 65 20 3C 73 74 64 6C 69    .#include <stdli
    00000040 :  62 2E 68 3E 0D 0A 0D 0A 23 64 65 66 69 6E 65 20    b.h>....#define
    00000050 :  52 45 54 20 31 33 20 20 20 20 20 20 20 20 20 20    RET 13
    00000060 :  20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 2F                   /
    00000070 :  2A 20 52 65 74 75 72 6E 2D 54 61 73 74 65 20 2A    * Return-Taste *
    00000080 :  2F 0D 0A 23 64 65 66 69 6E 65 20 42 45 45 50 20    /..#define BEEP
    00000090 :  70 75 74 63 68 61 72 28 27 5C 30 37 27 29 20 20    putchar('\07')
    000000A0 :  20 20 20 20 20 20 20 20 20 20 2F 2A 20 53 69 67              /* Sig
    000000B0 :  6E 61 6C 74 6F 6E 20 61 75 73 67 65 62 65 6E 20    nalton ausgeben
    000000C0 :  2A 2F 0D 0A 0D 0A 0D 0A 2F 2A 20 67 6C 6F 62 61    */....../* globa
    000000D0 :  6C 65 20 56 61 72 69 61 62 6C 65 20 2A 2F 0D 0A    le Variable */..
    000000E0 :  46 49 4C 45 20 2A 65 69 6E 3B 20 20 20 20 20 20    FILE *ein;
    000000F0 :  20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20

    Adresse      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F    0123456789ABCDEF

Anmerkungen zum Programm:

- Da immer Blöcke von 16 Zeilen zu je 16 Zeichen dargestellt werden und in jeder Zeile zweimal auf die Daten zugegriffen wird, ist es sinnvoll, zumindest eine Zeile komplett in einen Puffer (Feld) einzulesen. Weil seitenweise geblättert werden soll, ist es sogar ratsam, eine komplette Bildschirmseite (16*16 = 256 Bytes) auf einmal von der Datei in den Puffer zu lesen und dann darzustellen.
- Da es unwahrscheinlich ist, daß eine Datei genau ein Vielfaches von 256 Bytes lang ist, muß am Dateiende der restliche Puffer mit einem Wert gefüllt werden, der anzeigt, daß die Daten nicht mehr zur Datei gehören. Vorschlag: Wert der Datenbytes = 0 bis 255, Füller = -1). Die Datei muß natürlich Byte für Byte gelesen werden.
- Da vorwärts und rückwärts geblättert werden soll, muß wahlfrei auf die Datei zugegriffen werden. Dazu verwendet man eine Long-Variable, die die aktuelle Leseposition (erstes zu lesendes Byte) speichert und die dann um 256 incrementiert oder decrementiert wird. Beim vor- und zurückblättern muß man natürlich auch auf Dateianfang oder -ende testen(`feof()`). Das Positionieren innerhalb der Datei erfolgt mit der Funktion `fseek()`.
- Hexzahlen sollen mit führenden Nullen ausgeben werden. Bei der ASCII-Darstellung sollen Zeichen mit den Codes 0 bis 31 als Punkt dargestellt werden.

Das Programm sieht dann so aus (aus Gründen der übersichtlichkeit wurde auf ungepuffertes Einlesen verzichtet).

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

    #define RET 13                         /* Return-Taste */
    #define BEEP putchar('\07')            /* Signalton ausgeben */

    /* globale Variable */
    FILE *ein;                             /* Eingabedatei */
    char name[64];			               /* Dateiname */
    int  ende = 0;                         /* Dateiende erreicht? */
    long position;                         /* aktuelle Position in 'ein' */
    int puffer[16][16];                    /* Puffer fuer eine Seite */

    /* Funktions-Deklarationen */
    void seitelesen(long position);        /* Puffer aus Datei lesen */
    void ausgabe(long position);           /* Puffer darstellen */

    /* Hauptprogramm */
    int main(int argc, char *argv[])
      {
      int command;                         /* Bediener-Kommando */

      if (argc < 2)                        /* Datei ueber Kommandozeile? */
        {
        printf("Dateiname: ");             /* Dann ueber Tastatur */
        scanf("%s",&name);
        }
      else
        strcpy(name,argv[1]);
      ein = fopen(name,"rb");              /* Eingabe eroeffnen */
      if (ein == NULL)
        {
        printf("Eingabedatei kann nicht eroeffnet werden\n");
        exit(1);
        }

      position = 0;                        /* Dateianfang */
      for(;;)                              /* endlose       */
        {                                  /* Hauptschleife */
        seitelesen(position);
        ausgabe(position);
        command = getchar();               /* Befehlstaste lesen */
        switch (command)
          {
          case '+' : if (feof(ein) == 0)   /* naechster Block */
    		   position = position + 256;
    		 else BEEP;
    		 break;
          case 'b' :                       /* vorhergehender Block */
          case 'B' :
          case '-' : if (position >= 256)
    		   position = position - 256;
    		 else BEEP;
    		 break;
          case 'Q' :                       /* Beenden */
          case 'q' : fclose(ein); exit(0);
    		 break;
          default  : BEEP;                 /* alle anderen Tasten */
          } /* end switch */
        } /* end for */
      return 0;
      }

    /* Funktionsdefinitionen */
    void seitelesen(long position)         /* Puffer aus Datei lesen */
      {
      int i, j;                            /* Feldindexe */
      int zeichen;	                       /* gelesendes Zeichen */

      fseek(ein,position,SEEK_SET);        /* Pos. ab Dateianfang */
      i = j = 0;
      do
        {                                  /* zeichenweise einlesen */
        zeichen = getc(ein);
        puffer[i][j] = zeichen;
        j++;
        if (j == 16) (i++, j = 0);
        }                                  /* bis Puffer voll oder EOF */
      while (i*16+j < 256 && zeichen != EOF);
      while (i*16+j < 256)                 /* Rest-Puffer auffuellen */
        {
        puffer[i][j] = -1;
        j++;
        if (j == 16) (i++,j = 0);
        }
      }

    void ausgabe(long position)            /* Puffer darstellen */
      {
      int i, j;                            /* Feldindexe */

      printf("Dump von  *** %s ***  \n",name);
      printf("\n");
      printf("Adresse      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F");
      printf("    0123456789ABCDEF\n");
      printf("\n");
      for (i = 0; i < 16; i++)             /* 16 Zeilen */
        {
        printf("%08lX : ",position);
        for (j = 0; j < 16; j++)           /* Inhalt hexadezimal */
          if (puffer[i][j] < 0)
    	printf("   ");
          else
    	printf(" %02X",puffer[i][j]);
        printf("    ");
        for (j = 0; j < 16; j++)           /* Inhalt als ASCII-Zeichen */
          if (puffer[i][j] > 31 && puffer[i][j] != 127)
    	printf("%c",puffer[i][j]);
          else
    	if (puffer[i][j] < 0)          /* Datei zuende - Blanks */
    	  printf(" ");
    	else
    	  printf(".");                 /* nicht druckbares Zeichen */
        printf("\n");
        position = position + 16;
        }
      printf("\n");
      printf("Adresse      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F");
      printf("    0123456789ABCDEF\n");
      printf("\n");
      printf("Weiter mit <+>         Zurueck mit <B>/<->");
      printf("          Abbruch mit <Q>\n");
      fflush(stdout);
      }

## 5.6 Weitere Dateifunktionen (tabellarisch)

- `FILE *freopen(const char *filename, const char *mode, FILE *stream)`
  liefert `stream` oder `NULL`. Sie wird oft verwendet zum Umleiten von `stdin`, `stdout` oder `stderr`.

- `int remove(const char *filename)`
  Löscht eine Datei. Die Funktion liefert `0` oder einen Fehlercode.

- `int rename(const char *oldname, const char *newname)`
- Wenn die letzte Standardeingabefunktion oder -ausgabefunktion eine Fehlerbedingung erzeugt hat, enthält `int errno` (definiert in `<errno.h>`) eine Fehlernummer.

- `int ferror(FILE *f)`
  liefert bei einem Fehler den Fehlercode oder 0, wenn kein Fehler auftrat.

- `void perror(const char *s)`
  gibt eine Fehlermeldung aus. Der Aufruf hat die gleiche Wirkung wie:
  `printf("%s: %s\n",s,strerror(errno))`.

- `void clearerr(FILE *f)`
  löscht den Fehlerstatus.

## 5.7 Verarbeitung von Verzeichnissen

Um ein Verzeichnis auszulesen, wird es zunächst geöffnet (`opendir`), danach werden die Einträge gelesen (`readdir`) und schließlich wird es wieder geschlossen (`closedir`). Analog zum Dateihandle gibt es ein Verzeichnishandle, das ein Zeiger auf den Datentyp DIR ist. Informationen über den Eintrag liefert die von `readdir` gelieferte Struktur `dirent`.

    #include <sys/types.h>   /* manchmal benoetigt */
    #include <dirent.h>      /* Headerfile fuer die Verzeichnis-Funktionen */

    DIR *opendir(const char *name);
    struct dirent *readdir(DIR *dir);
    int closedir(DIR *dir);

#### opendir()

Die Funktion `opendir()` erhält als Parameter den Namen des Verzeichnisses als Zeichenkette. Der Rückgabewert ist ein Zeiger auf das Verzeichnishandle. Ein Fehler wird dadurch angezeigt, daß dieser Zeiger den Wert NULL hat.

#### readdir()

Die Funktion `readdir()` liest den nächsten Eintrag im Verzeichnis und erhält als Rückgabewert einen Zeiger auf eine Struktur `dirent`. Dieser Zeiger verweist auf eine statische Variable, die nur bis zum nächsten `readdir()` gültig ist (wird jedesmal überschrieben). Unterschiedliche DIR-Handles habe aber unterschiedliche Variablen. Die Variable hat unter UNIX folgende Struktur (bei anderen Betriebssystemen kann auch die Struktur anders sein):

    struct dirent
      {
      long            d_ino;              /* Inode Nummer */
      off_t           d_off;              /* Offset zum naechsten dirent */
      unsigned short  d_reclen;           /* Laenge dieses Eintrags */
      char            d_name[NAME_MAX+1]; /* Dateiname */
      };

Für das Anwenderprogramm ist meist nur der Name des Eintrags interessant. Will man mehr über diesen Eintrag erfahren, beispielsweise, ob es wieder ein Verzeichnis ist, verwendet man andere Systemaufrufe, z. B. `stat()`.

#### closedir()

Zuletzt wird das Verzeichnis mit `closedir()` wieder geschlossen. Ein Beispielprogramm für das Auslesen eines Verzeichnisses sieht so aus:

    #include <sys/types.h>
    #include <dirent.h>

    int main(void)
      {
      DIR *dirHandle;
      struct dirent * dirEntry;

      dirHandle = opendir("."); /* oeffne aktuelles Verzeichnis */
      if (dirHandle != NULL)
        {
        while ((dirEntry = readdir(dirHandle)) != NULL)
          {
          puts(dirEntry->d_name);
          }
        closedir(dirHandle);
        }
      return(0);
      }

Wenn man mit den Dateinamen noch weiter arbeiten will, kann der jeweilige Name auch in eine eigene Variable übertragen werden. Die Länge des Arrays passen Sie am besten an die Größe an, die das Betriebssystem vorgibt. Sie ist als Konstante in `types.h` vorgegeben. Eine Funktion zum Lesen eines Verzeichnisses könnte dann folgendermaßen aussehen:

    void read_dir(char *dirname)
      {
      DIR *dir;
      struct dirent *dirzeiger;
      char dateiname[MAXNAMELEN+1];      /* MAXNAMELEN definiert in types.h */
      printf("%s\n",dirname);

      dir = opendir(dirname);
      if(dir != NULL)
        {
        while((dirzeiger = readdir(dir)) != NULL)
          {
          strcpy(dateiname,(*dirzeiger).d_name);
          if (dateiname[0] != '.') printf("%s\n",dateiname);
          }
        closedir(dir);
        }

#### rewinddir()

Diese Funktion setzt den Lesezeiger wieder auf den Anfang des Verzeichnisses. Die Syntax des Befehls lautet:

    void rewinddir(DIR *dir);

#### getcwd()

Die Funktion `getcwd()` ermittelt das aktuelle Arbeitsverzeichnis. Dazu hat das aufrufende Programm einen Puffer für den Namen zur Verfügung zu stellen, der groß genug ist. Die Größe wird als weiterer Parameter übergeben. Reicht dieser Speicher nicht aus, gibt `getcwd()` NULL zurück.

    #include <unistd.h>

    char * getcwd(char *namebuffer, size_t size);

In einigen Systemen ist es zulässig, NULL als Parameter für `namebuffer` zu übergeben. Dann alloziiert `getcwd()` selbst den benötigten Speicher und gibt den Zeiger darauf zurück. Die Anwendung muß dann durch einen Aufruf von `free()` den Speicher wieder freigeben.

#### chdir()

Mit der Funktion `chdir()` wird das aktuelle Arbeitsverzeichnis gewechselt.

    #include <unistd.h>

    int chdir(char *Pfad);

Bei Erfolg gibt die Funktion 0, sonst -1 zurück. Die Fehlernummer findet sich in der Variablen `errno`.

#### Anlegen und Löschen von Verzeichnissen: mkdir(), rmdir()

Die Funktionen zum Anlegen und Löschen der Verzeichnisse heißen wie die entsprechenden UNIX-Befehle. Beim Anlegen werden Zugriffsrechte übergeben. Wie das UNIX-Kommando `rmdir` kann auch die Funktion nur leere Verzeichnisse löschen.

    #include <fcntl.h>
    #include <unistd.h>

    int mkdir(char *Pfadname, mode_t mode);
    int rmdir(char *Pfadname);

Bei Erfolg geben die Funktionen 0, ansonsten -1 zurück. Die Fehlernummer findet sich in der Variablen `errno`.

## 5.8 Pipes und Named Pipes

Für ein Betriebssystem ist es wichtig, daß mehrere Prozesse miteinander kommunizieren können. Im Laufe der Zeit haben sich zahlreiche Verfahren dafür entwickelt, wobei die Pipe sich sehr einfach anwenden läßt. Gerade bei Unix oder Linux spielen Pipes eine wichtige Rolle. Nehmen wir an, Sie wollen ein Programm zur Anzeige der gerade laufenden Prozesse schreiben oder die Ausgabe eines beliebigen anderen Kommandos in Ihrem C-Programm weiter verarbeiten. Dazu wird einfach eine Pipe zwischen dem externen und dem eigenen Programm eingerichtet, wobei das externe Programm in die Pipe schreiben und das eigene Programm die Daten lesen soll.

Dazu wird zuerst eine Pipe und ein Kindprozess erzeugt. Der Kindprozess schließt die nicht verwendete Seite der Pipe (in diesem Fall die Leseseite). Nun überlagert sich der Kindprozess mittels `exec()` mit einer Shell, die ihrerseits das externe Programm startet. Das alles muss man aber nicht selbst erledigen, sondern die Arbeit wird von der Bibliotheksfunktion `popen()` übernommen.

#### popen()

Öffnen einer Pipe

    #include <stdio.h>

    FILE* popen(const char* commandline, const char* mode);

Der erste Parameter `commandline` ist eine Zeichenkette mit dem gewünschten Kommandoaufruf. Der zweite Parameter `mode` ist entweder "r" für das Lesen aus der Pipe oder "w" für das Schreiben in die Pipe. Bei erfolreicher Ausführung erhält man einen Dateizeiger zurück, bei einem Fehler wird `NULL` geliefert. Nach Beendigung kann man die Pipe wieder schließen:

#### pclose()

Schliessen einer Pipe.

    #include <stdio.h>

    int pclose(FILE* filep);

Das folgende Beispiel zeigt das Abrufen der Uhrzeit:

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

    FILE* pipe;         /* Filehandle fuer die Pipe */
    char Datum[100];    /* Eingabepuffer */

    int main(void)
      {
      /* Pipe zum date-Programm lesend oeffnen */
      pipe = popen("date +%H-%M-%S", "r");

      /* genau 10 Zeichen (hh-mm-ss\n\0) lesen,
         Stringterminator '\0' wird von fgets angehaengt  */
      fgets(Datum,10,pipe);

      /* Pipe wieder schliessen */
      pclose(pipe);

      /* irgendwas machen ... */
      printf ("%s\n",Datum);
      return 0;
      }

Solche Pipes erlauben das "Anzapfen" beliebiger Programme um entweder deren Ausgabe im eigenen Programm weiterzuverarbeiten oder um den Input eines externen Programms zu erzeugen. So kann man beispielsweise mit relativ wenig Aufwand aus den eigenen Daten eine Steuerdatei für das Tool `GnuPlot` erzeugen und so aus dem eigenen Programm heraus beliebige Zwei- oder Dreidimensionale Grafiken erzeugen.

Die bisher besprochenen Pipes fordern eine "Verwandtschaft" zwischen den jeweiligen Prozessen. Will man eine Verbindung zwischen beliebigen Prozessen herstellen, kann man sich auf "Named Pipes" (FIFOs) stützen. Mit dieser Mehode läßt sich auch einfach eine Client/Server-Anwendung realisieren. Named Pipes kann man wie Dateien mit dem Kommando `mknod` in einem beliebigen Verzeichnis anlegen. Über die FIFO läuft dann die Kommunikation zwischen den Prozessen (FIFO bezeichent übrigens das Arbeitsprinzip: First In, First Out).

Natürlich kann eine FIFO auch gleich vom C-Programm erzeugt werden. Dazu existiert eine Bibliotheksfunktion:

#### mkfifo()

Einrichten einer Named Pipe

    #include <sys/types.h>
    #include <sys/stat.h>

    int mkfifo(const char* pathname, mode_t mode)

Der erste Parameter gibt Pfad und Namen an, also die Position im Dateisystem, wo die Named Pipe angelegt werden soll. Der zweite Parameter gibt den Erzeugungsmodus an. Hier gibt es sehr viele Möglichkeiten, die im der Manualpage erklärt werden. Das folgende Beispiel legt eine FIFO an, um danach in diese Pipe etwas zu schreiben. Dach werden die Daten wieder aus der Pipe gelesen.

    #include <stdlib.h>
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>

    int main(void)
      {
      int pipe;                             /* File-Handle der Pipe */
      char puffer[500];                     /* Lese-Puffer */
      char test[] = "Dies ist ein Test\n";  /* Ausgabestring */

     /* Pipe erzeugen Mode = Schreiben/Lesen */
     mkfifo("fifi", O_RDWR);

     /*  Pipe als Datei oeffnen */
     pipe = open("fifo01", O_RDWR);

     /* irgend etwas in die Pipe schreiben */
     write (pipe, test, strlen(test));

     /* nun versuchen wir, das wieder zu lesen */
     if (read(pipe, &puffer, sizeof(puffer)) < 0)
       { printf("Pipe leer !\n"); }
     else
       { printf("%s\n",puffer); }

     /* Pipe schliessen */
     close(pipe);
     /* Die Pipe ist weiter vorhanden! */
     }

Schreibt ein Prozess in eine Pipe, die noch nicht von einem anderen Prozess zum Lesen geöffnet wurde, wird das Signal SIGPIPE generiert.

Das folgende Beispiel zeigt, wie eine Client-Server-Anwendung aussehen könnte. Client und Server werden einfach aus dem obigen Beispiel "herausgezogen". Der Server legt eine Pipe mit Lese/Schreibzugriff an. Danach wartet er auf Daten, die in diese Pipe geschrieben werden, um sie auf der Standardausgabe auszugeben. Der Client darf erst nach dem Server gestartet werden, da die Pipe dann schon existieren muss. Er schreibt in die Pipe und beendet sich dann.

    /*** CLIENT ***/
    #include <stdlib.h>
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>

    int main(void)
      {
      int pipe;                             /* File-Handle der Pipe */
      char test[] = "Dies ist ein Test\n";  /* Ausgabestring */

      /*  Pipe als Datei oeffnen */
     pipe = open("fifo01", O_RDWR);

     /* irgend etwas in die Pipe schreiben */
     write (pipe, test, strlen(test));

     /* Pipe schliessen */
     close(pipe);
     }

    /*** SERVER ***/
    #include <stdlib.h>
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>

    int main(void)
      {
      int pipe;                             /* File-Handle der Pipe */
      char puffer[500];                     /* Lese-Puffer */

     /* Pipe erzeugen Mode = Schreiben/Lesen */
     mkfifo("fifi", O_RDWR);

     /*  Pipe als Datei oeffnen */
     pipe = open("fifo01", O_RDWR);

     /* aus der Pipe lesen */
     read(pipe, &puffer, sizeof(puffer));

     /* Pipe schliessen */
     close(pipe);
     }

## 5.9 Low-Level-I/O

Die folgenden Funktionen sind nicht Teil des Posix-Standards. Sie sind trotzdem fast überall implementiert, da sie für den Low-Level-Zugriff auf Dateien und Geräte (z. B. die serielle Schnittstelle) notwendig sind. Hier begeben Sie sich ins Reich der Systemprogrammierung. Die fünf elementaren Low-Level-Funktionen sind:

- `open()` - Datei- oder Gerätehandle öffnen
- `close()` - Handle schließen
- `read()` - Datenblock lesen
- `write()` - Datenblock schreiben
- `lseek()`- Positionieren

Das Dateihandle (Dateideskriptor) ist hier nicht wie weiter oben der Pointer auf eine Struktur, sondern eine schlichte ganze Zahl. Die Standardhandles sind 0 (Standardeingabe), 1 (Standardausgabe) und 2 (Standardfehlerausgabe). Als Headerdateien werden benötigt:

    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/types.h>
    #include <sys/stat.h>

#### Open und Close

Öffnen einer Datei/eines Gerätes

    #include <fcntl.h>

    int open(const char *pfad, int modus);
    int open(const char *pfadname, int flags, mode_t zugriffsrechte);

    int  close(int handle);

Der Funktion wird der Name und der gewünschte Zugriffsmodus übergeben, der optionale dritte Parameter erlaubt das Setzen von Zugriffsrechten. Die Funktion liefert einen Dateideskriptor zurück, der in den weiteren Funktionsaufrufen als Parameter die Datei identifiziert. Die möglichen Zugriffsmodi sind:

      O_RDONLY   - nur lesen
      O_WRONLY   - nur schreiben
      O_RDWR     - lesen und schreiben
      O_APPEND   - anhaengen
      O_CREAT    - anlegen, falls noch nicht vorhanden
      O_EXCL     - in Kombination mit O_CREAT verhindern des Überschreibens
      O_NOCTTY   - kein Kontrollterminal des Prozesses
      O_NONBLOCK - bei Pipe/Gerät sind die I/O-Operationen nicht blockierend (Socket)
      O_SYNC     - Schreibvorgang wird direkt ausgeführt
      O_BINARY   - Binaerdatei (DOS/Windows)
      O_TEXT     - Textdatei (DOS/Windows)

Gegebenenfalls kann man die Werte "verodern" (z. B. O_WRONLY|O_CREAT).

Die Zugriffsrechte lauten:

      S_IWRITE         - Schreibrecht
      S_IREAD          - Leserecht
      S_IWRITE|S_IREAD - Schreib- und Leserecht

Die Funktion liefert die Handlenummer zurück oder bei Fehler -1.

Die Datei wird mit `close(handle)` wieder geschlossen. Die Funktion liefert 0 zurück oder bei Fehler -1. Beispiel:

    #include <unistd.h>
    #include <fcntl.h>

    int main(void)
      {
      int fd;

      fd = open("Testdatei",O_RDONLY);  /* Datei muß existieren */
      if (fd == -1)
        perror("Datei existiert nicht");
      else
        close(fd);
      return 0;
      }

Der Aufruf von `open` erzeugt keine Datei, falls sie nicht existiert. Das muss der Zugriffsparameter regeln, z. B.:

    #include <unistd.h>
    #include <fcntl.h>

    int main(void)
      {
      int fd;

      fd = open("Testdatei",O_WRONLY|O_EXCL|O_CREAT,0644);
      close(fd);
      return 0;
      }

Sofern Sie mit der oktalen Schreibweise der Zugriffsrechte unter UNIX/Linux vertraut sind, können Sie diese wie oben verwenden. Die führende Null nicht vergessen, sonst ist es keine Oktalzahl.

#### Read und Write

Lesen und Schreiben, sowohl auf geblockte als auch ungeblockte Dateien/Geräte.

    #include <unistd.h>
    #include <fcntl.h>

    int write(int fh, const void *puffer, size_t bytezahl);
    int read(int fh, const void *puffer, size_t bytezahl);

Transfermenge sind immer Bytes. Wichtig ist, dass die Transfermenge (`bytezahl`) nie mehr als die Puffergröße betragen darf. Das folgende Beispielprogramm kopiert eine Datei:

    #include <unistd.h>
    #include <fcntl.h>

    int main(void)
      {
      int in, out;           /* Filehandles  */
      char buffer[1024];     /* Datei-Puffer */
      int count;

      in = open("Quelle",O_RDONLY);   /* muss vorhanden und lesbar sein */
      out = open("Ziel",O_WRONLY|O_TRUNC|O_CREAT,0644);

      while((count = read(in,buffer,1024)) > 0)
        write(out,buffer,count);

      close(in);
      close(out);
      return 0;
      }

`read()` versucht, die angegebene Anzahl Bytes in den Puffer einzulesen, wobei die tatsächliche Anzahl vom Aufruf zurückgeliefert wird. Deshalb sollten stets nur `count` Bytes weggeschrieben werden. Der letzte Block kann auch weniger als 1024 Bytes enthalten, deshalb kann man nicht einfach immer 1024 wegschreiben.

#### Lseek

`lseek()` leistet das Gleiche wie `fseek()` und dient zum Verschieben des File-Deskriptors innerhalb der geöffneten Datei.

    #include <unistd.h>
    #include <fcntl.h>

    long lseek(int fh, long offset, int mode);

Die Datei wird wieder durch den File-Deskriptor `fh` identifiziert, der natürlich zuvor geöffnet bzw. erzeugt wurde. Um wieviele Bytes der File-Deskriptor von der Position wie verschoben werden soll, wird mit `offset` angegeben. Die Angaben von `mode` sind dieselben wie bei `fseek()`:

    SEEK_SET oder 0 - vom Dateianfang aus um offset Bytes verschieben
    SEEK_CUR oder 1 - von der aktuellen Position um offset Bytes verschieben
    SEEK_END oder 2 - vom Dateiende um offset Bytes verschieben

Die Funktion gibt den Wert der aktuellen Position des File-Deskriptors zurück. Bei einem Fehler gibt diese Funktion –1 zurück. Es sollte keinesfalls auf " < 0" geprüft werden, sondern auf –1, da es möglicherweise Gerätedateien gibt, die einen negativen Wert zurückliefern. Beispiele:

    long akt_pos;
    akt_pos = lseek(fh, 0L, SEEK_CUR);

Deskriptor auf den Dateianfang setzen:

    lseek(fh, 0L, SEEK_SET);

Deskriptor um 222 Bytes nach vorn versetzen:

    lseek(fh, 222L, SEEK_CUR);

Deskriptor um 100 Bytes zurücksetzen:

    lseek(fh, -100L, SEEK_CUR);

Oft wird von einem Gerät mittels `read()` gelesen (serielle Schnittstelle, USB-Schnittstelle, I/O-Port bei Embedded Systemen etc.), aber man will die Daten zeilenweise weiterverarbeiten. Der Puffer enthält natürlich irgendwo Newline-Zeichen, man muss sie nur Suchen. Aus diesem Grund soll hier das Splitten des Puffers in einzelne Zeilen etwas näher betrachtet werden. Normalerweise wäre es ein Zufall, wenn ein Zeilenende genau mit dem Pufferende zusammenfällt, und man den String per Standardfunktion splitten kann. Die Regel ist, dass ein Puffer den Anfang und der nächgste Puffer das Ende einer Zeile enthält. Darum soll jetzt die Arbeit mit mehreren aufeinanderfolgenden Datenblöcken näher betrachtet werden:

Die erste Möglichkeit, die sich anbietet, ist das zeichenweise Lesen von der Netzwerk-Schnittstelle (das Betriebssystem puffert ja die Pakete für uns). Sobald dann ein Newline ('\n') auftritt, wird die Funktion verlassen und gibt den String zurück. Es versteht sich von selbst, dass vom aufrufenden Programm ein Zeichen-Array zur Verfügung gestell werden muss und dass man dessen Länge nicht überschreitet. Die folgende Funktion `get_line()` liest von einer vorhergeöffneten Datei genau eine Zeile ein (der Unterstrich in `get_line()` ist notwendig, weil `getline()` eine Bibliotheksfunktion ist). Als Parameter werden neben dem Datei-Descriptor das Array und dessen maximale Länge übergeben:

    int get_line(int fd, char *buffer, unsigned int len)
      {
      /* read a '\n' terminated line from fd into buffer
       * of size len. The line in the buffer is terminated
       * with '\0'. It returns -1 in case of error and -2 if the
       * capacity of the buffer is exceeded.
       * It returns 0 if EOF is encountered before reading '\n'.
      */
      int numbytes = 0;
      int ret;
      char buf;

      buf = '\0';
      while ((numbytes <= len) && (buf != '\n'))
        {
        ret = read(fd, &buf, 1);   /* read a single byte */
        if (ret == 0) break;       /* nothing more to read */
        if (ret < 0) return -1; /* error or disconnect */
        buffer[numbytes] = buf;    /* store byte */
        numbytes++;
        }
     if (buf != '\n') return -2;   /* numbytes > len */
     buffer[numbytes-1] = '\0';    /* overwrite '\n' */
     return numbytes;
     }

Nachteil dieser Lösung ist die Geschwindigkeit bzw. deren Fehlen. Durch die vielen `read()`-Aufrufe ist die Funktion ziemlich langsam. Besser wäre eine Lösung, bei der ein Datenpaket komplett eingelesen und dann Zeile für Zeile ans aufrufende Programm weitergereicht wird. Genau das macht die folgende Funktion, bei der die Parameter die gleiche Aufgabe haben wie oben. Diese Funktion hat einen internen Puffer, der mittels `read()` gefüllt wird und dessen Inhalt Stück für Stück bei jedem Aufruf weitergegeben wird. Dazu verwendet die Funktion die statischen Variablen `bufptr`, `count` und `mybuf`, deren Werte erhalten bleiben und bei jedem Aufruf wieder zur Verfügung stehen. Werden mit `read()` mehrere Zeilen gelesen, bleibt der jeweilige Rest in `mybuf` erhalten und wird beim nächsten Aufruf der Funktion verarbeitet:

    int readline(int fd, char *buffer, unsigned int len)
      {
      /* read a '\n' terminated line from fd into buffer
       * bufptr of size len. The line in the buffer is terminated
       * with '\0'. It returns -1 in case of error or -2 if the
       * capacity of the buffer is exceeded.
       * It returns 0 if EOF is encountered before reading '\n'.
       * Notice also that this routine reads up to '\n' and overwrites
       * it with '\0'. Thus if the line is really terminated with
       * "\r\n", the '\r' will remain unchanged.
      */
      static char *bufptr;
      static int count = 0;
      static char mybuf[1500];
      char *bufx = buffer;
      char c;

      while (--len > 0)            /* repeat until end of line  */
        {                             /* or end of external buffer */
        count--;
        if (count <= 0)            /* internal buffer empty --> read data */
          {
          count = read(fd, mybuf, sizeof(mybuf));
          if (count < 0) return -1;/* error or disconnect */
          if (count == 0) return 0;   /* nothing to read - so reset */
          bufptr = mybuf;             /* internal buffer pointer    */
          }
        c = *bufptr++;                /* get c from internal buffer  */
        if (c == '\n')
          {
          *buffer = '\0';             /* terminate string and exit  */
          return buffer - bufx;
          }
        else
          {
          *buffer++ = c;              /* put c into  external buffer */
          }
        }
      return -2;                      /* external buffer to short */
      }

Beim Senden von Daten sollte man eigentlich nicht zwischenpuffern, sondern jede Zeile sofort auf die Reise schicken - schließlich wartet der Empfänger darauf.

Zum Inhaltsverzeichnis | Zum nächsten Abschnitt

###### Copyright © FH München, FB 04, Prof. Jürgen Plate