Algorithmen & Datenstrukturen
Programmieren 1


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:

5.1 Zugriffsart

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

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 "+":

    Durch anhängen eines Zusatzes kann festgelegt werden, ob es sich bei der zu bearbeitenden Datei um eine Binär- oder Textdatei handelt:

    Die Funktion fopen reagiert folgendermaßen:

    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:

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

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:

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:

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;
  }

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:

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:

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)

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

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.

Zum Inhaltsverzeichnis Zum nächsten Abschnitt


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