9 Dateien und Ströme |
Dieses Kapitel behandelt die folgenden Themen:
Funktionsweise der Standardein- und -ausgabe
Formatierung der Ausgabe
Fehlerbehandlung
Arbeit mit Dateien
Ausgabe in einen String umleiten
Formatierte Daten schreiben und lesen
Blockweise lesen und schreiben
In Abschnitt 1.10 werden Ein- und Ausgaben einführend beschrieben. In diesem Kapitel wird das Thema vertieft, indem weitere nützliche Funktionen und Operatoren angegeben werden. Auch wird die Formatierung von Daten besprochen und auf die Fehlerbehandlung eingegangen. Die C++-Standardbibliothek stellt die notwendigen Mechanismen für die Ein- und Ausgabe von Grunddatentypen zur Verfügung, erlaubt aber auch die Konstruktion eigener Ein- und Ausgabefunktionen für selbst definierte Klassen. Auf der untersten Ebene wird ein stream als Strom oder Folge von Bytes aufgefasst. Das Byte ist die Dateneinheit des Stroms, andere Datentypen wie int, char* oder vector erhalten erst durch die Bündelung und Interpretation von Bytesequenzen auf höherer Ebene ihre Bedeutung. Die Basisklasse heißt ios_base; aus ihr werden die anderen Klassen abgeleitet, wie Abbildung 9.1 zeigt. Die dort mit dem Wort basic beginnenden Klassen sind Templates, die für beliebige Zeichentypen geeignet sind, zum Beispiel Unicode-Zeichen oder andere »wide characters«.
Für den am häufigsten benötigten Typ char sind die Klassen durch Typdefinitionen wie
spezialisiert worden, wie Abbildung 9.2 zeigt. Im Folgenden liegt der Schwerpunkt auf der Benutzung der Klassen. Die im Header <iostream> deklarierten Standardstreams sind Objekte dieser Klassen:
9.1 | Eingabe |
Die Klasse istream enthält Operatoren für die in C++ eingebauten Grunddatentypen, die für eine Umwandlung der eingelesenen Zeichen in den richtigen Datentyp sorgen:
Der Operator >> ist als überladener Operator definiert, wie wir ihn im Prinzip schon kennengelernt haben. Für einen Datentyp T (T steht für einen der Grunddatentypen) sei hier das Schema der Definition gezeigt:
Wie bei der Ausgabe bewirkt die Rückgabe einer Referenz auf istream, dass mehrere Eingaben verkettet werden können:
Falls Zeichen oder Bytes eingelesen und Zwischenraumzeichen nicht ignoriert werden sollen, kann die Elementfunktion istream& get() genommen werden, die in mehreren überladenen Versionen bereitsteht, von denen eine ein einzelnes Zeichen einliest, wie das Beispiel zeigt.
Zum sicheren Einlesen einer Zeichenkette in einen Pufferbereich wird einer anderen überladenen Version von get() der Zeiger auf den Pufferbereich und die Puffergröße mitgegeben, die wegen des abschließenden ’\0’-Zeichens um eins größer als die Anzahl der maximal einzulesenden Elemente sein muss. Die Deklaration im Header <iostream> lautet istream& get(char*, streamsize, char t=’\n’);. streamsize ist ein implementationsabhängiger vorzeichenbehafteter Ganzzahltyp, zum Beispiel long int. Die Zeichenkette wird bis zu einem festzulegenden Zeichen t übernommen (t steht für »Terminator«), wobei das Terminatorzeichen im Eingabestrom verbleibt. Es ist mit der Zeilenendekennung vorbesetzt. Beispiel:
Wegen der Vorbesetzung des Terminatorzeichens ist der Aufruf gleichbedeutend mit cin.get(buf, n, ’\n’);. Als letztes Zeichen trägt get() in buf[] die String-Endekennung ’\0’ ein. Darüber hinaus gibt es weitere istream-Funktionen, von denen ein kleiner Teil hier aufgeführt ist:
Diese Funktion wirkt wie get(), aber das Terminatorzeichen wird gelesen (jedoch nicht mit in den Puffer übernommen).
Diese Funktion liest und verwirft alle Zeichen des Eingabestroms, bis entweder das Terminatorzeichen oder n andere Zeichen gelesen worden sind. Das Terminatorzeichen verbleibt nicht im istream. EOF ist ein in <cstdio> vordefiniertes Makro, das das Dateiende (englisch end of file) kennzeichnet und das häufig durch den Wert –1 repräsentiert wird. Ein Aufruf könnte zum Beispiel cin.ignore(max_Zeilenlaenge, ’\n’); lauten.
Hinweis
Die allgemeine Form der Deklaration von ignore() ist
charT ist der Zeichentyp. traits ist ein struct, das verschiedene Funktionen für den jeweiligen Zeichentyp bereitstellt, unter anderem eof(). traits ist identisch mit der im System vorhandenen Klasse char_traits<charT>, wenn nicht anders bei der Konstruktion eines basic_istream-Objekts angegeben. int_type ist in der Klasse traits definiert und könnte zum Beispiel int sein. istream ist die Spezialisierung eines basic_istream für den Zeichentyp char (siehe Abbildungen auf Seite 424). Die Funktion traits::eof() für diese Spezialisierung gibt EOF zurück. Entsprechendes gilt für alle Stellen, an denen EOF vorkommt.
Diese Funktion gibt ein Zeichen c an den istream zurück. Die Funktion
holt das nächste Zeichen und gibt es als int-Wert entsprechend seiner Position in der ASCII-Tabelle zurück. Bei EOF wird –1 zurückgegeben. Man könnte die Standardeingabe daher auf folgende Art kopieren:
Eine andere Möglichkeit ist die Abfrage mit eof(), eine Funktion, die true zurückgibt, wenn ein Leseversuch wegen Erreichen des Dateiendes erfolglos bleibt:
cin.good() ist vorzuziehen, weil nicht nur EOF, sondern auch Lesefehler berücksichtigt werden. Eine Vorschau auf das nächste Zeichen im Eingabestrom wird durch die Funktion
erlaubt. Die Wirkung von c = cin.peek() ist wie die Hintereinanderschaltung der Anweisungen (mit impliziter Typumwandlung):
9.2 | Ausgabe |
Die Klasse ostream umfasst überladene Operatoren für eingebaute Datentypen. Der Operator << dient dazu, ein Objekt eines internen Datentyps in eine Folge von Zeichen zu verwandeln.
Der Rückgabetyp des Operators ist eine Referenz auf ostream. Das Ergebnis des Operators ist das ostream-Objekt selbst, sodass ein weiterer Operator darauf angewendet werden kann. Damit ist die Hintereinanderschaltung von Ausgabeoperatoren möglich:
wird interpretiert als
Überladen von Operatoren ermöglicht die Ausgabe beliebiger Objekte benutzerdefinierter Klassen. Ein ausführliches Beispiel dazu wird auf den Seiten 371 ff. gezeigt, wo es darum geht, Objekte der Klasse rational auszugeben. Nach diesem Muster sind eigenen Kreationen des <<-Operators keine Grenzen gesetzt.
In der Klasse ostream sind weitere Elementfunktionen für ostream-Objekte definiert:
gibt ein Zeichen aus. Sie haben put() bereits in Abschnitt 1.10.2 kennengelernt, wo die Funktion in einem Beispielprogramm zum Kopieren von Dateien eingesetzt wird (Listing 1.32). Die Funktion
wird in Abschnitt 4.9 zur binären Ausgabe verwendet. Es gibt eine Menge zusätzlicher Funktionen, zum Beispiel zum Positionieren des Ausgabestroms auf eine bestimmte Stelle, zum Herausfinden der aktuellen Stelle und weitere, auf die hier zum Teil eingegangen werden soll. Die Deklarationen dieser Funktionen finden Sie im Header <iostream>.
Die Pufferung der Ausgabe kann in C++ mit dem Flag unitbuf gesteuert werden. Wenn in Ihrem System gelegentlich die Ausgabe nicht zum erwarteten Zeitpunkt geschieht, liegt es wahrscheinlich daran, dass unitbuf nicht voreingestellt ist. Manchmal ist es aber sinnvoll den Programmfortschritt anzuzeigen. Damit die entsprechende Ausgabe nicht im Puffer hängenbleibt, schreiben Sie cout.setf(ios::unitbuf). Jedes Zeichen einer Ausgabe wird sofort ausgegeben. Eine Alternative ist flush nach jeder Ausgabe, etwa cout << ’.’ << flush;. In Listing 9.1 wird eine langwierige Berechnung der Kürze halber durch Warten simuliert. Ignorieren Sie chrono und thread zunächst. Sie finden diese Themen in Kapitel 15.
#include <chrono> #include <iostream> #include <thread> int main() { std::cout << "Langwierige˽Berechnung,˽bitte˽warten." << std::endl; std::cout.setf(std::ios::unitbuf); for (int i = 0; i < 10; ++i) { std::cout << ’.’; // Berechnungsfortschritt anzeigen // Aktivität durch Warten simulieren: std::this_thread::sleep_for(std::chrono::milliseconds(500)); } std::cout << "\nBerechnung˽beendet\n"; // Nach Wegfall des Anlasses ’Berechnungsfortschritt anzeigen’:Pufferung wieder einschalten: std::cout.unsetf(std::ios::unitbuf); // ... weitere Berechnungen und Ausgaben }
Statt der Zeile mit setf() können Sie auch std::cout << std::unitbuf; schreiben. Falls unitbuf nicht gesetzt ist, schlummert die Aufforderung zum Warten im Ausgabepuffer und erscheint erst nach dem Ende der Berechnung. Sie können das sehen, wenn Sie in Listing 9.1 setf() durch unsetf() ersetzen und das Programm laufen lassen. Manchmal genügt es aber auch, dass nur eine Zeile sofort ausgegeben werden soll. Dazu nehmen Sie endl, wie auf Seite 107 erklärt. Denken Sie daran, dass das Ausschalten der Pufferung Programmlaufzeit kostet! Mit cout.unsetf(ios::unitbuf) schalten Sie die Pufferung wieder ein.
9.3 | Formatierung mit std::format |
Oft ist es notwendig, die Ausgabe besonders aufzubereiten, sei es als Tabelle, in der alle Spalten die gleiche Breite haben müssen, oder sei es, dass spezielle Zahlenformate verlangt werden. Seit C++20 gibt es dafür einen einfachen Mechanismus mit der Funktion format(). Sie gibt einen formatierten String zurück. Wer die Sprache C kennt, wird Ähnlichkeiten zur C-Ausgabefunktion printf() sehen. Natürlich gab es schon vor C++20 Möglichkeiten zur Formatierung und es gibt auch diese immer noch. Sie werden in den Abschnitten 9.4 und 9.5 behandelt. Wer also nicht die neuesten Compiler benutzt, kann darauf zurückgreifen. Dieser Abschnitt stellt die wichtigsten Möglichkeiten zur Formatierung der Ausgabe mit der Funktion format() vor. Der Funktion werden ein Format-String (Typ string_view) sowie die auszugebenden Daten übergeben. Der Format-String kann Text enthalten, aber auch Platzhalter für die Daten in Form von geschweiften Klammern {} (in der einfachsten Form). Im folgenden Beispiel werden die Platzhalter der Reihe nach durch die Daten ersetzt:
Die Ausgabe des Strings msg ergibt 2 Kaffee bitte und 1 Wasser. Die automatische Indizierung kann durch eine manuell festgelegte Reihenfolge ersetzt werden, wobei die Nummerierung mit 0 beginnt. So bewirkt
die Ausgabe 1 Kaffee bitte und 2 Wasser. Platzhalter können wiederholt werden. Was die Ausgabe von format("{0}{0}\n", "blah") ergibt, ist damit wohl klar.
9.3.1 | Syntax für Platzhalter |
Manchmal benötigt man mehr als die einfache Ausgabe. Zum Beispiel soll eine Zahl mit vier Nachkommastellen dargestellt werden usw. Das wird durch eine besondere Syntax der Platzhalter ermöglicht. Die Syntax ist etwas vereinfacht
Platzhalter : { [Index] [:Formatspezifikation] }
Die geschweiften Klammern kennen Sie von oben. Anschließend kommt optional der Index, d.h. die Nummer des Arguments in der Liste der Daten. Die eckigen Klammern bedeuten, dass der Inhalt auch wegfallen kann. Wenn ein Doppelpunkt und eine Formatspezifikation folgen, wird diese für die tatsächliche Umwandlung in den String abgearbeitet. Wenn geschweifte Klammern selbst dargestellt werden sollen, werden sie verdoppelt. Beispiele:
"{}" // automatische Indizierung "{1}" // Argument Nr. 1 nehmen "{:0>12}" // Die Formatspezifikation ist 0>12. Sie bedeutet rechtsbündig (>), // autom. Indizierung, Weite = 12, ggf. führende Nullen "{2:˽<12}" // Argument 2, Weite = 12, linksbündig (<), ggf. folgende Leerzeichen
Das Zeichen nach dem Doppelpunkt ist ein Füllzeichen. Listing 9.3 zeigt die wichtigsten Formatierungsmöglichkeiten. Das Ergebnis sehen Sie direkt danach.
#include <format> #include <iostream> using namespace std; int main() { cout << format("{:-<40}\n", "linksbuendig˽mit˽Strichen"); cout << format("{:.>40}\n", "rechtsbuendig˽mit˽Punkten"); cout << format("{:*^40}\n", "zentriert˽mit˽Sternchen"); // ganze Zahlen cout << format("{:0>10}\n", 12345); // ggf. führende Nullen, Weite = 10 cout << format("{:˽>10}\n", -12345); // Leerzeichen statt Nullen // weitere Ganzzahl-Formate (0 ist die Nr. des Arguments): cout << format("binär˽{0:b},˽oktal˽{0:o},˽dezimal˽{0:d},˽hex˽{0:x}\n" "dezimal˽{1:d}˽=˽hex˽{1:X}\n", 1024, 255); // mit Weite 15 und Kennung # : 0b 0x usw. cout << format("binär˽˽˽{0:#15b}\noktal˽˽˽{0:#15o}\ndezimal˽{0:#15d}\n" "hex˽˽˽˽˽˽{0:#15x}\n", 255); // Gleitkomma-Zahlen (auch F statt f) cout << format("{:+f}\n", 12345.6); // immer mit +-Zeichen und // 6 Nachkommastellen (Vorgabe) cout << format("{:˽f}\n{:˽f}\n", 12345.6, -12345.6); // Leerzeichen statt +-Zeichen cout << format("{:f}\n", -12345.6); // nur - Zeichen, kein + cout << format("{:˽15.3f}\n", -12345.6); // Weite=15, 3 Nachkommastellen cout << format("{:015.3f}\n", -12345.6); // dito, aber führende Nullen cout << format("{:15.3e}\n", -12345.6); // Wissenschaftliche Notation cout << format("{:e}\n", -12345.6); // dito, 6 Nachkommastellen (Vorgabe) cout << format("{:E}\n", -12345.6); // dito, großes E cout << format("{:g}\n", -12345.6); // mit Komma oder Exponent, je nach Größe cout << format("{:g}\n", -123456789123.4); // dito // geschweifte Klammern mit {{}} cout << format("{{{}}}\n", 10); // { als Escape-Zeichen // Die Option L berücksichtigt die Sprachumgebung. locale::global(locale("de_DE.utf8")); // deutsche Sprachumgebung setzen cout << format("{:15.8L}\n", // Dezimalkomma statt -punkt -12345.678); // 15.8 : Weite 15, 8 Ziffern }
Die Ausgabe des Programms in Listing 9.3 ist:
Als praktisches Beispiel gibt das Programm in Listing 9.4 eine Tabelle von Sinus- und Kosinuswerten formatiert aus. Die Weite der Gradzahlen ist 4, die der Sinus- und Kosinuswerte 12.
#include <cmath> // cos(), sin() #include <format> #include <iostream> #include <numbers> // π using namespace std; int main() { cout << "Grad˽˽˽˽˽˽sin(x)˽˽˽˽˽˽cos(x)\n"; for (int grad = 0; grad <= 90; grad += 10) { const double rad = grad / 180.0 * numbers::pi; // Grad in Bogenmaß umwandeln cout << format("{:˽>4}{:˽12.6f}{:˽12.6f}\n", grad, sin(rad), cos(rad)); } }
Das Programm ist dank format() ziemlich kurz. Die Ausgabe ist:
Das Programm in Listing 9.5 zeigt, wie Datum und Uhrzeit ausgegeben werden. Die Formatierung wird durch die Formatspezifikation bestimmt. Diese enthält mit %-Zeichen beginnende Steuerzeichen. Die Tabelle 9.1 zeigt eine Auswahl. Hier fehlt der Platz, um alle Steuerzeichen aufzulisten. Sie finden eine vollständige Aufstellung in [ISOC++, time.format].
Zeichen |
Bedeutung |
%A |
Wochentag als Text (reagiert auf die Sprachumgebung) |
%B |
Monat als Text (reagiert auf die Sprachumgebung) |
%d |
Tag (01 – 31) |
%m |
Monat (01 – 12) |
%Y |
Jahr als vierstellige Zahl |
%H |
Stunde (00 – 23) |
%M |
Minute (00 – 59) |
%S |
Sekunde (00 – 59) |
#include<chrono> #include <format> #include <iostream> #include <locale> using namespace std; int main() { auto jetzt = chrono::system_clock::now(); cout << "System˽locale:˽" << locale().name() << ’\n’ << format("{:%A,˽%d.˽%B,˽%Y,˽%H:%M:%S}\n", jetzt); locale de("de_DE.utf8"); cout << "locale˽für˽Deutsch:˽" << de.name() << ’\n’ << format(de, "{:%A,˽%d.˽%B,˽%Y,˽%H:%M:%S}\n", jetzt); }
9.3.2 | Formatierung eigener Datentypen |
Einfach ist es, wenn die eigene Klasse einen passenden String zurückgibt, etwa mithilfe eines Typumwandlungsoperators. Hier geht es darum, Objekte der eigenen Klasse Datum, bekannt von Abschnitt 8.3, formatiert auszugeben. Der Typumwandlungsoperator operator string() aus Abschnitt 8.4 gibt das Datum als String zurück. Das Programm in Listing 9.6 gibt das aktuelle Datum und den darauffolgenden Tag aus.
#include <Datum.h> #include <format> #include <iostream> using namespace std; int main() { Datum heute; Datum morgen {++Datum()}; cout << format("heute:˽{}˽und˽morgen:˽{}˽\n", string(heute), string(morgen)); }
Anders ist es, wenn der eigene Datentyp nicht in einen String umgewandelt werden kann. Am einfachsten ist es, wenn man mithilfe einer Funktion einen String oder etwas Ähnliches (string_view, const char*) zur Bearbeitung mit format herbekommt. Die Umwandlung in einen String ist aber nicht zwingend, wenn man einen eigenen formatter verwendet, der intern von format() benutzt wird. Hier wird nicht weiter darauf eingegangen.
9.4 | Formatierung mit Flags |
Dieser Abschnitt stellt die wichtigsten Möglichkeiten zur Formatierung der Ausgabe mit den Funktionen width() und fill() und den ios-Flags vor.
Die Methode width() bestimmt die Weite der unmittelbar folgenden Zahlen- oder Textausgabe. Dabei entstehende Leerplätze werden mit Leerzeichen aufgefüllt. Anstelle der Leerzeichen können mittels der Funktion fill() andere Füllzeichen definiert werden. Das Programmstück
erzeugt die Ausgabe 000012(34). Die Weite wurde auf 6 gesetzt, mit 0 als Füllzeichen. Weil die Weite aber bei jeder Ausgabe auf 0 zurückgesetzt wird, erscheint (34) ohne Füllzeichen. Es ist erkennbar, dass mindestens die notwendige Weite genommen wird, dass also eine zu kleine Angabe für die Weite die Ausgabe nicht beschränkt:
ergibt 123456 und nicht etwa 1234 oder ****.
Ein Flag (englisch für Flagge, Fahne) ist ein Zeichen für ein Merkmal, das entweder vorhanden (Flag ist gesetzt) oder nicht vorhanden ist (Flag ist nicht gesetzt). Zur Formatsteuerung sind Flags des (compilerabhängigen) Datentyps ios_base::fmtflags definiert, der als Bitmaske benutzt wird. Tabelle 9.2 zeigt eine Aufstellung.
Name |
Bedeutung |
boolalpha |
true/false alphabetisch ausgeben oder lesen |
skipws |
Zwischenraumzeichen ignorieren |
left |
linksbündige Ausgabe |
right |
rechtsbündige Ausgabe |
internal |
zwischen Vorzeichen und Wert auffüllen |
dec |
dezimal |
oct |
oktal |
hex |
hexadezimal |
showbase |
Basis anzeigen |
showpoint |
folgende Nullen ausgeben |
uppercase |
E,X statt e,x |
showpos |
+ bei positiven Zahlen anzeigen |
scientific |
Exponential-Format |
fixed |
Gleitkomma-Format |
hexfloat |
Gleitkomma-Format hexadezimal ausgeben |
defaultfloat |
Gleitkomma-Format normal ausgeben Puffer leeren (flush): |
unitbuf |
– nach jeder Ausgabeoperation |
stdio |
– nach jedem Textzeichen |
Weil die Klasse ios von ios_base erbt und im Folgenden nur die auf den Typ char spezialisierten Klassen der Ein- und Ausgabe betrachtet werden, können alle öffentlichen Attribute und Methoden auf die Klasse ios bezogen werden. ios::fmtflags ist dasselbe wie ios_base::fmtflags, weil ios_base kein Template ist. Statt ios_base wird im Folgenden nur noch einfach ios geschrieben. Im folgenden Programmbeispiel wird gezeigt, wie die Flags gelesen und gesetzt werden können. Die typische Voreinstellung mit Zweierpotenzen im Typ fmtflags definiert eine Bitleiste, deren einzelne Bits durch Oder-Operationen gesetzt werden. Die Funktion flags() dient zum Lesen und Setzen aller Flags und setf() wird zum Setzen eines Flags verwendet.
Wenn ein Flag gesetzt werden soll, ist es besser, sicherheitshalber möglicherweise kollidierende Flags zurückzusetzen. Die Funktion setf() mit zwei Parametern sorgt dafür:
bewirkt, dass zuerst alle Bits der Maske zurückgesetzt und danach die Flags gesetzt werden. Es gibt drei vordefinierte Masken für diesen Zweck, die in der Tabelle 9.3 zusammengefasst sind. Wenn die hexadezimale Ausgabe eingestellt werden soll, ist es daher besser, ausgabe.setf(ios::hex, ios::basefield) zu schreiben, um gleichzeitig die möglicherweise gesetzten Flags oct oder dec zurückzusetzen.
Name |
Wert |
adjustfield |
left | right | internal |
basefield |
oct | dec | hex |
floatfield |
fixed | scientific |
Die Weite von Fließkommazahlen wird mit der Funktion precision() gesteuert, die die Anzahl der Ziffern bei Fließkomma-Ausgabe festlegt, sofern fixed oder scientific nicht gesetzt sind. Andernfalls legt precision die Anzahl der Nachkommastellen fest. Die eingestellte Anzahl ist gültig bis zum nächsten precision()-Aufruf. Mit dieser Funktion kann auch die aktuell eingestellte Ziffernzahl festgestellt werden. Das Programm
erzeugt die Ausgaben (mit automatischer Rundung)
Falls die Anzahl der Ziffern vor dem Komma den Wert von precision überschreitet, wird auf die wissenschaftliche Notation, also Ausgabe mit Exponent, umgeschaltet.
Falls fixed oder scientific gesetzt ist, legt precision() die Anzahl der Nachkommastellen fest. Das Beispiel spricht für sich:
Zum Vergleich soll genau wie auf Seite 431 eine Tabelle mit Sinus- und Kosinuswerten formatiert ausgegeben werden. Durch das fixed-Bit wird die Darstellung mit Dezimalpunkt erreicht. Mit precision(6) wird die Anzahl der Nachkommastellen auf sechs festgelegt. Um ein gleichmäßiges Bild zu erzeugen, werden folgende Nullen ausgegeben (showpoint wirkt nur in Verbindung mit fixed). Es fällt auf, dass das Programm nicht so einfach wie das in Listing 9.4 ist. Es ist auch länger.
#include <cmath> // cos(), sin() #include <iostream> #include <numbers> // π using namespace std; int main() { cout << "Grad˽˽˽˽˽˽sin(x)˽˽˽˽˽˽cos(x)\n"; cout.setf(ios::showpoint | ios::fixed, ios::floatfield); cout.precision(6); for (int grad = 0; grad <= 90; grad += 10) { const double rad = grad / 180.0 * numbers::pi; // Grad in Bogenmaß umwandeln cout.width(4); cout << grad; cout.width(12); cout << sin(rad); cout.width(12); cout << cos(rad) << ’\n’; } }
9.5 | Formatierung mit Manipulatoren |
Manipulatoren sind Operationen, die direkt in die Ausgabe oder Eingabe zur Erledigung bestimmter Funktionen, zum Beispiel zur Formatierung, eingefügt werden. In diesem Abschnitt werden Manipulatoren nur für die Ausgabe beschrieben, das Prinzip lässt sich jedoch gleichermaßen auch für die Eingabe anwenden. Um eine int-Zahl ungepuffert oktal auszugeben, schreibt man:
Die Wirkung ist genauso, als ob ein Flag zur Formatänderung gesetzt worden wäre:
Sowohl oct als auch endl sind Manipulatoren. endl haben wir bereits kennengelernt. Wie funktioniert so ein Manipulator? Es gibt spezielle überladene Formen des Ausgabeoperators und verschiedene Funktionen, zum Beispiel oct() und endl():
Der überladene Operator operator<<() erwartet einen Parameter vom Typ »Zeiger auf eine Funktion, die eine Referenz auf einen ostream als Parameter hat und eine Referenz auf einen ostream als Ergebnis zurückliefert«. Die Funktion endl() erfüllt genau dieses Kriterium und die Funktion oct() entsprechend für die Klasse ios. Es werden in der einfachen Schreibweise
Funktionsnamen, also Zeiger auf eine Funktion, übergeben. Der Operator führt diese Funktionen aus, sodass die Einstellung auf die oktale Zahlenbasis beziehungsweise die Ausgabe einer neuen Zeile bewirkt werden. Die uns nicht vorliegende Implementierung zum Beispiel von endl() könnte wie folgt aussehen:
Der Ausgabeoperator ruft die übergebene Funktion auf:
Die Anweisung cout << zahl << endl; wird ausgewertet zu:
In Kenntnis dieses Mechanismus können Sie nun selbst Manipulatoren schreiben! Aber ehe Sie loslegen, schauen Sie sich erst die bereits vorhandenen an (Tabellen 9.4 bis 9.6). Einige Manipulatoren sind in den Headern <ios> und <iostream> deklariert, andere (die mit Parametern) jedoch in <iomanip> (iomanip = input output manipulator). Einschließen von <iostream> impliziert Inkludieren von <ios>.
Name |
Bedeutung |
boolalpha |
true/false alphabetisch ausgeben oder lesen |
noboolalpha |
true/false numerisch (1/0) ausgeben oder lesen |
showbase |
Basis anzeigen |
noshowbase |
keine Basis anzeigen |
showpoint |
folgende Nullen ausgeben |
noshowpoint |
keine folgenden Nullen ausgeben |
showpos |
+ bei positiven Zahlen anzeigen |
nowshowpos |
kein + bei positiven Zahlen anzeigen |
skipws |
Zwischenraumzeichen ignorieren |
noskipws |
Zwischenraumzeichen berücksichtigen |
uppercase |
E,X statt e,x |
nouppercase |
e,x statt E,X |
unitbuf |
Puffer nach jeder Ausgabe leeren |
nounitbuf |
Ausgabe puffern |
adjustfield: |
|
internal |
zwischen Vorzeichen und Wert auffüllen |
left |
linksbündige Ausgabe |
right |
rechtsbündige Ausgabe |
basefield: |
|
dec |
dezimal |
oct |
oktal |
hex |
hexadezimal |
floatfield: |
|
fixed |
Gleitkomma-Format |
scientific |
Exponential-Format |
Der Parameter intl in Tabelle 9.5 gibt an, ob die internationalen Symbole wie EUR oder USD verwendet werden sollen. Der Typ MT ist entweder long double oder eine Spezialisierung von basic_string, also zum Beispiel string.
Manipulatoren sind sehr einfach anzuwenden – auch wenn die Erklärung ihrer Wirkungsweise vielleicht nicht so einfach wie die Anwendung ist. Die Beispiele mit cout.precision() und cout.width() können umgeschrieben werden:
Name |
Bedeutung |
resetiosflags(ios::fmtflags M) |
Flags entsprechend Bitmaske M zurücksetzen |
setiosflags(ios::fmtflags M) |
Flags entsprechend M setzen |
setbase(int B) |
Basis 8, 10 oder 16 definieren |
setfill(char c) |
Füllzeichen festlegen |
setprecision(int n) |
Fließkommaformat (siehe Seite 435) |
setw(int w) |
Weite setzen (entspricht width()) |
get_money(MT& m, bool intl = false) |
Geldobjekt formatiert eingeben |
put_money(const MT& m, bool intl = false) |
Geldobjekt formatiert ausgeben |
quoted(const char* s, char delim=char(’"’), char escape=char(’\\’)) |
siehe Seite 441 |
quoted(string& s, ...) |
restliche Parameter wie vorstehend |
quoted(const string& s, ...) |
restliche Parameter wie vorstehend |
Name |
Bedeutung |
Typ |
endl |
neue Zeile ausgeben |
ostream& |
ends |
Nullzeichen (’\0’) ausgeben |
ostream& |
flush |
Puffer leeren |
ostream& |
ws |
Zwischenraumzeichen aus der Eingabe entfernen |
istream& |
Das Ergebnis ist: 000999 1234.5679 (Annahme: fixed und scientific sind nicht gesetzt). Ist Ihnen etwas aufgefallen? Wie die Syntax zeigt, ist ein Manipulator mit Parameter(n) kein Zeiger auf eine Funktion, sondern ein Funktionsaufruf. Die Autoren der iostream-Bibliothek konnten den Operator << nicht für alle nur denkbaren Fälle von Funktionen mit Parametern überladen. Daher wurde folgender Ausweg gewählt: Die aufgerufene Funktion muss ein Objekt einer compilerspezifischen Klasse, hier nur als Beispiel omanip genannt, zurückgeben, das vom Ausgabeoperator verarbeitet werden kann. Im Header <iomanip> sind diese Klasse und ein friend-Operator << etwa der folgenden oder einer ähnlichen Art zu finden:
Ein omanip-Objekt hat zwei private Variablen: funktPtr ist ein Zeiger auf eine Funktion, die eine Referenz auf einen ostream zurückgibt und einen Parameter des Typs ostream& sowie ein Objekt des Typs T erwartet. In der Regel wird das Objekt vom Typ int oder char sein. Die zweite Variable arg enthält das Objekt vom Typ T. Der Konstruktor initialisiert beide Variablen, die ihm als Parameter übergeben werden. Ferner finden Sie in <iomanip> eine Funktion (zum Beispiel) setprecision(). Die Funktion setprecision() gibt ein Objekt vom Typ omanip zurück:
<int> rührt daher, dass die Klasse omanip als Template deklariert ist, um Manipulatorfunktionen mit verschiedenen Parametertypen zu erlauben. Dem Konstruktor eines omanip-Objekts wird die Adresse einer Funktion (precision) und der Wert p übergeben. Bei der Konstruktion dieses Objekts anlässlich der return-Anweisung werden diese Daten als Elementdaten abgelegt. Der Operator << ruft die im omanip-Objekt referenzierte Funktion auf, die wiederum die ostream-Funktion precision() aufruft, die uns ja schon bekannt ist (Seite 435). Solcherart Objekte werden auch Funktionsobjekte genannt, eine Variante der in Abschnitt 8.6 beschriebenen Funktoren. Grund: Über den Weg der Erzeugung eines Objekts wird eine Funktion aufgerufen, wenn auch nicht mit operator()(), sondern innerhalb operator<<() über einen Funktionszeiger. Entsprechend zu der Klasse omanip für die Ausgabe gibt es die Klasse imanip für die Eingabe und die Klasse smanip für ios-Funktionen. Diese Namen werden jedoch nicht vom C++-Standard vorgeschrieben.
Die Manipulatoren der Tabelle 9.5 für Geld sollen die Ein- bzw. Ausgabe entsprechend den nationalen Konventionen steuern. Die Ausgabe des folgenden Programms ist im Kommentar angegeben. Die Konsole muss dabei auf UTF8 eingestellt sein und das System muss die verwendeten locales unterstützen. get_money() dient zum Einlesen von Geldformaten.
#include <exception> #include <iomanip> #include <iostream> #include <string> using namespace std; int main() { try { cout << showbase; // Währungsymbol anzeigen long money{12345}; // Betrag in Cent, Typ long cout.imbue(locale("de_DE.utf8")); // siehe Kapitel 30 cout << put_money(money) << ’\n’; // 123,45 e cout << put_money(money, true) << ’\n’; // 123,45 EUR string money_str{"12345"}; // Betrag in Cent, Typ string cout.imbue(locale("en_US.utf8")); // siehe Kapitel 30 cout << put_money(money_str) << ’\n’; // $123.45 cout << put_money(money_str, true) << ’\n’; // USD 123.45 } catch (exception& ex) { cerr << ex.what() << "\nlocale˽wird˽von˽diesem˽System˽nicht˽unterstützt\n"; } }
Bei der Verarbeitung von Strings mit Leer- und Anführungszeichen sind die Ein- und Ausgabe nicht symmetrisch. Das heißt, dass ein eingelesener String nicht genau so wieder ausgegeben wird. Der Grund liegt im Verhalten der <<- und >>-Operatoren. Der Eingabeoperator liest bis zum nächsten Zwischenraumzeichen (englisch whitespace). Ein auszugebendes Anführungszeichen muss mit einem Backslash \ maskiert werden. Das führt zu Problemen bei der automatisierten Verabeitung von Text, wenn der zu analysierende Text genauso (oder nur wenig verändert) wieder ausgegeben werden soll. Ein Beispiel:
Die Ausgabe ist abc "def" ijk. Im Vergleich zum Original fehlen die Backslashes. Bei der Eingabe ist die Veränderung noch gravierender: Wird "abc \"def\" ijk" eingegeben, ist die Ausgabe nur "abc:
Um die Verarbeitung von solchen Strings zu erleichtern, wurde der quoted-Manipulator eingeführt. Die Schnittstelle ist:
s kann auch vom Typ string& oder const string& sein. Anstelle des üblichen Anführungszeichens kann ein anderes Zeichen gewählt werden; das gilt entsprechend für den Backslash. Mit dem quoted-Manipulator bleiben Anführungszeichen und Backslashes erhalten (Auszug aus cppbuch/k9/manipulator/quoted.cpp):
Dies ist der einfachste Fall. Es muss nur eine Funktion mit der passenden Schnittstelle geschrieben werden. Der Manipulator endl von Seite 437 ist ein gutes Beispiel dafür.
Es gibt zwei Wege, eigene Manipulatoren zu schreiben. Der eine Weg führt über den beschriebenen Ansatz: Funktionen, die ein omanip-Objekt zurückgeben und sich auf den dazugehörenden Ausgabeoperator verlassen. Dieser Weg hat den gravierenden Nachteil, dass man sich auf nicht standardisierte Klassennamen verlassen muss. Deswegen wird hier nur der zweite Weg, die Realisierung mit einem Funktor, vorgeschlagen. Die Header-Datei enthält die Definition des Manipulators:
#ifndef LEERZEILEN_H #define LEERZEILEN_H #include <iostream> class Leerzeilen { public: Leerzeilen(int i = 1) : anzahl{i} {} std::ostream& operator()(std::ostream& os) const { for (int i = 0; i < anzahl; ++i) { os << ’\n’; } return os.flush(); } private: int anzahl; }; inline std::ostream& operator<<(std::ostream& os, const Leerzeilen& leerz) { return leerz(os); // Funktoraufruf } #endif
Ein Funktor ist ein Objekt, das wie eine Funktion behandelt werden kann, wie Sie von Seite 396 wissen. Das Objekt kann beliebige Daten mit sich tragen. Dazu benötigt man nur noch einen mit der Klasse des Funktors überladenen Ausgabeoperator. Dies soll an dem einfachen Beispiel eines Manipulators Leerzeilen(int z), der z Leerzeilen ausgibt, gezeigt werden. Eine mögliche Anwendung:
#include "Leerzeilen.h" int main() { std::cout << Leerzeilen(25) << "Ende\n"; // Konstruktoraufruf }
Die Anwendung würde den Bildschirm durch Ausgabe von 25 Leerzeilen löschen und dann den Text Ende ausgeben. Der Ablauf in main() umfasst mehrere Schritte:
1. Zunächst wird ein Objekt vom Typ Leerzeilen konstruiert, das mit 25 als Argument initialisiert wird.
2. Der Ausdruck cout << Leerzeilen(25) wird vom Compiler in die Langform operator<<(cout, Leerzeilen(25)) umgewandelt. Daran ist zu sehen, dass das erzeugte Objekt als Parameter an den überladenen Operator weitergereicht wird.
3. Innerhalb des Ausgabeoperators wird der Funktionsoperator der Klasse Leerzeilen aufgerufen. Die Schreibweise leerz(os) wird vom Compiler zu leerz.operator()(os) umgewandelt (leerz ist ein Funktor). Damit ist der Ausgabestrom (hier cout) innerhalb der Operatorfunktion bekannt und es kann die gewünschte Zahl von Leerzeilen ausgegeben werden.
4. Durch das per Referenz zurückgegebene ostream-Objekt ist eine Verkettung mit weiteren Operatoren denkbar.
Der Vorteil des Einsatzes von Funktoren liegt in der Einfachheit und darin, dass bei entsprechender Gestaltung eine beliebige Zahl von Parametern möglich ist.
9.6 | Fehlerbehandlung |
Bei der Ein- und Ausgabe können natürlich Fehler auftreten. Zur Erkennung und Behandlung von Fehlern stehen verschiedene Funktionen zur Verfügung. Ein Fehler wird durch das Setzen eines Status-Bits markiert, das durch den Aufzählungstyp iostate der Klasse ios_base (und damit auch ios) definiert wird. Die tatsächlichen Bitwerte sind implementationsabhängig. Der Stream ist nicht mehr benutzbar, falls badbit gesetzt ist.
Tabelle 9.7 zeigt die Elementfunktionen, die auf die Statusbits zugreifen.
Funktion |
Ergebnis |
iostate rdstate() |
aktueller Status |
bool good() |
wahr, falls »gut«, d.h. rdstate() == 0 |
bool eof() |
wahr, falls Dateiende |
bool fail() |
wahr, falls failbit oder badbit gesetzt |
bool bad() |
wahr, falls badbit gesetzt |
void clear() |
Status auf goodbit setzen |
void clear(iostate s) |
Status auf s setzen |
void setstate(iostate) |
einzelne Statusbits setzen |
Das folgende Demonstrationsprogramm zeigt die Anwendung der Funktionen zur Diagnose und Behebung von Syntaxfehlern beim Einlesen von int-Zahlen. Es wird dabei angenommen, dass beliebig oft int-Zahlen i eingelesen und angezeigt werden sollen, wobei vorher auftretende falsche Zeichen zu ignorieren sind.
#include <iostream> using namespace std; int main() { ios::iostate status {}; while (true) { // Schleifenabbruch mit break int i {0}; cout << "Zahl˽(Ctrl+D˽oder˽Ctrl+Z˽=˽Ende):"; cin >> i; status = cin.rdstate(); // Ausgabe der Statusbits cout << "status˽=˽" << status << ’\n’; cout << "good()˽=˽" << cin.good() << ’\n’; cout << "eof()˽=˽" << cin.eof() << ’\n’; cout << "fail()˽=˽" << cin.fail() << ’\n’; cout << "bad()˽=˽" << cin.bad() << ’\n’; if (cin.eof()) { break; // Abbruch } // Fehlerbehandlung bzw. Ausgabe if (status) { cin.clear(); // Fehlerbits zurücksetzen cin.get(); // ggf. fehlerhaftes Zeichen entfernen } else cout << "***˽" << i << ’\n’; } }
Die Funktion void clear(iostate statuswort = goodbit) erlaubt es, den Status zu setzen. Beispielsweise kann das badbit bei Erhaltung der anderen Bits mit
gesetzt werden. Die Eingabe von Buchstaben erfüllt nicht die Syntax von int-Zahlen, was mit fail() angezeigt wird. Die Tastenkombination oder (systemabhängig) liefert einen Wert ungleich 0 für eof() und führt zum Verlassen der Schleife.
Die Klasse ios_base::failure ist die Basisklasse für alle Exceptions, die von Funktionen der Iostream-Bibliothek geworfen werden. Da die Klasse ios von ios_base erbt, kann sie kürzer ios::failure genannt werden. ios::failure erbt von system_error. Man kann sie in eigenen Programmen selbst werfen und auswerten.
9.7 | Typumwandlung von Dateiobjekten nach bool |
Die Abfrage, ob eine Datei geöffnet werden kann, wird über eine if-Abfrage etwa der folgenden Art gelöst:
Ob das Ende einer Datei erreicht worden ist, kann in einer Bedingung wie folgt festgestellt werden:
Wie funktioniert dieser geheimnisvolle Mechanismus? Erinnern wir uns daran, dass der Compiler automatisch versucht, bei Bedarf einen nicht ganz passenden Datentyp in einen passenden umzuwandeln. Dazu kann er Typumwandlungskonstruktoren (siehe Seite 190) und eingebaute oder selbst definierte Typumwandlungsoperatoren (siehe Seite 389) benutzen. Zur Klasse ios gibt es einen vordefinierten Typumwandlungsoperator, der ein Dateiobjekt oder eine Referenz darauf in einen Wert vom Typ bool umwandelt. Letztlich wird der Wert von fail() zurückgegeben. Die Anweisung while (cin) cin.get(c); wird interpretiert als:
Die Anweisung if (!cin.get(c)) {...} entspricht:
9.8 | Arbeit mit Dateien |
Die Arbeit mit Dateien ist teilweise aus Kapitel 1.10 bekannt. In diesem Abschnitt finden sich einige Ergänzungen. Zum Öffnen einer Datei wird ein bestimmter Modus angegeben (englisch openmode) wie zum Beispiel ios::binary. Tabelle 9.8 zeigt die möglichen Werte.
Modus |
Bedeutung |
app |
bei jedem Schreiben Daten an die Datei anhängen |
ate |
sofort nach dem Öffnen an das Dateiende springen |
binary |
keine Umwandlung verschiedener Zeilenendekennungen |
in |
zur Eingabe öffnen |
out |
zur Ausgabe öffnen |
trunc |
vorherigen Inhalt der Datei löschen |
Bestimmte Werte sind streamabhängig voreingestellt. Beispiel: Bei einem ifstream-Objekt ist ios::in voreingestellt. Die verschiedenen Öffnungsarten können durch Verknüpfung mit dem Oder-Operator kombiniert werden. Tabelle 9.9 zeigt die Kombinationen der Kürze halber ohne binary und ate, die zusätzlich gesetzt werden können. Die in den beiden rechten Spalten genannten Aktionen ändern sich dadurch nicht.
in |
out |
trunc |
app |
die Datei existiert |
die Datei existiert nicht |
• |
vom Anfang lesen |
Öffnen scheitert |
|||
• |
Inhalt wird zerstört |
Datei wird angelegt |
|||
• |
• |
Inhalt wird zerstört |
Datei wird angelegt |
||
• |
am Ende schreiben |
Datei wird angelegt |
|||
• |
• |
am Ende schreiben |
Datei wird angelegt |
||
• |
• |
vom Anfang lesen |
Fehler |
||
• |
• |
• |
Inhalt wird zerstört |
Datei wird angelegt |
|
• |
• |
lesen und am Ende schreiben |
Datei wird angelegt |
||
• |
• |
• |
lesen und am Ende schreiben |
Datei wird angelegt |
Die Wirkung von out und out|trunc ist gleich, weil sie der Voreinstellung für out entspricht. app bewirkt in jedem Fall, dass Schreiben nur am Ende möglich ist. Zwei Beispiele für die Anwendung der Dateiöffnungsart:
9.8.1 | Positionierung in Dateien |
Manchmal ist es wünschenswert, eine Datei nicht nur sequenziell zu lesen oder zu schreiben, sondern die Position frei zu bestimmen. Dazu gehört auch, die aktuelle Position zu ermitteln. Tabelle 9.10 zeigt die vorhandenen Funktionen. Die Endung »g« steht für »get« (= zu lesende Datei) und »p« steht für »put« (= zu schreibende Datei).
Rückgabetyp |
Funktion |
Bedeutung |
ios::pos_type |
tellg() |
aktuelle Leseposition |
ios::pos_type |
tellp() |
aktuelle Schreibposition |
istream& |
seekg(p) |
absolute Position p aufsuchen |
istream& |
seekg(r, Bezug) |
relative Position r aufsuchen |
ostream& |
seekp(p) |
absolute Position p aufsuchen |
ostream& |
seekp(r, Bezug) |
relative Position r aufsuchen |
Bezug in Tabelle 9.10 kann einen von drei möglichen Werten annehmen:
ios::beg |
relativ zum Dateianfang |
ios::cur |
relativ zur aktuellen (englisch current) Position |
ios::end |
relativ zum Dateiende |
Das folgende Programmfragment zeigt eine Anwendung:
ifstream einIfstream; einIfstream.open("seek.dat", ios::binary); einIfstream.seekg(9); // absolute Leseposition 9 suchen (Zählung ab 0) char c; einIfstream.get(c); // an Pos. 9 lesen, get schaltet Position um 1 weiter einIfstream.seekg(2, ios::cur); // 2 Positionen weitergehen ios::pos_type position = einIfstream.tellg(); // akt. Position merken // ... einIfstream.seekg(-4, ios::end); // 4 Positionen vor dem Ende // ... einIfstream.seekg(position, ios::beg); // zur gemerkten Position gehen
Daraus ergibt sich, dass die relative Positionierung zum Dateianfang redundant ist, d. h. seekg(x, ios::beg) ist dasselbe wie seekg(x);
9.8.2 | Lesen und Schreiben in derselben Datei |
Wenn eine Datei als Datenbasis genutzt wird, ist es interessant, Daten zu lesen und geänderte Daten in derselben Datei zu aktualisieren. Für diesen Zweck gibt es die Klasse fstream, die von der Klasse iostream erbt, die wiederum von den Klassen istream und ostream abgeleitet ist. Damit hat fstream die Eigenschaft, sowohl für die Ein- als auch für die Ausgabe geeignet zu sein. Alle Klassen, die mit Dateien arbeiten, benutzen Puffer. In fstream wird derselbe Pufferspeicher zum Lesen und zum Schreiben benutzt. Ein einfaches Programm zeigt die Nutzung eines fstream, bei dem die Funktionen zum Aufsuchen von Positionen naturgemäß eine starke Rolle spielen.
#include <fstream> #include <iostream> #include <string> using namespace std; int main() // Lesen und Schreiben derselben Datei { fstream filestream("fstream2.dat", ios::out | ios::trunc); // Datei anlegen filestream.close(); // leere Datei existiert jetzt // Datei zum Lesen und Schreiben öffnen: filestream.open("fstream2.dat", ios::in | ios::out); for (int i = 0; i < 20; ++i) { filestream << i << ’˽’; // schreiben } filestream << ’\n’; filestream.seekg(0); // Anfang zum Lesen suchen while (filestream.good()) { int i {}; // Hilfsvariable filestream >> i; // lesen if (filestream.good()) { cout << i << ’˽’; // Kontrollausgabe } else { cout << "\nDateiende˽erreicht˽(oder˽Lesefehler)"; } } cout << ’\n’; // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 filestream.clear(); // EOF-Status löschen filestream.seekp(5); // Position 5 suchen filestream << "neuer˽Text˽"; // ab Pos. 5 überschreiben filestream.seekg(0); // Anfang zum Lesen suchen string buf {}; getline(filestream, buf); // Zeile lesen cout << buf << ’\n’; // 0 1 2neuer Text 9 10 11 12 13 14 15 16 17 18 19 }
9.9 | Umleitung auf Strings |
Die Ausgabe kann mithilfe von ostringstream-Objekten (output string stream) auf Strings umgeleitet werden. Dies kann sinnvoll sein, wenn die Ausgabe nicht unmittelbar erfolgen soll. Ähnliches gilt auch für das Lesen aus Strings anstelle einer Datei (istringstream). stringstream-Klassen sind im Header <sstream> deklariert. Hier sei beispielhaft nur die Ausgabe behandelt. Listing 9.14 zeigt ein einfaches Beispiel. Es zeigt auch, dass diese Art der Umleitung mit format() überflüssig geworden ist.
#include <format> #include <iostream> #include <sstream> using namespace std; int main() { ostringstream wandler; // Hineinschreiben. ends liefert ein abschließendes Nullzeichen wandler << "Ergebnis˽=˽" << 12345314159.26 << ends; cout << wandler.str() << ’\n’; // als String auslesen // Einfacher geht es mit format(): cout << format("Ergebnis˽=˽{:g}\n", 12345314159.26); }
Mit einem istringstream-Objekt kann aus einem String gelesen werden, ein stringstream-Objekt erlaubt Lesen und Schreiben.
9.10 | Formatierte Daten lesen |
Die Art des Einlesens hängt von der Formatierung ab und auch von den Objekten, die die Daten aufnehmen sollen. Es bieten sich zwei verschiedene Möglichkeiten an, von denen die zweite vorgestellt wird:
Einlesen einer Zeichenkette, die dann ausgewertet wird
Überladen des Eingabeoperators >> für benutzerdefinierte Datentypen
9.10.1 | Eingabe benutzerdefinierter Typen |
C++ ermöglicht die Eingabe benutzerdefinierter Datentypen, indem der Eingabeoperator >> überladen wird. Ebenso wie beim Überladen des Ausgabeoperators muss der erste Parameter eine Referenz auf den Stream (hier also istream) sein, um eine Hintereinanderschaltung zu erlauben. Die Analogie zum auf Seite 371 behandelten Ausgabeoperator << liegt auf der Hand. An dieser Stelle wird ein Eingabeoperator für die uns schon bekannte Klasse Datum angegeben. Es soll möglich sein, ein Datum im Format Tag⋄Monat⋄Jahr einzugeben, wobei das Trennzeichen ⋄ entweder ein Punkt (.) oder ein Schrägstrich (/) sein darf. Listing 9.15 zeigt ein Beispiel.
#include "datumeingabeoperator.h" using namespace std; int main() { Datum einDatum; try { cout << "Bitte˽Datum˽eingeben:˽"; cin >> einDatum; cout << "Eingegeben˽wurde˽der˽" << einDatum << ".\n"; } catch (const std::runtime_error& e) { cerr << e.what() << "˽Abbruch!" << ’\n’; } }
Die Datei datumeingabeoperator.h definiert die Schnittstelle:
#ifndef DATUMEINGABEOPERATOR_H #define DATUMEINGABEOPERATOR_H #include <Datum.h> #include <iostream> std::istream& operator>>(std::istream& eingabe, Datum& d); #endif
Um das Programm zu realisieren, wird der >>-Operator als freie Funktion deklariert. Ein möglicher Typumwandlungsoperator zur Umwandlung eines Datum-Objekts in einen String muss explicit sein oder durch eine Methode toString() ersetzt werden, damit der Compiler nicht versucht, operator>>(istream&, string&) zu benutzen.
#include "datumeingabeoperator.h" // Einleseoperator für ein Datum. Erlaubte Formate: Tag.Monat.Jahr oder // Tag/Monat/Jahr std::istream& operator>>(std::istream& eingabe, Datum& d) { char c{’\0’}; int tag{0}; int monat{0}; int jahr{0}; eingabe >> tag >> c; // Tag und 1. Trennzeichen if (c != ’.’ && c != ’/’) { eingabe.setstate(std::ios::failbit); // Status setzen } else { eingabe >> monat >> c; // Monat und 2. Trennzeichen if (c != ’.’ && c != ’/’) { eingabe.setstate(std::ios::failbit); // Status setzen } else { eingabe >> jahr; try { Datum temp(tag, monat, jahr); d = temp; } catch (const UngueltigesDatumException& err) { eingabe.setstate(std::ios::failbit); } } } if (!eingabe.good()) { throw UngueltigesDatumException("˽ungueltige˽Eingabe!"); } return eingabe; }
Die Variable c wird mit 0 initialisiert, damit sie nicht zufällig einen der erlaubten Werte annimmt, wenn die Eingabe von tag fehlschlagen sollte. Eine falsche Syntax beim Einlesen, zum Beispiel wenn ein Buchstabe statt einer Zahl eingegeben wird, bewegt das C++-Laufzeitsystem zum Setzen des failbit. Per Programm führt ein falsches Trennzeichen oder ein ungültiges Datum ebenfalls zu einem failbit-Fehler. In all diesen Fällen wird eine entsprechende Exception geworfen.
9.11 | Blockweise lesen und schreiben |
Das Beispiel auf Seite 259 zeigt bereits das binäre Schreiben eines C-Arrays als Block. Vermutlich geht es aber häufiger darum, ein vector- oder array-Objekt möglichst effizient in eine Datei zu schreiben oder aus einer Datei zu lesen. Damit befasst sich dieser Abschnitt. Im Folgenden werden Lesen und Schreiben zur einfacheren Verwendung in jeweils eine einfach aufzurufende Funktion gepackt. Der Template-Parameter T steht für den Typ eines Array-Elements. falls eine Datei nicht geöffnet werden kann, wird eine Exception des Typs ios::failure geworfen, die Auskunft über die Stelle gibt, an der der Fehler auftrat. Der Fehler kann darin bestehen, in eine schreibgeschützte Datei schreiben zu wollen oder zu versuchen, eine nicht existierende Datei zu lesen.
Hinweis
Die Funktionen dieses Abschnitts sind nur für Arrays und Vektoren geeignet, deren Elemente keine Zeiger enthalten. Von den Zeigern referenzierte Objekte würden nicht mitkopiert werden. Die gespeicherten Werte von Zeigern wären beim Einlesen bedeutungslos.
9.11.1 | vector-Objekt binär lesen und schreiben |
Um zu sehen, wie das Ganze funktioniert, zeigt Listing 9.18 nicht nur die Funktionen zum Lesen und Schreiben, sondern auch ein main-Programm dazu. Zur Lokalisierung von Fehlern wird das von Seite 144 bekannte source_location verwendet. Dem failure-Konstruktor wird ein String aus Dateiname und der Zeilennummer übergeben.
Die Klasse vector hat eine Methode data(), die einen Zeiger auf das erste Element zurückgibt. Damit kann der Datenbereich eines Vektors wie ein C-Array an die Funktion write() übergeben werden. Die Anzahl der Bytes errechnet sich aus dem Produkt der Anzahl der Elemente des Vektors mit der Größe eine Elements.
#include <cassert> #include <fstream> #include <iostream> #include <source_location> #include <string> #include <system_error> #include <vector> template <typename T> void writeVector(const std::string& filename, const std::vector<T>& v) { std::ofstream dest(filename, std::ios::binary); if (!dest) { const auto& wo {std::source_location::current()}; throw std::ios::failure(std::string(wo.file_name()) + ",˽Zeile˽" + std::to_string(wo.line())); } dest.write(reinterpret_cast<const char*>(v.data()), v.size() * sizeof(T)); } template <typename T> void readVector(const std::string& filename, std::vector<T>& v) { std::ifstream source(filename, std::ios::binary); if (!source) { const auto& wo {std::source_location::current()}; throw std::ios::failure(std::string(wo.file_name()) + ",˽Zeile˽" + std::to_string(wo.line())); } source.read(reinterpret_cast<char*>(v.data()), v.size() * sizeof(T)); } int main() { try { const std::string dateiname("binaerdaten.bin"); std::vector<double> v1{1.27, 3.4, 5.678, 9.01234, 100.836}; writeVector(dateiname, v1); // v1 schreiben decltype(v1) v2(v1.size(), 0.0); // v2 mit 0 initialisieren readVector(dateiname, v2); // v2 lesen assert(v1 == v2); // auf Gleichheit prüfen } catch(const std::ios::failure& exc) { std::cerr << exc.what() << ’\n’; } }
Um das Ganze zu testen, wird in main() ein Vektor v1 in die Datei geschrieben. Dann wird ein zweiter Vektor v2 angelegt und mit Nullwerten initialisiert. Um den Datentyp nicht zu wiederholen, wird decltype verwendet. Nach dem Lesen der Datei mit readVector() wird geprüft, ob beide gleich sind. Die Größe des geschriebenen Vektors (hier v1) wird für die Deklaration des zu lesenden Vektors v2 herangezogen. Wenn diese Größe in einem anderen Fall nicht vorliegt, kann sie mithilfe der Dateigröße ermittelt werden. Die Deklaration von v2 in Listing 9.18 sähe dann so aus:
9.11.2 | array-Objekt binär lesen und schreiben |
Auch die Klasse array hat eine Funktion data(), die den Zugriff auf den Inhalt erlaubt. Um array-Objekte binär lesen und schreiben zu können, müssen nur die Funktionsparameter entsprechend angepasst werden. Im Unterschied zur Klasse vector steht die Anzahl der Elemente des Arrays schon zur Compilationszeit fest.
#include <array> #include <cassert> #include <fstream> #include <iostream> #include <string> #include <source_location> #include <system_error> template <typename T, auto N> void writeArray(const std::string& filename, const std::array<T, N>& a) { std::ofstream dest(filename, std::ios::binary); if (!dest) { const auto& wo {std::source_location::current()}; throw std::ios::failure(std::string(wo.file_name()) + ",˽Zeile˽" + std::to_string(wo.line())); } dest.write(reinterpret_cast<const char*>(a.data()), a.size() * sizeof(T)); } template <typename T, auto N> void readArray(const std::string& filename, std::array<T, N>& a) { std::ifstream source(filename, std::ios::binary); if (!source) { const auto& wo {std::source_location::current()}; throw std::ios::failure(std::string(wo.file_name()) + ",˽Zeile˽" + std::to_string(wo.line())); } source.read(reinterpret_cast<char*>(a.data()), a.size() * sizeof(T)); } int main() { try { const std::string dateiname("binaerdaten.bin"); std::array<double, 5> a1{1.2, 3.4, 5.678, 9.01234, 100.83653}; writeArray(dateiname, a1); // a1 schreiben decltype(a1) a2{}; // enthält 0.0 readArray(dateiname, a2); // a2 lesen assert(a1 == a2); // auf Gleichheit prüfen // Rest folgt unten
9.11.3 | Matrix binär lesen und schreiben |
Ein array-Objekt wird vollständig auf dem Stack angelegt. Es hat keinerlei dynamische Anteile. Deshalb muss die Größe zur Compilationszeit vollständig bekannt sein. Das erlaubt es, zwei- oder mehrdimensionale Matrizen auf array-Basis mit denselben Funktionen zu lesen und zu schreiben. Das Listing 9.20 zeigt einen weiteren Teil des main()-Programms aus Listing 9.19. Es werden dieselben Funktionen benutzt. Der Unterschied ist, dass der Datentyp T in der Funktionsparameterliste selbst ein Array ist. Sein Typ ist array<double, 3>.
const std::string dateiname1("binaerdatenMatrix.bin"); std::array<std::array<double, 3>, 2> am1 // zweidimensionale Matrix {{ {1.1, 2.2, 3.3}, {4.4, 5.5, 6.6} }}; writeArray(dateiname1, am1); // am1 schreiben decltype(am1) am2{}; // enthält 0.0 readArray(dateiname1, am2); // am2 lesen assert(am1 == am2); // auf Gleichheit prüfen } catch(const std::ios::failure& exc) { std::cerr << exc.what() << ’\n’; } }
Das funktioniert nicht für eine Matrix, die einen Vektor von Vektoren zur Grundlage hat – es sei denn, man würde die Matrix-Dimensionen mit abspeichern. Der Grund ist, dass einem vector-Objekt erst zur Laufzeit Speicher zugewiesen wird. Es ist nur garantiert, dass die Daten eines eindimensionalen Vektors im Speicher nacheinander liegen. Für die innere Struktur eines Vektors von Vektoren, also zum Beispiel vector<vector<double>>, gilt das nicht. Aus diesem Grund muss man bei einer derartigen Matrix anders vorgehen. Am einfachsten ist es, so eine Matrix zeilenweise in eine Datei zu schreiben bzw. aus der Datei zu lesen. Listing 9.21 zeigt eine Möglichkeit. Eine mögliche, hier nicht realisierte Ergänzung wäre das zusätzliche Abspeichern von Zeilen- und Spaltenzahl, um beim Einlesen zu prüfen, ob das Format auch wirklich stimmt.
#include <cassert> #include <fstream> #include <iostream> #include <source_location> #include <string> #include <system_error> #include <vector>\ template <typename T> using Matrix = std::vector<std::vector<T>>; // zur Abkürzung template <typename T> void writeVectorMatrix(const std::string& filename, const Matrix<T>& matrix) { std::ofstream dest(filename, std::ios::binary); if (!dest) { const auto& wo {std::source_location::current()}; throw std::ios::failure(std::string(wo.file_name()) + ",˽Zeile˽" + std::to_string(wo.line())); } for (const auto& zeile : matrix) { // zeilenweise schreiben dest.write(reinterpret_cast<const char*>(zeile.data()), zeile.size() * sizeof(T)); } } template <typename T> void readVectorMatrix(const std::string& filename, Matrix<T>& matrix) { std::ifstream source(filename, std::ios::binary); if (!source) { const auto& wo {std::source_location::current()}; throw std::ios::failure(std::string(wo.file_name()) + ",˽Zeile˽" + std::to_string(wo.line())); } for (auto& zeile : matrix) { // zeilenweise lesen source.read(reinterpret_cast<char*>(zeile.data()), zeile.size() * sizeof(T)); } } int main() { try { const std::string dateiname("binaerdatenMatrix.bin"); Matrix<double> mat1 { {1.1, 2.2, 3.3}, {4.4, 5.5, 6.6} }; writeVectorMatrix(dateiname, mat1); // mat1 schreiben // neue Matrix mat2 in der richtigen Größe anlegen: Matrix<double> mat2(mat1.size(), // Zeilenzahl std::vector<double>(mat1[0].size())); // damit vorbesetzen readVectorMatrix(dateiname, mat2); // mat2 lesen assert(mat1 == mat2); // auf Gleichheit prüfen } catch(const std::ios::failure& exc) { std::cerr << exc.what() << ’\n’; } }
1 Dieser Abschnitt kann wegen des Bezugs auf folgende Kapitel beim ersten Lesen übersprungen werden.