14.6    Weitere Funktionen für die Ein- und Ausgabe

Im folgenden Abschnitt lernen Sie die Funktionen kennen, mit denen Sie einzelne Zeichen, Zeilen oder ganze Datenblöcke an einen Stream übertragen oder aus diesem lesen können. Übertragen in einen Stream heißt dabei meistens, dass die Daten erst einmal in einem internen Puffer gesammelt werden. Erst wenn der Puffer voll ist, werden die Daten in die Datei geschrieben – dies beschleunigt den Zugriff auf den Datenträger enorm, auf den auch immer nur Blöcke geschrieben werden können. Meistens ist der Datenpuffer so groß wie eine Zugriffseinheit des entsprechenden Dateisystems; bei Linux beträgt die Blockgröße beispielsweise 4 kB (ext4), bei Windows zwischen 4 kB und 64 kB (NTFS). Auf USB-Sticks und SD-Karten für Smartphones verwenden Sie meist das FAT32-Dateisystem, und die Größe einer Zugriffseinheit variiert hier zwischen 8 kB und 64 kB.

14.6.1    Einzelne Zeichen aus einem Stream lesen

Zum Lesen von einzelnen Zeichen aus einem Stream sind in der Header-Datei stdio.h folgende Funktionen vorhanden:

int fgetc(FILE* fp);
int getc(FILE* fp);
int getchar();

Mit fgetc() können Sie das nächsten Zeichen aus dem Stream fp als unsigned char lesen. Als Rückgabewert erhalten Sie bei Erfolg das gelesene Zeichen als int-Wert (also konvertiert) oder bei einem aufgetretenen Fehler EOF. Ebenfalls wird EOF zurückgegeben, wenn der Stream als nächstes Zeichen auf das Dateiende verweist. Der Unterschied zwischen fgetc() und getc() besteht darin, dass getc() als Makro implementiert sein darf. Mit getchar() lesen Sie hingegen ein Zeichen von der Standardeingabe; (stdin). getchar() ist daher gleichwertig mit fgetc(stdin).

14.6.2    Zeichen in den Stream zurückstellen

Haben Sie ein zu viel gelesenes Zeichen aus dem Stream geholt, können Sie es mit der Funktion ungetc() wieder in den Stream zurückschieben. Die Syntax dieser Funktion lautet:

int ungetc(int c, FILE *fp);

Damit schieben Sie das Zeichen c (das in ein unsigned char konvertiert wird) in den Eingabe-Stream fp zurück. Die Funktion gibt das zurückgeschobene Zeichen zurück, oder sie meldet bei einem Fehler EOF. Damit ist c das erste Zeichen, das bei der nächsten Leseoperation aus dem Stream fp erneut gelesen wird. Das gilt allerdings nicht mehr, wenn vor der nächsten Leseoperation eine der Funktionen fflush(), rewind(), fseek() oder fsetpos() aufgerufen wurde.

14.6.3    Einzelne Zeichen in einen Stream schreiben

Die Funktionen zum Schreiben von einzelnen Zeichen in den Stream lauten:

#include <stdio.h>
int fputc(int c, FILE *fp);
int putc(int c, FILE *fp);
int putchar(int c);

Mit fputc() schreiben Sie das von int in unsigned char umgewandelte Zeichen c in den verbundenen Stream fp. Zurückgegeben wird ein nicht negativer Wert bei Erfolg oder EOF im Falle eines Fehlers. Die Version putc() entspricht der Funktion fputc(). Es handelt sich hierbei aber um ein Makro. Mit putchar() wird das angegebene Zeichen c auf die Standardausgabe (stdout) geschrieben. putchar(c) entspricht somit fputc(c, stdout).

Nachfolgend sehen Sie ein einfaches Beispiel, das das zeichenweise Lesen und Schreiben demonstrieren soll:

00  // Kapitel14/fputc_fgetc.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #define FILENAME "fputc_fgetc.c" // Anpassen
04 #define COPY "fputc_fgetc.bak" // Anpassen

05 int main(void) {
06 FILE *fpr = fopen( FILENAME, "r" );
07 if( fpr == NULL ) {
08 fprintf(stderr, "Fehler beim Oeffnen: %s\n", FILENAME);
09 return EXIT_FAILURE;
10 }
11 FILE *fpw = fopen( COPY, "w" );
12 if( fpw == NULL ) {
13 fprintf(stderr, "Fehler beim Oeffnen: %s\n", COPY);
14 return EXIT_FAILURE;
15 }
16 int c;
17 while ( (c=fgetc(fpr)) != EOF ) {
18 if( c > 127 ) {
19 fputc('-', stdout);
20 }
21 else {
22 fputc(c, stdout);
23 }
24 if( fputc(c, fpw ) == EOF ) {
25 fprintf(stderr, "Fehler beim Schreiben\n");
26 break;
27 }
28 }
29 if( c == EOF ) {
30 if( feof(fpr) ) {
31 printf("Dateiende erreicht\n");
32 }
33 else if( ferror(fpr) ) {
34 printf("Ein Fehler beim Lesen ist aufgetreten!\n");
35 }
37 }
38 fclose(fpr);
39 fclose(fpw);
40 return EXIT_SUCCESS;
41 }

Listing 14.2    fputc_fgetc.c demonstriert das zeichenweise Lesen und Schreiben mit fgetc() und fputc().

In Zeile (06) wird eine Datei zum Lesen geöffnet. In Zeile (11) wird eine weitere Datei zum Schreiben geöffnet. Ziel des Beispiels ist es, eine Datei zeichenweise bzw. byteorientiert zu kopieren. In Zeile (17) lesen Sie so lange Zeichen für Zeichen aus dem Lese-Stream fpr, bis Sie auf das Dateiende (EOF) der Eingabedatei stoßen. In Zeile (18) überprüfen Sie, ob der Wert des Zeichens größer als 127 war. In diesem Fall bedeutet das, dass dieses Zeichen oberhalb des 7-Bit-ASCII-Zeichensatzes ist. Dort könnten Sie auf einigen Systemen beispielsweise mit Umlauten und anderen speziellen Zeichen Probleme bekommen. Das kommt aber immer auf den Zeichensatz an, der verwendet wird. Daher wird anstatt eines solchen Zeichens, das vielleicht nicht richtig ausgegeben wird, einfach ein Trennstrich in Zeile (19) auf dem Bildschirm ausgegeben. Alle anderen Zeichen unter dem ASCII-Wert 127 werden ganz normal in Zeile (22) ausgegeben. Unbehandelt wird allerdings jedes Zeichen auf jeden Fall so in den Schreib-Stream fpw in Zeile (24) geschrieben, wie es gelesen wurde. Bedenken Sie an dieser Stelle, dass auch hier Zeichen und Bytes nicht unbedingt dasselbe sind und nicht unbedingt eine 1:1-Kopie erzeugt wird.

Das letzte Beispiel zeigt aber dennoch eine interessante Möglichkeit auf, nämlich, wie Sie einen Filter schreiben können. In diesem Falle setzen Sie nicht wie in Zeile (19) einen Trennstrich, sondern Sie schreiben eine spezielle Funktion, die sich mit diesem Problem von nicht portablen Zeichen auseinandersetzt.

14.6.4    Zeilenweise aus einem Stream lesen

Zum zeilenweisen Einlesen steht Ihnen folgende bereits wohlbekannte Funktion zur Verfügung:

#include <stdio.h>
char *fgets(char *buf, int n, FILE *fp );

Die Funktion wurde in diesem Buch schon des Öfteren verwendet. Damit lesen Sie vom Stream fp maximal n-1 Zeichen in den Puffer buf und hängen immer ein Stringende-Zeichen am Ende an. Ein Lesevorgang wird beim Erreichen eines Newline-Zeichens (Zeilenende) oder beim Dateiende beendet. Als Rückgabewert gibt diese Funktion entweder die Anfangsadresse von buf oder, wenn kein einziges Zeichen eingelesen wurde, den NULL-Zeiger zurück.

14.6.5    Zeilenweise in einen Stream schreiben

Die Gegenstücke zum Schreiben eines nullterminierten Strings in einen Stream sind:

#include <stdio.h>
int fputs(const char* str, FILE *fp);
int puts(const char* str);

Mit fputs() schreiben Sie den nullterminierten String, auf den der Zeiger str verweist, in den Stream fp. Das Nullzeichen '\0' wird nicht (!) mit in den Stream geschrieben. Die Funktion puts() hingegen gibt den String auf den Zeiger str auf die Standardausgabe stdout auf dem Bildschirm mit einem zusätzlichen Newline-Zeichen aus. Der Rückgabewert ist ein nicht negativer Wert bei Erfolg oder EOF im Fall eines Fehlers.

Hierzu ein einfaches Beispiel, das einige gängige Funktionen zum zeilenweise Lesen und Schreiben in der Praxis demonstriert:

00  // Kapitel14/line_read_write.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <string.h>
04 #define LINEBUF 1024

05 void killNL( char *str ) {
06 size_t p = strlen(str);
07 if(str[p-1] == '\n') {
08 str[p-1] = '\0';
09 }
10 }

11 void dump_buffer(FILE *fp) {
12 int ch;
13 while( (ch = fgetc(fp)) != EOF && ch != '\n' )
14 /* Kein Anweisungsblock nötig */ ;
15 }

16 void countLineOut( FILE *rfp ) {
17 char buf[LINEBUF];
18 int count = 1;
19 while( fgets(buf, LINEBUF, rfp) != NULL ) {
20 printf("%3d | ", count);
21 fputs( buf, stdout );
22 count++;
23 }
24 }

25 void search( FILE *fp ) {
26 char str[LINEBUF], buf[LINEBUF];
27 int count = 1;
28 printf("Wonach wollen Sie suchen: ");
29 if( fgets(str, LINEBUF, stdin) == NULL ) {
30 fprintf(stderr, "Fehler bei der Eingabe\n");
31 return;
32 }
33 killNL(str);
34 while( fgets(buf, LINEBUF, fp) != NULL ) {
35 if(strstr(buf, str) != 0) {
36 printf("%3d : %s", count, buf);
37 }
38 count++;
39 }
40 }

41 void copyFile( FILE *rfp, FILE *wfp ) {
42 char buf[LINEBUF];
43 while( fgets(buf, LINEBUF, rfp ) != NULL ) {
44 if( fputs(buf, wfp) == EOF ) {
45 if( ferror(wfp) ) {
46 fprintf(stderr, "Fehler beim Schreiben\n");
47 return;
48 }
49 }
50 }
51 }

52 int main(void) {
53 char filename1[LINEBUF], filename2[LINEBUF];
54 FILE *wfp = NULL;
55 int input = 0;
56 printf("Datei zum Lesen: ");
57 if( fgets(filename1, LINEBUF, stdin) == NULL ) {
58 fprintf(stderr, "Fehler bei der Eingabe\n");
59 return EXIT_SUCCESS;
60 }
61 killNL(filename1);
62 FILE *rfp = fopen(filename1, "r");
63 if( rfp == NULL ) {
64 fprintf(stderr, "Fehler beim Oeffnen\n");
65 return EXIT_FAILURE;
66 }
67 printf("Was wollen Sie mit der Datei machen?\n");
68 printf("-1- Zeilen zaehlen (Bildschirmausgabe)\n");
69 printf("-2- Zeilen zaehlen (in Datei schreiben)\n");
70 printf("-3- Suchen\n");
71 printf("-4- Kopieren\n");
72 printf("Ihre Auswahl: ");
73 if( scanf("%d", &input) != 1 ) {
74 fprintf(stderr, "Fehler bei der Eingabe\n");
75 return EXIT_FAILURE;
76 }
77 dump_buffer(stdin);
78 switch( input ) {
79 case 1 :
80 case 2 : if( input == 2 ) {
81 printf("Dateiname der Kopie: ");
82 if(fgets(filename2,LINEBUF,stdin)==NULL) {
83 fprintf(stderr,"Fehler bei der Eingabe");
84 break;
85 }
86 killNL(filename2);
87 wfp = freopen(filename2, "w", stdout);
88 if( wfp == NULL ) {
89 fprintf(stderr, "Fehler bei Oeffnen\n");
90 break;
91 }
92 }
93 countLineOut(rfp);
94 break;
95 case 3 : rfp = fopen(filename1, "r");
96 if( rfp != NULL ) {
97 search(rfp);
98 }
99 break;
100 case 4 : printf("Dateiname der Kopie: ");
101 if(fgets(filename2, LINEBUF, stdin)==NULL) {
102 fprintf(stderr, "Fehler bei der Eingabe\n");
103 break;
104 }
105 killNL(filename2);
106 wfp = fopen(filename2, "w");
107 if( wfp != NULL ) {
108 copyFile( rfp, wfp );
109 }
110 else {
111 fprintf(stderr, "Fehler beim Oeffnen\n");
112 }
113 break;
114 }
115 if( rfp != NULL )
116 fclose(rfp);
117 if( wfp != NULL )
118 fclose(wfp);
119 return EXIT_SUCCESS;
120 }

Listing 14.3    line_read_write.c demonstriert einige Funktionen zum zeilenweisen Zugriff auf Textdateien.

In diesem etwas umfangreicheren Listing werden mehrere typische Anwendungsfälle demonstriert, bei denen das zeilenweise Lesen und Schreiben hilfreich sein kann. In den Zeilen (05) bis (10) finden Sie mit killNL() eine Funktion, mit der Sie das Newline-Zeichen aus einem String entfernen können. Es wird z. B. bei fgets() von der Standardeingabe mit eingelesen (außer wenn der Dateiname bei den Beispielen LINEBUF-1 ist); dies ist aber bei den Dateinamen nicht erwünscht. Anstelle des Newline-Zeichens setzen Sie hiermit gegebenenfalls einfach das Stringende-Zeichen ein.

Mit der Funktion countLineOut() in den Zeilen (16) bis (24) können Sie die Zeilen einer Datei mit Zeilennummern ausgeben lassen. Hierbei werden die Daten in der while-Schleife (Zeilen (19) bis (23)) so lange zeilenweise eingelesen und wieder ausgegeben, wie die Funktion fgets() ungleich NULL zurückgibt. In der main()-Funktion wurde zusätzlich noch die Option angeboten, die Ausgabe der Funktion countLineOut() in eine Datei anstatt in die Standardausgabe zu schreiben. Hierbei müssen Sie lediglich die Standardausgabe in Zeile (87) mittels freopen() umleiten. Jetzt wissen Sie übrigens auch, auf welche Weise wir die Zeilennummern der Listings in diesem Buch erzeugt haben. Wir haben die Programme ganz normal kompiliert und uns dann den Quellcode nach einigen Tests in eine Textdatei mit Zeilennummern zurückschreiben lassen.

In den Zeilen (25) bis (40) finden Sie mit der Funktion search() eine einfache Möglichkeit, innerhalb einer Datei nach Zeilen mit einer bestimmten String-Folge zu suchen. Es wird hierbei die Datei ebenfalls Zeile für Zeile in einer while-Schleife durchlaufen, und in jeder Zeile wird dabei nach einer bestimmten String-Folge mit der Funktion strstr() gesucht. Wurde die entsprechende String-Folge gefunden, wird diese Zeile zusammen mit der Zeilennummer ausgegeben.

Mit der Funktion copyFile() sehen Sie in den Zeilen (41) bis (51) einen Klassiker. Mittels fgets() und fputs() können Sie ganz einfach zeilenweise den Inhalt von einem Lese-Stream in einen Schreib-Stream kopieren. Dies funktioniert natürlich nur mit Textdateien.

14.6.6    Lesen und Schreiben in ganzen Blöcken

Die Funktionen fread() und fwrite() sind speziellere Funktionen, die nicht mit einzelnen Zeichen oder Strings arbeiten, sondern mit einzelnen Blöcken.

Wenn Sie Strukturen speichern und wieder auslesen wollen, sind fread() und fwrite() die perfekten Funktionen dazu.

Die Syntax zum blockweisen Lesen mit fread() lautet:

#include <stdio.h>
size_t fread( void *buffer, size_t size, size_t n, FILE *fp );

Mit der Funktion fread() lesen Sie n Elemente der Größe size aus dem Stream fp aus und schreiben diese in das Array, auf das der Zeiger buffer zeigt. Wurden weniger als n Elemente eingelesen, kann es sein, dass vorher das Dateiende erreicht wurde oder ein Fehler auftrat.

Jetzt zur Syntax von fwrite(), dem Gegenstück von fread() zum Schreiben:

#include <stdio.h>
size_t fwrite( const void *buffer, size_t size,
size_t n, FILE *fp );

Sie schreiben n Elemente mit der Größe von size aus dem durch buffer adressierten Speicherbereich in den Ausgabe-Stream fp. Hier ist der Rückgabewert die Anzahl der erfolgreich geschriebenen Elemente. Wurden weniger als n Elemente geschrieben, ist ein Fehler aufgetreten.

Vielleicht haben Sie an dieser Stelle schon den typenlosen void-Zeiger vor buffer bei fread() und fwrite() bemerkt und sich dazu gedacht, dass man dann ja mit fread() und fwrite() sämtliche möglichen Datentypen inklusive Strukturen »in einem Rutsch« verarbeiten kann.

Das nächste Beispiel soll Ihnen zeigen, dass Sie hier genau richtig gedacht haben. Nachfolgend sehen Sie also ein Beispiel dafür, wie Sie Datensätze von Strukturen speichern und wieder lesen können, ohne auf verkettete Listen oder Arrays von Strukturen zurückgreifen zu müssen. Sie tun dies, indem Sie einen neuen Datensatz direkt in eine Datei schreiben bzw. direkt daraus lesen. Wie immer handelt es sich hier um ein für das Buch vereinfachtes Beispiel:

00  // Kapitel14/fread_fwrite.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <string.h>
04 #define BUF 256
05 #define DATAFILE "data.dat"

06 void killNL( char *str ) {
07 size_t p = strlen(str);
08 if(str[p-1] == '\n') {
09 str[p-1] = '\0';
10 }
11 }

12 void dump_buffer(FILE *fp) {
13 int ch;
14 while( (ch = fgetc(fp)) != EOF && ch != '\n' )
15 /* Kein Anweisungsblock nötig */ ;
16 }

17 typedef struct _plz {
18 char ort[BUF];
19 unsigned int plz;
20 } Plz_t;

21 void newPLZ(void) {
22 Plz_t data;
23 printf("Ort : ");
24 if( fgets(data.ort, BUF, stdin) == NULL ) {
25 fprintf(stderr, "Fehler bei der Eingabe\n");
26 return;
27 }
28 killNL(data.ort);
29 printf("Postleitzahl : ");
30 if( scanf("%u", &data.plz) != 1 ) {
31 fprintf(stderr, "Fehler bei der Eingabe\n");
32 return;
33 }
34 dump_buffer(stdin);
35 FILE *fp = fopen(DATAFILE, "a+b");
36 if( fp == NULL ) {
37 printf("Fehler beim Oeffnen: %s\n", DATAFILE);
38 exit(EXIT_FAILURE);
39 }
40 if(fwrite(&data, sizeof(data), 1, fp) != 1) {
41 fprintf(stderr,"Fehler beim Schreiben in %s\n",
DATAFILE);
42 fclose(fp);
43 return;
44 }
45 fclose(fp);
46 }

47 void printPLZ(void) {
48 Plz_t data;
49 FILE *fp = fopen(DATAFILE, "r+b");
50 if( fp == NULL ) {
51 fprintf(stderr, "Fehler beim Oeffnen: %s\n", DATAFILE);
52 return;
53 }
54 while(fread(&data, sizeof(data), 1, fp) == 1 ) {
55 printf("\nOrt : %s\n", data.ort);
56 printf("Postleitzahl : %u\n\n", data.plz);
57 }
58 fclose(fp);
59 }

60 int main(void) {
61 int input = 0;
62 do {
63 printf("-1- Neuer Datensatz\n");
64 printf("-2- Datensaetze ausgeben\n");
65 printf("-3- Programm beenden\n\n");
66 printf("Ihre Auswahl : ");
67 if( scanf("%d", &input) != 1 ) {
68 fprintf(stderr,"Fehler bei der Eingabe\n");
69 input = 0;
70 }
71 dump_buffer(stdin);
72 switch( input ) {
73 case 1 : newPLZ( ); break;
74 case 2 : printPLZ( ); break;
75 }
76 }while(input!=3);
77 return EXIT_SUCCESS;
78 }

Listing 14.4    fread_fwrite.c implementiert eine einfache Postleitzahlenverwaltung und speichert die einzelnen Datensätze als Elemente mit fwrite().

Zur Vereinfachung wurde in den Zeilen (17) bis (20) ein Strukturtyp deklariert, der Postleitzahlen mit deren Ort und Nummer speichert. Das Programm selbst besteht aus zwei Funktionen, zum einen aus einer Schreibfunktion newPLZ(), die in den Zeilen (21) bis (46) definiert wurde. In dieser Funktion werden jeweils die Daten für die Struktur in einer Strukturvariablen Plz_t eingelesen (Zeilen (23) bis (34)). Anschließend wird eine Datei in Zeile (35) im Modus "a+b" geöffnet, in der die Daten der Strukturvariablen binär geschrieben und immer an das Ende angehängt werden sollen. Existiert diese Datei noch nicht, wird sie angelegt. In Zeile (40) wird die Strukturvariable als Ganzes in den Stream geschrieben. Am Ende wird der Stream in Zeile (45) wieder ordnungsgemäß geschlossen.

Die zweite Funktion printPLZ() liest diese Daten der Datei wieder aus. Zunächst wird die Datei in Zeile (49) zum binären Lesen geöffnet und in den Zeilen (54) bis (57) Datensatz für Datensatz gelesen. Solange fread() in Zeile (54) 1 für einen erfolgreich gelesenen Datensatz zurückgibt, wird einen kompletter Datensatz Plz_t gefunden und ausgegeben. Am Ende wird der Stream in Zeile (58) mit fclose() wieder geschlossen.