04. File IO in C++
1 Grundsätzliches
Um Daten eines Programms permanent speichern zu können, müssen diese aus dem flüchtigen Arbeitsspeicher in eine Datei auf einem beliebigen Datenträger (Festplatte, CD-ROM, USB-Stick, etc.) transferiert werden.
Wenn per Programmanweisung (siehe später) Daten in eine Datei geschrieben werden, dann werden diese Daten nicht unbedingt sofort auf dem externen Datenspeicher gespeichert. Vielmehr werden sie zunächst in einem Dateipuffer abgelegt, der sich im Arbeitsspeicher (Hauptspeicher, RAM) befindet. Erst wenn der Dateipuffer voll ist wird der Inhalt in die Datei geschrieben. Alternativ kann dieser Vorgang auch per Programmanweisung erzwungen werden.
Die Daten in der Datei kann man sich als Aufeinanderfolge einzelner Datensätze vorstellen, ähnlich einem eindimensionalen Array. Die Datensatzgröße kann dabei von einem Byte bis beliebig vielen Bytes reichen.
Eine besondere Bedeutung kommt den Markierungen BOF (Begin Of File) und EOF (End Of File) zu. BOF markiert den Dateianfang , EOF das Dateiende. Jede Datei, egal ob Inhalte vorhanden sind oder nicht, hat die Markierungen BOF und EOF.
Der Zugriff auf eine Datei erfolgt über einen Dateizeiger (grafisch als Pfeil dargestellt). Dieser Zeiger steht beim Öffnen einer Datei an der Position, an der die nächste Dateioperation beginnt. Nach dem Öffnen einer Datei zeigt er auf die erste Position, also BOF.
2 Ablauf einer beliebigen Dateioperation
3 Dateioperationen
3.1 Datei-Stream öffnen: fopen_s()
Eine Datei muss Immer erst geöffnet werden, bevor mit Ihr gearbeitet werden kann (siehe Skizze oben).
Syntax:
errno_t fopen_s(FILE** pFile, const char *Dateiname, const char *Zugriffsmodus);
Bedeutung der Parameter:
Parameter | Beschreibung |
---|---|
FILE** pFile |
Ein Zeiger auf den Dateizeiger, der den Zeiger auf die geöffnete Datei erhält. Das genau Ist der Dateizeiger, von dem oben schon die Rede war, und der von allen weiteren Dateifunktionen benötigt wird! |
const char *dateiname |
Angabe des Dateinamens der zu verarbeitenden Datei. Der Name muss in doppelten Anführungszeichen stehen. Beispiele: “C:\Projekte\Prozessdaten.dat” oder “..\Prozessdaten.dat” |
const char *Zugriffsmodus |
Mit dem Zugriffsmodus wird angegeben, ob die Datei zum Lesen, Schreiben oder Anhängen geöffnet werden soll. |
Bedeutung der Zugriffsmodi:
Bewirkt | r | w | a |
---|---|---|---|
Datei ist lesbar | x | ||
Datei ist beschreibbar | x | x | |
Vorhandener Dateiinhalt wird gelöscht und Inhalte an den Anfang der Datei geschrieben | x | ||
Vorhandener Dateiinhalt bleibt erhalten und neue Inhalte werden ans Ende der Datei geschrieben | x |
r = read, w = write, a = append
Bedeutung des Rückgabewertes:
Der Rückgabewert der Funktion ist NULL
, wenn es erfolgreich war oder ein Fehlercode, wenn ein Fehler auftritt.
Beispiel:
FILE* fpDatei = nullptr;
fopen_s(&fpDatei, "Kunde.dat", "r"); // Öffnen im Lesemodus
3.2 Datei-Stream schließen: fclose()
Die Funktion fclose()
schließt eine Datei, die zuvor mit fopen_s()
geöffnet wurde. Dies ist notwendig, da die Anzahl
der geöffneten Dateien begrenzt ist. Zudem wird eine im Schreibmodus geöffnete Datei erst dann beschrieben, wenn der
Puffer voll ist. Ist der Puffer nur teilweise voll und das Programm beendet sich mit einem Fehler, dann sind die Daten
im Puffer verloren.
Syntax
int fclose(FILE *stream);
Bedeutung der Parameter:
Parameter | Beschreibung |
---|---|
FILE* stream |
Der File-Stream der geschlossen werden soll. |
Bedeutung des Rückgabewertes:
Die Funktion liefert bei fehlerfreier Funktion den Wert O
zurück.
Beispiel: fclose(fpDatei);
3.3 Schreiben einer bestimmten Anzahl von Bytes in eine Datei: fwrite()
Das Schreiben In Dateien kann auf unterschiedliche Arten erfolgen. In diesem Abschnitt wird das blockweise Schreiben beschrieben. Dabei werden so viele Byte eingelesen bzw. geschrieben, wie beim Aufruf der entsprechenden Routine angegeben wurden.
Syntax
size t fwrite(const void *ptr, size_t size, size_t n, FILE *stream);
Der Rückgabewert der Funktion ist die Anzahl der geschrieben Speicherobjekte.
Bedeutung der Parameter
Parameter | Beschreibung |
---|---|
const void *ptr |
Hier erwartet die Funktion die Anfangsadresse des zu schreibenden Speicherbereichs. Das kann die Adresse einer gewöhnlichen Variable (z. B. vom Typ int) sein, genauso gut aber auch die Anfangsadresse eines Arrays eines beliebigen Datentyps oder gar die Adresse einer Strukturvariablen von einem selbst definierten Datentyp. |
size_t size |
Das ist der Parameter für die Größe eines zu schreibenden Speicherblocks. Bei einer int-Variablen müsste man hier 4 angeben, bei double 8 und bei einer Variablen eines selbst definierten Datentyps eben die Größe der entsprechenden Datenstruktur. Da die Größe von der Rechnerarchitektur abhängig ist, ist es möglich mit der Funktion sizeof(Datentyp) die erforderliche Größe vom Compiler bestimmen zu lassen. |
size_t n |
Die Anzahl von zu schreibenden Speicherblöcken. Handelt es sich um eine einzelne Variable, muss hier immer 1 stehen. Im Falle eines Arrays muss die Zahl der zu speichernden Arrayelemente angegeben werden. |
FILE *stream |
Das ist der Dateizeiger, der nach dem Aufruf fopen_s() auf einen Dateipuffer im Arbeitsspeicher (und damit letztlich auf die Datei an sich) zeigt. |
Bedeutung des Rückgabewertes:
Liefert die ANzahl der Geschriebenen Speicherobjekte (nicht Bytes!) zurück.
Beispiel
fwrite(&iZahl, 4, 1, fpDatei); // oder
fwrite(&iZahlenreihe[0], sizeof(int), 5, fpDatei); // oder
fwrite(iZahlenreihe, sizeof(iZahlenreihe[0]), 5, fpDatei);
3.4 Lesen einer bestimmten Anzahl von Bytes aus einer Datei: fread()
Syntax
size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
Bedeutung der Parameter:
Die Bedeutung der Parameter und des Rückgabewertes ist identisch zu der der Funktion fwrite()
. Lediglich der erste
Parameter unterscheidet sich. Hier geben Sie den Beginn des Speicherbereiches an, in den die Datei geschrieben werden
soll. Wichtig dabei ist: Die Größe des Zieldatenbereichs muss mindestens so groß sein, wie die Größe der zu lesenden
Daten!
Beispiel
fread(&iZahl, 4, 1, fpDatei); // oder
fread(&iZahlenreihe[0], sizeof(int), 5, fpDatei); // oder
fread(iZahlenreihe, sizeof(iZahlenreihe[0]), 5, fpDatei);
3.5 Test auf Dateiende: feof() (Array mit unbekannter Größe)
Die bisherige Lösung funktioniert nur, solange Sie die Anzahl an zu lesenden Datensätzen kennen. Ist diese unbekannt, dann können Sie beim Elnlesen nicht die Anzahl der Blöcke vorgeben. Sie können aber nach jedem Lesevorgang prüfen, ob das Dateiende (EOF) bereits erreicht wurde.
Syntax
int feof(FILE *stream);
Bedeutung des Parameters
Parameter | Beschreibung |
---|---|
FILE *stream |
Das ist der Dateizeiger, der nach dem Aufruf von fopen_s() auf einen Dateipuffer im Arbeitsspeicher (und damit letztlich auf die Datei an sich) zeigt. |
Bedeutung des Rückgabewertes:
Die Funktion liefert O ( = false) zurück, wenn das Dateiende noch nicht erreicht wurde. Bei
erreichtem Dateiende liefert sie einen Zahlenwert<> O zurück(= true).
Wichtig zu wissen ist hier: Das Flag, dessen Wert die Funktion feof()
zurückliefert, wird immer nur durch eine
Dateifunktion verändert, die den Dateizeiger positioniert.
Beispiel
FILE *fpDatei = nullptr; // Dateizeiger
T_Artikel tArtikel[100]; // Platz für 100 Artikel
T_Artikel tArtikelBuffer; // Platz für einen temporären Artikel
int iAktlndex = 0;
// Öffnen der Datei im Dateimodus „read"
fopen_s(&fpDatei, "Artikel.sbd", "r");
// Prüfen, ob Öffnen erfolgreich war
if (fpDatei == nullptr)
{
cout << "Fehler: Die Datei konnte nicht geöffnet werden!";
}
else
{
// NOTE: Dieser Block enthält die EOF Prüfung
do
{
//Zunächst wird in einen Buffer gelesen
fread(&tArtikelBuffer ,sizeof(T_Artikel), 1, fpDatei);
if(!feof(fpDatei)) // wenn EOF noch nicht erreicht
{
tArtikel[iAktlndex] = tArtikelBuffer; //Puffer ins Array
iAktindex++; // Erhöhe Zähler um 1
}
} while(!feof(fpDatei)); // Solange Dateiende nicht erreicht
//Datei schließen
fclose(fpDatei);
}
// Ausgabe der gelesenen Daten auf dem Bildschirm
for(int i=0; i < iAktlndex; i++){
cout <<"Datensatz"<< i+l << ": "<< adMesswerte[i];
}
4 Beispiel für Test auf Dateiende
Nehmen wir an, dass die Datei, aus der wir lesen wollen, leer ist. In diesem Fall steht der Dateizeiger nach dem Öffnen
zunächst auf BOF
. Mit einem Aufruf von feof()
werden wir also false
zurück geliefert bekommen. Daraus zu
schließen, dass eine anschließende Leseoperation einen gültigen Datensatz liefert, wäre natürlich falsch, denn die Datei
ist ja erst leer gelesen wird. Deshalb muss [z. die B. mit Reihenfolge fread()
] und dann Einlesen mithilfe von
feof()
geprüft wird, ob das Dateiende erreicht ist. Falls ja, muss der mit der letzten Leseoperation “eingelesene”
Datensatz verworfen werden.
Betrachten wir der Deutlichkeit halber eine Datei mit Datensätzen, kurz DS:
- Nach dem Öffnen steht der Dateizeiger wieder auf BOF. Nun wird mittels
fread()
-Aufruf der erste Datensatz gelesen. - Nach dem ersten Aufruf von
fread()
steht der Dateizeiger auf dem ersten Datensatz und die Prüfungfeof()
istfalse
. - Entsprechend nach dem zweiten Aufruf von
fread()
auf dem zweiten Datensatz und die Prüfungfeof()
ist erneutfalse
. - Nun erfolgt ein dritter Aufruf von
fread()
. Das führt nun dazu, dass der Dateizeiger auf die Ende-MarkierungEOF
gesetzt wird. Somit liefert die Funktionfeof()
erst nach dem dritten Aufruf vonfread()
tatsächlichtrue
!
Hätten wir die Prüfung auf Dateiende jeweils vor fread()
durchgeführt, damit also auch vor dem dritten Aufruf von
fread()
, hätte feof()
false
ergeben und wir wären fälschlicherweise davon ausgegangen, dass die Datei noch
(mindestens) einen Datensatz enthält!
5 Vollständiges Datei-Beispiel für Schreiben fwrite()
FILE *fpDatei = nullptr;
double adMesswerte[25];
int iAnzahl;
// Öffnen der Datei im Dateimodus "write"
fopen_s(&fpDatei, "Messwerte.sbd", "w");
// Prüfen, ob Öffnen erfolgreich war
if (fpDatei == nullptr)
cout « "Fehler: Die Datei konnte nicht geöffnet werden!";
else {
iAnzahl = fwrite(adMesswerte, sizeof(double), 25, fpDatei);
if (iAnzahll= 25){
cout << "Nicht alle Werte konnten gespeichert werden!";
}
}
fclose(fpDatei);