Die meisten Programme arbeiten aus dem einen oder anderen Grund mit Dateien: Datenspeicherung, Konfigurationsdaten und so weiter. Heute lernen Sie:
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.
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.
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
.
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.
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:
b
hängen. Von Linux und anderen Systemen, die keine Unterscheidung
zwischen Text- und Binärdateien vornehmen, wird das b
ignoriert.
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,
r
-Modus zu öffnen.
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.
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 (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.
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.
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 als100.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.
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.
Es gibt drei Funktionen zur Zeicheneingabe: getc()
und fgetc()
für einzelne Zeichen
und fgets()
für Zeilen.
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.
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.
EOF
wird eingelesen, bevor str
irgendwelche
Zeichen zugewiesen wurden. In diesem Fall wird NULL
zurückgeliefert, und der
Speicher, auf den str
zeigt, bleibt unverändert.
EOF
wird eingelesen, nachdem str
ein oder
mehrere Zeichen zugewiesen wurden. In diesem Fall wird NULL
zurückgeliefert,
und der Speicher, auf den str
zeigt, enthält Müll.
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.
Für die Zeichenausgabe gibt es zwei Funktionen, die man kennen sollte: putc()
und
fputs()
.
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).
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.
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 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 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.
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.
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.
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.
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).
Verschiebt den Anzeiger um | ||
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.
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()
undclose()
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 wiefread()
,fgets()
undfwrite()
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.
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.
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.
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:
old
existiert nicht.
new
.
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.
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:
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.
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.
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);
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. |
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.
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.
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.
fopen()
zum Öffnen einer Datei übergeben,
und wie lautet der Rückgabewert der Funktion?
EOF
und wann wird er benutzt?
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.
atexit().
)