C++/Tutorial: 18. Standardbibliothek: Streams
In den vorausgehenden 17 Kapiteln haben wir nun schon die meisten relevanten Spracheigenschaften von C++ besprochen, sind auf die Standardbibliothek bisher allerdings nur rudimentär eingegangen. Nunja, die Standardbibliothek ist in ihrem Umfang relativ klein und den größten Teil machen die Container aus, die wir im nächsten Kapitel behandeln werden, aber auch Streams sind ein wichtiger Bestandteil der C++ Standardbibliothek. Streams sind Objekte, die etwas repräsentieren, an das wir Bytes schreiben oder von dem wir Bytes lesen können, wir haben bereits mit ihnen gearbeitet, cin und cout sind Streamobjekte, mit denen wir an die Konsole schreiben oder von ihr lesen können. Die C++ Standardbibliothek hat neben den Standardausgabeobjekten cin,cout,cerr und clog(und die wide-char Alternativen wcin, wcout, wcerr und wclog) filestreams, mit denen wir in Dateien schreiben und aus ihnen lesen können sowie stringstreams im Repertoire. Weitere mögliche Streams, die nicht von der Standardbibliothek abgedeckt werden, wären z.B. Netzwerkstreams.
Die C++ Standardbibliothek hat für Streams 2 Basisklassen istream und ostream(beide erben von der gemeinsamen Basisklasse ios), bei ersterem handelt es sich um einen input stream, also einen Stream aus dem gelesen werden kann, bei letzterem um einen outputstream. Darüber hinaus gibt es eine Klasse iostream, die von beiden Streambasisklassen erbt und selbst die Basisklasse für Streams darstellt, die sowohl lesenden als auch schreibenden Zugriff ermöglichen.
Die Standardbibliothek vollständig darzulegen und zu erklären würde den Umfang des Tutorials sprengen, ich lege daher noch einmal die C++ Referenz nahe.
Inhaltsverzeichnis
Die Standardstreamobjekte cout, cin, cerr und clog
cout
Mit das erste, was wir von C++ mitgekriegt haben war cout. Mit unserem neu erlangten Wissen können wir cout etwas genauer beschreiben. Bei cout handelt es sich um ein Objekt vom Typ ostream. ostream überläd den Operator <<(es handelt sich natürlich um den links-shift Operator, im Kontext von Streams spricht man jedoch von dem `Inserter`) für die formatierte Ausgabe. Formatierte Ausgabe bedeuted in dem Fall, dass << für verschiedene Typen überladen ist und diese entsprechend richtig an die Konsole schreibt, wir können jedoch auch unformatiert in ostream schreiben und einfach einen Satz von Bytes schreiben, dazu dienen die Memberfunktionen put, die ein char nimmt und diesen unverändert in den Stream schreibt, und write, die einen Pointer als Array als ersten Parameter und die Größe dieses Pointers als Parameter nimmt(ein praktisches Beispiel kommt im Kontext der Filestreams noch).
Fehlerflags
Alle Streamklassen verfügen über 3 Fehlerflags, die gesetzt sein können oder eben auch nicht: eofbit, failbit und badbit. eof steht für end of file und bedeuted, dass über das Ende der Daten hinaus gelesen wurde. failbit weist normalerweise auf eine fehlerhafte Operation, badbit auf Probleme mit dem Stream hin.
Diese Fehlerflags erlauben uns Fehler in lesenden und schreibenden Operationen(die immer auftreten können) zu behandeln. Wollen wir wissen, ob eine Operation einwandfrei gelungen ist können wir die Fehlerflags überprüfen. Die Memberfunktion good() gibt dabei true zurück, wenn keines der drei Bits gesetzt ist. Wir können den Stream selbst überprüfen (if(cout) in diesem Beispiel), wenn es uns reicht zu wissen, ob weder failbit noch badbit gesetzt ist. Die Memberfunktionen fail(), bad() und eof() erlauben uns die Fehlerflags einzeln zu überprüfen, wollen wir sie zurücksetzen können wir die Memberfunktion clear() verwenden, die wir bereits auf cin angewendet haben.
Manipulatoren
Im Kontext von cout haben wir bereits endl kennen gelernt, es handelt sich hierbei um einen sogenannten Manipulator. Manipulatoren sind Funktionen, die ios_base& (die Basisklasse von ios) als Parameter nehmen und ios_base& auch zurück geben. Schreiben wir einen Manipulator in einen Stream oder lesen wir aus ihm wird die Funktion auf diesen Stream aufgerufen und modifizieren dann üblicherweise das Verhalten des Streams.
endl ist ein Manipulator, der eigentlich eine Kombination aus "\n" und dem Manipulator flush ist. Der Manipulator flush weist den String an noch nicht geschriebene Bytes nun in das dazugehörige Medium zu schreiben, falls das noch nicht getan ist. Streams können nämlich auch Daten im buffer speichern, wenn es sinnvoll erscheint, sodass sie dann erst später geschrieben werden.
cout << "hallo Welt!(hatten wir das nicht schon?)" << flush; //schreibt den String in den Standardausgabestrom und weist ihn dann an, dies auch sofort zu tun.
Weitere Manipulatoren können z.B. das Ausgabeformat modifizieren, oder anderes tun, da wäre z.B. hex, der den Stream anweist Zahlen als Hexadezimalzahlen anzugeben, eine vollständige Liste gibt es hier
clog und cerr
Neben dem Standardausgabestrom(stdio) gibt es noch einen Standardfehlerausgabestrom stderr, der für Logging und Fehlernachrichten verwendet werden kann und meist ebenso auf die Konsole verweist. clog und cerr sind beides ebenfalls ostream Objekte und lassen sich somit identisch zu cout bedienen, nur dass sie in stderr schreiben und nicht in stdio. clog ist wie cout ein gepufferter(=buffered) Ausgabestrom und eignet sich somit fürs Logging. Für die Ausgabe von Fehlern macht es jedoch manchmal eher Sinn einen ungepufferten Strom zu verwenden, sodass alles was in den Stream geschrieben wird sofort auch ausgegeben wird.
cin
Der vierte der vordefinierten Standardstreams unterscheidet sich von den anderen dreien dahingehend, dass es ein istream ist und auch mit diesem haben wir bereits gearbeitet: cin.
Mit dem Gegenstück des Inserters, dem Extractor >> können wir aus einem Stream formatiert lesen, wir lesen also entsprechend dem Datentyp der Variable, die wir angegeben haben. Es ist ebenso möglich unformattiert aus einem istream zu lesen, also einfach eine Reihe von Daten in Form von Bytes einzulesen, dazu verwenden wir die get() Methode.
Rufen wir get() ohne Parameter auf, wie wir es am Ende tun um die Konsole offen zu halten, liest die Funktion genau ein Byte ein, castet dieses in ein int und gibt dieses zurück. Wollen wir mehr als ein Byte unformattiert einlesen ist get dafür überladen, sodass es als ersten Parameter ein Array von Bytes als char* nehmen kann und als zweiten Parameter die Anzahl der Bytes die einzulesen sind. Gibt es keine Daten mehr die einzulesen sind wird der eof-Flag gesetzt.(Ein Beispiel zur Verwendung von istreams folgt bei der Behandlung von ifstream, da es dort sinnvoller ist, als bei cin)
An dieser Stelle können wir auch die beiden Zeilen aus Kapitel 2 genauer verstehen:
cin.clear(); cin.ignore(numeric_limits<streamsize>::max(), '\n');
was cin.clear macht sollte nun klar sein, es löscht den eof-, den fail- und den badflag, cin.ignore dient dazu im Buffer noch folgende Bytes zu verwerfen. Der erste Parameter gibt dabei die Anzahl der maximal zu verwerfenden Bytes an. Es handelt sich dabei um einen Parameter vom Typ streamsize. streamsize ist dabei ein typedef, auf den entsprechenden, signed integralen Datentyp, der für die Größe eines Streams verwendet wird. Mit numeric_limits können wir Zahlengrenzen abfragen, numeric_limits<streamsize>::max() gibt also die größt mögliche Zahl an, die der Datentyp streamsize haben kann(default ist 1). Der zweite Parameter gibt ein Zeichen an, an dem das Verwerfen unterbrochen werden soll, wir löschen demnach alle Zeichen im Buffer bis einschließlich dem nächsten '\n' (default ist EOF).
Filestreams
Schreiben in eine Datei
Nun ist cin und cout nicht das einzige, was wir mit Streams machen wollen, wir wollen zum Beispiel auch noch in Dateien schreiben oder aus ihnen lesen. Wir wissen bereits, dass wir mit istream aus Streams lesen, mit ostream in Streams schreiben und mit iostream beides tun können, mit den Klassen an sich können wir allerdings nichts anfangen. Interessant für uns sind nun die Klassen ifstream, ofstream und fstream, die von istream, ostream und iostream erben und das lesen und schreiben in Dateien ermöglichen.
Schauen wir uns also mal an, wie wir eine Datei erstellen und in diese einen einfachen Text schreiben:
#include <fstream> using namespace std; int main() { ofstream test_stream("test.txt"); test_stream << "test" << endl; }
Wie wir sehen befindet sich die Klasse ofstream im Header fstream, wir erstellen einen Stream mit einem String, der den Dateinamen kennzeichnet und schreiben schließlich in ihn, wie wir es auch mit cout getan haben. Soweit so gut, damit können wir schon einmal einfachen Text in Dateien schreiben, nun wollen wir dem Text von einem anderen Programm aus noch etwas hinzufügen, schreiben wir aber noch einmal auf die gleiche Weise in die Datei, wird der Inhalt der Datei überschrieben, wir müssen beim öffnen der Datei durch den Konstruktor noch einen Parameter angeben, der besagt wie die Datei geöffnet werden soll, eine Liste findet sich hier: http://www.cplusplus.com/reference/iostream/ofstream/ofstream/. Wie die Doku zeigt ist der Defaultparameter für openmode ios_base::out, für uns auch noch interessant ist ios_base::app, womit wir beim schreiben in die Datei den Text anfügen und den alten nicht überschreiben. Da wir out ebenso noch benötigen, da wir immernoch in die Datei schreiben wollen fügen wir unseren Text folgendermaßen an:
ofstream test_stream("test.txt", ios_base::out | ios_base::app); test_stream << "test2" << endl;
Und wie wir nach dem ausführen sehen können steht in der Datei nun:
test
test2
vorausgesetzt natürlich, wir haben jedes Programm genau einmal ausgeführt.
Nun habe ich gesagt, dass wir mitteilen, wie wir die Datei öffnen wollen, das impliziert schon, dass es so etwas wie das öffnen einer Datei gibt. Während wir die Datei geöffnet haben, können andere Programme nicht frei über sie verfügen, das ist zwar praktisch für uns, aber verdeutlicht schon, dass wir sie wieder schließen sollten. C++ bietet an der Stelle zwar ebenso open und close Methoden, aber wie bereits für Speicher beschrieben gilt auch hier, dass RAII uns die Arbeit abnehmen kann. Sobald die Variable out of scope ist wird test_stream zerstört und die Datei im Konstruktor automatisch geschlossen, sodass wir uns im Regelfall über das Schließen der datei keine Gedanken machen müssen, es sei denn die Variable lebt länger, als nötig.
Widmen wir uns also lieber einem anderen openmode ios_base::binary, der andeuted, dass wir die Datei für Binärdaten und nicht für Text verwenden wollen. Der Vorteil des ganzen liegt auf der Hand, zwar könnten wir alle Daten irgendwie ins Textformat überführen, doch ist das binärformat wesentlich platzsparender. Gehen wir also davon aus, dass wir ein long in einer Datei speichern wollen. Wir können dazu nicht den Operator << verwenden, da dieser für eine formatierte Ausgabe ist, den long also als Text in die Datei schreiben würde, wir benötigen stattdessen die write Methode. write nimmt 2 Paramter, der erste Parameter ist ein char const*, welche eine Sequenz von bytes darstellt und der zweite Paramter ein streamsize, der die größe dieser Sequenz darstellt. Einen long in seine Bytes zu zerlegen, dazu brauchen wir lediglich den Pointer zu mit reinterpret_cast zu casten, die Größte kriegen wir mit dem sizeof-Operator, so dass das ganze letzten Endes wie folgt aussieht:
ofstream example_file("test.txt", ios_base::out | ios_base::binary); long example_long(9); example_file.write(reinterpret_cast<char const*>(&example_long), sizeof(long));
Nun haben wir also ein long gespeichert und können auf diese Weise jede Form von Daten abspeichern, dennoch gilt es einige Dinge zu beachten:
- Beim abspeichern von Datenstrukturen und Objekten ist diese Methode vermutlich nicht geeignet, da Pointer vorkommen könnte und wir die Addresse in die Datei speichern würden, die uns anschließend nichts nützt, wir benötigen für diese Datenstrukturen also spezielle Methoden, die die Daten entsprechend richtig liefern.
- Dinge, die von der binären Darstellung her nicht normiert sind, sind unportabel, wenn wir sie in eine Datei schreiben.
Der erste Punkt dürfte klar sein, der letzt könnte einer Erläuterung bedürfen: schauen wir uns unser Beispiel noch einmal genauer an. Speichern wir die Datei und laden sie anschließend mit der selben Executable stellt das kein Problem dar. Nehmen wir aber mal an, wir kompilieren das Programm einmal auf einem 32-Bit System mit dem GCC und einmal auf einem 64-Bit System mit dem GCC, nun haben wir ein Problem: die Länge von long ist nicht normiert und variiert in diesem Fall. Die erste Version würde 4 Byte abspeichern, die zweite Version würde 8 Byte speichern. Die Lösung ist in dem Fall einfach: wir dürfen kein long nehmen, sondern müssen einen Typ nehmen, der immer die selbe Größe hat und die kennen wir bereits: sie befinden sich im TR1 header cstdint und könnte z.B. int32_t lauten:
ofstream example_file("test.txt", ios_base::out | ios_base::binary); int32_t example_int32(9); example_file.write(reinterpret_cast<char const*>(&example_int32), sizeof(int32_t)); // sizeof(int32_t) ist immer 4
Lesen aus einer Datei
Nun wollen wir in einem anderen Programm, die geschriebene 9 lesen und auf der Konsole ausgeben, das ist nun relativ einfach. Zunächst erstellen wir ein ifstream Objekt ähnlich wie wir auch das ofstream Objekt erstellt haben:
ifstream example_file("test.txt", ios_base::in | ios_base::binary);
Als Parameter nehmen wir anstelle von ios_base::out nun natürlich ios_base::in, da wir ja aus der Datei lesen wollen.
Nun überprüfen wir ob die Datei auch wirklich offen ist, das wäre z.B. nicht der Fall, wenn sie einfach nicht existent ist, das können wir mit der is_open() Methode der Streamklassen machen. Mit der get Methode können wir den Stream dann auslesen, wir geben ein char* an, welches ein Pointer ist in den die Bytes geschrieben werden und die streamsize, wie viele Bytes gelesen werden sollen. Der Pointer sollte in dem Fall auf die Addresse eines int32_t zeigen. Insgesamt sieht unser Code dann so aus:
ifstream example_file("test.txt", ios_base::in | ios_base::binary); if(example_file.is_open()) { int32_t example_int32; example_file.get(reinterpret_cast<char*>(&example_int32), sizeof(int32_t)); cout << example_int32 << endl; }
Und schon haben wir die 9 gelesen.
Zusätzlich interessant zu kennen sind die Methoden seekg und tellg, die es uns ermöglichen in der Datei umherzuspringen. Mit seekg springen wir an eine angegebene Position, mit tellg können wir uns die aktuelle Position geben lassen.
Sringstreams
Nun da wir schon mit der Konsole und mit Dateien umgehen können fehlt uns noch eine letzte Anwendung von Streams: Stringstreams. Stringstreams sind in Mechanismus in C++ um Strings außeinanderzunehmen, oder zu konstruieren. Analog zu den Filestreams gibt es die Klasse istringstream, ostringstream und stringstream, die sich alle im Header sstream befinden.
Das häufigste Anwendungsbeispiel ist es zum Beispiel, wenn wir eine Zahl zu einem String konvertieren wollen, dazu erstellen wir einen Stringstream auf dem Stream und schreiben die Zahl formattiert wie in jeden anderen Stream in diesen Stringstream rein.
Schauen wir uns das ganze nun einmal in der Praxis an:
#include <sstream> using namespace std; int main() { string text; ostringstream(text); text << 9; }
Wir erstellen also zunächst ein string-Objekt ohne Inhalt und anschließend ein ostringstream mit dem wir in diesen String schreiben können. Mit dem << Operator schreiben wir wie gehabt die 9 formatiert in diesen String, sodass text letztlich "9" ist.