Anfang   zurück   weiter

Datei-Ein/Ausgabe

So gut wie jedes Programm produziert in irgendeiner Form eine Eingabe oder Ausgabe. Mit den Funktionen scanf() und printf() sind die Ein-Ausgabe Operationen für die Tastatur und den Bildschirm schon vertraut. Die Daten, die ein Programm in Form von Variablen etc. verwendet, müssen dem Programm auf irgendeine Weise während der Ausführung zur Verfügung gestellt werden. Dies kann nur von ausserhalb des Programmes erfolgen. Genauso muß ein Ergebnis einer Berechnung im Programm, auf irgendeine Weise nach aussen gesendet werden. In C erfolgt dies über sogenannte Datenströme, wobei Datenquellen und Datensenken kollektiv als devices bezeichnet werden. Manche dieser Geräte sind nur zur Eingabe fähig, manche nur zur Ausgabe, andere wieder sowohl zur Eingabe als auch zur Ausgabe. Dies zeigt das Bild unten.

Datei- Ein/ausgabe

Beim Start eines C-Programmes werden vom Betriebssystem automatisch 5 Datenströme eröffnet. Einen Datenstrom muss man dabei einfach als eine Folge von Zeichen auffassen. Der Hauptvorteil von Datenströmen ist, dass Ein/Ausgabe Programmierung geräteunabhängig erfolgen kann. Als Programmierer muß man nicht spezielle Ein/Ausgaberoutinen für jedes Gerät schreiben. Das Programm "sieht" Ein/Ausgabe als kontinuierlichen Strom von Bytes, unabhängig davon, woher dieser kommt oder wohin dieser geht. In C ist jeder Strom mit einer Datei verbunden, wobei hier der Begriff Datei allgemeiner aufzufassen ist. Eine Datei stellt eine Zwischenstufe zwischen dem Datenstrom und dem aktuellen Gerät dar, das für die Ein/Ausgabe verwendet wird. Der Programmierer muß sich dabei nicht um die Einzelheiten dieser Zwischenstufe kümmern, das wird von den C Bibliotheksfunktionen automatisch erledigt. In C fallen die Datenströme in zwei Kategorien, in einen Text-Datenstrom und einen binären Datenstrom. Ein Text-Datenstrom besteht dabei nur aus Textzeichen in der Struktur von Zeilen mit einer Maximallänge von 255 Zeichen und einem abschließenden Zeilenendzeichen. Ein binärer Datenstrom kann jede Sorte von Daten behandeln, inklusive die von Textdaten. Die in ANSI C vordefinierten 5 Datenströme, welche, wie schon erwähnt, automatisch vom Betriebssystem eröffnet und bei Programmennde wieder geschlossen werden, sind:

Name Datenstrom Gerät
 stdin   standard input   Tastatur
 stdout   standard output   Bildschirm
 stderr   standard error   Bildschirm
 stdaux   standard auxiliary   Serieller Port (COM1:)
 stdprn   standard printer   Printer (LPT1:)

Alle 5 vordefinierten Datenströme sind Text-Datenströme. Wann immer man also die Funktionen printf() oder puts() zur Darstellung von Text am Bildschirm verwendet hat, wurde der stdout Strom verwendet. Analog wird bei der Eingabe von der Tastatur mit scanf() oder gets() der stdin Strom angesprochen. Andere Datenströme, wie sie zur Manipulation von Informationen auf der Festplatte oder der Diskette notwendig sind, müssen explizit geöffnet und nach Verwendung wieder geschlossen werden. Um mit Diskdateien zu arbeiten, muß ein Dateizeiger auf eine Datenstruktur existieren, der für das Lesen und Schreiben der Datei verwendet wird. Bevor eine Datei gelesen oder geschrieben werden kann, muß der Zugriff mit der Bibliotheksfunktion fopen() eröffnet werden. Für die Eröffnung eines Dateistromes zum lesen aus einer Datei namens TEST.TXT lauten die nötigen Vereinbarungen;

FILE *fp;
char filename[41] = "TEST.TXT", mode[4]="r";
fp = fopen(filename, mode);
Diese Vereinbarungen legen fest, dass fp ein Zeiger auf FILE ist und fopen() einen entsprechenden Zeiger liefert. FILE ist so ähnlich wie auch z.B. int oder char ein Datentypname und ist in <stdio.h> definiert. Das zweite Argument von fopen() definiert die Zugriffsart. Dabei steht "r" für Lesen, "w" für Schreiben und "a" für Anfügen. Tritt bei der Dateieröffnung ein Fehler auf, so wird NULL zrückgegeben. Darüberhinaus kann an das Argument der Zugriffsart noch ein + und ein b angefügt werden. Mit dem angefügten + wird die Datei sowohl zum Lesen als auch zum Schreiben geöffnet. Das angefügte b ist notwendig, falls eine binäre Datei eröffnet werden soll.
/* fileopen.c
   Zeigt die fopen() Funktion. */

#include <stdio.h>

int main(void)
{
  FILE *fp;
  char ch, filename[40], mode[4];

  while (1)
  {

/* Input filename and mode. */

   printf("\nEnter a filename: ");
   gets(filename);
   printf("\nEnter a mode (max 3 characters): ");
   gets(mode);

/* Try to open the file. */

   if ((fp = fopen( filename, mode )) != NULL )
   {
    printf("\nSuccessful opening %s in mode %s.\n",
    filename, mode);
    fclose(fp);
    puts("Enter x to exit, any other to continue.");
    if ((ch = getc(stdin)) == 'x')
     break;
    else
     continue;
    }
   else
   {
    fprintf(stderr, "\nError opening file %s in mode %s.\n", filename, mode);
    puts("Enter x to exit, any other to try again.");
    if ((ch = getc(stdin)) == 'x')
     break;
    else
     continue;
   }
  }
  return 0;
}

Formatierte Datei-ein/ausgabe

Für die formatierte Ein/ausgabe am Bildschirm wird scanf() und printf() verwendet. Die formatierte Datei-ein/ausgabe erfolgt mit den Funktionen fscanf() und fprintf(). Dabei sind die bekannten Funktionen scanf() und printf() eigentlich nur Spezialfälle dieser neuen Funktionen. Wird als Augabestrom für fprintf() stdout angegeben, so ist die Funktion fprintf() identisch mit printf(). Dies zeigt das folgende Beispiel:
/* fileprnt.c
   Demonstriert fprintf(). */

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

void clear_kb(void);

int main(void)
{
  FILE *fp;
  int data[5];
  int count;
  char filename[20];

  puts("Enter 5 integer numerical values.");

  for (count = 0; count < 5; count++)
	 scanf("%i", &data[count]);

/* Get the filename and open the file. First clear stdin
	of any extra characters. */

  clear_kb();

  puts("Enter a name for the file.");
  gets(filename);

  if ((fp = fopen(filename, "w")) == NULL)
  {
   fprintf(stderr, "Error opening file %s.", filename);
   exit(1);
  }

/* Write the numerical data to the file and to stdout. */

  for (count = 0; count < 5; count++)
  {
   fprintf(fp, "\ndata[%d] = %i", count, data[count]);
   fprintf(stdout, "\ndata[%d] = %i", count, data[count]);
  }

  fclose(fp);
  return 0;
}

void clear_kb(void)
/* Clears stdin of any waiting characters. */
{
  char junk[80];
  gets(junk);
}

Das Programm gibt fünf numerische Werte als formatierten Text sowohl am Bildschirm als auch in eine Datei aus. Die Funktion clear_kb() liest alle Zeichen, die möglicherweise von einem Aufruf von scanf() im Eingabestrom übrigblieben, um sie nicht als zusätzliche falsche Zeichen zum Dateinamen zu erhalten.

Die formatierte Dateieingabe erfolgt mit
int fscanf(FILE *fp, char *fmt, ...);
fscanf() funktioniert analog zu scanf(), nur dass die Eingabe von einer Datei erfolgt.

Zeichen Ein/ausgabe

Für die Zeichen Ein/ausgabe existieren in der Definitionsdatei stdio.h folgende Vereinbarungen:
int fgetc(FILE *fp);
fp ist der von fopen() bei der Dateieröffnung zurückgegebene Zeiger.
char *fgets(char *str, int n, FILE *fp);
Die Funktion fgets liest maximal n-1 Zeichen aus der Datei, auf die mit fp gezeigt wird, bis zu (einschließlich) einem Zeilenendezeichen \n. Die gelesenen Zeichen werden mit einem \0 abgeschlossen und in str gespeichert. Bei fehlerfreier Ausführung wird ein Zeiger auf den gelesenen String str zurückgeliefert, im Fehlerfall (oder am Dateiende) ist das Ergebnis NULL.
int fputc(int ch, FILE *fp);
Hier ist ch das auszugebende Zeichen und fp wieder der von fopen() bei der Dateieröffnung zurückgegebene Zeiger.
char *fputs(char *str, FILE *fp);
str ist ein Zeiger auf den auszugebenden null-terminierten String.

Direkte Datei-Ein/ausgabe

Die direkte Datei-Ein/ausgabe wird nur mit binären Dateien verwendet. Mit der direkten Dateiausgabe werden Blöcke von Daten aus dem Speicher auf die Platte geschrieben. Die direkte Dateieingabe kehrt diesen Prozess um, ein Datenblock wird von der Festplatte ins Memory geladen. Die Funktionen zur direkten Ein/ausgabe sind fread() und fwrite().

Die Bibliotheksfunktion fwrite() schreibt einen Block von Daten aus dem Speicher in eine binäre Datei. Der Prototyp aus STDIO.H ist
size_t fwrite(void *buf, int size, int count, FILE *fp);
Das Argument buf ist ein Zeiger auf eine Speicherregion, in der die Daten, die in die Datei geschrieben werden sollen, enthalten sind. Der Zeiger hat den Typ void, kann also ein Zeiger jeden Typs sein. Das Argument size spezifiziert in Bytes die Größe der einzelnen Daten, und count gibt die Anzahl der zu schreibenden Werte an. Soll also ein Integer Array mit 100 Elementen gespeichert werden, so wird size 2 und count 100 sein. Für die Bestimmung des size Argumentes kann der sizeof() Operator verwendet werden. fp ist natürlich ein Zeiger vom Typ FILE, der bei der Dateieröffnung zurückerhalten wurde. Der Wert den die Funktion fwrite() zurückliefert, ist die Anzahl der erfolgreichen Schreibvorgänge, ist der Wert also kleiner als count, ist ein Fehler aufgetreten. Dies kann für eine Fehlerüberprüfung verwendet werden:
if ((fwrite(buf, size, count, fp) != count)
   fprintf(stderr, "Error writing file.");
Hier nun noch einige Beispiele für den Gebrauch von fwrite().
Die Anweisung um eine einzelne Variable x vom Typ double in eine Datei auszugeben lautet:
fwrite(&x, sizeof(double), 1, fp);
Für die Ausgabe eines Arrays data[] mit 50 Elementen vom Typ float gibt es zwei Möglichkeiten:
fwrite(data, sizeof(float), 50 ,fp);
oder
fwrite(data, sizeof(data), 1, fp);

Beide Möglichkeiten ergeben dasselbe Ergebnis, nur wird im ersten Fall das Array als mit 50 Elementen der Größe float ausgegeben, während im zweiten Fall das Array als "Einzelelement" behandelt wird.

Der Prototyp von fread() lautet:
size_t fread(void *buf, int size, int count, FILE *fp);
buf ist dabei ein Zeiger auf einen Speicherbereich in den die Daten aus der Datei abgelegt werden. Wie bei fwrite() ist der Zeiger auch hier vom Typ void. Die anderen Argumente sind ebenfalls analog zu denen der Funktion fwrite(). Der Rückgabewert der Funktion ist auch hier die Anzahl der gelesenen Werte, und kann kleiner als count sein, falls das Dateiende erreicht wurde, oder ein Fehler auftrat.
/* fwrt-frd.c
   Direktes file I/O mit fwrite() und fread(). */

#include <stdio.h>
#include <conio.h>

#define SIZE 11

int main(void)
{
 int count, array1[SIZE], array2[SIZE];
 FILE *fp;

 clrscr();

/* Initialisiere array1[]. */

 for (count = 0; count < SIZE; count++)
  array1[count] = count * count;

/* Oeffne eine Datei im binaeren Modus. */

 if ((fp = fopen("direct.bin", "wb")) == NULL)
 {
  fprintf(stderr, "Error opening file.");
  exit(1);
 }
/* Speicher array1[] in der Datei. */

 if (fwrite(array1, sizeof(int), SIZE, fp) != SIZE)
 {
  fprintf(stderr, "Error writing to file.");
  exit(1);
 }

 fclose(fp);

/* Oeffne dieselbe Datei zum Lesen im binaeren Modus. */

 if ((fp = fopen("direct.bin", "rb")) == NULL)
 {
  fprintf(stderr, "Error opening file.");
  exit(1);
 }

/* Lies die Daten in array2[] ein. */

 if (fread(array2, sizeof(int), SIZE, fp) != SIZE)
 {
  fprintf(stderr, "Error reading file.");
  exit(1);
 }

 fclose(fp);

/* Nun zeige beide Arrays an (sollten gleich sein). */

 for (count = 0; count < SIZE; count++)
  printf("%d\t%d\n", array1[count], array2[count]);

 getch();

 return 0; 
}
Jede Datei sollte nach Verwendung wieder geschlossen werden. Dies geschieht mit fclose(). In den Beispielen ist die Verwendung von fclose() schon gezeigt worden. Die Funktionsdeklaration lautet
int fclose(FILE *fp);
Dir Funktion liefert 0 oder -1 zurück, je nachdem ob ein Erfolg oder ein Fehler eingetreten ist. Sollen alle offenen Dateien geschlossen werden kann
int fcloseall(void);
verwendet werden. Die Funktion gibt die Anzahl der Ströme, die geschlossen wurden, zurück

Wird ein Programm beendet, werden automatisch alle Dateiströme geleert und geschlossen. Es ist aber trotzdem ratsam, Dateien explizit zu schliessen, sobald sie nicht mehr in Verwendung sind. Der Grund dafür liegt in der Dateipufferung. Für jede Diskdatei wird ein Speicherblock reserviert, in dem die zu schreibenden Daten zwischengespeichert werden, um dann blockweise auf Diskette oder Festplatte geschrieben zu werden. Das heißt also, dass nicht jedes Byte sofort auf die Festplatte geschrieben wird, sondern erst wenn der Puffer voll ist. Wird die Datei geschlossen, wird der gesamte Pufer geleert, und auf die Fesplatte gespeichert. Diese Leerung des Puffers kann man auch ohne die Datei zu schliessen mittels der Funktionen

int fflush(FILE *fp);
und
int flushall(void);
erreichen. fflush() leert den Puffer einer speziellen Datei, während flushall() dies für alle offenen Dateien erledigt. Der Rückgabewert von fflush() ist 0 bei Erfolg oder EOF bei einem Fehler. flushall() liefert die Zahl der offenen Dateien zurück.

Datei Positionsindikator

Jede offene Datei hat einen mit ihr verknüpften Index, der die aktuelle Schreib- oder Lese- Position angibt. Die Position wird immer in Bytes vom Dateianfang angegeben. Wird eine Datei zum Lesen oder Schreiben geöffnet, so ist der Indikator immer am Dateianfang bei Position 0. Wird eine existierende Datei im "append mode" geöffnet, dann befindet sich der Indikator am Dateiende. Über den aktuellen Wert des Datei Positionsindikators gibt die Funktion ftell() Auskunft.
long ftell(FILE *fp);
Bei einem Fehler liefert ftell() -1L zurück.

Um den Indikator wieder an den Dateianfang zu setzen wird rewind() aus STDIO.H verwendet.

void rewind(FILE *fp);
rewind() wird verwendet, falls man aus einer geöffneten Datei schon Daten gelesen hat und neuerlich vom Dateianfang die Daten lesen möchte, ohne die Datei schliessen und wieder öffnen zu müssen.
/* ftl-rwnd.c
   Zeigt Verwendung von ftell() and rewind(). */

#include <stdio.h>
#include <conio.h>

#define BUFLEN 6

char msg[] = "abcdefghijklmnopqrstuvwxyz";

int main(void)
{
 FILE *fp;
 char buf[BUFLEN];

 clrscr();

 if ((fp = fopen("TEXT.TXT", "w")) == NULL)
 {
  fprintf(stderr, "Fehler bei Dateieroeffnung zum Schreiben.");
  exit(1);
 }

 if (fputs(msg, fp) == EOF)
 {
  fprintf(stderr, "Fehler beim Schreiben in die Datei.");
  exit(1);
 }

 fclose(fp);

 if ((fp = fopen("TEXT.TXT", "r")) == NULL)
 {
  fprintf(stderr, "Fehler bei Dateieroeffnung zum Lesen.");
  exit(1);
 }
 printf("\nUnmittelbar nach Dateieroeffnung, Position = %ld", ftell(fp));

/* Lies 5 Zeichen. */
 fgets(buf, BUFLEN, fp);
 printf("\nNach Lesen von %s, Position = %ld", buf, ftell(fp));

/* Lies weitere 5 Zeichen. */
 fgets(buf, BUFLEN, fp);
 printf("\n\nDie naechsten 5 Zeichen sind %s, und die Position ist nun = %ld", buf, ftell(fp));

/* Zurueck an den Dateianfang. */
 rewind(fp);
 printf("\n\nNach dem Rueckspulen, ist die Position wieder bei %ld", ftell(fp));

/* Lies 5 Zeichen. */
 fgets(buf, BUFLEN, fp);
 printf("\nund das Einlesen beginnt wieder am Anfang: %s\n", buf);
 fclose(fp);

/* Loesche die Datei! */
 remove("TEXT.TXT");
 
 getch();

 return 0;
}

Eine präzisere Kontrolle über den Positionsindikator ist mit fseek() möglich. Mittels fseek() kann der Positionsindikator überall in der Datei positioniert werden. Die Deklaration in STDIO.H ist:

int fseek(FILE *fp, long offset, int origin);
Die Distanz in Bytes um den der Indikator versetzt wird, ist durch offset gegeben. Das Argument origin gibt den Startpunkt an. Es gibt drei mögliche Werte für den Startpunkt, die in STDIO.H definiert sind.
Konstante Wert Bedeutung
SEEK_SET 0 Startpunkt ist der Dateianfang
SEEK_CUR 1 Startpunkt ist die aktuelle Position
SEEK_END 2 Startpunkt ist das Dateiende
Bei Erfolg gibt fseek() 0 zurück, falls ein Fehler aufgetreten ist, einen Wert ungleich Null.
/* fseek.c
   Wahlfreier Zugriff mit fseek(). */

#include <stdio.h>
#include <conio.h>

#define MAX 9

int main(void)
{
 FILE *fp;
 int data, count, array[MAX];
 long offset;

/* Initialisiere das Array. */

 for (count = 0; count < MAX; count++)
 array[count] = (count + 1) * 10;

/* Oeffne eine binaere Datei zum Schreiben. */

 if ((fp = fopen("RANDOM.DAT", "wb")) == NULL)
 {
  fprintf(stderr, "Fehler bei Dateieroeffnung zum Schreiben.");
  exit(1);
 }

/* Schreibe das Array,und schliesze die Datei. */

 if ((fwrite(array, sizeof(int), MAX, fp)) != MAX)
 {
  fprintf(stderr, "Fehler beim Schreiben in die Datei.");
  exit(1);
 }

 fclose(fp);

 if ((fp = fopen("RANDOM.DAT", "rb")) == NULL)
 {
  fprintf(stderr, "Fehler bei Dateieroeffnung zum Lesen.");
  exit(1);
 }

/* Frage den Benutzer welches Element gelesen werden soll.
	Lies das Element ein und zeige es an.
	Brich ab, falls 0 eingegeben wird. */

 clrscr();
 while (1)
 {
  printf("\nWelches Element soll gelesen werden, 1-%d, 0 um abzubrechen: ",MAX);
  scanf("%ld", &offset);

  if (offset <= 0)
	break;
  else if (offset > MAX)
	continue;

/* Verschiebe den Position Indicator zum angegebenen Element. */

  if ((fseek(fp, ((offset - 1) * sizeof(int)), SEEK_SET)) != NULL)
  {
   fprintf(stderr, "\nFehler bei der Verwendung von fseek().");
   exit(1);
  }

/* Lies einen Integer Wert. */

  fread(&data, sizeof(int), 1, fp);

  printf("\nDas %ld.te Element hat den Wert %d.", offset, data);
 }

 fclose(fp);

 return 0;
}

Dateiende

In manchen Fällen ist die Dateilänge eindeutig von Anfang an gegeben, so dass es nicht notwendig ist, das Dateiende fetstellen zu können. Ist die Dateilänge aber nicht bekannt, so möchte man aber doch vom Dateianfang bis zum Ende Daten lesen können. Es gibt zwei Möglichkeiten das Dateiende festzustellen.

Wird von einer Textdatei gelesen so kann auf das Vorhandensein des EOF Zeichens geachtet werden. Die symbolische Konstante EOF wird in STDIO.H als -1 definiert, ein Wert, der nie als "echtes" Zeichen vorkommt. Liest eine Zeicheneingabefunktion aus einer Textdatei den Wert EOF, so kann man sicher sein, dass das Dateiende erreicht ist. Folgende Anweisung reicht aus, um alle Zeichen von einer Textdatei einzulesen.

while ((c=fgetc(fp)) != EOF )
Dies funktioniert jedoch nicht mit einer binären Datei, da in einer solchen ein Wert von -1 durchaus vorkommen kann. Um also das Dateiende einer binären Datei festzustellen muß die Funktion feof() verwendet werden. Diese Funktion läßt sich auf Text- und Binär- Dateien anwenden.
int feof(FILE *fp);
Ein Rückgabewert von 0 signalisert, dass das Dateiende noch nicht erreicht ist, am Dateiende wird ein Wert ungleich Null zurückgegeben.

Dateimanagement-Funktionen

Für das Dateimanagement von bestehenden Dateien aus einem C-Programm heraus, existieren ebenfalls Funktionen. Die C Standardbibliothek enthält Funktionen zum Löschen und Umbenennen von Dateien. Zum Löschen wird die Funktion remove() verwendet, zum Umbenennen von Dateien die Funktion rename(). Der Funktionsprototyp von remove() lautet
int remove(const char *filename);
Der Rückgabewert ist 0 falls der Löschvorgang erfolgreich war, im Fehlerfall wird -1 zurückgegeben. Für rename() gilt
int rename(const char *oldname, const char *newname);
wobei auch hier bei Erfolg eine 0, oder bei einem Fehler eine -1 zurückgeliefert wird.


Strukturen