vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 3

Tag 15

Mit Dateien arbeiten

Die meisten Programme arbeiten aus dem einen oder anderen Grund mit Dateien: Datenspeicherung, Konfigurationsdaten und so weiter. Heute lernen Sie:

Streams und Dateien

Wie Sie am Tag 13, »Mit Bildschirm und Tastatur arbeiten«, gelernt haben, erfolgt die Ein- und Ausgabe in C über Streams - und das gilt auch für die Dateien. Es wurde Ihnen gezeigt, wie Sie die vordefinierten C-Streams anwenden, die mit bestimmten Geräten wie der Tastatur und dem Bildschirm verbunden sind. Datei-Streams funktionieren praktisch genauso. Dies ist einer der Vorteile der Stream-Ein-/-Ausgabe - die Techniken, die bei der Arbeit mit einem Stream angewendet werden, lassen sich mit wenigen oder sogar ohne Änderungen auf andere Streams übertragen. Der Hauptunterschied zwischen der Programmierung mit Standardstreams und der Programmierung mit Datei-Streams besteht darin, dass Sie in Ihren Programmen explizit einen Stream für die jeweiligen Dateien erzeugen müssen.

Dateitypen

Im Gegensatz zu anderen Betriebssystemen unterscheiden Linux und die anderen Mitglieder der Unix-Familie nicht zwischen Textdateien und binären Dateien. Die Funktionen, die auf Streams ausgeführt werden, müssen nicht wissen, mit welcher Art von Stream sie es gerade zu tun haben. Als Programmierer jedoch müssen Sie entscheiden, was Sie mit den Streams machen wollen - ob Sie aus einer Datei zeilenweise Text auslesen wollen oder ob die Daten binär sind und nur in Blöcken ausgelesen werden sollten. Der Unterschied wird wichtig, wenn Sie versuchen, Code, der unter Linux problemlos läuft, auf andere Betriebssysteme wie z.B. Microsoft Windows zu portieren. Wenn nichts anderes vorgegeben ist, geht Windows davon aus, dass Dateien im Textmodus geöffnet werden. Wenn Sie dies bei der Portierung von Code von Linux nach Windows nicht berücksichtigen, können Sie Ärger bekommen. Diese Fallen werden im Zusammenhang mit den binären Dateien weiter hinten in diesem Kapitel besprochen.

Dateinamen

Jede Datei hat einen Namen. Der Umgang mit Dateinamen sollte Ihnen vertraut sein, wenn Sie mit Dateien arbeiten. Dateinamen werden wie andere Textdaten als Strings gespeichert. Unter Linux und Unix können fast alle ASCII-Zeichen Teil eines gültigen Dateinamens sein. Es ist jedoch nicht ratsam, diese Möglichkeiten voll auszuschöpfen. Dateinamen sollten auf die alphanumerischen Zeichen, +, -, Unterstriche und so viele Punkte, wie nötig, beschränkt sein.

Ein Dateiname in einem C-Programm kann darüber hinaus Pfadinformationen enthalten. Ein Pfad bezeichnet das Verzeichnis (häufig wird auch von Ordnern gesprochen), in dem sich eine Datei befindet. Wenn Sie einen Dateinamen ohne Pfad angeben, wird davon ausgegangen, dass sich die Datei in dem Verzeichnis befindet, das Linux gerade als Standardverzeichnis ansieht. Es ist guter Programmierstil, sich beim Umgang mit Dateien immer der Bedeutung der Pfade bewusst zu sein.

Unter Linux wird ein Schrägstrich verwendet, um die Verzeichnisnamen in einem Pfad voneinander zu trennten. So bezieht sich zum Beispiel der Name

/tmp/liste.txt

auf die Datei liste.txt im Verzeichnis /tmp.

Eine Datei öffnen

Den Schritt, einen Stream zu erzeugen, der mit einer Datei verbunden ist, nennt man auch Öffnen einer Datei. Wenn Sie eine Datei öffnen, steht sie zum Lesen (das heißt für den Datentransfer von der Datei in das Programm), zum Schreiben (das heißt für die Sicherung der Programmdaten in die Datei) oder für beides bereit. Wenn Sie die Datei nicht mehr benötigen, müssen Sie sie schließen. Das Schließen einer Datei besprechen wir später.

Um eine Datei zu öffnen, verwenden Sie die Bibliotheksfunktion fopen(). Der Prototyp von fopen() steht in stdio.h und lautet folgendermaßen:

FILE *fopen(const char *filename, const char *mode);

Dieser Prototyp teilt Ihnen mit, dass fopen() einen Zeiger auf den Typ FILE, eine in stdio.h deklarierte Struktur, zurückliefert. Die Elemente der FILE-Struktur werden von dem Programm bei den verschiedenen Operationen des Dateizugriffs verwendet, und es ist nicht ratsam, Änderungen daran vorzunehmen. Wie auch immer, für jede Datei, die Sie öffnen wollen, müssen Sie einen Zeiger auf den Typ FILE deklarieren. Mit dem Aufruf von fopen() erzeugen Sie eine Instanz der Struktur FILE und liefern einen Zeiger auf diese Strukturinstanz zurück. Diesen Zeiger verwenden Sie dann bei allen nachfolgenden Operationen auf diese Datei. Scheitert der Aufruf von fopen(), lautet der Rückgabewert NULL. Ein solches Scheitern kann durch einen Hardware-Fehler ausgelöst werden oder durch einen Dateinamen, dessen Pfadangaben ungültig sind.

Das Argument filename ist der Name der Datei, die geöffnet werden soll. Wie ich schon eingangs erwähnt haben, kann - und sollte - filename Pfadinformationen enthalten. Dieses Argument kann ein literaler String in doppelten Anführungszeichen sein oder ein Zeiger auf eine String-Variable.

Das Argument mode gibt den Modus an, in dem die Datei geöffnet werden soll. In diesem Kontext steuert mode, ob die Datei zum Lesen, Schreiben oder für beides geöffnet wird. Die zulässigen Werte für mode finden Sie in Tabelle 15.1.

Modus

Bedeutung

r

Öffnet die Datei zum Lesen. Wenn die Datei nicht existiert, liefert fopen() NULL zurück.

w

Öffnet die Datei zum Schreiben. Wenn es noch keine Datei des angegebenen Namens gibt, wird sie erzeugt. Existiert bereits eine Datei mit gleichlautendem Namen, wird sie ohne Warnung gelöscht und eine neue, leere Datei erzeugt.

a

Öffnet die Datei zum Anhängen. Wenn es noch keine Datei des angegebenen Namens gibt, wird sie erzeugt. Existiert die Datei, werden die neuen Daten an das Ende der Datei angehängt.

r+

Öffnet die Datei zum Lesen und Schreiben. Wenn es noch keine Datei des angegebenen Namens gibt, wird sie erzeugt. Existiert die Datei, werden die neuen Daten an den Anfang der Datei geschrieben, wodurch bestehende Daten überschrieben werden.

w+

Öffnet die Datei zum Lesen und Schreiben. Wenn es noch keine Datei des angegebenen Namens gibt, wird sie erzeugt. Existiert die Datei, wird sie überschrieben.

a+

Öffnet die Datei zum Schreiben und Anhängen. Wenn es noch keine Datei des angegebenen Namens gibt, wird sie erzeugt. Existiert die Datei, werden die neuen Daten an das Ende der Datei angehängt.

Tabelle 15.1: Werte von mode für die Funktion fopen().

Wenn Ihre Programme nur unter Linux ausgeführt werden, reichen Ihnen die Informationen zu den verschiedenen Datei-Modi aus Tabelle 15.1. Wenn Sie jedoch Code schreiben wollen, der auf Linux und Windows auszuführen ist, müssen Sie auf Folgendes achten:

Denken Sie daran, dass fopen() NULL zurückliefert, wenn ein Fehler auftritt. Fehler, die einen Rückgabewert von NULL zur Folge haben, werden ausgelöst,

Immer wenn Sie fopen() verwenden, müssen Sie auf Fehler testen. Wenn ein Fehler auftritt, wird die externe Variable errno auf den entsprechenden Wert gesetzt. Mit Hilfe der Funktion perror() kann man auf der Grundlage von errno eine Fehlermeldung ausgeben, die erläutert, was geschehen ist. Die Funktion perror() ist in stdio.h definiert und wird am Tag 17, »Die Bibliothek der C-Funktionen«, ausführlicher behandelt.

Listing 15.1 enthält ein Beispiel für die Verwendung von fopen().

Listing 15.1: Mit fopen() Dateien in verschiedenen Modi öffnen.

1 :  /* Beispiel für die Funktion fopen(). */
2 : #include <stdlib.h>
3 : #include <stdio.h>
4 :
5 : int main(void)
6 : {
7 : FILE *fp;
8 : char ch, dateiname[40], modus[5];
9 :
10: while (1)
11: {
12:
13: /* Eingabe des Dateinamens und des Modus. */
14:
15: printf("\nGeben Sie einen Dateinamen ein: ");
16: fgets(dateiname,40,stdin);
17: dateiname [strlen(dateiname)-1] = 0 ;
18: printf("\nGeben Sie einen Modus ein (max. 3 Zeichen): ");
19: fgets(modus,5,stdin);
20: modus [strlen(modus)-1] = 0 ;
21:
22: /* Versucht die Datei zu öffnen. */
23:
24: if ( (fp = fopen( dateiname, modus )) != NULL )
25: {
26: printf("\n%s im Modus %s erfolgreich geöffnet.\n",
27: dateiname, modus);
28: fclose(fp);
29: puts("x für Ende, Weiter mit Eingabetaste.");
30: if ( (ch = getc(stdin)) == 'x')
31: break;
32: else
33: continue;
34: }
35: else
36: {
37: fprintf(stderr, "\nFehler beim Öffnen von %s im Modus %s.\n",
38: dateiname, modus);
39: perror("list1501");
40: puts("x für Ende, neuer Versuch mit Eingabetaste.");
41: if ( (ch = getc(stdin)) == 'x')
42: break;
43: else
44: continue;
45: }
46: }
47: return 0 ;
48: }

Geben Sie einen Dateinamen ein: hallo.txt

Geben Sie einen Modus ein (max. 3 Zeichen): w

hallo.txt im Modus w erfolgreich geöffnet.
x für Ende, weiter mit Eingabetaste.

Geben Sie einen Dateinamen ein: Wiedersehen.txt

Geben Sie einen Modus ein (max. 3 Zeichen): r

Fehler beim Öffnen von Wiedersehen.txt im Modus r.
list1501: No such file or directory1
x für Ende, neuer Versuch mit Eingabetaste.
x

Dieses Programm fordert Sie in den Zeilen 15 bis 20 auf, den Dateinamen und den Modus-Spezifizierer einzugeben. Die Zeilen 17 und 20 entfernen die Neue-Zeile- Zeichen am Ende der eingelesenen Strings, indem sie es mit einem Nullzeichen überschreiben. Nach dem Einlesen des Dateinamens versucht Zeile 24 die Datei zu öffnen, und weist deren Dateizeiger fp zu. Es entspricht gutem Programmierstil, mit der if-Anweisung in Zeile 24 zu prüfen, ob der Zeiger der geöffneten Datei ungleich NULL ist. Wenn fp ungleich NULL ist, wird eine Nachricht ausgegeben, dass die Datei erfolgreich geöffnet wurde und der Anwender jetzt fortfahren kann. Wenn der Zeiger NULL ist, wird die else-Bedingung der if-Schleife ausgeführt. Die else-Bedingung in den Zeilen 35 bis 45 gibt eine Meldung aus, dass ein Problem aufgetreten ist. Anschließend wird der Anwender aufgefordert, anzugeben, ob das Programm fortfahren soll.

Sie können mit verschiedenen Namen und Modi experimentieren, um festzustellen, bei welchen Eingaben Sie einen Fehler erhalten. In unserem Beispiel oben konnten Sie sehen, dass die Eingabe von Wiedersehen.txt im r-Modus einen Fehler ausgelöst hat, da die Datei nicht auf der Platte existierte. Wenn ein Fehler auftritt, haben Sie die Wahl, die Informationen erneut einzugeben oder das Programm zu verlassen. Vielleicht möchten Sie auch prüfen, welche Zeichen in einem Dateinamen akzeptiert werden. Um Dateien mit problematischen Zeichen im Namen zu löschen, müssen Sie den Dateinamen in Anführungszeichen setzen, wenn Sie den Betriebssystembefehl rm (Löschen) ausführen.

Schreiben und Lesen

Ein Programm, das eine Datei verwendet, kann Daten in die Datei schreiben, Daten aus der Datei lesen oder beide Vorgänge miteinander kombinieren. Sie können Daten auf drei Arten in eine Datei schreiben:

Wenn Sie Daten aus einer Datei lesen wollen, stehen Ihnen die gleichen drei Optionen zur Verfügung: formatierte Eingabe, Zeicheneingabe oder direkte Eingabe. Welche Art von Eingabe Sie in einem bestimmten Fall verwenden, hängt dabei fast völlig von der Art der Datei ab, die gelesen wird. Im Allgemeinen werden die Daten im gleichen Modus gelesen, in dem sie gesichert wurden, aber das ist keine notwendige Bedingung.

Formatierte Dateieingabe und -ausgabe

Formatierte Dateieingabe und -ausgabe (E/A oder I/O für Englisch »Input/Output«) betrifft Textdaten und numerische Daten, die in einer bestimmten Art und Weise formatiert sind. Sie entspricht der formatierten Tastatureingabe und Bildschirmausgabe der Funktionen printf() und scanf() aus Tag 13. Beginnen wir mit der formatierten Dateiausgabe.

Formatierte Dateiausgabe

Für formatierte Dateiausgaben verwendet man die Bibliotheksfunktion fprintf(). Der Prototyp von fprintf() steht in der Header-Datei stdio.h und lautet:

int fprintf(FILE *fp, char *fmt, ...);

Das erste Argument ist ein Zeiger auf den Typ FILE. Um Daten in eine bestimmte Datei zu schreiben, übergeben Sie den Zeiger, der beim Öffnen der Datei mit fopen() zurückgeliefert wurde.

Das zweite Argument ist der Formatstring. Formatstrings haben Sie bereits in der Diskussion der Funktion printf() am Tag 13 kennen gelernt. Der Formatstring von fprintf() folgt genau den gleichen Regeln, die bei der Diskussion der Funktion printf() beschrieben wurden. Schlagen Sie gegebenenfalls in Kapitel 13 nach.

Das letzte Argument lautet »...«. Was verbirgt sich dahinter? In einem Funktionsprototyp, stellen Punkte eine beliebige Anzahl von Argumenten dar. Mit anderen Worten, zusätzlich zu dem Dateizeiger und den Formatstring-Argumenten übernimmt fprintf() keine, ein oder mehrere weitere Argumente. Darin entspricht die Funktion genau printf(). Bei diesen optionalen Argumenten handelt es sich um die Namen von Variablen, die in den angegebenen Stream geschrieben werden.

Denken Sie daran, dass fprintf() genauso funktioniert wie printf() - nur dass die Ausgabe an den Stream geht, der in der Argumentenliste angegeben wird. Wenn Sie also als Stream-Argument stdout angeben, entspricht fprintf() der Funktion printf().

Listing 15.2 zeigt ein Beispiel für die Verwendung von fprintf().

Listing 15.2: Äquivalenz der formatierten Ausgabe in eine Datei und an stdout mit fprintf().

1 : /* Beispiel für die Funktion fprintf(). */
2 :
3 : #include <stdio.h>
4 : #include <stdlib.h>
5 : void tastatur_loeschen(void);
6 :
7 : int main(void)
8 : {
9 : FILE *fp;
10: float daten[5];
11: int count;
12: char dateiname[20];
13:
14: puts("Geben Sie 5 Fließkommazahlen ein.");
15:
16: for (count = 0; count < 5; count++)
17: scanf("%f", &daten[count]);
18:
19: /* Liest den Dateinamen ein und öffnet die Datei. Zuerst */
20: /* werden aus stdin alle verbliebenen Zeichen gelöscht. */
21:
22: tastatur_loeschen();
23:
24: puts("Geben Sie einen Namen für die Datei ein.");
25: scanf("%s", dateiname);
26:
27: if ( (fp = fopen(dateiname, "w")) == NULL)
28: {
29: fprintf(stderr, "Fehler beim Öffnen der Datei %s.\n", dateiname);
30: exit(1);
31: }
32:
33: /* Schreibt die numerischen Daten in die Datei und in stdout. */
34:
35: for (count = 0; count < 5; count++)
36: {
37: fprintf(fp, "daten[%d] = %f\n", count, daten[count]);
38: fprintf(stdout, "daten[%d] = %f\n", count, daten[count]);
39: }
40: fclose(fp);
41: printf("\n");
42: return(0);
43: }
44:
45: void tastatur_loeschen(void)
46: /* Löscht alle verbliebenen Zeichen in stdin. */
47: {
48: char muell[80];
49: fgets(muell,80,stdin);
50: }

Geben Sie 5 Fließkommazahlen ein.
3.14159
9.99
1.50
3.
1000.0001
Geben Sie einen Namen für die Datei ein.
zahlen.txt

daten[0] = 3.141590
daten[1] = 9.990000
daten[2] = 1.500000
daten[3] = 3.000000
daten[4] = 1000.000122

Vielleicht fragen Sie sich, warum das Programm 1000.000122 anzeigt, wo Sie doch nur 1000.0001 eingegeben haben. Dieser Wert ergibt sich aus der Art und Weise, wie C intern Zahlen speichert. Einige Fließkommawerte können nicht exakt gespeichert werden; daraus resultieren kleinere Ungenauigkeiten wie diese.

Dieses Programm verwendet in den Zeilen 37 und 38 die Funktion fprintf(), um formatierten Text und numerische Daten an stdout und eine von Ihnen vorgegebene Datei zu senden. Der einzige Unterschied zwischen den beiden Aufrufen liegt in dem ersten Argument - das heißt in dem Stream, an den die Daten gesendet werden. Nachdem Sie das Programm ausgeführt haben, werfen Sie einen Blick auf den Inhalt der Datei zahlen.txt (oder welchen Namen auch immer Sie angeben haben), die in dem gleichen Verzeichnis steht wie die Programmdateien. Sie werden feststellen, dass der Text in der Datei eine genaue Kopie des Textes auf dem Bildschirm ist.

Ich möchte Sie darauf aufmerksam machen, dass das Listing 15.2 die bereits am Tag 13 diskutierte Funktion tastatur_loeschen() verwendet. Diese gewährleistet, dass alle Extrazeichen, die nach einem vorangehenden Aufruf von scanf() übrig geblieben sein könnten, aus stdin gelöscht werden. Wenn Sie stdin nicht leeren, werden diese Extrazeichen (besonders das Neue-Zeile-Zeichen) von der Funktion fgets() gelesen, die den Dateinamen einliest. Die Folge davon ist ein Fehler bei der Dateierzeugung.

Formatierte Dateieingabe

Für die formatierte Dateieingabe steht die Bibliotheksfunktion fscanf() zur Verfügung, die bis auf die Ausnahme, dass die Eingabe von einem angegebenen Stream anstatt von stdin kommt, der Funktion scanf() entspricht (siehe Tag 13). Der Prototyp für fscanf() lautet:

int fscanf(FILE *fp, const char *fmt, ...);

Das Argument fp ist ein Zeiger auf den Typ FILE, der von fopen() zurückgegeben wird, und fmt ist ein Zeiger auf den Formatstring, der angibt, wie fscanf() die Eingabe zu lesen hat. Die Komponenten des Formatstrings entsprechen denen von scanf(). Die Punkte »...« geben an, dass ein oder mehrere weitere Argumente aufgenommen werden können: die Adressen der Variablen, in denen fscanf() die Eingabe ablegen soll.

Bevor Sie sich mit fscanf() beschäftigen, schlage ich vor, dass Sie sich den Abschnitt zu scanf() vom Tag 13 noch einmal anschauen. Die Funktion fscanf() funktioniert genauso wie scanf() - nur dass die Zeichen von einem angegebenen Stream und nicht von stdin stammen.

Um uns ein Beispiel zu fscanf() anzuschauen, brauchen wir eine Textdatei mit Zahlen oder Strings, deren Format die Funktion lesen kann. Verwenden Sie Ihren Editor, um eine Datei namens eingabe.txt zu erzeugen, und geben Sie fünf Fließkommazahlen ein, die durch Leerzeichen oder Zeilenumbruch getrennt sind. Ihre Datei könnte zum Beispiel folgendermaßen aussehen:

123.45     87.001
100.02
0.00456 1.0005

Kompilieren Sie jetzt das Programm aus Listing 15.3 und führen Sie es aus.

Listing 15.3: Mit fscanf() formatierte Daten aus einer Datei einlesen.

1: /* Mit fscanf() formatierte Daten aus einer Datei lesen. */
2: #include <stdlib.h>
3: #include <stdio.h>
4:
5: int main(void)
6: {
7: float f1, f2, f3, f4, f5;
8: FILE *fp;
9:
10: if ( (fp = fopen("eingabe.txt", "r")) == NULL)
11: {
12: fprintf(stderr, "Fehler beim Öffnen der Datei.\n");
13: exit(1);
14: }
15:
16: fscanf(fp, "%f %f %f %f %f", &f1, &f2, &f3, &f4, &f5);
17: printf("Die Werte lauten %f, %f, %f, %f und %f\n.",
18: f1, f2, f3, f4, f5);
19:
20: fclose(fp);
21: return(0);
22: }

Die Werte lauten 123.449997, 87.000999, 100.019997, 0.004560 und 1.000500.

Die Genauigkeit des verwendeten Datentyps kann dazu führen, dass für einige Zahlen nicht exakt die gleichen Werte ausgegeben werden, die eingelesen wurden. So kann zum Beispiel 100.02 in der Ausgabe als 100.01999 erscheinen.

Dieses Programm liest die fünf Werte aus der Datei, die Sie erzeugt haben, ein und gibt die Werte auf den Bildschirm aus. Mit dem Aufruf von fopen() in Zeile 10 wird die Datei im Lesemodus geöffnet. Gleichzeitig wird geprüft, ob die Datei korrekt geöffnet wurde. Wenn die Datei nicht geöffnet werden konnte, wird in Zeile 12 eine Fehlermeldung angezeigt und das Programm abgebrochen (Zeile 13). Zeile 16 enthält die Funktion fscanf(). Mit Ausnahme des ersten Parameters ist fscanf() identisch mit scanf(), die wir schon des öfteren benutzt haben. Der erste Parameter zeigt auf die Datei, aus der das Programm Daten auslesen soll. Experimentieren Sie ruhig ein wenig mit fscanf(). Erzeugen Sie mit Ihrem Programmier-Editor eigene Eingabedateien und beobachten Sie, wie fscanf() Daten einliest.

Zeicheneingabe und -ausgabe

Im Zusammenhang mit Dateien versteht man unter dem Begriff Zeichen-E/A das Einlesen sowohl einzelner Zeichen als auch ganzer Zeilen. Zur Erinnerung: Eine Zeile ist eine Folge von null oder mehr Zeichen, die mit einem Neue-Zeile-Zeichen abschließen. Verwenden Sie Zeichen-E/A mit Textdateien. Die folgenden Abschnitte beschreiben die Funktionen für die Zeicheneingabe und -ausgabe, gefolgt von einem Beispielprogramm.

Zeicheneingabe

Es gibt drei Funktionen zur Zeicheneingabe: getc() und fgetc() für einzelne Zeichen und fgets() für Zeilen.

Die Funktionen getc() und fgetc()

Die Funktionen getc() und fgetc() sind identisch und damit austauschbar. Sie lesen ein Zeichen aus einem angegebenen Stream ein. Sehen Sie im Folgenden den Prototyp für getc(), der in stdio.h deklariert ist:

int getc(FILE *fp);

Das Argument fp ist der Zeiger, der nach Öffnen der Datei von fopen() zurückgeliefert wurde. Die Funktion liefert das eingegebene Zeichen zurück oder EOF, wenn ein Fehler aufgetreten ist.

Die Funktion getc() haben wir bereits in früheren Programmen verwendet, um Zeichen von der Tastatur einzulesen. Das beweist nur wieder die Flexibilität der Streams in C - die gleiche Funktion kann sowohl für Eingaben von der Tastatur als auch zum Lesen aus Dateien verwendet werden.

Wenn getc() und fgetc() ein einzelnes Zeichen zurückliefern, warum wird im Prototyp der Rückgabewert als vom Typ int angegeben? Der Grund dafür ist Folgender: Wenn Sie Dateien lesen, müssen Sie auch in der Lage sein, das Dateiende- Zeichen zu lesen. Auf manchen Systemen ist dieses Dateiende-Zeichen nicht vom Typ char, sondern vom Typ int. Ein Beispiel für getc() finden Sie später in Listing 15.10.

Die Funktion fgets()

Um eine ganze Zeile von Zeichen aus einer Datei einzulesen, verwendet man die Bibliotheksfunktion fgets(). Der Prototyp lautet:

char *fgets(char *str, int n, FILE *fp);

Das Argument str ist ein Zeiger auf einen Puffer, in dem die Eingabe gespeichert wird. n ist die maximale Anzahl der einzulesenden Zeichen. fp ist der Zeiger auf den Typ FILE, der beim Öffnen der Datei von fopen() zurückgegeben wurde.

Wenn fgets() aufgerufen wird, liest die Funktion Zeichen aus fp ein und schreibt sie an die Stelle im Speicher, auf die str zeigt. Es werden so lange Zeichen gelesen, bis ein Neue-Zeile-Zeichen erscheint oder n-1 Zeichen eingelesen wurden - je nachdem welcher Fall zuerst eintritt. Indem Sie n gleich der Anzahl der Bytes setzen, die für den Puffer str reserviert wurden, verhindern Sie, dass Speicher über den reservierten Bereich hinaus beschrieben wird. (Mit n-1 stellen Sie sicher, dass Platz für das abschließende Nullzeichen \0 gelassen wird, das fgets() an das Ende des Strings anhängt). Geht alles glatt, liefert fgets() die Adresse von str zurück. Es können jedoch zwei Arten von Fehlern auftreten, die durch den Rückgabewert von NULL angezeigt werden.

Daran können Sie erkennen, dass fgets() nicht notwendigerweise eine ganze Zeile einlesen muss (das heißt alles bis zum nächsten Neue-Zeile-Zeichen). Wenn n-1 Zeichen gelesen wurden, bevor eine neue Zeile beginnt, hört fgets() mit dem Einlesen auf. Die nächste Lese-Operation in der Datei beginnt dort, wo die letzte aufgehört hat. Um sicherzustellen, dass fgets() ganze Strings einliest, das heißt, nur bei Neue-Zeile-Zeichen stoppt, sollten Sie darauf achten, dass Ihr Eingabepuffer und der entsprechende Wert von n, der fgets() übergeben wird, groß genug sind.

Zeichenausgabe

Für die Zeichenausgabe gibt es zwei Funktionen, die man kennen sollte: putc() und fputs().

Die Funktion putc()

Die Bibliotheksfunktion putc() schreibt ein einzelnes Zeichen an einen angegebenen Stream. Der Prototyp, der in stdio.h zu finden ist, lautet:

int putc(int ch, FILE *fp);

Das Argument ch ist das auszugebende Zeichen. Wie bei den anderen Zeichenfunktionen ist dieses Zeichen formal vom Typ int, es wird aber nur das untere Byte verwendet. Das Argument fp ist der mit der Datei verbundene Zeiger (der beim Öffnen der Datei von fopen() zurückgeliefert wird). Die Funktion putc() liefert im Erfolgsfall das geschriebene Zeichen zurück oder EOF (wenn ein Fehler auftritt). Die symbolische Konstante EOF ist in stdio.h definiert und hat den Wert -1. Da kein wirkliches Zeichen diesen numerischen Wert hat, kann EOF als Fehlerindikator verwendet werden (gilt nur für Textdateien).

Die Funktion fputs()

Um eine ganze Zeile von Zeichen in einen Stream zu schreiben, sollten Sie sich der Bibliotheksfunktion fputs() bedienen. Diese Funktion entspricht der bereits am Tag 13 besprochenen Funktion puts(). Der einzige Unterschied liegt darin, dass Sie bei fputs() den Ausgabestream angeben können. Außerdem fügt fputs() kein Neue- Zeile-Zeichen an das Ende des Strings. Wenn Sie dieses Zeichen benötigen, müssen Sie es explizit in den auszugebenden String mit aufnehmen. Der Prototyp aus stdio.h lautet:

char fputs(char *str, FILE *fp);

Das Argument str ist ein Zeiger auf den auszugebenden nullterminierten String und fp ein Zeiger auf den Typ FILE, der beim Öffnen der Datei von fopen() zurückgeliefert wurde. Der String, auf den str zeigt, wird ohne das abschließende \0 in die Datei geschrieben. Die Funktion fputs() liefert im Erfolgsfall einen nichtnegativen Wert zurück oder EOF bei einem Fehler.

Direkte Dateieingabe und -ausgabe

Die direkte Datei-E/A verwendet man meistens, wenn man Daten speichern möchte, die später vom gleichen oder einem anderen C-Programm gelesen werden. Die direkte Datei-E/A wird normalerweise nur bei binären Dateien eingesetzt. Bei der direkten Ausgabe werden Datenblöcke vom Speicher in eine Datei geschrieben. Die direkte Dateieingabe kehrt diesen Prozess um: Ein Datenblock wird von einer Datei in den Speicher gelesen. So kann zum Beispiel ein einziger Funktionsaufruf zur direkten Ausgabe ein ganzes Array vom Typ double in eine Datei schreiben und ein einziger Funktionsaufruf zur direkten Eingabe das ganze Array wieder aus der Datei in den Speicher zurückholen. Die Funktionen für die direkte Datei-E/A lauten fread() und fwrite().

Die Funktion fwrite()

Die Bibliotheksfunktion fwrite() schreibt einen Datenblock aus dem Speicher in eine Datei. Die Funktion fwrite() wird üblicherweise zum Schreiben binärer Daten verwendet. Ihr Prototyp steht in stdio.h und lautet:

int fwrite(void *buf, int size, int n, FILE *fp);

Das Argument buf ist ein Zeiger auf den Speicherbereich, in dem die Daten stehen, die in die Datei geschrieben werden sollen. Der Zeigertyp ist void und kann deshalb ein Zeiger auf alles sein.

Das Argument size gibt die Größe der einzelnen Datenelemente in Byte an und n die Anzahl der zu schreibenden Elemente. Wenn Sie zum Beispiel ein Integer-Array mit 100 Elementen sichern wollen, wäre size 4 (da jedes iNT 4 Byte belegt) und n wäre 100 (da das Array 100 Elemente enthält). Für die Berechnung des size-Arguments können Sie den sizeof()-Operator verwenden.

Das Argument fp ist natürlich wieder der Zeiger auf den Typ FILE , der beim Öffnen der Datei von fopen() zurückgegeben wurde. Die Funktion fwrite() liefert im Erfolgsfall die Anzahl der geschriebenen Elemente zurück. Ist der zurückgelieferte Wert kleiner als n, bedeutet dies, dass ein Fehler aufgetreten ist. Will man auf Fehler prüfen, ruft man fwrite() in der Regel wie folgt auf:

if( (fwrite(puffer, groesse, anzahl, fp)) != anzahl)
fprintf(stderr, "Fehler beim Schreiben in die Datei.");

Schauen wir uns einige Beispiele zur Verwendung von fwrite() an. Um eine einzelne Variable x vom Typ double in eine Datei zu schreiben, schreiben Sie:

fwrite(&x, sizeof(double), 1, fp);

Um ein Array daten[] mit 50 Strukturen vom Typ adresse in eine Datei zu schreiben, haben Sie zwei Möglichkeiten:

fwrite(daten, sizeof(adresse), 50, fp);
fwrite(daten, sizeof(daten), 1, fp);

Im ersten Beispiel wird das Array als Folge von 50 Elementen ausgegeben, wobei jedes Element die Größe einer Struktur vom Typ adresse hat. Das zweite Beispiel behandelt das Array als ein einziges Element. Das Resultat ist für beide Aufrufe das Gleiche.

Der folgende Abschnitt stellt Ihnen die Funktion fread() vor und präsentiert zum Schluss ein Beispielprogramm für den Einsatz von fread() und fwrite().

Die Funktion fread()

Die Bibliotheksfunktion fread() liest einen Datenblock aus einer Datei in den Speicher. fread() wird normalerweise zum Lesen von binären Daten verwendet. Der Prototyp aus stdio.h lautet:

int fread(void *buf, int size, int n, FILE *fp);

Das Argument buf ist ein Zeiger auf den Speicherbereich, in dem die eingelesenen Daten abgelegt werden. Wie bei fwrite() ist der Zeigertyp void.

Das Argument size gibt die Größe der einzelnen einzulesenden Datenelemente in Byte an und n die Anzahl der Elemente. Dabei sollten Ihnen die Parallelen dieser Argumente zu den Argumenten von fwrite() auffallen. Auch hier wird normalerweise der sizeof()-Operator dazu verwendet, um das Argument für size zu berechnen. Das Argument fp ist (wie immer) der Zeiger auf den Typ FILE, der beim Öffnen der Datei von fopen() zurückgeliefert wurde. Die fread()-Funktion liefert die Anzahl der gelesenen Elemente zurück. Dieser Wert kann kleiner als n sein, wenn vorher das Dateiende erreicht wurde oder ein Fehler aufgetreten ist.

Listing 15.4 veranschaulicht den Einsatz von fwrite() und fread().

Listing 15.4: Die Funktionen fwrite() und fread() für den direkten Dateizugriff.

1: /* Direkte Datei-E/A mit fwrite() und fread(). */
2: #include <stdlib.h>
3: #include <stdio.h>
4:
5: #define GROESSE 20
6:
7: int main(void)
8: {
9: int count, array1[GROESSE], array2[GROESSE];
10: FILE *fp;
11:
12: /* Initialisierung von array1[]. */
13:
14: for (count = 0; count < GROESSE; count++)
15: array1[count] = 2 * count;
16:
17: /* Öffnet die Datei und stellt sicher, dass */
18: /* der Code auch unter Windows läuft. */
19: if ( (fp = fopen("direkt.txt", "wb")) == NULL)
20: {
21: fprintf(stderr, "Fehler beim Öffnen der Datei.\n");
22: exit(1);
23: }
24: /* array1[] in der Datei speichern. */
25:
26: if (fwrite(array1, sizeof(int), GROESSE, fp) != GROESSE)
27: {
28: fprintf(stderr, "Fehler beim Schreiben in die Datei.");
29: exit(1);
30: }
31:
32: fclose(fp);
33:
34: /* Öffnet jetzt die gleiche Datei zum Lesen im Binärmodus. */
35:
36: if ( (fp = fopen("direkt.txt", "rb")) == NULL)
37: {
38: fprintf(stderr, "Fehler beim Öffnen der Datei.");
39: exit(1);
40: }
41:
42: /* Liest die Daten in array2[]. */
43:
44: if (fread(array2, sizeof(int), GROESSE, fp) != GROESSE)
45: {
46: fprintf(stderr, "Fehler beim Lesen der Datei.");
47: exit(1);
48: }
49:
50: fclose(fp);
51:
52: /* Gibt beide Arrays aus, um zu zeigen, dass sie gleich sind. */
53:
54: for (count = 0; count < GROESSE; count++)
55: printf("%d\t%d\n", array1[count], array2[count]);
56: return(0);
57: }

0       0
2 2
4 4
6 6
8 8
10 10
12 12
14 14
16 16
18 18
20 20
22 22
24 24
26 26
28 28
30 30
32 32
34 34
36 36
38 38

Das Listing 15.4 zeigt, wie sich die Funktionen fwrite() und fread() einsetzen lassen. In den Zeilen 14 und 15 wird ein Array initialisiert. Dieses Array wird dann in Zeile 26 mit fwrite() in eine Datei geschrieben. In Zeile 44 werden die Daten mit Hilfe der Funktion fread() wieder eingelesen und in einem zweiten Array abgelegt. Zum Schluss wird der Inhalt beider Arrays auf dem Bildschirm ausgegeben, um zu zeigen, dass sie beide die gleichen Daten enthalten (Zeilen 54 und 55).

Wenn Sie Daten mit fwrite() speichern, kann nicht viel schief gehen, außer dass ein Dateifehler auftritt. Mit fread() müssen Sie jedoch vorsichtig sein. Diese Funktion weiß nicht, was die Daten darstellen. So kann ein Block von 100 Byte sowohl aus 100 Variablen vom Typ char, 50 Variablen vom Typ short, 25 Variablen vom Typ int oder 25 Variablen vom Typ float bestehen. Wenn Sie fread() aufrufen, um diesen Block in den Speicher zu lesen, wird der Befehl gehorsam ausgeführt. Wenn die Daten in dem Block von einem Array vom Typ int stammen und Sie die Daten in ein Array vom Typ float zurückholen, tritt zwar kein Fehler auf, aber die Ergebnisse Ihres Programms werden unberechenbar sein. Beim Schreiben von Programmen müssen Sie sicher sein, dass fread() korrekt verwendet wird, das heißt, dass die Daten in die korrekten Typen von Variablen und Arrays gelesen werden. Beachten Sie, dass in Listing 15.4 alle Aufrufe von fopen(), fwrite() und fread() überprüft werden, ob sie ohne Fehler ausgeführt wurden.

Dateipuffer: Dateien schließen und leeren

Wenn Sie eine Datei nicht mehr benötigen, sollten Sie sie mit der Funktion fclose() schließen. Wenn Sie aufmerksam waren, werden Sie fclose() schon in einigen der heutigen Programme entdeckt haben. Der Prototyp von fclose() lautet:

int fclose(FILE *fp);

Das Argument fp ist der FILE-Zeiger, der mit dem Stream verbunden ist. fclose() liefert im Erfolgsfall 0 oder bei Auftreten eines Fehlers -1 zurück. Wenn Sie eine Datei schließen, wird der Puffer der Datei geleert (in die Datei geschrieben).

Wenn ein Programm beendet wird (entweder indem das Ende von main() erreicht wird oder die Funktion exit() zur Ausführung kommt), werden alle Streams automatisch gelöscht und geschlossen. Es ist jedoch empfehlenswert, Streams explizit zu schließen, sobald Sie sie nicht mehr benötigen - vor allem diejenigen, die mit Dateien verbunden sind. Der Grund dafür sind die Stream-Puffer.

Wenn Sie einen Stream erzeugen, der mit einer Datei verbunden ist, wird automatisch ein Puffer erzeugt und mit dem Stream verknüpft. Ein Puffer ist ein Speicherblock für die temporäre Speicherung von Daten, die in eine Datei geschrieben oder aus ihr gelesen werden. Diese Puffer sind unentbehrlich, da Laufwerke blockorientierte Geräte sind, das heißt, sie arbeiten am effizientesten, wenn die Daten in Blöcken einer bestimmten Größe gelesen oder geschrieben werden. Die Größe des idealen Blocks ist je nach verwendeter Hardware unterschiedlich. Normalerweise bewegt sie sich in der Ordnung von einigen hundert bis tausend Byte. Sie brauchen sich jedoch nicht allzu viele Gedanken um die genaue Blockgröße zu machen.

Der Puffer, der mit einem Datei-Stream verbunden ist, dient als Schnittstelle zwischen dem Stream (der zeichenorientiert ist) und dem Speichermedium (das blockorientiert ist). Wenn Ihr Programm Daten in einen Stream schreibt, werden die Daten erst einmal in den Puffer geschrieben. Erst wenn der Puffer voll ist, wird der gesamte Inhalt des Puffers als Block in die Datei geschrieben. Das Lesen von Daten aus einer Datei erfolgt analog. Erzeugung und Operation des Puffers werden vom Betriebssystem übernommen und erfolgen automatisch. Sie brauchen sich nicht damit zu befassen. (C stellt Ihnen einige Funktionen zur Manipulation der Puffer zur Verfügung, doch Einzelheiten dazu würden den Rahmen dieses Buches sprengen.)

Für die Praxis bedeutet die automatische Pufferung, dass Daten, die Ihr Programm während der Programmausführung in eine Datei schreibt, danach womöglich noch längere Zeit im Puffer verweilen - und nicht in der Datei. Wenn sich also Ihr Programm aufhängt, wenn es einen Stromsausfall gibt oder ein anderes Problem auftritt, können die Daten in dem Puffer verloren sein, und Sie wissen nicht, was sich in der Datei befindet.

Sie können die Puffer eines Streams mit der Bibliotheksfunktion fflush() leeren, ohne ihn zu schließen. Rufen Sie fflush() auf, wenn Sie den Inhalt eines Dateipuffers in die Datei übertragen, die Datei aber noch weiter benutzen und daher nicht schließen wollen. Verwenden Sie flushall(), um die Puffer aller offenen Streams zu leeren. Der Prototyp für fflush() lautet:

int fflush(FILE *fp);

Das Argument fp ist der FILE-Zeiger, der beim Öffnen der Datei von fopen() zurückgegeben wurde. Wenn eine Datei zum Schreiben geöffnet wurde, schreibt fflush() den Inhalt des Puffers in die Datei. Wenn die Datei zum Lesen geöffnet wurde, wird der Puffer gelöscht. Im Erfolgsfall liefert fflush() 0 und bei Auftreten eines Fehlers EOF zurück.

Was Sie tun sollten

Was nicht

Öffnen Sie eine Datei, bevor Sie sie zum Lesen oder Schreiben verwenden.

Verwenden Sie den sizeof()-Operator für die Funktionen fwrite() und fread().

Schließen Sie alle von Ihnen geöffneten Dateien.

Verwenden Sie b als Teil des Modus- Strings, wenn Sie in einem Programm, das auch unter Windows kompilierbar sein soll, binäre Dateien öffnen und schließen.

Gehen Sie nicht davon aus, dass Zugriffe auf Dateien stets fehlerfrei ablaufen. Prüfen Sie nach jeder Lese-, Schreib- oder Öffnen-Operation, ob die Funktion wunschgemäß ausgeführt wurde.

Sequentieller und wahlfreier Zugriff auf Dateien

Jeder geöffneten Datei wird ein Datei-Positionszeiger zugeordnet. Der Positionszeiger gibt an, wo in der Datei Lese- und Schreiboperationen ausgeführt werden. Die Position wird dabei immer als Abstand in Byte vom Anfang der Datei angegeben. Wenn eine neue Datei geöffnet wird, steht der Positionszeiger auf dem Anfang der Datei, der Position 0. (Da die Datei neu ist und eine Länge von 0 hat, gibt es keine andere Position für den Positionszeiger.) Wenn eine existierende Datei geöffnet wird, steht der Positionszeiger am Ende der Datei, falls die Datei im Anhängemodus geöffnet wird, oder am Anfang der Datei, falls die Datei in irgendeinem anderen Modus geöffnet wird.

Die Datei-E/A-Funktionen, die weiter vorne behandelt wurden, verwenden diesen Positionszeiger, auch wenn man als Programmierer davon wenig bemerkt, da die entsprechenden Operationen im Hintergrund ablaufen. Sämtliche Lese- und Schreiboperationen finden jeweils an der Stelle statt, auf die der Positionszeiger verweist, und sorgen gleichzeitig durch Verrücken des Positionszeigers dafür, dass dieser stets aktuell bleibt. Wenn Sie zum Beispiel eine Datei zum Lesen öffnen und 10 Byte einlesen (die Byte an den Positionen 0 bis 9), befindet sich der Positionszeiger nach der Leseoperation an Position 10, und die nächste Leseoperation beginnt genau dort. Wenn Sie also alle Daten einer Datei sequentiell einlesen oder Daten sequentiell in eine Datei schreiben wollen, braucht Sie der Positionszeiger nicht zu kümmern. Überlassen Sie alles den Stream-E/A-Funktionen.

Wenn Sie mehr Einfluss nehmen wollen, gibt es dafür in C spezielle Bibliotheksfunktionen, mit denen Sie den Wert des Datei-Positionszeigers feststellen und ändern können. Indem Sie den Positionszeiger kontrollieren, können Sie auf jede beliebige Stelle in einer Datei zugreifen (»wahlfreier Zugriff«), d.h. Sie können an jeder beliebigen Position der Datei lesen oder schreiben, ohne dazu alle vorhergehenden Daten lesen oder schreiben zu müssen.

Die Funktionen ftell() und rewind()

Um den Positionszeiger auf den Anfang der Datei zu setzen, verwenden Sie die Bibliotheksfunktion rewind(). Ihr Prototyp steht in stdio.h und lautet:

void rewind(FILE *fp);

Das Argument fp ist der FILE-Zeiger, der mit dem Stream verbunden ist. Nach dem Aufruf von rewind() wird der Positionszeiger der Datei auf den Anfang der Datei (Byte 0) gesetzt. Verwenden Sie rewind(), wenn Sie bereits Daten aus einer Datei eingelesen haben und bei der nächsten Leseoperation wieder beim Anfang der Datei beginnen wollen, ohne dazu die Datei schließen und wieder öffnen zu müssen.

Um den Wert des Positionszeigers einer Datei zu ermitteln, verwenden Sie ftell(). Der Prototyp dieser Funktion ist in stdio.h deklariert und lautet:

long ftell(FILE *fp);

Das Argument fp ist der FILE-Zeiger, der beim Öffnen der Datei von fopen() zurückgeliefert wurde. Die Funktion ftell() liefert einen Wert vom Typ long zurück, der die aktuelle Dateiposition als Abstand in Byte vom Beginn der Datei angibt (das erste Byte steht an Position 0). Wenn ein Fehler auftritt, liefert ftell() -1L (der Wert -1 des Typs long).

Um ein Gefühl dafür zu bekommen, wie man mit rewind() und ftell() programmiert, werfen Sie ein Blick auf Listing 15.5.

Listing 15.5: Die Funktionen ftell() und rewind().

1: /* Beispiel für ftell() und rewind(). */
2: #include <stdlib.h>
3: #include <stdio.h>
4:
5: #define PUFFERLAENGE 6
6:
7: char msg[] = "abcdefghijklmnopqrstuvwxyz";
8:
9: int main(void)
10: {
11: FILE *fp;
12: char puffer[PUFFERLAENGE];
13:
14: if ( (fp = fopen("text.txt", "w")) == NULL)
15: {
16: fprintf(stderr, "Fehler beim Öffnen der Datei.");
17: exit(1);
18: }
19:
20: if (fputs(msg, fp) == EOF)
21: {
22: fprintf(stderr, "Fehler beim Schreiben in die Datei.");
23: exit(1);
24: }
25:
26: fclose(fp);
27:
28: /* Öffnet jetzt die Datei zum Lesen. */
29:
30: if ( (fp = fopen("text.txt", "r")) == NULL)
31: {
32: fprintf(stderr, "Fehler beim Öffnen der Datei.");
33: exit(1);
34: }
35: printf("\nDirekt nach dem Öffnen, Position = %ld", ftell(fp));
36:
37: /* Liest 5 Zeichen ein. */
38:
39: fgets(puffer, PUFFERLAENGE, fp);
40: printf("\nNach dem Einlesen von %s, Position = %ld",
puffer, ftell(fp));
41:
42: /* Liest die nächsten 5 Zeichen ein. */
43:
44: fgets(puffer, PUFFERLAENGE, fp);
45: printf("\n\nDie nächsten 5 Zeichen sind %s, Position jetzt = %ld",
46: puffer, ftell(fp));
47:
48: /* Positionszeiger des Streams zurücksetzen. */
49:
50: rewind(fp);
51:
52: printf("\n\nNach dem Zurücksetzen ist die Position wieder %ld",
53: ftell(fp));
54:
55: /* Liest 5 Zeichen ein. */
56:
57: fgets(puffer, PUFFERLAENGE, fp);
58: printf("\nund das Einlesen beginnt wieder vorne: %s\n", puffer);
59: fclose(fp);
60: return(0);
61: }

Direkt nach dem Öffnen, Position = 0
Nach dem Einlesen von abcde, Position = 5

Die nächsten 5 Zeichen sind fghij, Position jetzt = 10

Nach dem Zurücksetzen ist die Position wieder 0
und das Einlesen beginnt wieder vorne: abcde

Dieses Programm schreibt den String msg in eine Datei namens text.txt. Der String besteht aus den 26 Buchstaben des Alphabets in geordneter Reihenfolge. Die Zeilen 14 bis 18 öffnen text.txt zum Schreiben und prüfen, ob die Datei erfolgreich geöffnet wurde. Die Zeilen 20 bis 24 schreiben msg mit Hilfe der Funktion fputs() in die Datei und prüfen, ob die Schreiboperation erfolgreich war. Zeile 26 schließt die Datei mit fclose() und beendet damit die Erzeugung der Datei für den Rest des Programms.

Die Zeilen 30 bis 34 öffnen die Datei erneut, diesmal jedoch zum Lesen. Zeile 35 gibt den Rückgabewert von ftell() aus. Beachten Sie, dass diese Position am Anfang der Datei liegt. Zeile 39 führt die Funktion gets() aus, um fünf Zeichen einzulesen. Die fünf Zeichen und die neue Dateiposition werden in der Zeile 40 ausgegeben. Beachten Sie, dass ftell() die erwartete Position zurückliefert. Zeile 50 ruft rewind() auf, um den Zeiger wieder auf den Anfang der Datei zu setzen, bevor Zeile 52 diese Dateiposition erneut ausgibt und uns dadurch anzeigt, dass rewind() die Position tatsächlich zurückgesetzt hat. Bestätigt wird dies durch eine weitere Leseoperation in Zeile 57, die erneut die ersten Zeichen vom Anfang der Datei einliest. Zeile 59 schließt die Datei, bevor das Programm endet.

Die Funktion fseek()

Mehr Kontrolle über den Positionszeiger eines Streams bietet die Bibliotheksfunktion fseek(). Mit dieser Funktion können Sie den Positionszeiger an eine beliebige Stelle in der Datei setzen. Der Funktionsprototyp ist in stdio.h deklariert und lautet:

int fseek(FILE *fp, long offset, int ausgangspunkt);

Das Argument fp ist der FILE-Zeiger, der mit der Datei verbunden ist. Die Distanz, um die der Positionszeiger verschoben wird, wird in offset in Byte angegeben. Das Argument ausgangspunkt gibt die Position an, von der aus die Verschiebung berechnet wird. Der Parameter ausgangspunkt kann drei verschiedene Werte annehmen, für die in stdio.h symbolische Konstanten definiert sind (siehe Tabelle 15.2).

Konstante

Wert

Beschreibung

SEEK_SET

0

Verschiebt den Anzeiger um offset Byte vom Beginn der Datei

SEEK_CUR

1

Verschiebt den Anzeiger um offset Byte von der aktuellen Position

SEEK_END

2

Verschiebt den Anzeiger um offset Byte vom Ende der Datei

Tabelle 15.2: Mögliche Werte für den Parameter ausgangspunkt.

Die Funktion fseek() liefert 0 zurück, wenn der Anzeiger erfolgreich verschoben wurde, beziehungsweise einen Wert ungleich Null, wenn ein Fehler aufgetreten ist. Das Programm in Listing 15.6 verwendet fseek() für den wahlfreien Dateizugriff.

Listing 15.6: Wahlfreier Dateizugriff mit fseek().

1: /* Wahlfreier Dateizugriff mit fseek(). */
2:
3: #include <stdlib.h>
4: #include <stdio.h>
5:
6: #define MAX 50
7:
8: int main(void)
9: {
10: FILE *fp;
11: int daten, count, array[MAX];
12: long offset;
13:
14: /* Initialisiert das Array. */
15:
16: for (count = 0; count < MAX; count++)
17: array[count] = count * 10;
18:
19: /* Öffnet eine binäre Datei zum Schreiben. */
20:
21: if ( (fp = fopen("wahlfrei.dat", "wb")) == NULL)
22: {
23: fprintf(stderr, "\nFehler beim Öffnen der Datei.");
24: exit(1);
25: }
26:
27: /* Schreibt das Array in die Datei und schließt sie dann. */
28:
29: if ( (fwrite(array, sizeof(int), MAX, fp)) != MAX)
30: {
31: fprintf(stderr, "\nFehler beim Schreiben in die Datei.");
32: exit(1);
33: }
34:
35: fclose(fp);
36:
37: /* Öffnet die Datei zum Lesen. */
38:
39: if ( (fp = fopen("wahlfrei.dat", "rb")) == NULL)
40: {
41: fprintf(stderr, "\nFehler beim Öffnen der Datei.");
42: exit(1);
43: }
44:
45: /* Fragt den Anwender, welches Element gelesen werden soll. */
46: /* Liest das Element ein und zeigt es an. Programmende mit -1. */
47:
48: while (1)
49: {
50: printf("\nGeben Sie das einzulesende Element ein, 0-%d, -1 für \
Ende: ", MAX-1);
51: scanf("%ld", &offset);
52:
53: if (offset < 0)
54: break;
55: else if (offset > MAX-1)
56: continue;
57:
58: /* Verschiebt den Positionszeiger zum angegebenen Element. */
59:
60: if ( (fseek(fp, (offset*sizeof(int)), SEEK_SET)) != 0)
61: {
62: fprintf(stderr, "\nFehler beim Einsatz von fseek().");
63: exit(1);
64: }
65:
66: /* Liest einen einfachen Integer ein. */
67:
68: fread(&daten, sizeof(int), 1, fp);
69:
70: printf("\nElement %ld hat den Wert %d.", offset, daten);
71: }
72:
73: fclose(fp);
74: return(0);
75: }

Geben Sie das einzulesende Element ein, 0-49, -1 für Ende: 5

Element 5 hat den Wert 50.
Geben Sie das einzulesende Element ein, 0-49, -1 für Ende: 6

Element 6 hat den Wert 60.
Geben Sie das einzulesende Element ein, 0-49, -1 für Ende: 49

ElemeNT 49 hat den Wert 490.
Geben Sie das einzulesende Element ein, 0-49, -1 für Ende: 1

Element 1 hat den Wert 10.
Geben Sie das einzulesende Element ein, 0-49, -1 für Ende: 0

Element 0 hat den Wert 0.
Geben Sie das einzulesende Element ein, 0-49, -1 für Ende: -1

Die Zeilen 14 bis 35 finden sich in ähnlicher Form auch in Listing 15.5. Die Zeilen 16 und 17 initialisieren ein Array namens array mit 50 Werten vom Typ int. Der Wert, der in jedem Array-Element gespeichert ist, entspricht 10-mal dem Index. Anschließend wird das Array in eine binäre Datei namens wahlfrei.dat geschrieben.

Zeile 39 öffnet die Datei erneut im binären Lesemodus. Danach tritt das Programm in eine endlose while-Schleife ein. Die while-Schleife fordert den Anwender auf, den Index des Array-Elements anzugeben, das eingelesen werden soll. Beachten Sie die Zeilen 53 bis 56. In diesen Zeilen wird sichergestellt, dass der eingegebene Index auf ein in der Datei gespeichertes Element verweist. Kann man denn in C ein Element einlesen, das hinter dem Dateiende liegt? Ja. Genauso wie man im RAM über das Ende eines Arrays hinaus schreiben kann, kann man auch über das Ende einer Datei hinaus einlesen. Wenn Sie allerdings über das Ende hinaus (oder vor dem Anfang) lesen, sind die Ergebnisse unvorhersehbar. Am besten prüfen Sie immer, was Sie tun (wie es in den Zeilen 53 bis 56 der Fall ist).

Nachdem Sie angegeben haben, welches Element einzulesen ist, springt Zeile 60 mit einem Aufruf von fseek() an die entsprechenden Position. Da SEEK_SET verwendet wird, erfolgt die Verschiebung relativ zum Anfang der Datei. Beachten Sie, dass der Positionszeiger der Datei nicht um offset Bytes, sondern um offset Bytes mal der Größe der einzulesenden Elemente verschoben werden muss. Zeile 68 liest dann den Wert und Zeile 70 gibt ihn aus.

Das Ende einer Datei ermitteln

Manchmal wissen Sie genau, wie lang eine Datei ist, so dass Sie nicht umständlich prüfen müssen, wann das Ende der Datei erreicht ist. Wenn Sie zum Beispiel fwrite() verwenden, um ein Integer-Array mit 100 Elementen zu speichern, können Sie davon ausgehen, dass die Datei 400 Byte lang ist. Bei anderen Gelegenheiten wissen Sie jedoch nicht, wie lang eine Datei ist. Trotzdem werden Sie auch aus diesen Dateien Daten einlesen wollen - und zwar vom Anfang bis zum Ende der Datei. Dazu müssen Sie erkennen, wann das Dateiende erreicht ist, und dafür gibt es zwei Methoden.

Wenn Sie eine Textdatei zeichenweise auslesen, können Sie nach dem Zeichen für das Dateiende (im Englischen »End-Of-File«) Ausschau halten. Die symbolische Konstante EOF ist in stdio.h als -1 definiert - ein Wert, der kein wirkliches Zeichen codiert. Wenn eine Zeicheneingabefunktion im Textmodus EOF aus einem Stream einliest, können Sie sicher sein, dass Sie das Ende der Datei erreicht haben. So könnten Sie zum Beispiel schreiben:

while ( (c = fgetc( fp )) != EOF )

Das Ende binärer Dateien kann man nicht durch die Suche nach -1 feststellen, da praktisch jedes Datenbyte aus einem binären Stream diesen Wert innehaben könnte. Die Folge wäre, dass Sie die Eingabe vorzeitig abbrechen würden. Verwenden Sie statt dessen die Bibliotheksfunktion feof():

int feof(FILE *fp);

Listing 15.7 zeigt die Verwendung von feof(). Wenn Sie nach einem Dateinamen gefragt werden, geben Sie einfach den Namen einer beliebigen Textdatei ein - zum Beispiel einer Ihrer C-Quelltextdateien. Vergewissern Sie sich aber, dass die Datei in dem aktuellen Verzeichnis abgelegt ist, oder geben Sie einen Pfad als Teil des Dateinamens ein. Das Programm liest die Datei zeilenweise ein und gibt die einzelnen Zeilen an stdout aus. Wenn feof() das Ende der Datei feststellt, wird das Programm beendet.

Das Argument fp ist der FILE-Zeiger, der beim Öffnen der Datei von fopen() zurückgeliefert wurde. Die Funktion feof() liefert 0 zurück, solange das Ende der Datei noch nicht erreicht, oder einen Wert ungleich Null, wenn das Ende der Datei erreicht ist. Wenn ein Aufruf von feof() das Ende der Datei feststellt, dürfen danach so lange keine weiteren Lese-Operationen durchgeführt werden, bis der Positionszeiger der Datei mit rewind() zurückgesetzt oder mit fseek() verschoben oder die Datei geschlossen und erneut geöffnet ist.

Listing 15.7: Mit feof() das Ende einer Datei ermitteln.

1: /* Ende einer Datei (EOF) ermitteln. */
2: #include <stdlib.h>
3: #include <stdio.h>
4:
5: #define PUFFERGROESSE 100
6:
7: int main(void)
8: {
9: char puffer[PUFFERGROESSE];
10: char dateiname[60];
11: FILE *fp;
12:
13: puts("Geben Sie den Namen der auszugebenden Textdatei ein: ");
14: fgets(dateiname,60,stdin);
15: dateiname[strlen(dateiname)-1] = 0;
16:
17: /* Öffnet die Datei zum Lesen. */
18: if ( (fp = fopen(dateiname, "r")) == NULL)
19: {
20: fprintf(stderr, "Fehler beim Öffnen der Datei.\n");
21: exit(1);
22: }
23:
24: /* Zeilen einlesen und ausgeben. */
25:
26: while ( !feof(fp) )
27: {
28: fgets(puffer, PUFFERGROESSE, fp);
29: printf("%s",puffer);
30: }
31: printf("\n");
32: fclose(fp);
33: return(0);
34: }

Geben Sie den Namen der auszugebenden Textdatei ein:
hallo.c
#include <stdio.h>
int main(void)
{
printf("Hallo, Welt.");
return(0);
}

Der Aufbau der while-Schleife aus diesem Programm (Zeilen 26 bis 30) ist typisch für Schleifen, wie sie in komplexeren Programmen zur sequentiellen Verarbeitung von Daten eingesetzt werden. Solange das Ende der Datei noch nicht erreicht ist, wird der Code in der while-Anweisung (Zeilen 28 und 29) weiter ausgeführt. Erst wenn der Aufruf von feof() einen Wert ungleich Null zurückliefert, wird die Schleife verlassen, die Datei geschlossen, und das Programm endet.

Linux und andere Mitglieder der Unix-Familie verfügen über einen weiteren Satz an Funktionen zum Lesen, Schreiben und Suchen in Dateien. Diese Funktionen, die open(), creat(), read(), write(), lseek() und close() umfassen, gehören zu den Systemfunktionen (im Gegensatz zu den C-Bibliotheksfunktionen, die wir in der heutigen Lektion betrachtet haben). Sie werden vom Betriebssystem selbst definiert und nicht von der C-Bibliothek. Man bezeichnet Sie daher auch als Low-Level-Funktionen, und es wird Sie nicht verwundern, dass die C-Bibliotheksfunktionen wie fread(), fgets() und fwrite() so implementiert sind, dass Sie von diesen Low-Level-Systemfunktionen Gebrauch machen. Leider liegt die Besprechung dieser Low-Level-Systemfunktionen außerhalb der Möglichkeiten dieses Buches.

Was Sie tun sollten

Was nicht

Prüfen Sie die aktuelle Position in der Datei, so dass Sie nicht über das Ende hinaus oder vor dem Anfang der Datei lesen.

Verwenden Sie entweder rewind() oder fseek(fp, SEEK_SET, 0), um den Positionszeiger an den Anfang der Datei zu setzen.

Verwenden Sie feof(), um in binären Dateien nach dem Ende der Datei Ausschau zu halten.

Verwenden Sie bei binären Dateien nicht EOF.

Dateiverwaltungsfunktionen

Der Begriff Dateiverwaltung bezieht sich auf den Umgang mit bestehenden Dateien - nicht auf das Lesen aus oder das Schreiben in Dateien, sondern auf das Löschen, das Umbenennen und das Kopieren. Die C-Standardbibliothek enthält Funktionen zum Löschen und Umbenennen von Dateien. Außerdem können Sie Ihr eigenes Programm zum Kopieren von Dateien schreiben.

Eine Datei löschen

Um eine Datei zu löschen, verwenden Sie die Bibliotheksfunktion remove(). Ihr Prototyp ist in stdio.h deklariert und lautet:

int remove( const char *filename );

Die Variable *filename ist ein Zeiger auf den Namen der zu löschenden Datei. (Siehe auch den Abschnitt zu den Dateinamen weiter vorne in diesem Kapitel.) Die angegebene Datei muss nicht geöffnet sein. Wenn die Datei existiert, wird sie gelöscht (wie mit dem Befehl rm), und remove() liefert 0 zurück. Wenn die Datei nicht existiert, nur Lesezugriff zulässt, Ihre Zugriffsrechte nicht ausreichen oder wenn irgendein anderer Fehler auftritt, liefert remove() -1 zurück.

Listing 15.8 demonstriert den Einsatz von remove(). Seien Sie jedoch vorsichtig mit diesem Programm: Wenn Sie eine Datei mit remove() entfernen, ist diese unwiederbringlich gelöscht.

Listing 15.8: Mit der Funktion remove() eine Datei löschen.

1: /* Beispiel für die Funktion remove(). */
2:
3: #include <stdio.h>
4:
5: int main(void)
6: {
7: char dateiname[80];
8:
9: printf("Geben Sie den Namen der zu löschenden Datei ein: ");
10: fgets(dateiname,80,stdin);
11: dateiname[strlen(dateiname)-1] = 0;
12:
13: if ( remove(dateiname) == 0)
14: printf("Die Datei %s wurde gelöscht.\n", dateiname);
15: else
16: fprintf(stderr, "Fehler beim Löschen der Datei %s.\n", dateiname);
17: return(0);
18: }

Geben Sie den Namen der zu löschenden Datei ein: *.bak
Fehler beim Löschen der Datei *.bak.
Geben Sie den Namen der zu löschenden Datei ein: list1414.bak
Die Datei list1414.bak wurde gelöscht.

Zeile 9 fordert den Anwender auf, den Namen der zu löschenden Datei einzugeben. Zeile 13 ruft dann remove() auf, um die eingegebene Datei zu löschen. Wenn der Rückgabewert 0 ist, wurde die Datei gelöscht und es wird eine entsprechende Nachricht ausgegeben. Wenn der Rückgabewert ungleich Null ist, tritt ein Fehler auf und die Datei wird nicht gelöscht.

Eine Datei umbenennen

Die Funktion rename() ändert den Namen einer existierenden Datei. Der Funktionsprototyp ist in stdio.h deklariert und lautet:

int rename( const char *old, const char *new );

Die Dateinamen, auf die old und new verweisen, folgen den Regeln, die bereits weiter vorne in diesem Kapitel postuliert wurden. Die einzige zusätzliche Beschränkung ist, dass sich beide Namen auf das gleiche Laufwerk beziehen müssen. Sie können keine Datei umbenennen und gleichzeitig in ein anderes Laufwerk verschieben. Die Funktion rename() liefert im Erfolgsfall 0 oder bei Auftreten eines Fehlers -1 zurück. Fehler können (unter anderem) durch folgende Bedingungen ausgelöst werden:

Listing 15.9 zeigt ein Beispiel für den Einsatz von rename().

Listing 15.9: Mit der Funktion rename() den Namen einer Datei ändern.

1: /* Mit rename() einen Dateinamen ändern. */
2:
3: #include <stdio.h>
4:
5: int main(void)
6: {
7: char altername[80], neuername[80];
8:
9: printf("Geben Sie den aktuellen Dateinamen ein: ");
10: scanf("%80s",altername);
11: printf("Geben Sie den neuen Namen für die Datei ein: ");
12: scanf("%80s",neuername);
13:
14: if ( rename( altername, neuername ) == 0 )
15: printf("%s wurde in %s umbenannt.\n", altername, neuername);
16: else
17: fprintf(stderr, "Ein Fehler ist beim Umbenennen von %s \
aufgetreten.\n", altername);
18: return(0);
19: }

Geben Sie den aktuellen Dateinamen ein: list1509.c
Geben Sie den neuen Namen für die Datei ein: umbenennen.c
list1509.c wurde in umbenennen.c umbenannt.

Listing 15.9 zeigt, wie mächtig C sein kann. Mit nur 18 Codezeilen ersetzt dieses Programm einen Betriebssystem-Befehl und ist dabei noch wesentlich benutzerfreundlicher. Zeile 9 fordert den Namen einer Datei an, die umbenannt werden soll. Zeile 11 fragt nach dem neuen Dateinamen. Die Funktion rename() wird in Zeile 14 innerhalb einer if-Anweisung aufgerufen. Die if-Anweisung prüft, ob das Umbenennen der Datei korrekt ausgeführt wurde. Wenn ja, wird in Zeile 15 eine bestätigende Nachricht ausgegeben. Andernfalls gibt Zeile 17 die Nachricht aus, dass ein Fehler aufgetreten ist.

Eine Datei kopieren

Häufig ist es notwendig, eine Kopie einer Datei anzulegen - ein genaues Duplikat mit einem anderen Namen (oder mit dem gleichen Namen, aber in einem anderen Laufwerk oder Verzeichnis). In einer Konsole können Sie dazu den Betriebssystembefehl cp verwenden, aber wie kopieren Sie eine Datei in einem C- Programm? Es gibt keine Bibliotheksfunktion, so dass Sie Ihre eigene schreiben müssen.

Dies klingt vielleicht etwas kompliziert, ist aber in Wirklichkeit, dank der Ein- und Ausgabestreams von C, recht einfach. Gehen Sie in folgenden Schritten vor:

  1. Öffnen Sie die Quelldatei zum Lesen im Binärmodus (mit dem Binärmodus stellen Sie sicher, dass die Funktion alle Arten von Dateien und nicht nur Textdateien kopieren kann).
  2. Öffnen Sie die Zieldatei zum Schreiben im Binärmodus.
  3. Lesen Sie ein Zeichen von der Quelldatei. Denken Sie daran, dass der Zeiger beim ersten Öffnen einer Datei auf den Anfang der Datei zeigt, so dass Sie den Dateizeiger nicht explizit positionieren müssen.
  4. Wenn die Funktion feof() ergibt, dass Sie das Ende der Quelldatei erreicht haben, sind Sie fertig und können beide Dateien schließen und wieder zum aufrufenden Programm zurückkehren.
  5. Wenn Sie das Dateiende noch nicht erreicht haben, schreiben Sie das Zeichen in die Zieldatei und gehen dann zurück zu Schritt 3.

Listing 15.10 enthält eine Funktion namens datei_kopieren(), der die Namen der Quell- und Zieldatei übergeben werden und die die Quelldatei gemäß den oben stehenden Schritten kopiert. Wenn beim Öffnen einer der Dateien ein Fehler auftritt, versucht die Funktion gar nicht erst zu kopieren, sondern liefert -1 an das aufrufende Programm zurück. Wenn die Kopieroperation erfolgreich abgeschlossen ist, schließt das Programm beide Dateien und liefert 0 zurück.

Listing 15.10: Eine Funktion, die eine Datei kopiert.

1: /* Eine Datei kopieren. */
2:
3: #include <stdio.h>
4:
5: int datei_kopieren( char *altername, char *neuername );
6:
7: int main(void)
8: {
9: char quelle[80], ziel[80];
10:
11: /* Die Namen der Quell- und Zieldateien anfordern. */
12:
13: printf("\nGeben Sie die Quelldatei an: ");
14: scanf("%80s",quelle);
15: printf("\nGeben Sie die Zieldatei an: ");
16: scanf("%80s",ziel);
17:
18: if (datei_kopieren ( quelle, ziel ) == 0 )
19: puts("Kopieren erfolgreich");
20: else
21: fprintf(stderr, "Fehler beim Kopieren");
22: return(0);
23: }
24: int datei_kopieren( char *altername, char *neuername )
25: {
26: FILE *falt, *fneu;
27: int c;
28:
29: /* Öffnet die Quelldatei zum Lesen im Binärmodus. */
30:
31: if ( ( falt = fopen( altername, "rb" ) ) == NULL )
32: return -1;
33:
34: /* Öffnet die Zieldatei zum Schreiben im Binärmodus. */
35:
36: if ( ( fneu = fopen( neuername, "wb" ) ) == NULL )
37: {
38: fclose ( falt );
39: return -1;
40: }
41:
42: /* Liest jeweils nur ein Byte aus der Quelldatei. Ist das */
43: /* Dateiende noch nicht erreicht, wird das Byte in die */
44: /* Zieldatei geschrieben. */
45:
46: while (1)
47: {
48: c = fgetc( falt );
49:
50: if ( !feof( falt ) )
51: fputc( c, fneu );
52: else
53: break;
54: }
55:
56: fclose ( fneu );
57: fclose ( falt );
58:
59: return 0;
60: }

Geben Sie die Quelldatei an: list1510.c

Geben Sie die Zieldatei an: tmpdatei.c
Kopieren erfolgreich

Die Funktion datei_kopieren() funktioniert perfekt und ermöglicht das Kopieren von kleinen Textdateien bis hin zu großen Programmdateien. Sie hat jedoch auch ihre Nachteile. Wenn zum Beispiel die Zieldatei bereits existiert, wird sie von der Funktion ohne vorherige Warnung gelöscht. Es wäre für Sie eine gute Programmierübung, die Funktion datei_kopieren() dahingehend abzuändern, dass vor dem Kopieren geprüft wird, ob die Zieldatei bereits existiert, und wenn ja, den Anwender zu fragen, ob die alte Datei überschrieben werden soll.

Die main()-Funktion aus Listing 15.10 sollte Ihnen bekannt vorkommen. Sie ist praktisch identisch mit der main()-Funktion aus Listing 15.9, nur dass anstelle von rename() die Funktion datei_kopieren() aufgerufen wird. Da C keine Kopierfunktion kennt, wird in den Zeilen 24 bis 60 eine solche Funktion erzeugt. Die Zeilen 31 und 32 öffnen die Quelldatei falt im binären Lesemodus. Die Zeilen 36 bis 40 öffnen die Zieldatei fneu im binären Schreibmodus. Beachten Sie, dass Zeile 38 die Quelldatei schließt, wenn ein Fehler beim Öffnen der Zieldatei aufgetreten ist. Die while-Schleife in den Zeilen 46 bis 54 führt den eigentlichen Kopiervorgang aus. Zeile 48 liest ein Zeichen aus der Quelldatei falt ein. Zeile 50 prüft, ob es sich dabei um den Dateiende- Marker handelt. Wenn das Ende der Datei erreicht ist, wird eine break-Anweisung ausgeführt, um aus der while-Schleife auszusteigen. Ist das Ende der Datei noch nicht erreicht, wird das eingelesene Zeichen in die Zieldatei fneu geschrieben. Die Zeilen 56 und 57 schließen die zwei Dateien, bevor die Programmausführung zu main() zurückkehrt.

Temporäre Dateien verwenden

Einige Programme benötigen für ihre Ausführung eine oder mehrere temporäre Dateien. Eine temporäre Datei ist eine Datei, die vom Programm erzeugt wird, für einen bestimmten Zweck während der Programmausführung eingesetzt und dann wieder gelöscht wird, bevor das Programm endet. Wenn Sie eine temporäre Datei erzeugen, ist ihr Name eigentlich unerheblich, da die Datei wieder gelöscht wird. Sie müssen nur darauf achten, dass der verwendete Name nicht bereits für eine andere Datei vergeben ist. Die C-Standardbibliothek enthält die Funktion tmpnam(), die einen gültigen Dateinamen erzeugt, der sich nicht mit bereits existierenden Dateinamen überschneidet. Der Prototyp der Funktion steht in stdio.h und lautet:

char *tmpnam(char *s);

Das Argument s muss ein Zeiger auf einen Puffer sein, der groß genug ist, den Dateinamen aufzunehmen. Sie können auch einen Nullzeiger (NULL) übergeben. In einem solchen Fall wird der temporäre Name in einem Puffer gespeichert, der intern von tmpnam() angelegt wird, und die Funktion liefert einen Zeiger auf diesen Puffer zurück. Listing 15.11 enthält Beispiele für beide Möglichkeiten, mit tmpnam() temporäre Dateinamen zu erzeugen.

Listing 15.11: Mit tmpnam() temporäre Dateinamen erzeugen.

1: /* Beispiel für temporäre Dateinamen. */
2:
3: #include <stdio.h>
4:
5: int main(void)
6: {
7: char puffer[10], *c;
8:
9: /* Schreibt einen temporären Namen in den übergebenen Puffer. */
10:
11: tmpnam(puffer);
12:
13: /* Erzeugt einen weiteren Namen und legt ihn im internen */
14: /* Puffer der Funktion ab. */
15:
16: c = tmpnam(NULL);
17:
18: /* Gibt die Namen aus. */
19:
20: printf("Temporärer Name 1: %s", puffer);
21: printf("\nTemporärer Name 2: %s\n", c);
22: return 0;
23:}

Temporärer Name 1: /tmp/filefWM7l4
Temporärer Name 2: /tmp/fileW53KxY

Die temporären Namen, die auf Ihrem System erzeugt werden,
lauten wahrscheinlich anders.

Dieses Programm dient lediglich der Erzeugung und der Ausgabe von temporären Dateinamen. Es erzeugt nicht wirklich irgendwelche Dateien. Zeile 11 speichert einen temporären Namen in dem Zeichenarray puffer. Zeile 16 weist dem Zeichenzeiger c den von tmpnam() zurückgelieferten Namen zu. Ihr Programm muss den erzeugten Namen verwenden, um die temporäre Datei zu öffnen und vor Programmende wieder zu löschen,. Das folgende Codefragment veranschaulicht dies:

char tempname[80];
FILE *tmpdatei;
tmpnam(tempname);
tmpdatei = fopen(tempname, "w"); /* passenden Modus verwenden */
fclose(tmpdatei);
remove(tempname);

Was Sie nicht tun solllten

Entfernen Sie keine Dateien, die Sie vielleicht später noch benötigen.

Vergessen Sie nicht, temporäre Dateien, die Sie erzeugt haben, wieder zu entfernen. Sie werden nicht automatisch gelöscht.

Zusammenfassung

Heute haben Sie gelernt, wie man in C-Programmen mit Dateien arbeitet. C behandelt eine Datei wie einen Stream (eine Folge von Zeichen), das heißt wie die vordefinierten Streams, die Sie am Tag 13 kennen gelernt haben. Ein Stream, der mit einer Datei verbunden ist, muss erst geöffnet werden, bevor er verwendet werden kann, und er muss hinterher wieder geschlossen werden. Ein Datei-Stream kann entweder im Text- oder im Binärmodus geöffnet werden.

Nachdem eine Datei geöffnet ist, können Sie aus der Datei Daten in Ihr Programm einlesen, Daten vom Programm in die Datei schreiben oder beides machen. Es gibt drei allgemeine Formen der Datei-E/A: formatiert, als Zeichen und direkt, die jede ihr besonderes Einsatzgebiet haben.

Jede geöffnete Datei verfügt über einen Dateipositionszeiger, der die aktuelle Position in der Datei angibt. Die Position wird in Byte ab dem Beginn der Datei gemessen. Bei einigen Arten des Dateizugriffs wird der Positionszeiger automatisch aktualisiert und Sie brauchen nicht einzugreifen. Für den wahlfreien Dateizugriff stehen Ihnen in der C- Standardbibliothek Funktionen zur Manipulation des Positionszeigers zur Verfügung.

Außerdem stellt Ihnen C einige grundlegende Dateiverwaltungsfunktionen zur Verfügung, mit denen Sie Dateien löschen und umbenennen können. Zum Abschluss der heutigen Lektion haben Sie Ihre eigene Funktion zum Kopieren von Dateien aufgesetzt.

Fragen und Antworten

Frage:
Windows unterscheidet zwischen binären Dateien und Textdateien. Woher weiß ich, mit welcher Art von Datei ich zu tun habe?

Antwort:
Binäre Dateien werden normalerweise mit fread() und fwrite() bearbeitet. Wenn Sie die zeichenweise oder formatierte Ein- und Ausgabe verwenden, haben Sie wahrscheinlich mit einer Textdatei zu tun.

Frage:
Kann ich in den Dateifunktionen - remove(), rename(), fopen() etc. - die Dateinamen mit Pfadangaben angeben?

Antwort:
Ja. Sie können einen vollen Dateinamen mit dem Pfad oder nur einfach den Dateinamen angeben. Wenn Sie nur den Dateinamen verwenden, sucht die Funktion die Datei in dem aktuellen Verzeichnis.

Frage:
Kann ich über das Ende einer Datei hinaus lesen?

Antwort:
Ja. Sie können auch vor dem Anfang einer Datei lesen. Die Ergebnisse solcher Einleseoperationen können allerdings verheerend sein. Das Lesen von Dateien ist hierin mit dem Umgang mit Arrays zu vergleichen. Wenn Sie fseek() verwenden, müssen Sie sicherstellen, dass Sie das Ende der Datei nicht überschreiten.

Frage:
Was passiert, wenn ich eine Datei nicht schließe?

Antwort:
Es gehört zum guten Programmierstil, alle geöffneten Dateien wieder zu schließen. In der Regel werden die Dateien automatisch geschlossen, wenn das Programm beendet wird. Aber leider kann man sich darauf nicht immer verlassen. Wenn die Datei nicht korrekt geschlossen wird, können Sie unter Umständen später nicht auf die Datei zugreifen, da das Betriebssystem davon ausgeht, dass die Datei noch in Benutzung ist.

Frage:
Wie viele Dateien kann ich gleichzeitig öffnen?

Antwort:
Diese Frage kann nicht mit einer einfachen Zahl beantwortet werden. Die Anzahl der zu öffnenden Dateien hängt von den Einstellungen Ihres Betriebssystems ab. Linux-Systeme können normalerweise 1024 Dateien gleichzeitig öffnen.

Frage:
Kann ich eine Datei mit den Funktionen für den wahlfreien Zugriff auch sequentiell lesen?

Antwort:
Wenn eine Datei sequentiell gelesen wird, besteht kein Grund, eine Funktion wie fseek() einzusetzen. Da der Dateizeiger von den Schreib- und Leseoperationen automatisch bewegt wird, befindet er sich immer genau an der Position, auf die man beim sequentiellen Lesen als Nächstes zugreifen möchte. Sie können zwar fseek() verwenden, um eine Datei sequentiell zu lesen, doch bringt Ihnen das keine Vorteile.

Workshop

Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, sowie Übungen, die Sie anregen sollen, das Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Die Lösungen zu den Fragen und den Übungen finden Sie in Anhang C.

Quiz

  1. Was ist der Unterschied zwischen einem Stream im Textmodus und einem Stream im Binärmodus?
  2. Wie können Sie sicher stellen, dass ein Programm, das binäre Daten einliest, auf Windows portierbar ist?
  3. Was muss Ihr Programm machen, bevor es auf eine Datei zugreifen kann?
  4. Welche Informationen müssen Sie fopen() zum Öffnen einer Datei übergeben, und wie lautet der Rückgabewert der Funktion?
  5. Wie lauten die drei allgemeinen Methoden für den Dateizugriff?
  6. Wie lauten die zwei allgemeinen Methoden zum Lesen von Dateiinformationen?
  7. Wie lautet der Wert von EOF und wann wird er benutzt?
  8. Wie ermitteln Sie das Ende einer Datei im Text- beziehungsweise im Binärmodus?
  9. Was versteht man unter einem Dateipositionszeiger und wie können Sie ihn verschieben?
  10. Worauf zeigt der Dateipositionszeiger, wenn eine Datei das erste Mal geöffnet wird? (Wenn Sie unsicher sind, gehen Sie zurück zu Listing 15.5.)

Übungen

  1. Schreiben Sie Code, der alle Datei-Streams schließt.
  2. Geben Sie zwei verschiedene Möglichkeiten an, den Dateipositionszeiger auf den Anfang der Datei zu setzen.
  3. FEHLERSUCHE: Ist an dem folgenden Code etwas falsch?
    FILE *fp;
    int c;

    if ( ( fp = fopen( altername, "rb" ) ) == NULL )
    return -1;

    while (( c = fgetc( fp)) != EOF )
    fprintf( stdout, "%c", c );

    fclose ( fp );

Aufgrund der vielen möglichen Antworten gibt es zu den folgenden Übungen keine Lösungen.

  1. Schreiben Sie ein Programm, das eine Datei auf dem Bildschirm ausgibt.
  2. Schreiben Sie ein Programm, das eine Datei öffnet und die Anzahl der Zeichen zählt. Lassen Sie die Anzahl der Zeichen vom Programm ausgeben.
  3. Schreiben Sie ein Programm, das eine existierende Textdatei öffnet und sie in eine neue Textdatei kopiert, in der alle Kleinbuchstaben in Großbuchstaben geändert und die restlichen Zeichen unverändert bleiben.
  4. Schreiben Sie ein Programm, das eine Datei öffnet, diese in 128-Byte-Blöcken einliest und den Inhalt jedes Blocks in hexadezimalem und ASCII-Format auf den Bildschirm ausgibt.
  5. Schreiben Sie eine Funktion, die eine neue temporäre Datei öffnet. Alle temporären Dateien, die von dieser Funktion erzeugt werden, sollen automatisch geschlossen und gelöscht werden, wenn das Programm beendet wird. (Hinweis: Verwenden Sie die Bibliotheksfunktion atexit().)
1

vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbackKapitelanfangnächstes Kapitel


© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH