C++ Kurs

Dateizugriffe

Die Themen:

Einleitung
Text- und Binärdatei
Übersicht Dateistreams
Ausgabestream-Objekt
Schreiben in Datei
Eingabestream-Objekt
Lesen aus Datei
Gleichzeitiges Schreiben und Lesen
Sonstige Dateioperationen
Beispiel und Übung

Einleitung

Genauso wie für die Standard Ein- und Ausgabe die Streams cin bzw. cout verwendet werden, so werden auch für Dateizugriffe unter C++ die Streams ifstream bzw. ofstream eingesetzt. Im Folgenden werden Sie deshalb viele Gemeinsamkeiten zwischen der Standard Ein-/Ausgabe und der Datei Ein-/Ausgabe finden.

Bevor auf die Behandlung von Dateien eingegangen wird, müssen noch zwei Begriffe geklärt werden: den der Textdatei und den der Binärdatei. Die Unterscheidung ist deshalb notwendig, da die beiden Dateitypen unterschiedlich gehandhabt werden.

Zuvor aber noch zwei Hinweise:

Daten, die in Dateien abgelegt werden, werden oft auch als persistente Daten bezeichnet, da sie zwischen zwei Programmläufen ihre Werte beibehalten.

Sollten Sie für die Definition von Dateinamen auch deren Pfad mit angeben müssen, so verwenden Sie stets einen Slash / anstelle eines Backslash \ als Trennzeichen für die Verzeichnisnamen! Der Slash als Verzeichnistrenner funktioniert sogar auch unter WINDOWS.

Text- und Binärdatei

Textdatei

Textdateien sind Dateien die nur ASCII-Zeichen enthalten. Diese Dateien können mit jedem Editor bearbeitet werden, der 'reine' Textdateien erzeugen kann (z.B. der DOS-Editor EDIT oder auch NOTEPAD). Numerische Daten werden in diesen Dateien ebenfalls in ASCII-Form abgelegt. Beachten Sie bitte, wie die Werte  in der Datei abgelegt sind.

Aktion:
Schreiben des Strings "Emil Maier"
Schreiben des short-Wertes 100
Schreiben des long-Wertes 0x11223344

Dateiinhalt:
Hex-Darstellung ASCII-Darstellung
45 6D 69 6C 20 4D 61 69 65 72 Emil Maier
0x31 0x30 0x30 100
0x32 0x38 0x37 0x34 0x35 0x34 0x30 0x32 0x30   287454020

Diese Dateiform ist die einzige Möglichkeit Daten zwischen unterschiedlichen Plattformen auszutauschen, da das Dateiformat der im Anschluss erwähnten Binärdatei ist nicht standardisiert ist.

Binärdatei

Im Gegensatz dazu sind Binärdateien Dateien, in denen Daten in binärer Form, also in der Form, wie sie im Speicher des Rechners liegen, abgelegt sind. Diese Daten können in der Regel nicht mit einem Editor bearbeitet werden, da Editoren nur Dateien mit ASCII-Zeichen sinnvoll darstellen können. Der Aufbau einer Binärdatei ist, wie bereits erwähnt, aber nicht standardisiert, d.h. unterschiedliche Systeme können Daten unterschiedlich ablegen.

Die Ausgabe von Text in eine Binärdatei unterscheidet sich nicht von der Ausgabe in eine Textdatei. Der Unterschied wird erst bei der Ausgabe von Daten deutlich.

Aktion:
Schreiben des Strings "Emil Maier"
Schreiben des short-Wertes 100
Schreiben des long-Wertes 0x11223344

Dateiinhalt:
Hex-Darstellung ASCII-Darstellung
45 6D 69 6C 20 4D 61 69 65 72 Emil Maier
0x64 0x00 d.
0x44 0x33 0x22 0x11 D3".

Wenn Sie viele numerische Daten verarbeiten müssen und nicht auf den Datenaustausch mit anderen System angewiesen sind, so legen Sie die Daten am besten innerhalb einer Binärdatei ab. Dies kann zu einer erheblich kleineren Datei führen. Nehmen wir einmal an, Sie müssen einen 4-stelligen short-Wert 1000-mal in einer Datei ablegen. In einer Textdatei benötigen Sie dafür 1000*5 Bytes, gleich 5000 Bytes. Das zusätzliche 5. Byte wird als Trennzeichen zwischen den einzelnen Werten benötigt, damit die Daten später auch wieder eingelesen werden können. Die gleiche Datenmenge innerhalb einer Binärdatei belegt dagegen nur 1000*2 Bytes, gleich 2000 Bytes, da ein short-Wert 2 Bytes benötigt und in einer Binärdatei keine Trennzeichen notwendig sind (wie Sie nachher gleich sehen werden).

Übersicht Dateistreams

Um Dateien mithilfe von Streams zu bearbeiten, müssen folgende Schritte durchgeführt werden:

Außerdem müssen Sie, wenn Sie mit Dateistreams arbeiten, Sie die Datei fstream mit #include <fstream> einbinden.

Ausgabestream-Objekt

Ausgabestream-Objekt definieren

Ein Ausgabestream-Objekt wird mit folgender Anweisung definiert:

std::ofstream strName;

ofstream ist die Klasse, über die Dateiausgabe erfolgt und strName ist der Name des Stream-Objekts. Der Name des Stream-Objekts ist frei wählbar. Klassen und Objekte werden zwar erst später behandelt, aber stellen Sie sich unter der obigen Anweisung im Prinzip die Definition einer Variablen strName mit dem Datentyp ofstream vor.

Der Stream ofstream ist ebenfalls im Namensraum std definiert. Sie müssen also entweder vor jeder Verwendung dieses Streams den Namensraum std:: angegeben oder aber durch eine using-Anweisung den Namensraum des Streams explizit einbinden. In den nachfolgenden Ausführungen wird, der Übersichtlichkeit wegen, davon ausgegangen, dass Sie den Namensraum std mittels using namespace std; eingebunden haben.

Ausgabestream-Objekt mit Datei verbinden

Das so definiert Stream-Objekt strName besitzt aber noch keine Zuordnung zu irgend einer Datei. Diese Zuordnung erfolgt erst durch den Aufruf der Memberfunktion

void ofstream::open(const char *pFName, ios::openmode mode=ios::out);

Der erste Parameter pFName ist ein Zeiger auf den Namen der Datei, mit der der Stream verbunden werden soll.

Und noch mal, weil's so wichtig ist: Wenn Sie innerhalb des Dateinamens auch eine Pfadangabe stehen haben und bei der Pfadangabe einen Backslash verwenden,  denken Sie immer daran, dass Sie die Backslash-Zeichen verdoppeln müssen, da das Backslash-Zeichen innerhalb von Strings eine Escape-Sequenz einleitet!

Der zweite Parameter mode gibt an, in welchem Modus die Datei geöffnet werden soll. Er ist ein enumerated Datentyp (wird später noch erklärt) und kann eine sinnvolle Kombination aus folgenden Werten sein:

mode Bedeutung
ios::out Öffnen einer Datei zum Schreiben (default).
ios::trunc Öffnet eine Datei, wobei der ursprüngliche Inhalt wird verworfen (default).
ios::ate Öffnen einer Datei und positionieren des Schreibzeigers auf das Dateiende. Der Schreibzeiger kann bei Bedarf neu positioniert werden (seekp(...)) und die Daten werden dann an der neuen Position eingefügt.
ios::app Öffnen einer Datei und positionieren des Schreibzeigers auf das Dateiende. Die neuen Daten werden immer ans Ende der Datei eingefügt, unabhängig davon ob der Schreibzeiger inzwischen neu positioniert wurde.
ios::binary Öffnet Datei im Binärmodus. Ohne diese Angabe wird die Datei als Textdatei interpretiert.

Sie können beim Aufruf der Memberfunktion open(...) auch den zweiten Parameter ganz weglassen; in diesem Fall wird die Datei für die Ausgabe im Textmodus geöffnet und der ursprüngliche Inhalt wird verworfen.

Wenn Sie eine Datei zum Schreiben öffnen und den Mode dabei explizit vorgeben, so müssen Sie auch immer dem Mode ios::out mit angeben!

Es gibt auch noch eine zweite Möglichkeit, einen Ausgabestream zu definieren und dabei gleichzeitig mit einer Datei zu verbinden.

ofstream myFile(const char* pFName, ios::openmode mode = ios::out);

Hier wird beim Definieren des Stream-Objekts die zu öffnende Datei und der Mode gleich mit angegeben.

Sehen wir uns beide Möglichkeiten nun anhand eines Beispiels einmal an.


// 1. Möglichkeit
#include <fstream>
using namespace std;
...
// main() Funktion
int main ()
{
   // Stream-Objekt definieren
   ofstream myFile;
   // und anschliessend mit Datei verbinden
   myFile.open ("d:\\tmp\\test.dat");
   ...
}

// 2. Möglicheit
#include <fstream>
using namespace std;
...
// main() Funktion
int main ()
{
   // Stream-Objekt definieren und gleich mit Datei verbinden
   ofstream myFile ("d:/tmp/test.dat", ios::out|ios::binary);
   ...
}

Im ersten Fall wird zunächst das Stream-Objekt definiert und dann mit open(...) mit einer Datei verbunden. Beachten Sie, dass vor dem Namen der Memberfunktion der Name des Stream-Objekts und ein Punkt stehen. Wie Sie später noch sehen werden, ist dies die allgemeine Syntax zum Aufruf einer Memberfunktion für ein bestimmtes Objekt.

Im zweiten Fall wird das Stream-Objekt bei seiner Definition gleich mit einer Datei verbunden. Im Beispiel wird die Datei jetzt im Binärmodus geöffnet. Beachten Sie bitte, dass Sie hier auch den Modus ios::out explizit angeben müssen.

Und nochmals: Beachten Sie die Verdopplung des Backslash-Zeichens im Dateinamen im ersten Beispiel! Die Angabe des Pfads im zweiten Beispiel mit / ist weniger fehleranfällig und zudem noch systemunabhängig!

Da es nicht ausgeschlossen ist, dass beim Öffnen einer Datei auch einmal etwas schief geht, sollten Sie nach der Verbindung mit der Datei auch eventuell aufgetretene Fehler abfangen. Wie Sie dies bei Streams durchführen ist nachfolgend dargestellt.


#include <fstream>
using namespace std;
...
// main() Funktion
int main ()
{
   // Stream-Objekt definieren und anschliessend mit Datei verbinden
   ofstream myFile;
   myFile.open ("d:/tmp/test.dat");
   // Abfrage auf Fehler
   if (!myFile)
   {
      ... // Hier Fehlerbehandlung durchführen
   }
}

Diese if-Abfrage (die im Detail später noch behandelt wird) ist sowohl bei Text- wie auch bei Binärdateien einzusetzen, aber erst nachdem eine Datei mit einem Stream-Objekt verbunden wurde. Beachten Sie das kleine Ausrufzeichen vor dem Objekt-Namen, dies ist der NOT-Operator. Sie können sich die Funktionsweise des NOT-Operators auch nochmals ansehen.

Datei-Verbindung mit Ausgabestream-Objekt aufheben

Nach dem die (gleich noch beschriebene) Bearbeitung der Datei abgeschlossen ist, müssen Sie die Verbindung des Streams zur Datei wieder aufheben. Hierbei gibt es ebenfalls zwei verschiedene Verfahren.

Im ersten Fall heben Sie die Verbindung zur Datei einfach durch den Aufruf der Memberfunktion

void ofstream::close();

wieder auf. Das Stream-Objekt besteht danach weiter und Sie können mittels open(...) eine weitere Datei mit dem gleichen Stream-Objekt verbinden.


#include <fstream>
using namespace std;
...
// main() Funktion
int main ()
{
   // Stream-Objekt definieren und mit Datei verbinden
   ofstream myFile;
   myFile.open ("d:/tmp/test.dat");
   ...
   // Dateiverbindung aufheben
   myFile.close();
   ...
   // Stream erneut mit einer Datei verbinden
   myFile.open("c:/anydir/anotherfile.dat);
}

Die zweite Möglichkeit besteht darin, das Stream-Objekt zu löschen. Wenn Sie ein Stream-Objekt (wie im Beispiel unten) innerhalb eines Blocks definieren (Blöcke werden später noch genauer behandelt), so kann zunächst die Datei bei der Definition des Stream-Objekts mit diesem verbunden werden. Wird am Blockende das Stream-Objekt dann ungültig (gelöscht), so wird automatisch auch die Verbindung zur Datei aufgehoben. Mehr zur Gültigkeit von Variablen und Objekt später in der Lektion Gültigkeitsbereiche im Kapitel Daten II & Funktionen.


#include <fstream>
using namespace std;
...
// main() Funktion
int main ()
{
   ...
   // Blockbeginn mit Klammerung
   {
      ofstream myFile("d:/tmp/test.dat");
      ...
   }
   // Blockende bei schliessender Klammer
   // Hier wird das Stream-Objekt gelöscht!

   ...
}

Schreiben in Datei

Textdatei

Das Schreiben in eine Textdatei erfolgt prinzipiell gleich wie die Ausgabe auf die Standardausgabe. Anstelle des Stream-Objekts cout wird der Name des Dateistream-Objekts angegeben. Alle zu schreibenden Daten werden, genauso wie bei cout, durch den Operator << in den Stream übertragen. Beachten Sie aber beim Schreiben von Daten, dass Sie diese durch ein Trennzeichen (Leerzeichen, Zeilenvorschub o.Ä.) voneinander trennen müssen, da die Daten sonst später nicht mehr eingelesen werden können.

Dadurch, dass sowohl cout wie auch ofstream die gleiche Basisklasse basic_ostream besitzen, stehen für die Dateiausgabe auch die gleichen Manipulatoren, wie z.B. hex, setw(...) oder setprecision(...), zur Verfügung.


#include <fstream>
using namespace std;

// Variablen definieren
int var1 = 10, var2 = 20;

// main() Funktion
int main ()
{
   // Stream-Objekt definieren und mit Datei verbinden
   ofstream myFile;
   myFile.open ("d:/tmp/test.dat");
   // Wenn Stream ohne Fehler mit Datei verbunden
   if (myFile)
   {
      // Daten schreiben
      myFile << var1 << ' ';
      myFile << setw(5) << var2 << endl;

      // Datei schliessen
      myFile.close();
   }
}

Binärdatei

Sollen numerische Daten binär in einer Datei abgelegt werden, so müssen Sie beim Verbinden der Datei mit den Stream-Objekt die Modes ios::out | ios::binary angeben.

Beachten Sie, dass Sie hier sowohl ios::out wie auch ios::binary angeben müssen. Ferner sollten Sie immer daran denken, dass Binärdateien nicht für den Datenaustausch zwischen unterschiedlichen Plattformen benutzt werden dürfen. Dies gilt sogar auch für Programme, die auf dem gleichen Betriebssystem laufen, aber mit unterschiedlichen Compilern erstellt wurden. So kann z.B. ein Programm das mit dem MICROSOFT C++ Compiler erzeugt wurde ein anderes Format für Binärdatei verwenden als das gleiche Programm, das mit dem MinGW C++ Compiler erstellt wurde.

Das Schreiben eines einzelnen Bytes erfolgt mit der Memberfunktion

ostream& put(char data);

data ist das zu schreibende Byte.

Die Angabe des Returnwertes ostream& bei den obigen Memberfunktionen ist kein Schreibfehler. Nur so viel vorne weg: die Memberfunktionen put(...) und die gleich im Anschluss beschriebene write(...) liefern eine Referenz auf ein Objekt der Klasse ostream (oder einer davon abgeleiteten Klasse) zurück. Referenzen werden später noch gesondert behandelt.

Sollen Daten die aus mehreren Bytes bestehen (short, long usw.) in einer binären Datei abgelegt werden, so wird hierfür die Memberfunktion

ostream& write(const char *pBuffer, streamsize bytes);

eingesetzt. pBuffer ist ein const char-Zeiger auf den Beginn des Datenblocks, der in die Datei geschrieben werden soll und bytes gibt die Anzahl der zu schreibenden Bytes an. Da die Memberfunktion einen const char-Zeiger erwartet, müssen Sie in der Regel eine Typkonvertierung vornehmen, wenn Sie numerische Daten abspeichern (siehe Beispiel). Vergessen Sie diese Typkonvertierung, so erhalten Sie vom Compiler eine Fehlermeldung.


#include <fstream>
using namespace std;

// Variablen definieren
int var1 = 10, var2 = 20;

// main() Funktion
int main ()
{
   // Stream-Objekt definieren und mit Datei verbinden
   ofstream myFile;
   myFile.open ("d:/tmp/test.dat", ios::out|ios::binary);
   // Wenn Stream ohne Fehler mit Datei verbunden
   if (myFile)
   {
      // Daten schreiben
      myFile.write(reinterpret_cast<char*>(&var1), sizeof(var1));
      myFile.write(reinterpret_cast<char*>(&var2), sizeof(var2));

      // Datei schliessen
      myFile.close();
   }
}

Wenn Sie sich das Beispiel oben genauer ansehen, werden Sie feststellen, dass dort nur eine Typkonvertierung nach char* erfolgt und nicht nach const char*. Ein char* kann anstelle eines const char* übergeben werden, aber nicht umgekehrt! Das const char* bei der Signatur der Memberfunktion write(...) sagt nur aus, dass die Memberfunktion den Inhalt des übergebenen Puffers nicht verändert.

Beim Schreiben von Daten in Binärdateien können die Daten unmittelbar hintereinander geschrieben werden, d.h. es sind keine Trennzeichen wie bei einer Textdatei notwendig, um später die Daten wieder einlesen zu können.

Eingabestream-Objekt

Eingabestream-Objekt definieren

Ein Eingabestream-Objekt wird mit folgender Anweisung definiert:

ifstream strName;

ifstream ist der Stream, der für das Einlesen von Daten aus einer Datei eingesetzt wird und strName der (beliebige) Name des Stream-Objekts. Das so definierte Stream-Objekt strName besitzt aber ebenfalls noch keine Zuordnung zu irgend einer Datei.

Eingabestream-Objekt mit Datei verbinden

Nach dem das Stream-Objekt erstellt, ist kann es mit einer Datei verbunden werden. Dies erfolgt wieder durch den Aufruf der Memberfunktion

void ifstream::open(const char *pFName, ios::openmode mode=ios::in);

Der erste Parameter pFName ist ein char-Zeiger auf den Namen der zu öffnenden Datei und der zweite Parameter mode gibt an, in welchem Modus die Datei zu öffnen ist. Er kann hier eine sinnvolle Kombination aus folgenden Werten sein:

mode Bedeutung
ios::in Öffnen einer Datei zum Lesen
ios::binary Öffnet Datei im Binärmodus. Ohne diese Angabe wird die Datei als Textdatei interpretiert.

Sie können beim Aufruf der Memberfunktion auch diesen zweiten Parameter weglassen; in diesem Fall wird die Datei für die Eingabe im Textmodus geöffnet.

Und auch hier besteht die Möglichkeit, einen Eingabestream gleich bei seiner Definition mit einer Datei zu verbinden.

ifstream myFile(const char* pFName,  ios::openmode mode = ios::in);

Hier wird bei der Definition des Stream-Objekts die zu öffnende Datei und der Mode gleich mit angegeben.

Sehen wir uns beide Möglichkeiten anhand eines Beispiels einmal an.


// 1. Möglichkeit
#include <fstream>
using namespace std;
...
// main() Funktion
int main ()
{
   // Stream-Objekt definieren
   ifstream myFile;
   // und anschliessend mit Datei verbinden
   myFile.open ("d:/tmp/test.dat");
   // Fehlerabfrage!
   if (!myFile)
   {
      ...     // Fehlerbehandlung
   }
   ...
}

// 2. Möglichkeit
#include <fstream>
using namespace std;
...
// main() Funktion
int main ()
{
   // Stream-Objekt definieren und gleichzeitig mit Datei verbinden
   ifstream myFile("d:/tmp/test.dat", ios::in|ios::binary);
   // Fehlerabfrage!
   if (!myFile)
   {
      ...     // Fehlerbehandlung
   }
   ...
}

Im ersten Fall wird zunächst das Stream-Objekt definiert und dann mit open(...) mit einer Datei verbunden.

Im zweiten Fall wird das Stream-Objekt bei seiner Definition gleich mit einer Datei verbunden. Im Beispiel wird die Datei im Binärmodus geöffnet. Beachten Sie bitte, dass Sie hier auch den Modus ios::in explizit angeben müssen.

Und auch das Öffnen einer Datei zum Lesen kann natürlich schief gehen. Prüfen Sie deshalb immer ab, ob die Datei erfolgreich mit dem Stream verbunden werden konnte. Stellen Sie sich einmal vor, Sie wollen von einer Datei einlesen die es gar nicht gibt!

Datei-Verbindung mit Eingabestream-Objekt aufheben

Für das Aufheben der Dateiverbindung und das Löschen des Stream-Objekts gilt das Gleiche wie beim Ausgabestream.

Lesen aus Datei

Textdatei

Das Lesen aus einer Textdatei erfolgt auch hier prinzipiell gleich wie das Lesen von Daten von der Standard-Eingabe mit cin. Anstelle des Streamobjekts cin steht hier lediglich der Name des Eingabestream-Objekts. Alle zu lesenden Daten werden ebenfalls durch den Operator >> aus dem Stream ausgelesen.


#include <fstream>
using namespace std;

// Variablen definieren
int var1, var2;

// main() Funktion
int main ()
{
   // Stream-Objekt definieren und mit Datei verbinden
   ifstream myFile;
   myFile.open ("d:/tmp/test.dat");
   // Hier Fehler abfangen!

   // Daten lesen
   myFile >> var1 >> var2;
   // Datei schliessen
   myFile.close();
}

Da sowohl cin wie auch ifstream die gleiche Basisklasse basic_istream besitzen, stehen für die Dateieingabe auch die gleichen Manipulatoren, wie z.B. hex oder oct, zur Verfügung.

Binärdatei

Sollen numerische Daten aus einer Binärdatei ausgelesen werden, so müssen Sie beim Verbinden der Datei mit den Stream-Objekt den Mode ios::in | ios::binary angeben.

Das Lesen eines einzelnen Bytes erfolgt mit der Memberfunktion

istream& get(char& data);

Das gelesene Byte ist nach der Ausführung der Memberfunktion in der Variable data abgelegt. Beachten Sie, dass nach dem Datentyp char der Operator & steht. Dies ist hier nicht der Adressoperator, sondern der später bei den Funktionen noch aufgeführte Referenzoperator. In ihrem Programm übergeben Sie die einzulesende Variable 'ganz normal' an die Memberfunktion, also z.B. myFile.get(charVar);.

Sollen Daten die aus mehreren Bytes bestehen (short, long usw.) aus einer Binärdatei ausgelesen werden, so wird hierfür die Memberfunktion

istream& read(char *pBuffer, streamsize bytes);

verwendet. pBuffer ist ein char-Zeiger auf den Beginn des Datenblocks, in dem die aus der Datei ausgelesenen Daten abgelegt werden und bytes gibt die Anzahl der zu lesenden Bytes an. Da die Memberfunktion wiederum einen char-Zeiger erwartet, müssen Sie in der Regel eine Typkonvertierung verwenden wenn Sie numerische Daten einlesen (siehe Beispiel).


#include <fstream>
using namespace std;

// Variablen definieren
int var1, var2;

// main() Funktion
int main ()
{
   // Stream-Objekt definieren und mit Datei verbinden
   ifstream myFile;
   myFile.open ("d:/tmp/test.dat", ios::in|ios::binary);
   // Hier Fehler abfangen!

   // Daten lesen
   myFile.read(reinterpret_cast<char*>(&var1), sizeof(var1));
   myFile.read(reinterpret_cast<char*>(&var2), sizeof(var2));

   // Datei schliessen
   myFile.close();
}

Gleichzeitiges Schreiben und Lesen

Und auch dies ist möglich: Sie können einen Dateistreams zum gleichzeitigen Lesen und Schreiben öffnen. Hierzu benötigen Sie ein Stream-Objekt vom Typ fstream das Sie wie folgt definieren:

fstream strName;

fstream ist der Stream, der für das Ausgabe und das Einlesen von Daten aus einer Datei eingesetzt wird und strName der (beliebige) Name des Stream-Objekts. Das so definierte Stream-Objekt strName besitzt aber ebenfalls noch keine Zuordnung zu irgend einer Datei. Um das Stream-Objekt mit einer Datei zu verbinden, verwenden Sie entweder die bereits bekannte open(...) Memberfunktion oder aber Sie geben bei der Definition des Stream-Objekts gleich den Dateinamen mit an (genauso wie beim ifstream bzw. ofstream). Lediglich für den Parameter mode sind hier andere Kombinationen zugelassen:

mode Bedeutung
ios:in Öffnen einer Datei zum Schreiben
ios:out Öffnen einer Datei zum Lesen
ios::binary Öffnet Datei im Binärmodus. Ohne diese Angabe wird die Datei als Textdatei interpretiert.
ios:app Öffnen einer Datei und positionieren des Schreibzeigers auf das Dateiende. Die neuen Daten werden immer ans Ende der Datei eingefügt, unabhängig davon ob der Schreibzeiger inzwischen neu positioniert wurde.
ios::ate Öffnen einer Datei und positionieren des Schreibzeigers auf das Dateiende. Der Schreibzeiger kann bei Bedarf neu positioniert werden (seekp(...)) und die Daten werden dann an der neuen Position eingefügt.
ios::trunc Öffnet eine Datei, wobei der ursprüngliche Inhalt wird verworfen.

Beachten Sie, dass Sie den mode-Parameter hier immer angegeben müssen. Wenn Sie die Datei nur mit Lesezugriff öffnen, muss die Datei natürlich bereits existieren.

Wollen Sie eine Datei zum Lesen und Schreiben öffnen die noch nicht existiert, so müssen Sie den Mode ios::trunc mit angeben. Selbstverständlich können Sie dann zu Beginn nur Daten schreiben.

Das Lesen, Schreiben und Schließen erfolgt mit den bereits in den vorherigen Abschnitten erläuterten Memberfunktionen.

Alle 3 Streams (ofstream, ifstream und fstream) besitzen eigentlich noch einen 3. Parameter, der den gleichzeitigen Zugriff auf eine Datei von mehreren Streams aus kontrolliert. Dieser Parameter ist aber für fortgeschrittene Dateioperationen und wird deshalb hier nicht weiter betrachtet.

Sonstige Dateioperationen

eof Memberfunktion

In der Praxis werden Sie in vielen Fällen nicht von vornherein wissen, wie viele Daten in einer Datei abgelegt sind. Wenn Sie nun Daten aus einer Datei einlesen, müssen Sie also irgend wie feststellen können, ob das Dateiende erreicht ist und somit alle Daten eingelesen wurden. Die Abfrage ob das Dateiende erreicht wurde erfolgt mit der Memberfunktion

bool basic_ios::eof()

 Diese Memberfunktion liefert den Wert true zurück wenn das Dateiende erreicht ist.


...
// Stream-Objekt definieren und mit Datei verbinden
ifstream myFile("d:/test.dat");
// Einlesen aus Datei
myFile >> data1 >> ...;
// Prüfen, ob Dateiende noch nicht erreicht
if (!myFile.eof())
{
   ... // Daten aus Datei auswerten
}
...

Beachten Sie, dass weitere Einleseversuche nach dem Erkennen des Dateiendes zwar in der Regel nicht zu einem Programmabsturz führen, jedoch ungültige Daten zurückliefern. Damit eof() das Erreichen des Dateiendes feststellen kann, muss vorher ein Einleseversuch stattgefunden haben.

ignore Memberfunktion

Die Memberfunktion ignore(...), welche schon in der Lektion Eingabestream cin erläutert wurde, kann zum Überspringen von Daten aus Dateien verwendet werden.


#include <fstream>
#include <iostream>

// main() Funktion
int main()
{
   // Feld zum Einlesen eines Strings
   char array[100];

   // Stream-Objekt definieren und mit Datei verbinden
   std::ifstream infile("test.dat");
   // Wenn Datei geöffnet werden konnte
   if (infile)
   {
      // Erstes Wort einlesen
      infile >> array;
      std::cout << "1. Wort: " << array << std::endl;
      // Rest der 1. Zeile (max. 100 Zeichen) ueberspringen
      infile.ignore(100,'\n');
      // Erstes Wort der 2. Zeile einlesen
      infile >> array;
      std::cout << "2. Wort: " << array << std::endl;
   }
}

seek Memberfunktionen

Bei Dateistreams lassen sich die internen Dateizeiger (oder genau genommen sind es die Pufferzeiger) beeinflussen, um einen wahlfreien Zugriff auf die in der Datei abgelegten Daten zu erhalten. Hierzu werden die beiden Memberfunktionen

basic_istream& seekg(ios::off_type offset, ios_base::seek_dir dir);
basic_ostream& seekp(ios::off_type offset, ios_base::seek_dir dir);

Die Memberfunktion seekg(...) dient zum Positionieren des Lese-Dateizeigers und die Memberfunktion seekp(...) zum Positionieren des Schreib-Dateizeigers, d.h. beide Dateizeiger lassen sich unabhängig voneinander positionieren. offset gibt die Anzahl der Bytes an, um die der Dateizeiger von der Position dir aus bewegt werden soll. Für dir ist einer der folgenden Werte zulässig:

Wert Bedeutung
ios::cur Dateizeiger ab der aktuellen Position verschieben
ios::beg Dateizeiger ab Dateianfang verschieben
ios::end Dateizeiger auf Dateiende verschieben

Beachten Sie bitte, dass der Dateizeiger bei Angabe von ios::beg nur in positive Richtung und bei Angabe von ios::end nur in negative Richtung verschoben werden kann!

Im nachfolgenden Beispiel werden zunächst die Werte 1...5 in eine Binärdatei, die zum Lesen und Schreiben geöffnet ist, geschrieben. Anschließend werden mittels den erwähnten seek Memberfunktionen verschiedene Werte aus der Datei ausgelesen. Beachten Sie dazu die Kommentare im Listing. Zum Schluss wird der vorletzte Wert verändert und dann alle Daten zur Kontrolle nochmals ausgegeben. Beachten Sie bitte, welche Definitionen Sie aus dem std-Namensraum einbinden müssen!


// Beispiel zu wahlfreien Dateizugriff
#include <fstream>
#include <iostream>

// Bezeichner aus std-Namensraum einbinden
using std::cout;
using std::endl;
using std::fstream;
using std::ios;

// Dateiname
const char* const pFILENAME = "test.dat";
// Das zu schreibende und lesende Datum
short var;

// main() Funktion
int main()
{
   // Stream-Objekt definieren und mit binärer Datei verbinden
   fstream inOutFile(pFILENAME,ios::binary|ios::in|ios::out|ios::trunc);
   // Fehler abfangen
   if (!inOutFile)
   {
      cout << "Fehler beim Öffnen der Datei " << pFILENAME << endl;
      exit (1);
   }

   // Werte 1...5 in Datei schreiben
   // Mit einer Schleife geht das später einfacher!

   var = 1;
   inOutFile.write (reinterpret_cast<char*>(&var),sizeof(var));
   var++;
   inOutFile.write (reinterpret_cast<char*>(&var),sizeof(var));
   var++;
   inOutFile.write (reinterpret_cast<char*>(&var),sizeof(var));
   var++;
   inOutFile.write (reinterpret_cast<char*>(&var),sizeof(var));
   var++;
   inOutFile.write (reinterpret_cast<char*>(&var),sizeof(var));

   // Auf Dateianfang zurück
   inOutFile.seekg(static_cast<ios::off_type>(0),ios::beg);
   // 1. Datum einlesen
   inOutFile.read(reinterpret_cast<char*>(&var),sizeof(var));
   cout << "1. Wert: " << var << endl;

   // Auf letzten Wert positionieren
   inOutFile.seekg(-static_cast<ios::off_type>(sizeof(var)),ios::end);
   inOutFile.read(reinterpret_cast<char*>(&var),sizeof(var));
   cout << "Letzter Wert: " << var << endl;

   // Auf vorletzten Wert positionieren
   inOutFile.seekg(-static_cast<ios::off_type>(sizeof(var)*2),ios::end);
   inOutFile.read(reinterpret_cast<char*>(&var),sizeof(var));
   cout << "Vorletzter Wert: " << var << endl;

   // Vorletzten Wert überschreiben
   cout << "Überschreibe jetzt vorletzten Wert!" << endl;
   var = -1;
   inOutFile.seekp(-static_cast<ios::off_type>(sizeof(var)*2),ios::end);
   inOutFile.write(reinterpret_cast<char*>(&var),sizeof(var));

   // Und nun alle Werte auslesen
   cout << "Datei enthält jetzt:\n";
   // Lesezeiger zuerst auf Dateianfang
   inOutFile.seekg(static_cast<ios::off_type>(0),ios::beg);
   // Die nachfolgende while-Schleife wird später noch behandelt.
   // Nun alles wieder einlesen bis zum Dateiende

   do
   {
      inOutFile.read(reinterpret_cast<char*>(&var),sizeof(var));
      // Wenn nicht Dateiende erreicht, eingelesen Wert ausgeben
      if (!inOutFile.eof())
         cout << var << " ";
   } while(!inOutFile.eof());
   cout << endl;
}

1. Wert: 1
Letzter Wert: 5
Vorletzter Wert: 4
Überschreibe jetzt vorletzten Wert!
Datei enthält jetzt: 1 2 3 -1 5

Beispiel und Übung

Beispiel:

Für eine Kundendatenbank wird eine Textdatei erstellt die folgende Daten enthält:

  • Kundennummer als short-Wert
  • Umsatz in Tausend-EUR als double-Wert
  • Datum des letzten Umsatzes, bestehend aus Tag, Monat und Jahr, jeweils als short-Wert

Die Daten werden mit beliebigen Werten initialisiert und dann in die Datei übertragen. Danach werden die Daten aus der Datei eingelesen und zur Kontrolle ausgegeben.

Kundendaten:
------------
KD-Nummer : 123
Umsatz in TSD : 12.3
Letzter Umsatz am: 10.5.2008


// Beispiel zum Dateihandling mittels Streams
// Dateien einbinden

#include <iostream>
#include <fstream>
using namespace std;

// Dateiname
const char* const pszDATEI = "kunden.dat";

// main() Funktion
int main()
{
   // Definition der Kundendaten mit Initialisierung
   short kdNummer = 123;
   double umsatz = 12.3f;
   short tag=10,monat=5,jahr=2008;

   // Das Ausgabestream-Objekt wird innerhalb eines Blocks
   // definiert. Am Blockende wird es gelöscht und damit
   // die Datei geschlossen!

   {
      // Ausgabestream-Objekt definieren und mit Datei verbinden
      ofstream outFile(pszDATEI);
      // Fehler abfangen
      if (!outFile)
      {
         cout << "Fehler beim Erstellen der Datei!\n";
         exit (1);
      }
      // Daten in Datei schreiben
      // ACHTUNG! Nach jedem Datum entweder ein Leerzeichen oder ein
      // Zeilenvorschub eingeben damit Daten nachher auch wieder eingelesen können

      outFile << kdNummer << " " << umsatz << endl;
      outFile << tag << " " << monat << " " << jahr << endl;
   }
   // Blockende, Datei ist wieder geschlossen!

   // Variablen zur Kontrolle mit 0 belegen
   kdNummer = tag = monat = jahr = 0;
   umsatz = 0.0;

   // Eingabestream-Objekt definieren und jetzt mit open() mit
   // der Datei verbinden. Möglich wäre auch die Datei direkt
   // wie vorhin mit dem Stream zu verbinden

   ifstream inFile;
   inFile.open(pszDATEI);
   // Fehler abfangen!
   if (!inFile)
   {
      cout << "Fehler beim Öffnen der Datei!\n";
      exit(2);
   }
   // Daten aus Datei auslesen
   inFile >> kdNummer >> umsatz; inFile >> tag >> monat >> jahr;
   // Dateiverbindung aufheben
   inFile.close();

   // Eingelesene Daten formatiert auf Bildschirm ausgeben
   cout << "Kundendaten:\n------------\n";
   cout << "KD-Nummer : " << kdNummer << endl;
   cout << "Umsatz in TSD : " << umsatz << endl;
   cout << "Letzter Umsatz am: ";
   cout << tag << "." << monat << "." << jahr << endl;
}

Übung:

Wandeln Sie das obige Beispiel so ab, dass die Daten nun in einer binären(!) Datei abgelegt werden. Verwenden Sie hierfür ein Stream-Objekt das 'gleichzeitiges' Lesen und Schreiben zulässt.

Legen Sie zunächst einen beliebige Kundendaten-Satz in der Datei ab. Lesen Sie geschriebenen Daten wieder ein und geben Sie diese zur Kontrolle aus. Anschließend erhöhen Sie den Umsatz um 5 (Tsd-EUR) und überschreiben den alten Dateiinhalt nun mit den neuen Daten. Geben Sie zum Schluss die ein der Datei abgelegten Daten zur Kontrolle nochmals aus.

Kundendaten:
------------
KD-Nummer : 33
Umsatz in TSD : 12.3
Letzter Umsatz am:
1.3.2008
Kundendaten:
------------
KD-Nummer : 33
Umsatz in TSD : 17.3
Letzter Umsatz am: 1.3.2008

Lösung ansehen!