Programmieren mit C


Inhalt


Willkommen

Installation

Das Terminal

Exkurs: Scanf und Sonderzeichen

Exkurs: continue & break

Statische Arrays

Exkurs: sizeof

Dynamische Arrays

Exkurs: Speicherbereiche

Funktionen

Exkurs: Funktionen & Arrays

Kommandozeilenparameter

Structs

Dateien

Wie kann ich Dateien öffnen?

Wie kann ich lesen/schreiben?

Wie weiß ich wo ich in einer Datei bin?

Anwendung: Textdatei einlesen

Hintergrund: Unicode

Stringbibliothek

Ausblick

Dark Mode

Wie kann ich Dateien öffnen?

Hier bietet der Header <stdio.h> einen eigenen Datentyp FILE* an. Aus der Syntax können wir schließen, dass es sich um eine Art Zeiger handelt, der auf eine Datei zeigt. Zunächst brauchen wir also eine Möglichkeit, den Speicherort einer Datei auf unserer Festplatte herauszufinden und zu speichern.

Dazu liefert uns derselbe Header die Funktion FILE* fopen(char* filepath, char* mode). Die Funktion möchte dazu den Pfad zu einer Datei als erstes Argument und den Modus als zweites. Den Pfad kennen wir schon aus dem Terminal. Mit dem Befehl pwd können wir den Pfad des aktuellen Ordners ausgeben lassen.

mia@surface-mia:~$ pwd
/home/mia

Der Pfad zu einer Datei test.txt in diesem Ordner wäre also /home/mia/test.txt. Die bekannten Abkürzungen, wie ~ oder .. funktionieren natürlich ebenso. Liegt unser Programm also im selben Ordner, wie die zu öffnende Datei, so reicht es den Namen der Datei einzugeben.

Das zweite Argument steuert den Modus, mit dem die Datei geöffnet wird. Wir können hier zwischen w: (über)schreiben, r: lesen und a: anhängen wählen. Mit w(rite) wird eine Datei also gelöscht und dann zum schreiben geöffnet, mit a(ppend) können wir dagegen am Ende der Datei weiterschreiben. Wollen wir nur den Inhalt lesen, so können wir r(ead) wählen.

Wollen wir also im aktuellen Ordner die Datei test.txt zum schreiben öffnen, sieht der Befehl so aus:

FILE* my_file = fopen("test.txt", "w");

Sobald wir mit dem lesen/schreiben fertig sind, müssen wir die Datei schließen. Dazu können wir den Befehl

fclose(my_file)

nutzen.

Wie kann ich lesen/schreiben?

Hier bietet uns C einige Varianten. Eine einfache Möglichkeit bieten die Funktionen fprintf() und fscanf.

int fprintf(FILE* stream, char* format_string, ...);
int fscanf(FILE* stream, char* format_string, ...);

Die beiden Funktionen sind also verallgemeinerte Varianten von printf und scanf. Anstatt nur in die Standardausgabe zu schreiben bzw. aus ihr zu lesen, können wir auch Dateien als Ein-/Ausgabe wählen.

Ein kurzes Beispiel:

FILE * my_file = fopen("test.txt", "w");

fprintf(my_file, "Hallo\n");

fclose(my_file);

Kompilieren wir dieses Programm nun und führen es aus. Geben wir die Textdatei aus, sollten wir folgendes Ergebnis erhalten:

mia@surface-mia:~$ gcc test.c
mia@surface-mia:~$ ./a.out
mia@surface-mia:~$ cat test.txt
Hallo

Wir haben unser erstes Wort in eine externe Datei geschrieben!

Versuchen wir nun dieses Wort wieder aus der Datei auszulesen. Dazu müssen wir zunächst die Datei zum lesen öffnen, also statt fopen(..., "w") fopen(..., "r") aufrufen. So können wir nun mithilfe fscanf lesen.

char* str[50];
fscanf(my_file, "%s", str);

In diesem Fall wollen wir den String “Hallo” auslesen. Wie legen also einen String an und wählen den format specifier %s. Wir könnten natürlich - analog zu scanf - genauso Zahlen aus Dateien lesen, dazu müssten wir nur den specifier zu %d oder %f ändern.

Geben wir den gelesenen String zuletzt noch aus:

printf("%s\n", str);

Das Ergebnis sollte dann so aussehen:

mia@surface-mia:~$ ./a.out
Hallo

Hier ist es wichtig zu erwähnen, dass fscanf, wie auch scanf den Zeilenumbruch nach “Hallo” nicht mitliest. Lesen wir eine längere Datei ein, sollten wir diese also manuell an unseren String anfügen.

Wie weiß ich wo ich in einer Datei bin?

An dieser Stelle könnten wir uns fragen, was beim lesen in einer Datei überhaupt passiert. Denken wir an einen Texteditor, wie wir ihn für unseren Code nutzen, so können wir einige Mindestanforderungen an Dateioperationen stellen.

Wir wollen - wissen an welcher Stelle wir in der Datei sind - wissen ob die Datei zu Ende ist - in der Datei navigieren

Hier bietet C uns drei wichtige Funktionen.

ftell(my_file)

fseek(my_file, offset, origin)

feof(my_file)

Die erste Funktion ftell gibt ist die Anzahl an Bytes, die wir vom Dateianfang entfernt sind. Für ASCII Text ist das also die Länge des Textes, mit Leerzeichen und Zeilenumbrüchen. Die zweite Funktion fseek erlaubt uns in der Datei zu navigieren. Das Argument offset steuert hier, wie viele Bytes wir weiter gehen möchten. Das Argument origin steuert, von welcher Position wir offset Bytes weitergehen möchten. Es kann drei Werte annehmen.

Der Aufruf

fseek(my_file, SEEK_SET, 10);

verschiebt unseren Dateipointer my_file also 10 Bytes nach dem Dateianfang.

Die letzte Funktion feof sagt uns, ob wir das Ende einer Datei erreicht haben. Diese Funktion ist besonders mit Schleifen nützlich:

while(!feof(my_file)) {
	... lesen ...
}

So können wir eine längere Datei mit einer Schleife auslesen. Nehmen wir an, wir wollen die Datei test.txt mit folgendem Inhalt lesen und ausgeben.

Hallo
Hallo in der zweiten Zeile

Wir können einen String anlegen und so jede Zeile einzeln einlesen und ausgeben.

char BUFF[500];

while(!feof(my_file)) {
	fscanf(my_file, "%s", BUFF);
	printf("%s\n", BUFF);
}

Dann erhalten wir folgenden Output.

mia@surface-mia:~$ ./a.out
Hallo
Hallo
in
der
zweiten
Zeile
Zeile

An dieser Stelle fällt uns vielleicht die Standardeinstellung von scanf wieder ein: Lies bis zu einem Leerzeichen und höre dann auf. Um dieses Verhalten zu unterbinden ändern wir den format string wie gewohnt zu "%[^\n]s" ab.

Führen wir unser Programm so aus, bekommen wir einen unendlichen Loop, der das erste Wort der Datei “Hallo” immer wieder ausgiebt. Das Problem hier: fscanf liest das erste Wort, stoppt bei \n, liest dieses Zeichen aber nicht ein. In der nächsten Iteration wiederholt sich das ganze wieder.

Einerseits könnten wir dieses Zeichen durch einen Aufruf von fscanf(my_file, "%c", ...) einlesen. Eine bessere Methode bietet die Funktion fgets(char* str, int count, FILE* stream). Unser Programm ändert sich damit zu.

char BUFF[500];

while(!feof(my_file)) {
	fgets(BUFF, 500, my_file);
	printf("%s", BUFF);
}

Die Funktion liest dann eine Zeile inklusive Zeilenumbruch in unseren String BUFF ein. Ist eine Zeile länger als 500 Zeichen, wird stattdessen nach 500 Zeichen abgebrochen.

Damit erhalten wir nun folgenden Output.

mia@surface-mia:~$ ./a.out
Hallo
Hallo in der zweiten Zeile
Hallo in der zweiten Zeile

Die letzte Zeile wird zwei mal ausgegeben. Der Grund hierfür ist denkbar einfach. Nach dem auslesen der zweiten Zeile ist unser Pointer am Zeilenumbruch der zweiten Zeile, also noch nicht am Ende der Datei. In der 3. Iteration versucht fgets nochmal zu lesen und schlägt dabei fehl. Hierbei wird der Pointer auf das Ende der Datei bewegt. Unser String in BUFF steht aus der vorherigen Iteration noch fest und wird einfach nochmal ausgegeben.

In der Beschreibung zu fgets sehen wir, dass die Funktion NULL zurückgibt, wenn das Auslesen fehlschlägt. Wir können das Verhalten also leicht überprüfen:

while(!feof(my_file)) {
	if ( !fgets(BUFF, 500, my_file) ) {
		printf("Dateiende!\n");
	}
	printf("%s", BUFF);
}

Wir bekommen dann diese Ausgabe.

mia@surface-mia:~$ ./a.out
Hallo
Hallo in der zweiten Zeile
Dateiende!
Hallo in der zweiten Zeile

Das Dateiende ist also tatsächlich nach dem Lesen erreicht. Wir können also nach diesem Vorgang feof nochmal überprüfen und die Schleife an dieser Stelle abbrechen.

while(!feof(my_file)) {
	fgets(BUFF, 500, my_file);
	if ( feof(my_file) ) {
		break;
	}
	printf("%s", BUFF);
}

Damit ändert sich unser Output zu:

mia@surface-mia:~$ ./a.out
Hallo
Hallo in der zweiten Zeile

Super! Jetzt hat alles geklappt.