Was währe eine Programmiersprache, ohne das Daten auf einen Datenträger gespeichert werden können. In diesem sehr Umfangreichen Kapitel geht es Ausschließlich um den Fluss der Daten. Damit ist die Eingabe und Ausgabe von Daten gemeint. C C++ C/C++ Datei Stream Eingabe Ausgabe fopen fgets fputs fread fwrite fgetc fputc gets puts sscanf sspintf ... Datei Stream Eingabe Ausgabe fopen fgets fputs fread fwrite fgetc fputc gets puts sscanf sspintf ... Kapitel 19: Datei Ein- Ausgabe

Was wäre eine Programmiersprache, ohne dass Daten zum Beispiel auf einen Datenträger gespeichert werden können. In diesem sehr umfangreichen Kapitel geht es ausschließlich um den Fluss der Daten. Gemeint ist damit die Eingabe und Ausgabe von Daten.

19.1. Was ist eine Datei?            zurück  Ein Kapitel tiefer  zum Inhaltsverzeichnis

Häufig werden elementare Datei-E/A-Funktionen (E/A steht für Eingabe/Ausgabe) angewandt, ohne sich einen Gedanken darüber zu machen, was eine Datei eigentlich ist. Im Prinzip können Sie sich eine Datei als ein riesengroßes char-Array vorstellen. Das char-Array besteht dabei aus einer Folge Bits und Byte. Unabhängig davon, ob es sich um eine Textdatei oder eine ausführbare Datei handelt. Sehen Sie sich mit einem Hexeditor einmal eine ausführbare Datei aus der Sicht des Computers an. Wenn Sie dasselbe mit einer Textdatei machen, werden Sie feststellen, dass es keine gravierenden Unterschiede zwischen den beiden Dateien gibt. Es wird dabei von einem "Byte stream" oder einfach kurz einem "Stream" gesprochen.

19.2. Formatierte und Unformatierte Ein-/Ausgabe            zurück  Ein Kapitel tiefer  Ein Kapitel höher  zum Inhaltsverzeichnis

In C besteht die Möglichkeit, von zwei verschiedenen Ebenen auf eine Datei zuzugreifen. Zum einen auf der höheren Ebene (High-Level) und zum anderen auf der niedrigeren (Low-Level). Mit der höheren Ebene kann wesentlich komfortabler und vor allem portabler programmiert werden. Die Funktionen der höheren Ebene entsprechen dem ANSI C-Standard. Zu den Vorteilen der höheren Ebene gehören z.B. eine formatierte Ein- und Ausgabe und ein optimal eingestellter Puffer. Der Puffer ist ein Bereich im Arbeitsspeicher, der als Vermittler zwischen Daten und Zielort fungiert. Der Unterschied zwischen der höheren und der niedrigeren Ebene liegt in der Form, wie die Daten in einem Stream von der Quelle zum Ziel übertragen werden. Bei der höheren Ebene ist der Stream eine formatierte Dateneinheit (wie z.B. mit printf()). Hingegen handelt es sich bei der niedrigeren Ebene um einen unformatierten Byte-Stream.

19.3. Stream            zurück  Ein Kapitel tiefer  Ein Kapitel höher  zum Inhaltsverzeichnis

Streams sind einfache Datenströme, mit denen Daten von der Quelle zum Ziel bewegt werden. Es gibt Standard-Streams in C wie die Standardeingabe (stdin), die Standardausgabe (stdout) und die Standardfehlerausgabe (stderr). Auf sie wird in Abschnitt 19.10 noch genauer eingegangen.

19.4. Höhere Ein-/Augabe-Funktionen            zurück  Ein Kapitel tiefer  Ein Kapitel höher  zum Inhaltsverzeichnis

Zuerst ein kleiner Überblick der Funktionen, die in der Headerdatei deklariert sind und zur formatierten Ein-/Ausgabe verwendet werden:

Funktion Beschreibung
fopen Datenstrom öffnen
fclose Datenstrom schließen
feof Testet auf Dateiende im Stream
ferror Testet auf Fehler im Stream
fflush Leert den Puffer im Datenstrom
fgetc Zeichenweise lesen vom Stream
fgets Zeilenweise lesen vom Stream
fgetpos Position im Stream ermitteln
fprintf Formatierte Ausgabe an Stream
fputc Zeichenweise schreiben in den Stream
fputs Schreibt einen String in den Stream
freopen Datenstrom erneut öffnen
fscanf Formatierte Eingabe vom Stream
fseek Dateizeiger neu positionieren
fsetpos Dateizeiger neu positionieren
ftell Position im Stream ermitteln
getc Zeichenweise lesen vom Stream
getchar Zeichenweise lesen von stdin
gets Liest String von stdin (unsichereFunktion)
printf Formatierte Ausgabe an stdout
putc Zeichenweise schreiben in den Stream
putchar Zeichenweise schreiben an stdout
puts Zeichenkette an Stream stdout
rewind Position von Stream auf Anfang
scanf Formatierte Eingabe von stdin
setbuf Streampuffer einrichten
setvbuf Streampuffer verändern
sprintf Formatierte Ausgabe in einem String
sscanf Formatierte Eingabe aus einem String
ungetc Zeichen zurück in den Stream

Tabelle 19.1: Übersicht zu den Standard Ein/Ausgabe-Funktionen

All diese Funktionen werden Sie auf den nächsten Seiten etwas genauer kennen lernen und anwenden

19.5. Datei (Stream) öffnen - fopen            zurück  Ein Kapitel tiefer  Ein Kapitel höher  zum Inhaltsverzeichnis

Die Bearbeitung von Dateien erfolgt in C immer zeichenorientiert. Da Dateien zunächst nichts anderes sind als eine unstrukturierte Folge von Einzelzeichen, spielt es keine Rolle, mit welcher Art von Daten gearbeitet wird. Erst bei der Verarbeitung der Daten bekommen die Einzelzeichen eine Bedeutung und eine Struktur.

Zuerst soll eine einfache Textdatei zum Lesen geöffnet werden. Dabei wird folgendermaßen vorgegangen:

FILE *datei;
…
datei = fopen("textdatei.txt", "r");

Es wurde eine Textdatei mit dem Namen "textdatei.txt" geöffnet. Mithilfe des Zeigers datei vom Typ FILE wird dabei ein Lese-Stream zu dieser Textdatei eingerichtet. Hier die Syntax der Funktion fopen():

#include <stdio.h>

FILE *fopen(const char *pfadname, const char *modus);

Als Pfadangabe (pfadname) ist jeder zulässige String erlaubt. Sollten Sie unter einem Microsoft-Betriebssytem programmieren, kann auch eine Laufwerksangabe erfolgen. Die maximale Stringlänge für pfadname ist in der Konstante FILENAME_MAX, welche sich ebenso in der Headerdatei <stdio.h> befindet, deklariert. Mit modus geben Sie an, wie auf den Stream zugegriffen wird. Im Beispiel wurde der Modus "r" (für read) zum Lesen von der Datei verwendet. Auf die einzelnen möglichen Modi wird gleich eingegangen. Wenn beim Öffnen einer Datei alles planmäßig verlief, wird der FILE-Zeiger zurückgegeben. Bei einem Fehler erhalten Sie hingegen den NULL-Zeiger zurück.

Der FILE-Zeiger - es wird ja auch von einem FILE-Stream gesprochen - ist eine Struktur, die in der Headerdatei <stdio.h> deklariert ist. Diese Struktur beinhaltet alle Informationen, die für die höheren Datei-E/A-Funktionen benötigt werden, beispielsweise:

Natürlich können Sie auch mehrere Dateien auf einmal öffnen:

FILE *datei,*datei2;
…
/* Datei textdatei.txt zum Lesen öffnen */
datei = fopen("textdatei.txt","r");
/* Datei textdat2.txt zum Lesen öffnen */
datei2= fopen("textdat2.txt","r");

Jetzt zu einem ausführbaren Beispiel der Funktion fopen():

#include <stdio.h>

int main()
{
   FILE *datei;
   /* Bitte Pfad und Dateinamen anpassen */
   datei=fopen("test.txt","r");
   if(NULL == datei)
      {
         printf("Konnte Datei \"test.txt\" nicht öffnen!\n");
      }
   return 0;
}

Das Programm öffnet (falls vorhanden) die Datei "test.txt". Konnte diese Datei nicht geöffnet werden bzw. ist sie nicht vorhanden, liefert die Funktion fopen() den NULL-Zeiger zurück. In diesem Beispiel muss sich die Datei "test.txt" im selben Verzeichnis befinden wie das ausführbare Programm. Liegt die Datei "test.txt" hingegen im Verzeichnis

c:\Dokumentationen\Texte\test.txt 

dann muss das erste Argument in der Funktion fopen() folgendermaßen aussehen:

datei = fopen("c:\\Dokumentationen\\Texte\\test.txt","r"); 

Bei Microsoft-Systemen muss darauf geachtet werden, dass statt nur einem Backslash zwei (\\) geschrieben werden, um das Zeichen '\' anzuzeigen. Bei Linux/UNIX ist das einfacher. Ist das Verzeichnis folgendes

/home/Texte/test.txt  

dann muss sich Selbiges im ersten Argument befinden:

datei = fopen("/home/Texte/test.txt","r");

Unter UNIX/Linux gibt es außerdem keine Laufwerksbezeichnung, da dort jedes Gerät, ob Festplatte, CD/DVD-ROM oder Diskette als Datei betrachtet werden kann.

Hinweis
 

Bei den meisten Compilern unter MS-Windows kann mittlerweile die Pfadangabe ebenfalls mit einem einfachen Slash (c:/pfad/pfad) - wie bei Linux/UNIX üblich - erfolgen.

Es gibt noch weitere Unterschiede zwischen diesen beiden Betriebssystemen. Hierzu eine Gegenüberstellung von Linux/UNIX und Microsoft-Systemen:

Eigenschaft Linux MS-Windows
Erlaubte Zeichen alle Zeichen Buchstaben, Zahlen und einige Sonderzeichen
Laufwerksbezeichnung keine a:, b:, c:, … z:

Tabelle 19.2: Systemabhängiges bei Angabe der zu öffnenden Datei

19.5.1 Modus für fopen()
Außer dem Lesezugriff ("r"), den Sie bereits verwendet haben, gibt es eine Reihe weiterer Zugriffsmöglichkeiten auf einen Stream. Hier ein Überblick über die vorhandenen Modi und deren Bedeutung:

Modus Bedeutung
"r" Öffnen einer Datei zum Lesen. Wenn die Datei nicht existiert oder nicht geöffnet werden konnte, gibt fopen() NULL zurück.
"w" Anlegen einer Datei zum Ändern. Wenn die Datei nicht geändert werden kann bzw. wenn keine Schreibberechtigung besteht, liefert hier fopen() NULL zurück. Wenn unter Windows/MS-Dos die Datei ein Readonly-Attribut hat, kann diese nicht geöffnet werden.
"a" Öffnet die Datei zum Schreiben oder Anhängen ans Ende der Datei. Wenn die Datei nicht vorhanden ist, liefert fopen() wieder NULL zurück. Auch NULL wird zurückgeliefert, wenn keine Zugriffsrechte bestehen.
"r+" Öffnet die Datei zum Lesen und Schreiben, also zum Verändern. Bei Fehlern oder mangelnden Rechten liefert fopen() auch hier NULL zurück.
"w+" Anlegen einer Datei zum Ändern. Existiert eine Datei mit gleichem Namen, wird diese zuvor gelöscht. Bei Fehlern oder mangelnden Rechten liefert fopen() hier NULL zurück.
"a+" Öffnen einer Datei zum Lesen oder Schreiben am Ende der Datei bzw. die Datei wird angelegt, falls noch nicht vorhanden. Bei Fehlern oder mangelnden Rechten liefert fopen() NULL zurück.

Tabelle 19.3: Modus zum öffnen einer Datei mit fopen()

Damit dieses Buch auch als Referenz zu gebrauchen ist, folgt hierzu eine Tabelle für eine schnellere Übersicht der einzelnen Modi:
Bewirkt r w a r+ w+ a+
Datei ist lesbar x     x x x
Datei ist beschreibbar   x x x x x
Datei ist nur am Dateiende beschreibbar     x     x
Existierender Dateiinhalt geht verloren   x     x  

Tabelle 19.4: Schnellübersicht der Bearbeitungsmodi



Hinweis
 

Wird unter Linux eine neue Datei mit dem Modus "w" oder "a" angelegt, schreibt der POSIX-Standard vor, dass die Datei mit folgenden Rechten angelegt wird: -rw-rw-rw

An diese Modi können außerdem zwei weitere Zeichen angehängt werden, die zwischen Text- und Binärdateien unterscheiden:

Zusätzlicher Modus Bedeutung
b Die Datei wird im Binärmodus geöffnet. Die Zeichen werden dabei nicht verändert bzw. konvertiert. Das heißt, jedes Zeichen wird so weitergegeben, wie es in der Datei steht, und es wird so in die Datei geschrieben, wie die Schreibfunktion eingestellt ist. Der Modus b wird bei Linux nicht verwendet und bei Angabe ignoriert. Er wird nur aus Kompatibilitätsgründen zu ANSI C erhalten.
t Die Datei wird im Textmodus geöffnet und sollte daher auch lesbare Textzeichen beinhalten.

Tabelle 19.5: Text- und Binärmodus

Eine kurze Erklärung des Unterschieds zwischen Textdateien und Binärdateien: Textdateien sind für den Menschen mit einem Editor lesbar. Binärdateien bzw. binäre Zeichen (0,1) bilden die Sprache, die der Computer versteht. Für einen Menschen ist dies kaum lesbar. Daher bestehen Textdateien immer aus sichtbaren ASCII-Zeichen und ein paar Steuercodes, wie etwa Zeilenschaltungen oder Tabulatoren. Für die Bearbeitung reiner Textdateien ist der Modus t gedacht. Da bei MS-DOS ein Zeilenende mit der Sequenz \r\n angezeigt wird und bei Linux nur durch ein einzelnes \n, führen Compiler für MS-DOS/Windows im Textmodus t folgende Konvertierung durch:

Im Binärmodus wird diese Konvertierung nicht vorgenommen. Bei Linux/UNIX bedeutet das b nichts, wie in der Tabelle oben schon erwähnt, und wird bei Verwendung ignoriert. Unter Linux wird außerdem jede Datei binär gespeichert.

Den Namen der zu öffnenden Datei können Sie natürlich auch mit Hilfe von Argumenten aus der Kommandozeile angeben. Ein Beispiel:

#include <stdio.h>

int main(int argc, char **argv)
{
   FILE *datei;

   if(argc < 2)
      {
         printf("Verwendung : %s FILE\n",*argv);
         exit(0);
      }
   datei = fopen(argv[1], "r");
   if(datei != NULL)
      printf("Datei erfolgreich geöffnet\n");
   else
      printf("Fehler beim Öffnen der Datei");
 return 0;
}

Zuerst wird überprüft, ob zwei Argumente in der Kommandozeile eingegeben wurden. Ist dies nicht der Fall, wird eine entsprechende Fehlermeldung ausgegeben. Ansonsten wird versucht, die Datei, welche Sie in der Kommandozeile mit dem zweiten Argument angegeben haben, zu öffnen. Tritt dabei ein Fehler auf, liegt dies meistens an einer falschen Pfadangabe oder unzureichenden Rechten einer Datei.

19.5.2 Maximale Anzahl geöffneter Dateien - FOPEN_MAX
Bei einem Programm, bei dem sehr viele Dateien gleichzeitig geöffnet werden, sollte eine Überprüfung mit der Konstante FOPEN_MAX aus der Headerdatei <stdio.h> vorgenommen werden. Diese Konstante legt fest, wie viele Dateien gleichzeitig pro Prozess geöffnet werden dürfen. Testen können Sie dies z.B. so:

#include <stdio.h>

int main()
{
   printf("Max. offene Dateien : %d\n",FOPEN_MAX);
   return 0;
}

Hinweis für Fortgeschrittene
 

Laut ANSI C sollten Sie per fopen() mindestens acht Dateien mit einem Prozess öffnen können. Meistens liegt dieser Wert aber weitaus höher. Außerdem sind mit acht Dateien reale Streams gemeint, also ohne die Standard-Streams stdin, stdout und stderr. Dies sollte erwähnt werden für den Fall, dass Sie die Struktur FILE tatsächlich auf die Anzahl offener Dateien überprüfen und sich wundern, warum dabei immer mehr Streams offen sind, als Sie in Wirklichkeit geöffnet haben.

Damit alles reibungslos mit dem erfolgreich zurückgegeben Stream verläuft und Sie problemlos in Dateien schreiben bzw. aus diesen lesen können, müssen Sie bei der Anwendung der Funktion fopen() noch folgende Punkte berücksichtigen:

Falls Sie die Punkte noch nicht verstanden haben, keine Sorge, Sie werden auf den nächsten Seiten aufgeklärt.

19.6. Zeichenweise Lesen und Schreiben - getchar und putchar            zurück  Ein Kapitel höher  zum Inhaltsverzeichnis

Um zeichenweise aus dem Stream stdin (Standardeingabe) zu lesen und zeichenweise auf stdout (Standardausgabe) zu schreiben, können folgende Funktionen verwendet werden:

#include <stdio.h>

/* Lesen (zeichenweise) von stdin */
int getchar();
/* Schreiben (zeichenweise) auf stdout */
int putchar(int c);

getchar() dient zum Einlesen einzelner Zeichen von der Standardeingabe, normalerweise ist dies die Tastatur. Ein wenig verwirrend dürfte der Rückgabewert der Funktion getchar() sein, da dieser vom Datentyp int ist. Das liegt daran, dass ein char vor der Verwendung eines Ausdrucks in ein int konvertiert wird.

Etwas genauer: Das Problem der Verwendung von int liegt in der Konstante EOF (End of File), welche das Ende einer Eingabe anzeigt. EOF ist eine define-Konstante, die in der Headerdatei <stdio.h> mit dem Wert -1 deklariert ist, damit diese nicht mit den normalen ASCII-Zeichen kollidiert. Früher, als noch 127 Zeichen verwendet wurden, war das kein Problem. Heute sind die Werte der Zeichen größer als 127, um zum Beispiel Umlaute wie ä, ö ,ü und ß ausgeben zu können. Ist char dabei mit unsigned deklariert, könnten Zeichen zwischen 0 ... 255 Platz darin finden. Es ist dann aber kein Platz mehr für EOF (-1). Daher wurde einfach der Rückgabewert von getchar() als int deklariert, und damit können sowohl die 255 Zeichen als auch das EOF übermittelt werden - das Problem war gelöst.

Hierzu ein Listing:

#include <stdio.h>

int main()
{
   int c;

   while((c = getchar()) !='.')
      putchar(c);
   return 0;
}

Wenn hierbei mehrere Zeichen eingegeben werden und ENTER gedrückt wird, wird der Text Zeichen für Zeichen auf dem Bildschirm ausgegeben. Dies geschieht so lange, bis ein einzelnes Zeichen dem eines Punktes entspricht. Dann ist die while-Bedingung unwahr. Sie können als Abbruchbedingung auch EOF angeben:

while((c = getchar()) != EOF); 

Hiermit werden solange Zeichen eingelesen, bis die Tastenkombination STRG + Z (unter MS-Systemen) oder STRG + D (unter Linux) gedrückt wird, welche EOF nachbildet.

Hinweis für Programmierneulinge
 

Auch wenn es bei der Funktion getchar() den Anschein hat, dass hier mit ganzen Strings gearbeitet wird, ist dem nicht so. Diese Funktion liest Zeichen für Zeichen aus einem Puffer. Dies geschieht aber erst, wenn die Taste ENTER gedrückt wird. Suchen Sie nach einer Funktion, die auf Betätigung einer bestimmten Taste wartet, könnte die Funktion getch() für Sie interessant sein. Für MS-DOS steht diese Funktion sofort zur Verfügung. Beispiel: while( (c=getch()) != 'q');. Damit wird das Programm so lange angehalten, bis die Taste q gedrückt wird. Unter Linux müssen Sie dafür die Bibliothek <ncurses.h> oder <termios.h> verwenden. Der Nachteil von getch() ist, dass das Programm damit schlecht auf ein anderes System portiert werden kann.

Zur Funktion getchar() noch ein Listing:

#include <stdio.h>

int main ()
{
   int c,counter=0;
   printf("Bitte Eingabe machen:");
   /* Eingabe machen bis mit Return beendet wird */
   while((c=getchar()) != '\n')
      {  /*Leerzeichen und Tabulatorzeichen nicht mitzählen*/
         if( (c != ' ') && (c != '\t') )
            counter++;     /* counter erhöhen */
      }
   /* Gibt die Anzahl eingegeb. Zeichen von 0 bis counter-1 aus
      mit counter-1 wird das Zeichen '\0' nicht mitgezählt */
   printf("Anzahl der Zeichen beträgt %d Zeichen\n",counter-1);
   return 0;
}

Mit diesem Listing werden alle darstellbaren Zeichen gezählt, die Sie über die Tastatur eingeben. Leerzeichen und Tabulatoren werden jedoch nicht mitgezählt.

19.6.1 Ein etwas portableres getch()
Diese Frage wurde mir bereits unzählige Male gestellt: Wie kann ich den Programmablauf anhalten, bis eine bestimmte Taste gedrückt wird? Dafür gibt es leider keinen standardisierten Weg. Aber um Sie jetzt nicht im Regen stehen zu lassen, folgt hierfür ein etwas portableres getch(), welches sowohl unter Linux/UNIX als auch unter MS-Windows funktioniert. Ihnen diese Funktion speziell unter Linux/UNIX näher zu erklären, würde eine Spur zu weit gehen. Falls es Sie dennoch interessiert, können Sie mehr darüber auf meiner Homepage unter http://www.pronix.de erfahren. Bei MS-Windows gibt es dabei nicht viel zu sagen, nur dass die Headerdatei <conio.h> mit eingebunden werden muss, da sich darin diese Funktion befindet. Hier der Quellcode:

#include <stdio.h>
/* … übersetzt unter Linux/UNIX? */
#ifdef __unix__
#include <termios.h>
#include <unistd.h>

static struct termios new_io;
static struct termios old_io;

/* Funktion schaltet das Terminal in den cbreak-Modus: */
/* Kontrollflag ECHO und ICANON auf 0 setzen        */
/* Steuerzeichen: Leseoperation liefert 1 Byte VMIN=1 VTIME=1 */
int cbreak(int fd)
{
   /*Sichern unseres Terminals*/
   if((tcgetattr(fd, &old_io)) == -1)
      return -1;
   new_io = old_io;
   /*Wir verändern jetzt die Flags für den cbreak-Modus*/
   new_io.c_lflag = new_io.c_lflag & ~(ECHO|ICANON);
   new_io.c_cc[VMIN] = 1;
   new_io.c_cc[VTIME]= 0;

/*Jetzt setzen wir den cbreak-Modus*/
   if((tcsetattr(fd, TCSAFLUSH, &new_io)) == -1)
      return -1;
   return 1;
}

int getch()
{
   int c;
   if(cbreak(STDIN_FILENO) == -1)
      {
         printf("Fehler bei der Funktion cbreak......\n");
         exit(0);
      }
   c = getchar();
   /*Alten Terminal-Modus wiederherstellen*/
   tcsetattr(STDIN_FILENO, TCSANOW, &old_io);
   return c;
}
/* … oder wird das Programm unter MS-Windows übersetzt? */
#elif __WIN32__ || _MSC_VER || __MS_DOS__
  #include <conio.h>
#endif

int main()
{
  int zeichen;
  printf("Bitte 'q' drücken, um das Programm zu beenden!\n");
   /* Wartet auf das Zeichen q */
  while(( zeichen=getch() ) != 'q');
  return 0;
}

19.7. Zeichenweise Lesen und Schreiben - putc/fputc und getc/fgetc            zum Inhaltsverzeichnis