vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 3

Tag 16



Streams

Bisher haben Sie mit cout auf den Bildschirm geschrieben und mit cin von der Tastatur eingelesen, ohne eine genaue Vorstellung davon zu haben, was sich dahinter verbirgt. Heute lernen Sie,

Streams - ein Überblick

Wie man in C++ Daten auf dem Bildschirm ausgibt oder in eine Datei schreibt oder wie Daten in ein Programm eingelesen werden, ist nicht in der Sprachspezifikation festgelegt. Dies fällt vielmehr in den Bereich der Arbeit mit C++. Dafür enthält die C++-Standardbibliothek die iostream-Bibliothek, mit der die Ein- und Ausgabe (E/A) erleichtert wird.

Der Vorteil, die Ein- und Ausgabe von der Sprache zu trennen und in Bibliotheken unterzubringen, liegt darin, daß die Sprache so leichter »plattformunabhängig« gemacht werden kann. Für Sie bedeutet dies, daß C++-Programme, die auf einem PC geschrieben wurden, nach erneuter Kompilierung auf einer Sun-Workstation ausgeführt werden können. Der Compiler-Hersteller stellt die entsprechende Bibliothek bereit und alles läuft problemlos. Soweit zumindest die Theorie.

Eine Bibliothek ist eine Sammlung von .obj-Dateien, die in Ihr Programm eingebunden werden können und ihm zusätzliche Funktionalität verleihen. Dies ist die einfachste Form der Wiederverwertung von einem Code. Es gibt sie, seit die Programmierer begonnen haben Einsen und Nullen in die Wände ihrer Höhlen zu meißeln.

Kapselung

Für die iostream-Klassen ist der Datenfluß von Ihrem Programm zum Bildschirm ein Strom (engl. stream) von Daten, bei dem ein Byte dem anderen folgt. Ist das Ziel des Stream eine Datei oder der Bildschirm, ist die Quelle in der Regel Teil Ihres Programms. Wird die Richtung des Stream umgekehrt, können die Daten von der Tastatur oder einer Datei kommen und »ergießen« sich in Ihren Datenvariablen.

Eines der Hauptziele von Streams ist es, den Datenaustausch mit der Festplatte oder dem Bildschirm zu kapseln. Nachdem ein Stream erzeugt wurde, arbeitet das Programm nur noch mit diesem Stream, dem die Verantwortung für den korrekten Datentransfer obliegt. Den Grundgedanken dahinter entnehmen Sie Abbildung 16.1.

Abbildung 16.1:  Kapselung durch Streams

Pufferung

Auf die Festplatte (und im geringeren Maße auch auf den Bildschirm) zu schreiben ist sehr »teuer«. Im Vergleich zu anderen Operationen dauert das Schreiben von Daten auf die Festplatte oder das Lesen von der Festplatte ziemlich lange, und die Programmausführung wird für diese Zeitdauer im allgemeinen angehalten. Um dieses Problem zu umgehen, werden Streams »gepuffert«. Die Daten werden in den Stream geschrieben, der jedoch noch nicht sofort auf die Platte zurückgeschrieben wird. Statt dessen füllt sich der Streampuffer stetig, und wenn er voll ist, schreibt er seinen Inhalt auf einmal auf die Platte.

Stellen Sie sich vor, daß Wasser oben in ein Becken läuft und dieses kontinuierlich füllt. Es läuft jedoch unten kein Wasser ab. Sehen Sie dazu Abbildung 16.2.

Abbildung 16.2:  Den Puffer füllen

Wenn das Wasser (sprich: die Daten) den oberen Rand erreicht hat, öffnet sich unten das Ventil und das Wasser fließt in einem Rutsch ab. Abbildung 16.3 soll dieses illustrieren.

Abbildung 16.3:  Den vollen Puffer leeren

Nachdem der Puffer geleert ist, wird das Ventil am Boden wieder geschlossen und neues Wasser fließt in das Wasserbecken. (Abbildung 16.4).

Abbildung 16.4:  Den Puffer erneut füllen

Ab und zu müssen Sie das Wasser aus dem Becken ablassen, auch wenn es noch nicht voll ist. Dies nennt man auch »den Puffer leeren«. (Abbildung 16.5).

Abbildung 16.5:  Den teilweise gefüllten Puffer leeren

Streams und Puffer

Wie nicht anders zu erwarten, werden in C++ Streams und Puffer objektorientiert implementiert.

Standard-E/A-Objekte

Wenn ein C++-Programm, das die iostream-Klassen einbindet, ausgeführt wird, werden vier Objekte erzeugt und initialisiert.

Die iostream-Klassenbibliothek wird vom Compiler automatisch in Ihr Programm mit eingebunden. Alles, was Sie machen müssen, ist, die entsprechende include-Anweisung an den Anfang Ihres Programmlistings zu stellen.

Umleitung

Jedes der Standardgeräte, d.h. für Eingabe, Ausgabe und Fehler, kann auf ein anderes Gerät umgeleitet werden. Die Standardfehlerausgabe wird häufig in Dateien umgeleitet, die Standardein- und -ausgaben können mit Hilfe von Betriebssystembefehlen in Dateien gelenkt werden.

Umleiten bedeutet, die Ausgabe (oder Eingabe) nicht an das Standardziel zu schicken, sondern zu einem anderen Ziel zu dirigieren. Die Umleitungsoperatoren für DOS und UNIX lauten < für das Umleiten der Eingabe und > für das Umleiten der Ausgabe.

Beim Piping wird die Ausgabe eines Programms als Eingabe eines anderen verwendet.

DOS liefert rudimentäre Umleitungsbefehle wie (>) und (<). Unter UNIX sind die Umleitungsmöglichkeiten wesentlich umfangreicher. Die Idee jedoch ist dieselbe: Man nehme die Ausgabe, die für den Bildschirm bestimmt ist, und schreibe sie in eine Datei oder lenke sie in ein anderes Programm. Entsprechend kann die Eingabe für ein Programm von einer Datei extrahiert und nicht von der Tastatur eingelesen werden.

Die Umleitung ist eher eine Funktion des Betriebssystems als der iostream-Bibliotheken. In C++ haben Sie nur Zugriff auf die vier Standardgeräte. Es steht dem Anwender allerdings frei, die Umleitung je nach Bedarf an ein beliebiges anderes Gerät vorzunehmen.

Eingabe mit cin

Das globale Objekt cin ist verantwortlich für die Eingabe und steht Ihrem Programm automatisch zur Verfügung, wenn Sie die Header-Datei iostream.h einbinden. In den bisherigen Beispielen haben Sie den überladenen Eingabe-Operator (>>) verwendet, um Daten in Ihren Programmvariablen aufzunehmen. Wie funktioniert das? Die Syntax, wie Sie sich vielleicht erinnern, sieht folgendermaßen aus:

int eineVariable;
cout << "Geben Sie eine Zahl ein: ";
cin >> eineVariable;

Das globale Objekt cout übergehen wir erst einmal. Es wird in diesem Kapitel weiter hinten besprochen. Konzentrieren wir uns auf die dritte Zeile: cin >> eineVariable;. Was läßt sich über cin sagen?

Offensichtlich handelt es sich um ein globales Objekt, da Sie es nicht in Ihrem Code definiert haben. Aus früheren Erfahrungen mit Operatoren können Sie schließen, daß cin den Eingabe-Operator (>>) überladen hat, mit der Folge, daß alles, was im Puffer von cin steht, in Ihre lokale Variable eineVariable geschrieben wird.

Was vielleicht nicht so deutlich zu erkennen ist: cin überlädt den Eingabe-Operator für eine Vielzahl von Parametern, unter anderem int&, short&, long&, double&, float&, char&, char* und so weiter. In unserem Beispiel cin >> eineVariable; wird der Typ von eineVariable vom Compiler geschätzt. Da es sich um einen int-Wert handelt, wird die folgende Funktion aufgerufen:

istream & operator>> (int &)

Beachten Sie, daß der Parameter als Referenz übergeben wird. Deshalb kann der Eingabe-Operator auf der originalen Variablen operieren. Listing 16.1 demonstriert die Verwendung von cin.

Listing 16.1: cin arbeitet mit unterschiedlichen Datentypen

1:     //Listing 16.1 - Zeichenstrings und cin
2:
3: #include <iostream.h>
4:
5: int main()
6: {
7: int myInt;
8: long myLong;
9: double myDouble;
10: float myFloat;
11: unsigned int myUnsigned;
12:
13: cout << "int: ";
14: cin >> myInt;
15: cout << "Long: ";
16: cin >> myLong;
17: cout << "Double: ";
18: cin >> myDouble;
19: cout << "Float: ";
20: cin >> myFloat;
21: cout << "Unsigned: ";
22: cin >> myUnsigned;
23:
24: cout << "\n\nInt:\t" << myInt << endl;
25: cout << "Long:\t" << myLong << endl;
26: cout << "Double:\t" << myDouble << endl;
27: cout << "Float:\t" << myFloat << endl;
28: cout << "Unsigned:\t" << myUnsigned << endl;
29: return 0;
30: }

int: 2
Long: 70000
Double: 987654321
Float: 3.33
Unsigned: 25

Int: 2
Long: 70000
Double: 9.87654e+08
Float: 3.33
Unsigned: 25

Die Zeilen 7 bis 11 deklarieren Variablen unterschiedlichen Typs. In den Zeilen 13 bis 22 wird der Anwender aufgefordert, Werte für diese Variablen einzugeben, und die Ergebnisse werden (mit cout) in den Zeilen 24 bis 28 ausgegeben.

Die Ausgabe zeigt, daß den Variablen der richtige Datentyp zugeordnet wurde und das Programm wie erwartet abläuft.

Strings

cin verarbeitet auch Zeiger auf Zeichenketten (char*) als Argument. Sie können also einen Zeichenpuffer erzeugen und ihn mit cin füllen. Sie könnten beispielsweise schreiben:

char IhrName[50]
cout << "Bitte geben Sie Ihren Namen ein: ";
cin >> IhrName;

Wenn Sie Jesse eingeben, wird die Variable IhrName mit den Zeichen J, e, s, s, e, \0 gefüllt. Das letzte Zeichen ist eine Null. cin beendet den String automatisch mit einem Nullzeichen. Ihr Puffer muß groß genug sein, um den gesamten String plus das Nullzeichen zu fassen. Diese Null signalisiert den Standardbibliotheksfunktionen, die in Kapitel 21 diskutiert werden, »das Ende des String«.

Probleme mit Strings

Nachdem Sie gesehen haben, wie vielseitig cin ist, werden Sie vielleicht überrascht sein, wenn Sie versuchen, einen vollständigen Namen in einen String einzugeben. Für cin ist ein Leerzeichen gleich einem Trennzeichen. Stößt cin auf ein Leerzeichen oder auf ein Zeichen für den Zeilenumbruch, geht es davon aus, daß die Eingabe für diesen Parameter abgeschlossen ist, und fügt im Falle von Strings direkt ein Nullzeichen an das Ende. Listing 16.2 veranschaulicht das Problem.

Listing 16.2: Mehr als ein Wort mit cin einlesen

1:     //Listing 16.2 - Zeichenstrings und cin
2:
3: #include <iostream.h>
4:
5: int main()
6: {
7: char YourName[50];
8: cout << "Ihr Vorname: ";
9: cin >> YourName;
10: cout << "Er lautet: " << YourName << endl;
11: cout << "Ihr voller Name: ";
12: cin >> YourName;
13: cout << "Er lautet: " << YourName << endl;
14: return 0;
15: }

Ihr Vorname: Jesse
Er lautet: Jesse
Ihr voller Name: Jesse Liberty
Er lautet: Jesse

Zeile 7 erzeugt einen Array, der die Eingabe des Anwenders aufnimmt. Zeile 8 fragt den Anwender nach einem Namen, der dann, wie die Ausgabe zeigt, ordnungsgemäß abgelegt wird.

Zeile 11 fordert den Anwender auf, diesmal den vollständigen Namen einzugeben. cin liest die Eingabe, und wenn cin auf das Leerzeichen zwischen den Namen trifft, setzt es ein Nullzeichen hinter das erste Wort und beendet die Eingabe. Dies lag jedoch nicht in Ihrer Absicht.

Um zu verstehen, warum das so ist, sollten Sie Listing 16.3 genau untersuchen, da dort die Eingabe für mehrere Felder gezeigt wird.

Listing 16.3: Mehrfacheingabe

1:     //Listing 16.3 - Zeichenstrings und cin
2:
3: #include <iostream.h>
4:
5: int main()
6: {
7: int myInt;
8: long myLong;
9: double myDouble;
10: float myFloat;
11: unsigned int myUnsigned;
12: char myWord[50];
13:
14: cout << "int: ";
15: cin >> myInt;
16: cout << "Long: ";
17: cin >> myLong;
18: cout << "Double: ";
19: cin >> myDouble;
20: cout << "Float: ";
21: cin >> myFloat;
22: cout << "Word: ";
23: cin >> myWord;
24: cout << "Unsigned: ";
25: cin >> myUnsigned;
26:
27: cout << "\n\nInt:\t" << myInt << endl;
28: cout << "Long:\t" << myLong << endl;
29: cout << "Double:\t" << myDouble << endl;
30: cout << "Float:\t" << myFloat << endl;
31: cout << "Word: \t" << myWord << endl;
32: cout << "Unsigned:\t" << myUnsigned << endl;
33:
34: cout << "\n\nInt, Long, Double, Float, Word, Unsigned: ";
35: cin >> myInt >> myLong >> myDouble;
36: cin >> myFloat >> myWord >> myUnsigned;
37: cout << "\n\nInt:\t" << myInt << endl;
38: cout << "Long:\t" << myLong << endl;
39: cout << "Double:\t" << myDouble << endl;
40: cout << "Float:\t" << myFloat << endl;
41: cout << "Word: \t" << myWord << endl;
42: cout << "Unsigned:\t" << myUnsigned << endl;
43:
44:
45: return 0;
46: }

Int: 2
Long: 30303
Double: 393939397834
Float: 3.33
Word: Hello
Unsigned: 85

Int: 2
Long: 30303
Double: 3.93939e+11
Float: 3.33
Word: Hello
Unsigned: 85

Int, Long, Double, Float, Word, Unsigned: 3 304938 393847473 6.66 bye -2

Int: 3
Long: 304938
Double: 3.93847e+08
Float: 6.66
Word: bye
Unsigned: 4294967294

Auch hier werden wieder mehrere Variablen eingerichtet, diesmal einschließlich eines char-Array. Der Anwender wird aufgefordert, eine umfangreichere Eingabe zu machen, die danach wortgetreu ausgegeben wird.

Zeile 34 fordert den Anwender auf, alles auf einmal einzugeben. Jedes »Wort« der Eingabe wird der entsprechenden Variablen zugeordnet. Zur Erleichterung dieser Art von Mehrfachzuweisung muß cin jedes Wort der Eingabe als vollständige Eingabe für eine Variable betrachten. Würde cin die gesamte Eingabe als Eingabe für eine Variable betrachten, wäre diese Art der verketteten Eingabe nicht möglich.

Beachten Sie, daß in Zeile 42 das letzte angeforderte Objekt ein vorzeichenloser Integer war, der Anwender hingegen -2 eingegeben hat. Da cin davon ausgeht, daß es in einen vorzeichenloses Integer schreibt, wird das Bitmuster von -2 als vorzeichenloser Integer interpretiert und bei der Ausgabe mit cout erscheint der Wert 4294967294. Der vorzeichenlose Wert 4294967294 hat genau das gleiche Bitmuster wie der Wert -2. Weiter hinten werden Sie lernen, wie man einen ganzen String in einen Puffer schreibt, einschließlich mehrerer Worte. Jetzt beschäftigt uns aber erst einmal die Frage, »Wie gelingt dem Eingabe-Operator der Trick mit der Verkettung?«.

Der Operator >> liefert eine Referenz auf ein istream-Objekt zurück

Der Rückgabewert von cin ist eine Referenz auf ein istream-Objekt. Da cin selbst ein istream-Objekt ist, kann der Rückgabewert einer Einleseoperation als Eingabe für die nächste Einleseoperation verwendet werden.

int varEins, varZwei, varDrei;
cout << "Geben Sie drei Zahlen ein: "
cin >> varEins >> varZwei >> varDrei;

Wenn Sie cin >> varEins >> varZwei >> varDrei; schreiben, wird zuerst (cin >> VarEins) eingelesen. Der Rückgabewert davon ist ebenfalls ein istream-Objekt und dessen Eingabe-Operator liest die Variable varZwei ein. Genauso gut hätten Sie auch

((cin >> varEins) >> varZwei) >> varDrei;

schreiben können. Auf diese Technik gehen wir später im Zusammenhang mit cout noch näher ein.

Weitere Elementfunktionen von cin

Zusätzlich zu dem überladenen >>-Operator, verfügt cin noch über eine Reihe weiterer Elementfunktionen. Sie kommen zum Einsatz, wenn Sie eine genauere Kontrolle über die Eingabe wünschen.

Eingabe einzelner Zeichen

Ein >>-Operator, der eine Zeichenreferenz übernimmt, kann dazu verwendet werden, einzelne Zeichen aus der Standardeingabe einzulesen. Auch mit der Elementfunktion get() kann man einzelne Zeichen einlesen und dies sogar auf zwei Wegen: get() gibt es ohne Parameter, in welchem Falle der Rückgabewert verwendet wird, oder mit einer Referenz auf ein Zeichen.

Die Funktion get() ohne Parameter

Die erste Form von get() weist keine Parameter auf. Diese Version liefert den Wert des gefundenen Zeichens zurück beziehungsweise EOF (End of file), wenn das Ende der Datei erreicht wurde. Diese Form von get() ohne Parameter kommt selten zur Anwendung. Es ist hiermit nicht möglich, mehrere Eingaben zu verketten, da der Rückgabewert kein iostream-Objekt ist. Aus diesem Grund ist folgende Zeile nicht möglich:

cin.get() >>varEins >> varZwei; //   ungueltig

Der Rückgabewert von cin.get() >> varEins ist ein Integer und kein iostream-Objekt.

Eine typische Anwendung für get() ohne Parameter sehen Sie in Listing 16.4.

Listing 16.4: get() ohne Parameter

1:     // Listing 16.4 - get ohne Parameter
2: #include <iostream.h>
3:
4: int main()
5: {
6: char ch;
7: while ( (ch = cin.get()) != EOF)
8: {
9: cout << "ch: " << ch << endl;
10: }
11: cout << "\nFertig!\n";
12: return 0;
13: }

Um dieses Programm zu beenden, müssen Sie von der Tastatur aus das Dateiende-Zeichen eingeben. Auf DOS-PCs lautet der Befehl Strg+Z und auf UNIX-PCs Strg+D.

Hello
ch: H
ch: e
ch: l
ch: l
ch: o
ch:

World
ch: W
ch: o
ch: r
ch: l
ch: d
ch:

(Strg-z)
Fertig!

Zeile 6 deklariert eine lokale Zeichenvariable. Die while-Schleife weist die von cin.get() eingelesenen Eingaben ch zu. Solange nicht EOF eingelesen wird, wird der Meldungsstring ausgegeben. Allerdings wird die Ausgabe bis zum nächsten Zeilenende-Zeichen gepuffert. Trifft das Programm auf EOF (eingegeben als Strg+Z unter DOS oder Strg+D unter UNIX), wird die Schleife verlassen.

Beachten Sie, daß nicht jede Implementierung von istream diese Version von get() unterstützt, auch wenn sie inzwischen zum ANSI-Standard gehört.

Die Funktion get() mit einer Zeichen-Referenz als Parameter

Wenn ein Zeichen als Argument an get() übergeben wird, wird dieses Zeichen mit dem nächsten Zeichen im Eingabestrom gefüllt. Der Rückgabewert ist ein iostream- Objekt. Deshalb kann diese Form von get() verkettet werden. Sehen Sie dazu Listing 16.5.

Listing 16.5: get() mit Parametern

1:     // Listing 16.5 - get mit Parametern
2: #include <iostream.h>
3:
4: int main()
5: {
6: char a, b, c;
7:
8: cout << "Geben Sie drei Buchstaben ein: ";
9:
10: cin.get(a).get(b).get(c);
11:
12: cout << "a: " << a << "\nb: " << b << "\nc: " << c << endl;
13: return 0;
14: }

Geben Sie drei Buchstaben ein: elf
a: e
b: l
c: f

Zeile 6 erzeugt drei Zeichenvariablen. In Zeile 10 wird cin.get() dreimal verkettet aufgerufen. Zuerst ergeht der Aufruf an cin.get(a). Damit wird der erste Buchstabe in a abgelegt und cin kehrt zurück. Anschließend wird cin(b) aufgerufen und der nächste Buchstabe wird in b abgelegt. Zum Schluß wird cin.get(c) aufgerufen und der dritte Buchstabe in c abgelegt.

Da cin.get(a) als cin ausgewertet wird, hätten Sie auch folgendes schreiben können:

cin.get(a) >> b;

In dieser Form wird cin.get(a) als cin ausgewertet, so daß der zweite Teil cin >> b; lautet.

Was Sie tun sollten

Verwenden Sie den Eingabe-Operator (>>), wenn Sie Leerzeichen überspringen wollen.

Verwenden Sie get() mit einem Zeichenparameter, wenn Sie jedes Zeichen einschließlich Leerzeichen überprüfen wollen.

Strings von der Standardeingabe einlesen

Statt mit den Elementfunktionen get() und getline() kann man auch mit dem Eingabe-Operator (>>) ein Zeichen-Array füllen.

Die letzte Form von get() übernimmt drei Parameter. Der erste Parameter ist ein Zeiger auf einen Zeichen-Array, der zweite Parameter die maximale Anzahl der einzulesenden Zeichen plus eins und der dritte Parameter das Terminierungszeichen.

Wenn Sie als zweiten Parameter 20 eingeben, wird get() 19 Zeichen einlesen und den String, der im ersten Parameter gespeichert wird, mit dem Nullzeichen abschließen. Bei dem dritten Parameter, dem Terminierungszeichen, handelt es sich standardmäßig um das Zeichen für eine neue Zeile ('\n'). Stößt das Programm auf ein Terminierungszeichen, bevor die maximale Anzahl an Zeichen eingelesen wurde, wird eine Null geschrieben und das Terminierungszeichen bleibt im Puffer stehen.

Listing 16.6 zeigt, wie diese Form von get() eingesetzt wird.

Listing 16.6: get mit einem Zeichen-Array verwenden

1:     // Listing 16.6 - get mit einem Zeichen-Array verwenden
2: #include <iostream.h>
3:
4: int main()
5: {
6: char stringOne[256];
7: char stringTwo[256];
8:
9: cout << "Ersten String eingeben: ";
10: cin.get(stringOne,256);
11: cout << "stringOne: " << stringOne << endl;
12:
13: cout << " Zweiten String eingeben: ";
14: cin >> stringTwo;
15: cout << "StringTwo: " << stringTwo << endl;
16: return 0;
17: }

Ersten String eingeben: Die Zeit ist gekommen
stringOne: Die Zeit ist gekommen
Zweiten String eingeben: Fuer alle guten
StringTwo: Fuer

Die Zeilen 6 und 7 erzeugen zwei Zeichen-Arrays. In Zeile 9 wird der Anwender aufgefordert, einen String einzugeben. Zeile 10 ruft cin.get() auf. Der erste Parameter ist der zu füllende Puffer und der zweite Parameter die maximale Anzahl der Zeichen, die get() akzeptiert, plus 1 für den zusätzlichen Speicherplatz des Nullzeichens ('\0'). Der vorgegebene dritte Parameter ist das Zeichen für die neue Zeile.

Der Anwender gibt ein »Die Zeit ist gekommen.« Da der Anwender seine Eingabe mit einem Zeilenumbruch abschließt, wird diese Zeile in der Variablen stringOne mit einer abschließenden Null abgelegt.

Zeile 13 fordert den Anwender auf, einen weiteren String einzugeben. Diesmal wird der Eingabe-Operator verwendet. Da der Eingabe-Operator alles bis zum nächsten Leerzeichen einliest, wird in dem zweiten String nur der String »Fuer« mit einem abschließenden Zeichen gespeichert. Dies war jedoch nicht Ihre ursprüngliche Absicht.

Dies Problem läßt sich auch mit getline() lösen, wie Listing 16.7 zeigt.

Listing 16.7: getline()

1:     // Listing 16.7 - getline
2: #include <iostream.h>
3:
4: int main()
5: {
6: char stringOne[256];
7: char stringTwo[256];
8: char stringThree[256];
9:
10: cout << "Ersten String eingeben: ";
11: cin.getline(stringOne,256);
12: cout << "stringOne: " << stringOne << endl;
13:
14: cout << "Zweiten String eingeben: ";
15: cin >> stringTwo;
16: cout << "stringTwo: " << stringTwo << endl;
17:
18: cout << "Dritten String eingeben: ";
19: cin.getline(stringThree,256);
20: cout << "stringThree: " << stringThree << endl;
21: return 0;
22: }

Ersten String eingeben: eins zwei drei
stringOne: eins zwei drei
Zweiten String eingeben: vier fuenf sechs
stringTwo: vier
Dritten String eingeben: stringThree: fuenf sechs

Untersuchen Sie dieses Beispiel sorgfältig, es birgt einige Überraschungen. Die Zeilen 6 bis 8 deklarieren drei Zeichen-Arrays.

In Zeile 10 wird der Anwender aufgefordert, einen String einzugeben, der dann von getline() eingelesen wird. Wie get() übernimmt getline() einen Puffer und die maximale Anzahl der einzulesenden Zeichen. Während jedoch bei get() das abschließende Zeichen für die neue Zeile im Eingabepuffer gelassen wird, wird es bei getline() gelesen und gleich verworfen.

Zeile 14 enthält erneut eine Aufforderung zur Eingabe, diesmal unter Verwendung des Eingabe-Operators. Der Anwender gibt »vier fünf sechs« ein, und das erste Wort »vier« wird in stringTwo abgelegt. Anschließend wird die Aufforderung »Dritten String eingeben« angezeigt und getline() zum Einlesen aufgerufen. Da »fuenf sechs« noch im Eingabepuffer stehen, wird dieser Reststring bis zum Zeichen für die neue Zeile ausgelesen. Damit wird getline() beendet, und der String in stringThree wird in Zeile 20 ausgegeben.

Der Anwender bekommt keine Chance, den dritten String einzugeben, da der zweite Aufruf von getline() den Reststring im Eingabepuffer einliest, der nach dem Aufruf des Eingabe-Operators in Zeile 15 im Puffer verblieben ist.

Der Eingabe-Operator (>>) liest die Zeichen bis zum ersten Leerzeichen ein und legt das Wort im Zeichen-Array ab.

Die Elementfunktion get() ist überladen. In einer Version übernimmt sie keine Parameter und liefert den Wert des Zeichens zurück, das sie empfängt. In der zweiten Version übernimmt sie eine Referenz auf ein einzelnes Zeichen und liefert das istream- Objekt als Referenz zurück.

In der dritten und letzten Version übernimmt get() ein Zeichen-Array, die Anzahl der einzulesenden Zeichen und ein Terminierungszeichen (standardmäßig das Zeichen für die neue Zeile). Diese Version von get() liest solange Zeichen in das Array, bis die maximale Anzahl der übergebenen Zeichenzahl minus eins oder das Terminierungszeichen erreicht wurde. Trifft get() auf das Terminierungszeichen, bleibt das Zeichen im Eingabepuffer und die Einleseoperation wird beendet.

Die Elementfunktion getline() übernimmt drei Parameter: den zu füllenden Puffer, ein Zeichen mehr als die maximale Anzahl der Zeichen und das Terminierungszeichen. Die getline()-Funktion arbeitet genauso wie get() mit den gleichen Parametern. Der einzige Unterschied ist, daß getline() das Terminierungszeichen liest und verwirft.

cin.ignore()

Gelegentlich werden Sie die verbleibenden Zeilen einer Zeile (oder einer Datei) ignorieren wollen. Dies läßt sich mit der Elementfunktion ignore() realisieren. ignore() übernimmt zwei Parameter: die maximale Anzahl der Zeichen, die ignoriert werden sollen und das Terminierungszeichen. Wenn Sie ignore(80,'\n') schreiben, werden alle, maximal aber 80 Zeichen, auf dem Weg zum nächsten Neue-Zeile-Zeichen verworfen. Das Zeichen für die neue Zeile wird ebenfalls verworfen und die ignore()-Anweisung endet. Sehen Sie dazu das Listing 16.8.

Listing 16.8: ignore()

1:     // Listing 16.8 - ignore()
2: #include <iostream.h>
3:
4: int main()
5: {
6: char stringOne[255];
7: char stringTwo[255];
8:
9: cout << "Ersten String eingeben:";
10: cin.get(stringOne,255);
11: cout << "stringOne: " << stringOne << endl;
12:
13: cout << "Zweiten String eingeben: ";
14: cin.getline(stringTwo,255);
15: cout << "stringTwo: " << stringTwo << endl;
16:
17: cout << "\n\nNeuer Versuch...\n";
18:
19: cout << "Ersten String eingeben: ";
20: cin.get(stringOne,255);
21: cout << "stringOne: " << stringOne<< endl;
22:
23: cin.ignore(255,'\n');
24:
25: cout << "Zweiten String eingeben: ";
26: cin.getline(stringTwo,255);
27: cout << "stringTwo: " << stringTwo<< endl;
28: return 0;
29: }

Ersten String eingeben: Es war einmal
stringOne: Es war einmal
Zweiten String eingeben: stringTwo:

Neuer Versuch...
Ersten String eingeben: Es war einmal
stringOne: Es war einmal
Zweiten String eingeben: ein wunderschoenes
stringTwo: ein wunderschoenes

Die Zeilen 6 und 7 erzeugen zwei Zeichen-Arrays. Zeile 9 fordert den Anwender zur Eingabe auf. Der Anwender tippt »Es war einmal« ein und betätigt die Eingabetaste. In Zeile 10 wird dieser String mit get() eingelesen. get() füllt stringOne und endet mit der neuen Zeile. Das Zeichen für die neue Zeile bleibt im Eingabepuffer.

Zeile 13 enthält eine weitere Aufforderung zur Eingabe, aber getline() in Zeile 14 liest das Zeichen für neue Zeile, das im Puffer steht, und bricht direkt danach ab, bevor der Anwender eine Eingabe vornehmen kann.

In Zeile 19 wird der Anwender erneut zu einer Eingabe aufgefordert, die in diesem Fall aus der gleichen ersten Zeile besteht. Diesmal wird jedoch in Zeile 23 ignore() verwendet, um das Zeichen für die neue Zeile »aufzufressen«. Aus diesem Grund ist der Eingabepuffer bei dem Aufruf von getline() in Zeile 26 leer und der Anwender kann mit der Eingabe seiner Geschichte fortfahren.

peek() und putback()

Das Eingabe-Objekt cin weist noch zwei weitere Methoden auf, die gelegentlich ganz nützlich sein können: peek() und putback(). peek() betrachtet das nächste Zeichen aber liest es nicht ein, und putback() schreibt ein Zeichen in den Eingabestrom. In Listing 16.9 sehen Sie, wie sich diese Methoden anwenden lassen.

Listing 16.9: peek() und putback()

1:     // Listing 16.9 - peek() und putback()
2: #include <iostream.h>
3:
4: int main()
5: {
6: char ch;
7: cout << "Geben Sie einen Satz ein: ";
8: while ( cin.get(ch) )
9: {
10: if (ch == '!')
11: cin.putback('$');
12: else
13: cout << ch;
14: while (cin.peek() == '#')
15: cin.ignore(1,'#');
16: }
17: return 0;
18: }

Geben Sie einen Satz ein: Jetzt!ist#es!Zeit#fuer!ein#wenig!Spass#!
Jetzt$istes$Zeitfuer$einwenig$Spass$

Zeile 6 deklariert eine Zeichenvariable ch und Zeile 7 fordert den Anwender auf, einen Satz einzugeben. Der Zweck dieses Programms ist es, alle Ausrufezeichen (!) in Dollar-Zeichen ($) umzuwandeln und die Pfund-Zeichen (#) zu entfernen.

Dies Programm durchläuft eine Schleife, aus der es erst austritt, wenn es das Zeichen EOF einliest (Strg+C unter Windows und Strg+Z oder Strg+D unter anderen Betriebssystemen). Denken Sie jedoch daran, daß cin.get() eine 0 für EOF zurückliefert. Ist das aktuelle Zeichen ein Ausrufezeichen, wird es verworfen und statt dessen ein $-Zeichen in den Eingabepuffer gestellt, das beim nächsten Mal eingelesen wird. Ist das aktuelle Zeichen kein Ausrufezeichen, wird es ausgegeben. Danach wird das nächste Zeichen untersucht, und wenn es sich dabei um ein Pfund-Zeichen handelt, wird es gelöscht.

Dieser Weg ist zwar nicht unbedingt der effizienteste (und es wird auch kein Pfund- Symbol gefunden, das als erstes Zeichen auftritt), aber das Beispiel zeigt zumindest, wie diese Methoden funktionieren. Sie sind relativ schwer zu durchschauen. Deshalb sollten Sie sich jetzt nicht allzu viele Gedanken darum machen, sondern sie in Ihrer Trickkiste verstauen, bis Sie sie vielleicht eines Tages gebrauchen können.

peek() und putback() werden in der Regel zum Parsen von Strings oder anderer Daten eingesetzt, zum Beispiel beim Schreiben eines Compilers.

Ausgabe mit cout

Bisher haben Sie cout zusammen mit dem überladenen Ausgabe-Operator (<<) verwendet, um Strings, Integer und andere numerische Daten auf dem Bildschirm auszugeben. Es ist dabei möglich, die Daten zu formatieren, spaltenweise auszurichten und numerische Zahlen in dezimaler und hexadezimaler Schreibweise auszugeben. In diesem Abschnitt möchte ich Ihnen zeigen, wie das geht.

Die Ausgabe leeren

Sie haben bereits sehen können, daß bei der Ausgabe von endl der Ausgabepuffer geleert wird. endl ruft die cout-Elementfunktion flush() auf, die alle im Puffer abgelegten Daten ausgibt. Sie können flush() auch direkt aufrufen, indem Sie die Elementfunktion flush() aufrufen oder an cout schicken:

cout << flush

Dies kann ganz nützlich sein, wenn Sie sicherstellen wollen, daß der Ausgabepuffer leer ist und auf dem Bildschirm ausgegeben wurde.

Verwandte Funktionen

Genau wie der Eingabe-Operator mit get() und getline() ergänzt werden kann, gibt es für den Ausgabe-Operator die Funktionen put() und write().

Mit Hilfe der Funktion put() wird ein einzelnes Zeichen auf dem Ausgabegerät ausgegeben. Da put() eine ostream-Referenz zurückliefert und cout ein ostream-Objekt ist, können Sie put() verketten wie beim Ausgabe-Operator. Listing 16.10 veranschaulicht dies Konzept.

Listing 16.10: put()

1:     // Listing 16.10 - put()
2: #include <iostream.h>
3:
4: int main()
5: {
6: cout.put('H').put('e').put('l').put('l').put('o').put('\n');
7: return 0;
8: }

Hello

Einige Compiler haben Schwierigkeiten, auf diese Weise Text auszugeben. Wenn Ihr Compiler das Wort Hello nicht ausgibt, sollten Sie dieses Listing einfach überspringen.

Zeile 6 wird wie folgt ausgewertet: cout.put('H') gibt den Buchstaben H auf dem Bildschirm aus und liefert das cout-Objekt zurück. Was übrigbleibt, ist:

cout.put('e').put('l').put('l').put('o').put('\n');

Als nächstes wird der Buchstabe e ausgegeben und cout.put('l') bleibt übrig. So geht es weiter. Die Buchstaben werden ausgegeben und cout wird jedes Mal zurückgegeben, bis das letzte Zeichen ('\n') ausgegeben ist und die Funktion zurückkehrt.

Die Funktion write() ist fast identisch zum Ausgabe-Operator, mit der Ausnahme, daß sie einen Parameter übernimmt, der der Funktion die maximale Anzahl der auszugebenden Zeichen mitteilt. Ein Beispiel finden Sie in Listing 16.11.

Listing 16.11: write()

1:     // Listing 16.11 - write()
2: #include <iostream.h>
3: #include <string.h>
4:
5: int main()
6: {
7: char One[] = "Einer fuer alle";
8:
9:
10:
11: int fullLength = strlen(One);
12: int tooShort = fullLength -4;
13: int tooLong = fullLength + 6;
14:
15: cout.write(One,fullLength) << "\n";
16: cout.write(One,tooShort) << "\n";
17: cout.write(One,tooLong) << "\n";
18: return 0;
19: }

Einer fuer alle
Einer fuer
Einer fuer alle i?!

Die letzte Zeile der Ausgabe kann bei Ihrem Computer zuerst erscheinen.

Zeile 7 erzeugt einen Satz. Zeile 11 setzt den Integer-Wert fullLength auf die Länge dieses Satzes. tooShort erhält diese Länge von fullLength minus vier Zeichen und tooLong die Länge von fullLength plus sechs Zeichen.

Zeile 15 gibt den kompletten Satz mit Hilfe von write() aus. Die Länge der Ausgabe wird auf die eigentliche Länge des Satzes gesetzt und der Satz korrekt ausgegeben.

Zeile 16 gibt den Satz erneut aus. Diesmal ist die Ausgabe jedoch vier Zeichen kürzer als der Originalsatz.

Zeile 17 gibt ebenfalls den Satz aus, wobei write() angewiesen wird, zusätzliche sechs Zeichen auszugeben. Zusätzlich zu dem Satz werden auf dem Bildschirm die nächsten 6 Byte des angrenzenden Speicherplatzes ausgegeben.

Manipulatoren, Flags und Formatierungsanweisungen

Der Ausgabestrom verfügt über eine Reihe von Statusflags, die festlegen, welche Basis (dezimal oder hexadezimal) verwendet wird, wie groß die Felder werden müssen und mit welchem Zeichen die Felder aufgefüllt werden. Ein Statusflag ist ein Byte, dessen einzelnen Bits jeweils eine eigene Bedeutung zugewiesen wird. In Kapitel 21 werden wir noch genauer diskutieren, wie Sie solche Bits manipulieren können. Die ostream- Flags können mit Elementfunktionen und Manipulatoren gesetzt werden.

cout.width()

Die Standardbreite einer Ausgabe ist immer gerade so groß, daß die Zahl, das Zeichen oder der String im Ausgabepuffer vollständig ausgegeben werden kann. Sie können die Breite mit width() ändern. Da width() eine Elementfunktion ist, muß sie über ein cout-Objekt aufgerufen werden, wodurch allerdings nur die Breite des nächsten Ausgabefeldes geändert wird. Danach gilt wieder der Standardwert. Zur Veranschaulichung schauen Sie sich Listing 16.12 an.

Listing 16.12: Die Breite der Ausgabe anpassen

1:     // Listing 16.12 - Breite der Ausgabe anpassen
2: #include <iostream.h>
3:
4: int main()
5: {
6: cout << "Start >";
7: cout.width(25);
8: cout << 123 << "< Ende\n";
9:
10: cout << "Start >";
11: cout.width(25);
12: cout << 123<< "< Weiter >";
13: cout << 456 << "< Ende\n";
14:
15: cout << "Start >";
16: cout.width(4);
17: cout << 123456 << "< Ende\n";
18:
19: return 0;
20: }

Start >                  123< Ende
Start > 123< Weiter >456< Ende
Start >123456< Ende

Die erste Ausgabe in den Zeilen 6 bis 8 gibt die Zahl 123 in einem Feld aus, dessen Länge in Zeile 7 auf 25 festgesetzt wurde. Das Ergebnis sehen Sie in der ersten Ausgabezeile.

Die zweite Ausgabezeile gibt den Wert 123 in dem gleichen Feld (Größe 25) aus. Anschließend erfolgt die Ausgabe des Wertes 456. Beachten Sie, daß 456 in einem Feld steht, das auf eine Breite zurückgesetzt wurde, die genau paßt. Wie bereits oben erwähnt, gilt die width()-Anweisung nur für die nächste Ausgabe.

Die letzte Ausgabe zeigt, daß Sie zwar eine Breite angeben können, die kleiner als die Ausgabe ist, der Aufruf der width()-Methode dann aber ohne Wirkung bleibt.

Füllzeichen setzen

Normalerweise füllt cout den leeren Raum, der durch einen Aufruf von width() erzeugt wurde, wie oben gezeigt mit Leerzeichen. Vielleicht wünschen Sie jedoch diesen Bereich mit anderen Zeichen zu füllen, zum Beispiel Sternchen. Dazu müssen Sie fill() aufrufen und das gewünschte Füllzeichen als Parameter übergeben. Ein Beispiel hierfür sehen Sie in Listing 16.13.

Listing 16.13: fill()

1:     // Listing 16.13 - fill()
2:
3: #include <iostream.h>
4:
5: int main()
6: {
7: cout << "Start >";
8: cout.width(25);
9: cout << 123 << "< Ende\n";
10:
11:
12: cout << "Start >";
13: cout.width(25);
14: cout.fill('*');
15: cout << 123 << "< Ende\n";
16: return 0;
17: }

Start >                  123< Ende
Start >******************123< Ende

Die Zeilen 7 bis 9 zeigen die gleiche Funktionalität wie das vorige Beispiel. Das Ganze wird dann in den Zeilen 12 bis 15 wiederholt. Diesmal wird jedoch in Zeile 14 ein Sternchen als Füllzeichen festgesetzt. Das Ergebnis sehen Sie in der Ausgabe.

Flags setzen

iostream-Objekte halten ihren Status in Flags fest. Sie können diese Flags durch den Aufruf von setf() und die Übergabe einer der vordefinierten Aufzählungskonstanten setzen.

Man spricht davon, daß Objekte einen Status haben, wenn einige oder alle Daten des Objekts eine Bedingung darstellen, die sich im Laufe des Programms ändern kann.

So können Sie zum Beispiel angeben, daß Nachkommanullen angezeigt werden sollen (so daß 20,00 nicht zu 20 abgekürzt wird). Um Nachkommanullen einzuschalten, geben Sie folgenden Befehl ein setf(ios::showpoint).

Der Gültigkeitsbereich der Aufzählungskonstanten beschränkt sich auf die iostream- Klasse (ios). Deshalb werden sie mit voller Qualifizierung ios::flagname aufgerufen (z.B. ios::showpoint).

Um vor positiven Zahlen das Plus-Zeichen (+) anzeigen zu lassen, verwenden Sie ios::showpos. Um die Ausrichtung der Ausgabe zu ändern, verwenden Sie ios::left, ios::right oder ios::internal.

Und schließlich können Sie die Basis der anzuzeigenden Zahlen mit ios::dec (dezimal), ios::oct (oktal - Basis acht), oder ios::hex (hexadezimal - Basis sechszehn) setzen. Diese Flags können auch direkt an den Ausgabe-Operator geschickt werden - siehe Listing 16.14. Zusätzlich stellt Ihnen das Listing den Manipulator setw() vor, der die Ausgabebreite festlegt und darüber hinaus mit dem Ausgabe-Operator verkettet werden kann.

Listing 16.14: setf()

1:     // Listing 16.14 - setf
2: #include <iostream.h>
3: #include <iomanip.h>
4:
5: int main()
6: {
7: const int number = 185;
8: cout << "Die Zahl lautet " << number << endl;
9:
10: cout << "Die Zahl lautet " << hex << number << endl;
11:
12: cout.setf(ios::showbase);
13: cout << "Die Zahl lautet " << hex << number << endl;
14:
15: cout << "Die Zahl lautet " ;
16: cout.width(10);
17: cout << hex << number << endl;
18:
19: cout << "Die Zahl lautet " ;
20: cout.width(10);
21: cout.setf(ios::left);
22: cout << hex << number << endl;
23:
24: cout << "Die Zahl lautet " ;
25: cout.width(10);
26: cout.setf(ios::internal);
27: cout << hex << number << endl;
28:
29: cout << "Die Zahl lautet:" << setw(10) << hex << number << endl;
30: return 0;
31: }

Die Zahl lautet 185
Die Zahl lautet b9
Die Zahl lautet 0xb9
Die Zahl lautet 0xb9
Die Zahl lautet 0xb9
Die Zahl lautet 0x b9
Die Zahl lautet:0x b9

Zeile 7 initialisiert die konstante int-Zahl mit dem Wert 185. Die Ausgabe erfolgt in Zeile 8.

In Zeile 10 wird der Wert erneut angezeigt. Diesmal wird jedoch der Manipulator hex angehängt, wodurch der Wert in hexadezimaler Schreibweise als b9 ausgegeben wird. (Der hexadezimale Wert b steht für 11. 11 mal 16 ist gleich 176. Addieren Sie dazu 9, erhalten sie einen Gesamtwert von 185.)

Zeile 12 setzt das Flag showbase. Damit wird der Präfix 0x, wie in der Ausgabe zu sehen, an alle hexadezimalen Zahlen angehängt.

Zeile 16 setzt die Größe auf 10, und der Wert wird ganz nach rechts verschoben. Zeile 20 setzt die Größe erneut auf 10, doch diesmal erfolgt die Ausrichtung am linken Rand (Zeile 21) und die Zahl wird linksbündig ausgegeben.

Zeile 25 setzt die Größe auf 10 und die Ausrichtung auf internal. Als Ergebnis wird 0x linksbündig ausgegeben und die Zahl b9 steht am rechten Rand.

Zum Schluß wird in Zeile 29 der Verkettungsoperator setw() verwendet, um die Breite auf 10 zu setzen. Danach wird der Wert erneut ausgegeben.

Streams und die Funktion printf()

Die meisten C++-Implementierungen stellen auch die Standard-E/A-Bibliotheken von C einschließlich der printf()-Anweisung zur Verfügung. Doch auch wenn die Funktion printf() in einigen Fällen einfacher anzuwenden ist als cout, ist von ihrem Gebrauch dennoch abzuraten.

So bietet printf() keine Typensicherheit. Leicht vertut man sich und läßt einen Integer als Zeichen anzeigen und umgekehrt. Außerdem unterstützt printf()keine Klassen. Deshalb können Sie der Funktion auch nicht beibringen, wie Ihre Klassendaten auszugeben sind; statt dessen müssen Sie jedes Klassenelement einzeln mit printf() ausgeben.

Auf der anderen Seite fällt das Formatieren mit printf()wesentlich leichter, da Sie die Formatierungszeichen direkt in die printf()-Anweisung aufnehmen können. Da printf() also durchaus seine Anwendungsbereiche hat und viele Programmierer noch häufig diese Funktion einsetzen, werde ich sie in diesem Abschnitt kurz beschreiben.

Für die Verwendung von printf() müssen Sie sicherstellen, daß Sie die Header-Datei stdio.h eingebunden haben. In seiner einfachsten Form übernimmt printf()als ersten Parameter einen Formatierungsstring und dazu eine Reihe von Werten für die verbleibenden Parameter.

Der Formatierungsstring besteht aus Text und Konvertierungsspezifizierern. Alle Konvertierungsspezifizierer müssen mit einem Prozentzeichen (%) beginnen. Die geläufigsten Konvertierungsspezifizierer sind in Tabelle 16.1 zusammengefaßt.

Spezifizierer

Verwendet für

%s

Strings

%d

Integer

%l

long integer

%ld

double

%f

float

Tabelle 16.1: Häufig verwendete Konvertierungsspezifizierer

Jeder dieser Konvertierungsspezifizierer kann durch eine Größen- und Genauigkeitsangabe ergänzt werden. Diese werden als Fließkommazahl angegeben, wobei die Stellen links des Dezimalzeichens die Größe und die Stellen rechts des Dezimalzeichens die Genauigkeit angeben. So lautet der Spezifizierer für einen 5stelligen Integer-Wert %5d. Und mit %15.5f definieren Sie den Spezifizierer für eine 15stellige Fließkommazahl, bei der die letzten fünf Stellen dem Dezimalanteil gewidmet sind. Listing 16.15 zeigt mehrere Anwendungsbeispiele für printf().

Listing 16.15: Ausgabe mit printf()

1:     #include <stdio.h>
2: int main()
3: {
4: printf("%s","hello world\n");
5:
6: char *phrase = "Hello again!\n";
7: printf("%s",phrase);
8:
9: int x = 5;
10: printf("%d\n",x);
11:
12: char *phraseTwo = "Hier einige Werte: ";
13: char *phraseThree = " und dann diese: ";
14: int y = 7, z = 35;
15: long longVar = 98456;
16: float floatVar = 8.8f;
17:
18: printf("%s %d %d %s %ld %f\n",phraseTwo,y,z,
phraseThree,longVar,floatVar);
19:
20: char *phraseFour = "Formatiert: ";
21: printf("%s %5d %10d %10.5f\n",phraseFour,y,z,floatVar);
22: return 0;
23: }

hello world
Hello again!
5
Hier einige Werte: 7 35 und dann diese: 98456 8.800000
Formatiert: 7 35 8.800000

Die erste printf()-Anweisung in Zeile 4 erfolgt in der Standardform: printf() gefolgt von einem Konvertierungsspezifizierer in Anführungszeichen (hier %s) und einem Wert (hier der String), der in den Konvertierungsspezifizierer eingefügt werden soll.

Mit %s geben Sie zu verstehen, daß es um einen String geht; der Wert für diesen String ist in diesem Falle das String-Literal »hello world«.

Die zweite printf()-Anweisung ist zu der ersten identisch. Allerdings wird hier ein Zeiger auf char statt eines String-Literals verwendet.

Die dritte printf()-Anweisung in Zeile 10 verwendet den Konvertierungsspezifizierer für Integer-Werte und erhält als Wert die Integer-Variable x. Die vierte printf()-Anweisung in Zeile 18 ist etwas komplizierter. Hierbei werden sechs Werte aneinandergehängt. Zuerst werden die jeweiligen Konvertierungsspezifizierer angegeben und anschließend, durch Kommata getrennt, die dazugehörigen Werte.

In Zeile 21 schließlich werden Formatspezifikationen verwendet, um die Größe und die Genauigkeit festzulegen. Wie Sie sehen können, geht das alles etwas einfacher als die Arbeit mit Manipulatoren.

Wie jedoch bereits erwähnt, findet für printf() leider keine Typenprüfung statt, und die Funktion kann auch nicht als friend oder Elementfunktion einer Klasse deklariert werden. Wenn Sie also die verschiedenen Datenelemente einer Klasse ausgeben wollen, müssen Sie der printf()-Anweisung jede Zugriffsmethode explizit übergeben.

Können Sie zusammenfassen, wie man die Ausgabe manipulieren kann?

Antwort: Um die Ausgabe in C++ zu formatieren, verwendet man eine Kombination aus Sonderzeichen, Ausgabemanipulatoren und Flags.

Die folgenden Sonderzeichen werden in den Ausgabe-String mit eingeschlossen, der mit dem Ausgabe-Operator an cout gesendet wird.

\n - neue Zeile

\r - Zeilenrücklauf

\t - Tabulator

\\ - Backslash

\ddd (Oktalzahl) - ASCII-Zeichen

\a - Signal (Alarmzeichen)

Beispiel:

cout << "\aEin Fehler ist aufgetreten\t"

Es ertönt ein Signal, eine Fehlermeldung wird ausgegeben und es erfolgt ein Sprung zum nächsten Tab-Stop. Manipulatoren werden zusammen mit dem cout-Operator verwendet. Für Manipulatoren, die Argumente übernehmen, müssen Sie die Header-Datei iomanip.h in Ihre Datei mit einbinden.

Die folgende Liste führt die Manipulatoren auf, die keine Argumente übernehmen:

flush - leert den Ausgabe-Puffer

endl - fügt eine neue Zeile ein und leert den Ausgabe-Puffer

oct - setzt die Basis der Ausgabe auf oktal

dec - setzt die Basis der Ausgabe auf dezimal

hex - setzt die Basis der Ausgabe auf hexadezimal

Die folgenden Manipulatoren übernehmen Argumente (aus iomanip.h):

setbase (base) - setzt die Ausgabebasis (0 = dezimal, 8 = oktal, 10 = dezimal, 16 = hexadezimal)

setw (Größe) - setzt die minimale Größe des Ausgabefeldes

setfill (ch) - das zu verwendende Füllzeichen, wenn die Größe definiert wird

setprecision (p) - setzt die Genauigkeit für Fließkommazahlen

setiosflags (f) - setzt ein oder mehrere ios-Flags

resetiosflags (f) - setzt ein oder mehrere ios-Flags neu

Beispiel:

cout << setw(12) << setfill('#') << hex << x << endl;

In diesem Beispiel wird die Feldbreite auf 12 gesetzt, als Füllzeichen wird '#' gewählt, die Ausgabe erfolgt hexadezimal und ausgegeben wird der Wert von 'x'. Das Zeichen für eine neue Zeile wird im Puffer abgelegt und der Puffer wird geleert. Alle Manipulatoren bis auf flush, endl und setw bleiben solange gültig, bis sie geändert werden, oder das Ende des Programms erreicht wurde. setw() nimmt nach dem aktuellen cout wieder den Standardwert an.

Die folgenden ios-Flags können zusammen mit den Manipulatoren setiosflags und resetiosflags verwendet werden:

ios::left - richtet die Ausgabe innerhalb der angegebenen Feldbreite links aus

ios::right - richtet die Ausgabe innerhalb der angegebenen Feldbreite rechts aus

ios::internal - Vorzeichen wird links ausgerichtet, Wert rechts

ios::dec - dezimale Ausgabe

ios::oct - oktale Ausgabe

ios::hex - hexadezimale Ausgabe

ios::showbase - fügt Ox an hexadezimale Zahlen und 0 an oktale Zahlen an

ios::showpoint - fügt gegebenenfalls Nachkomma-Nullen für die Kennzeichnung der gewünschten Genauigkeit an

ios::uppercase - Zahlen der hexadezimalen und wissenschaftlichen Schreibweise in Großbuchstaben ausgeben

ios::showpos - das +-Zeichen für positive Zahlen anzeigen

ios::scientific - Fließkommazahlen in wissenschaftlicher Schreibweise

ios::fixed - Fließkommazahlen in dezimaler Schreibweise

Weitere Informationen entnehmen Sie bitte der Header-Datei ios.h und Ihren Compiler-Handbüchern.

Eingabe und Ausgabe für Dateien

Streams stellen eine Möglichkeit dar, Daten, die von der Tastatur oder der Festplatte eingelesen und auf dem Bildschirm oder in eine Datei auf der Festplatte ausgegeben werden, einheitlich zu behandeln. In beiden Fällen können Sie die Eingabe- und Ausgabe-Operatoren oder die zugehörigen Funktionen und Manipulatoren verwenden. Um Dateien zu öffnen und zu schließen, können Sie wie in den folgenden Abschnitten beschrieben, ifstream- und ofstream-Objekte verwenden.

ofstream

Die Objekte, mit denen man von Dateien liest oder in Dateien schreibt, werden als ofstream -Objekte bezeichnet. Sie leiten sich von den bisher verwendeten iostream-Objekten ab.

Bevor Sie in eine Datei schreiben können, müssen Sie zuerst ein ofstream-Objekt erstellen und dann dieses Objekt mit einer bestimmten Datei auf Ihrer Festplatte verbinden. Damit Sie ofstream-Objekte verwenden können, müssen Sie sicherstellen, daß Sie die Header-Datei fstream.h in Ihr Programm einbinden.

Da iostream.h in fstream.h bereits enthalten ist, müssen Sie iostream nicht mehr explizit mit aufnehmen.

Streamstatus

iostream-Objekte enthalten Flags, die Ihnen Auskunft über den Status Ihrer Eingabe und Ausgabe geben. Sie können diese Flags mit den Boole'schen Funktionen eof(), bad(), fail() und good() abfragen. Die Funktion eof() liefert true zurück, wenn das iostream-Objekt auf das Ende der Datei (EOF) gestoßen ist. Die Funktion bad() liefert true zurück, wenn Sie eine ungültige Operation versuchen. fail() liefert immer dann true zurück, wenn bad() ebenfalls true ist oder eine Operation fehlgeschlagen ist. Und schließlich gibt es noch die Funktion good(), die jedes Mal true zurückliefert, wenn alle drei anderen Funktionen zu false ausgewertet werden.

Dateien für die Eingabe und die Ausgabe öffnen

Um die Datei meineDatei.cpp mit einem ofstream-Objekt zu öffnen, müssen Sie eine Instanz eines ofstream-Objekts deklarieren und den Dateinamen als Parameter übergeben.

ofstream fout("meineDatei.cpp");

Um die Datei für die Eingabe zu öffnen, gehen Sie auf genau die gleichen Art und Weise vor, verwenden aber ein ifstream-Objekt.

ifstream fin("meineDatei.cpp");

Beachten Sie, daß fin und fout Namen sind, die Sie selbst vergeben. In diesem Fall wurden die Bezeichner fout und fin gewählt, um die Beziehung zu cout beziehungsweise cin widerzuspiegeln.

Eine wichtige Funktion für Dateistreams ist close(). Jedes Dateistream-Objekt, das Sie erzeugen, öffnet eine Datei - entweder zum Lesen oder zum Schreiben (oder für beides). Achten Sie daher darauf, daß Sie die Datei mit close() schließen, nachdem Sie die Lese- oder Schreiboperationen beendet haben. Damit stellen Sie sicher, daß die Datei nicht beschädigt wird und daß die ausgegebenen Daten auf die Festplatte geschrieben werden.

Nachdem die Streamobjekte mit Dateien verbunden wurden, können Sie wie alle anderen Streamobjekte verwendet werden. Sehen Sie dazu Listing 16.16.

Listing 16.16: Dateien zum Lesen und Schreiben öffnen

1:     #include <fstream.h>
2: int main()
3: {
4: char fileName[80];
5: char buffer[255]; // fuer die Benutzereingabe
6: cout << "Dateiname: ";
7: cin >> fileName;
8:
9: ofstream fout(fileName); // zum Schreiben oeffnen
10: fout << "Diese Zeile wird direkt in die Datei geschrieben...\n";
11: cout << "Bitte Text fuer die Datei eingeben: ";
12: cin.ignore(1,'\n'); // Neue Zeile nach dem Dateinamen entfernen
13: cin.getline(buffer,255); // Benutzereingabe einlesen
14: fout << buffer << "\n"; // und in die Datei schreiben
15: fout.close(); // Datei schliessen, bereit zum erneuten Oeffnen
16:
17: ifstream fin(fileName); // zum Lesen erneut oeffnen
18: cout << "So lautet der Inhalt der Datei:\n";
19: char ch;
20: while (fin.get(ch))
21: cout << ch;
22:
23: cout << "\n***Ende des Dateiinhalts.***\n";
24:
25: fin.close(); // Ordnungssinn zahlt sich aus
26: return 0;
27: }

Dateiname: test1
Bitte Text für die Datei eingeben: Dieser Text wird in die Datei geschrieben!
So lautet der Inhalt der Datei:
Diese Zeile wird direkt in die Datei geschrieben...
Dieser Text wird in die Datei geschrieben!

***Ende des Dateiinhalts.***

Zeile 4 richtet einen Puffer für den Dateinamen ein und Zeile 5 einen weiteren Puffer für die Benutzereingabe. Zeile 6 fordert den Anwender auf, einen Dateinamen einzugeben, der dann in den Puffer filename geschrieben wird. Zeile 9 erzeugt das ofstream -Objekt fout, das mit dem neuen Dateinamen verbunden ist. Damit wird die Datei geöffnet. Existiert die Datei bereits, wird ihr Inhalt verworfen.

In Zeile 10 wird Textstring direkt in die Datei geschrieben. Zeile 11 fordert den Anwender zur Eingabe auf. Das Zeichen für Neue Zeile, das von der Eingabe des Dateinamens übrig geblieben ist, wird in Zeile 12 gelöscht und die Benutzereingabe wird vorerst in einem Puffer gespeichert (Zeile 13). In Zeile 14 wird die Eingabe dann zusammen mit dem Zeichen für eine Neue Zeile in die Datei geschrieben. Zeile 15 schließt die Datei.

In Zeile 17 wird die Datei erneut geöffnet - diesmal jedoch im Lesemodus - und der Inhalt wird zeichenweise in den Zeilen 20 und 21 eingelesen.

Das Standardverhalten von ofstream beim Öffnen ändern

Standardmäßig wird eine Datei, die noch nicht existiert, beim Öffnen erst einmal erzeugt. Existiert sie bereits, wird ihr Inhalt gänzlich gelöscht. Wenn Sie von diesem Standardverhalten abweichen wollen, müssen Sie explizit ein zweites Argument an den Konstruktor Ihres ofstream-Objekts übergeben.

Gültige Argumente sind:

Beachten Sie, daß app kurz für append (anhängen) steht, ate für at end (am Ende) und trunc für truncate (abschneiden). Listing 16.17 zeigt, wie append verwendet wird, indem die Datei aus Listing 16.16 neu geöffnet und dann etwas angehängt wird.

Listing 16.17: An das Ende einer Datei anhängen

1:     #include <fstream.h>
2: int main() // liefert bei Fehler 1 zurueck
3: {
4: char fileName[80];
5: char buffer[255];
6: cout << "Bitte Dateiname erneut eingeben: ";
7: cin >> fileName;
8:
9: ifstream fin(fileName);
10: if (fin) // existiert bereits?
11: {
12: cout << "Aktueller Dateiinhalt:\n";
13: char ch;
14: while (fin.get(ch))
15: cout << ch;
16: cout << "\n***Ende des Dateiinhalts.***\n";
17: }
18: fin.close();
19:
20: cout << "\nDie Datei " << fileName <<
" im Anhaenge-Modus oeffnen...\n";
21:
22: ofstream fout(fileName,ios::app);
23: if (!fout)
24: {
25: cout << "Es ist nicht moeglich, " << fileName <<
" zum Anhaengen zu oeffnen.\n";
26: return(1);
27: }
28:
29: cout << "\nBitte Text fuer die Datei eingeben: ";
30: cin.ignore(1,'\n');
31: cin.getline(buffer,255);
32: fout << buffer << "\n";
33: fout.close();
34:
35: fin.open(fileName); // bestehendes fin-Objekt erneut verwenden!
36: if (!fin)
37: {
38: cout << "Es ist nicht moeglich, " << fileName <<
" zum Lesen zu oeffnen.\n";
39: return(1);
40: }
41: cout << "\nSo lautet der Inhalt der Datei:\n";
42: char ch;
43: while (fin.get(ch))
44: cout << ch;
45: cout << "\n***Ende des Dateiinhalts.***\n";
46: fin.close();
47: return 0;
48: }

Bitte Dateiname erneut eingeben: test1
Aktueller Dateiinhalt:
Diese Zeile wird direkt in die Datei geschrieben...
Dieser Text wird in die Datei geschrieben!

***Ende des Dateiinhalts.***

Die Datei test1 im Anhaenge-Modus oeffnen

Bitte Text für die Datei eingeben: Mehr Text für die Datei!

So lautet der Inhalt der Datei:
Diese Zeile wird direkt in die Datei geschrieben...
Dieser Text wird in die Datei geschrieben!
Mehr Text für die Datei!

***Ende des Dateiinhalts.***

Der Anwender wird erneut aufgefordert, den Dateinamen einzugeben. Diesmal wird in Zeile 9 ein ifstream-Objekt zum Schreiben in die Datei erzeugt. In Zeile 10 wird getestet, ob die Datei geöffnet werden konnte, und wenn die Datei bereits existiert, wird ihr Inhalt in den Zeilen 12 bis 16 ausgegeben. Beachten Sie, daß if(fin) identisch ist zu if (fin.good()).

Danach wird die Eingabedatei geschlossen und die gleiche Datei wird in Zeile 22 erneut geöffnet, diesmal jedoch im Anhänge-Modus. Wie nach jedem Öffnen-Vorgang üblich, wird die Datei auch hier wieder überprüft, um sicherzustellen, daß die Datei korrekt geöffnet wurde. Beachten Sie, daß if(!fout) gleichbedeutend ist mit if (fout.fail()). Vom Anwender wird eine Texteingabe angefordert und in Zeile 33 wird die Datei erneut geschlossen.

Zum Schluß wird, wie in Listing 16.16, die Datei im Lese-Modus geöffnet. Diesmal muß jedoch fin nicht noch einmal deklariert werden, es braucht lediglich der Dateiname erneut zugewiesen zu werden. Auch hier wird der Öffnen-Vorgang getestet (in Zeile 36), und wenn alles korrekt ist, wird der Inhalt der Datei auf dem Bildschirm ausgegeben und die Datei endgültig geschlossen.

Was Sie tun sollten

... und was nicht

Prüfen Sie jedes Öffnen einer Datei, um sicherzugehen, daß sie ordnungsgemäß geöffnet wurde.

Verwenden Sie bereits existierende ifstream- und ofstream-Objekte.

Schließen Sie alle fstream-Objekte, wenn Sie sie nicht mehr benötigen.

Versuchen Sie nicht, cin oder cout zu schließen oder neu zuzuweisen.

Binärdateien und Textdateien

Einige Betriebssysteme wie DOS unterscheiden zwischen Textdateien und binären Dateien. Textdateien speichern alles als Text (wie der Name bereits verrät). Demzufolge werden große Zahlen wie 54.325 als ein numerischer String ('5', '4', '.', '3', '2', '5') gespeichert. Dies mag nicht sehr effizient sein, hat aber den Vorteil, daß der Text von einfachen Programmen, wie unter DOS üblich, gelesen werden kann.

Damit das Dateisystem besser zwischen Textdateien und binären Dateien unterscheiden kann, gibt es in C++ das Flag ios::binary. Auf vielen Systemen wird dieses Flag ignoriert, da alle Daten im binären Format abgelegt werden. In einigen eher empfindlichen Systemen ist das Flag ios::binary nicht gültig und läßt sich nicht kompilieren!

Binäre Dateien können nicht nur Integer und Strings speichern, sondern auch ganze Datenstrukturen. Sie können alle Daten auf einmal mit der Methode write() von fstream ausgeben.

Haben Sie write() verwendet, können Sie die Daten mit read() wieder zurückholen. Jede dieser Funktionen erwartet jedoch einen Zeiger auf ein Zeichen, so daß Sie die Adresse Ihrer Klasse erst zu einem Zeiger auf char umwandeln müssen.

Das zweite Argument zu diesen Funktionen ist die Anzahl der Zeichen, die geschrieben werden sollen. Diese Angabe können Sie mit sizeof ermitteln. Beachten Sie, daß die Daten und nicht die Methoden ausgegeben werden. Zurückgeholt werden ebenfalls nur die Daten. Listing 16.18 zeigt, wie der Inhalt einer Klasse in eine Datei geschrieben wird.

Listing 16.18: Eine Klasse in eine Datei schreiben

1:     #include <fstream.h>
2:
3: class Animal
4: {
5: public:
6: Animal(int weight, long days):itsWeight(weight),
itsNumberDaysAlive(days){}
7: ~Animal(){}
8:
9: int GetWeight()const { return itsWeight; }
10: void SetWeight(int weight) { itsWeight = weight; }
11:
12: long GetDaysAlive()const { return itsNumberDaysAlive; }
13: void SetDaysAlive(long days) { itsNumberDaysAlive = days; }
14:
15: private:
16: int itsWeight;
17: long itsNumberDaysAlive;
18: };
19:
20: int main() // liefert bei Fehler 1 zurueck
21: {
22: char fileName[80];
23:
24:
25: cout << "Bitte Dateinamen eingeben: ";
26: cin >> fileName;
27: ofstream fout(fileName,ios::binary);
28: if (!fout)
29: {
30: cout << "Es ist nicht moeglich, " << fileName <<
" zum Schreiben zu oeffnen.\n";
31: return(1);
32: }
33:
34: Animal Bear(50,100);
35: fout.write((char*) &Bear,sizeof Bear);
36:
37: fout.close();
38:
39: ifstream fin(fileName,ios::binary);
40: if (!fin)
41: {
42: cout << "Es ist nicht moeglich, " << fileName <<
" zum Lesen zu oeffnen.\n";
43: return(1);
44: }
45:
46: Animal BearTwo(1,1);
47:
48: cout << "BearTwo Gewicht: " << BearTwo.GetWeight() << endl;
49: cout << "BearTwo Tage: " << BearTwo.GetDaysAlive() << endl;
50:
51: fin.read((char*) &BearTwo, sizeof BearTwo);
52:
53: cout << "BearTwo Gewicht: " << BearTwo.GetWeight() << endl;
54: cout << "BearTwo Tage: " << BearTwo.GetDaysAlive() << endl;
55: fin.close();
56: return 0;
57: }

Bitte Dateinamen eingeben: Animals
BearTwo Gewicht: 1
BearTwo Tage: 1
BearTwo Gewicht: 50
BearTwo Tage: 100

Die Zeilen 3 bis 18 deklarieren eine einfache Animal-Klasse. Die Zeilen 22 bis 32 erzeugen eine Datei und öffnen sie im binären Modus. In Zeile 34 wird ein Tier (ein Animal -Objekt) mit einem Gewicht von 50 erzeugt, das 100 Tage alt ist. Diese Daten werden in Zeile 35 in die Datei geschrieben.

Zeile 37 schließt die Datei, und Zeile 39 öffnet sie wieder zum Lesen im Binärmodus. In Zeile 46 wird ein zweites Tier mit dem Gewicht von 1 erzeugt, das nur einen Tag alt ist. Die Daten von der Datei werden in das neue Animal-Objekt eingelesen und überschreiben damit die bereits bestehenden Daten (Zeile 51).

Befehlszeilenverarbeitung

In vielen Betriebssystemen, wie zum Beispiel DOS oder UNIX, hat der Anwender die Möglichkeit, bereits zu Programmbeginn Parameter an Ihr Programm zu übergeben. Diese werden auch als Befehlszeilenoptionen bezeichnet und werden in der Regel durch Leerzeichen in der Befehlszeile getrennt. Zum Beispiel:

EinProgramm Param1 Param2 Param3

Diese Parameter werden nicht direkt an main() übergeben. Statt dessen wird der main()-Funktion eines jeden Programms zwei Parameter übergeben. Der erste Parameter ist die Anzahl der Argumente in der Befehlszeile. Da der Programmname selbst mitzählt, hat jedes Programm mindestens einen Parameter. Die als Beispiel gedachte Befehlszeile über diesem Absatz hat demnach vier Parameter. (Der Name EinProgramm plus die drei Parameter ergeben zusammen vier Befehlszeilenargumente).

Der zweite Parameter, der an main() übergeben wird, ist ein Array von Zeigern auf Zeichenstrings. Da ein Array-Name ein konstanter Zeiger auf das erste Element im Array ist, können Sie dieses Argument als Zeiger auf einen Zeiger auf char, als Zeiger auf einen Array von char oder als ein Array von Arrays von char deklarieren.

In der Regel wird das erste Argument als argc (argument count = Argumentenzähler) bezeichnet. Sie können aber auch einen beliebigen anderen Namen wählen. Das zweite Argument trägt oft den Namen argv (argument vector = Argumentenvektor). Aber auch dies ist nur Konvention.

Es ist üblich, argc zu prüfen, um sicherzustellen, daß Sie die erwartete Anzahl von Argumenten erhalten haben, und argv zu benutzen, um auf die Strings selbst zuzugreifen. Beachten Sie, daß argv[0] der Name des Programms ist und argv[1] der erste Parameter für das Programm, dargestellt als ein String. Wenn Ihr Programm zwei Zahlen als Argumente übernimmt, müssen Sie diese Zahlen in Strings umwandeln. Listing 16.19 veranschaulicht die Verwendung von Befehlszeilenargumenten.

Listing 16.19: Befehlszeilenargumente

1:     #include <iostream.h>
2: int main(int argc, char **argv)
3: {
4: cout << "Uebergeben wurden " << argc << " Argumente...\n";
5: for (int i=0; i<argc; i++)
6: cout << "Argument " << i << ": " << argv[i] << endl;
7: return 0;
8: }

TestProgram C++ in 21 Tagen
Uebergeben wurden 5 Argumente...
Argument 0: TestProgram.exe
Argument 1: C++
Argument 2: in
Argument 3: 21
ArgumeNT 4: Tagen

Sie müssen diesen Code entweder von der Befehlszeile aus starten (das heißt vom DOS-Fenster aus) oder die Befehlszeilenparameter in Ihrem Compiler setzen (schlagen Sie dazu in Ihrer Compiler-Dokumentation nach).

Die Funktion main() deklariert zwei Argumente: argc ist ein Integer, der die Anzahl der Befehlszeilenargumente enthält, und argv ist ein Zeiger auf ein Array von Strings. Jeder String in dem Array, auf den argv zeigt, ist ein Befehlszeilenargument. Beachten Sie, daß argv genausogut als char *argv[] oder char argv[][] deklariert werden könnte. Es ist eine Frage des Programmierstils, wie Sie argv deklarieren. Auch wenn es in diesem Programm als ein Zeiger auf einen Zeiger deklariert ist, werden Array-Indizes benutzt, um auf die einzelnen Strings zuzugreifen.

Zeile 4 verwendet argc, um die Anzahl der Befehlszeilenargumente auszugeben: insgesamt fünf einschließlich des Programmnamens.

In den Zeilen 5 und 6 werden die Befehlszeilenargumente nacheinander ausgegeben, wobei die nullterminierten Strings über Array-Indizes angesprochen und an cout übergeben werden.

Häufiger werden Befehlszeilenargumente jedoch dazu verwendet, einen Dateinamen als Befehlszeilenargument zu übernehmen. Sehen Sie dazu Listing 16.18 in geänderter Fassung.

Listing 16.20: Befehlszeilenargumente

1:     #include <fstream.h>
2:
3: class Animal
4: {
5: public:
6: Animal(int weight, long days):itsWeight(weight),
itsNumberDaysAlive(days){}
7: ~Animal(){}
8:
9: int GetWeight()const { return itsWeight; }
10: void SetWeight(int weight) { itsWeight = weight; }
11:
12: long GetDaysAlive()const { return itsNumberDaysAlive; }
13: void SetDaysAlive(long days) { itsNumberDaysAlive = days; }
14:
15: private:
16: int itsWeight;
17: long itsNumberDaysAlive;
18: };
19:
20: int main(int argc, char *argv[]) // liefert bei Fehler 1 zurueck
21: {
22: if (argc != 2)
23: {
24: cout << "Aufruf: " << argv[0] << " <dateiname>" << endl;
25: return(1);
26: }
27:
28: ofstream fout(argv[1],ios::binary);
29: if (!fout)
30: {
31: cout << "Es ist nicht moeglich, " << argv[1] <<
" zum Schreiben zu oeffnen.\n";
32: return(1);
33: }
34:
35: Animal Bear(50,100);
36: fout.write((char*) &Bear,sizeof Bear);
37:
38: fout.close();
39:
40: ifstream fin(argv[1],ios::binary);
41: if (!fin)
42: {
43: cout << "Es ist nicht moeglich, " << argv[1] <<
" zum Lesen zu oeffnen.\n";
44: return(1);
45: }
46:
47: Animal BearTwo(1,1);
48:
49: cout << "BearTwo Gewicht: " << BearTwo.GetWeight() << endl;
50: cout << "BearTwo Tage: " << BearTwo.GetDaysAlive() << endl;
51:
52: fin.read((char*) &BearTwo, sizeof BearTwo);
53:
54: cout << "BearTwo Gewicht: " << BearTwo.GetWeight() << endl;
55: cout << "BearTwo Tage: " << BearTwo.GetDaysAlive() << endl;
56: fin.close();
57: return 0;
58: }

BearTwo Gewicht: 1
BearTwo Tage: 1
BearTwo Gewicht: 50
BearTwo Tage: 100

Die Deklaration der Animal-Klasse entspricht der aus Listing 16.18. Diesmal wird der Anwender jedoch nicht im Programm aufgefordert, einen Dateinamen einzugeben, statt dessen werden Befehlszeilenargumente verwendet. Zeile 2 deklariert main() mit zwei Parametern: der Anzahl der Befehlszeilenargumente und einem Zeiger auf das Array mit den Strings der Befehlszeilenargumente.

In den Zeilen 22 bis 26 stellt das Programm sicher, daß die erwartete Anzahl an Argumenten (exakt zwei) empfangen wurden. Wenn der Anwender es versäumt, einen Dateinamen anzugeben (oder mehr als einen Dateinamen übergibt), wird eine Fehlermeldung ausgegeben:

Aufruf: TestProgramm <dateiname>

Dann wird das Programm verlassen. Beachten Sie, daß Sie Dank der Verwendung von argv[0] anstelle eines hartkodierten Programmnamens dieses Programm mit jedem Namen kompilieren können. Die Fehlermeldung ist immer korrekt.

In Zeile 28 versucht das Programm, die angegebene Datei für die binäre Ausgabe zu öffnen. Es gibt keinen Grund, den Dateinamen dazu in einen lokalen temporären Puffer zu kopieren. Er kann direkt durch den Zugriff auf argv[1] verwendet werden.

Diese Technik kommt in Zeile 40 erneut zum Einsatz, wenn die gleiche Datei zur Eingabe erneut geöffnet wird, und sie wird ebenfalls in den Zeilen 31 und 43 in den Anweisungen zu den Fehlerbedingungen verwendet, wenn die Dateien nicht geöffnet werden können.

Zusammenfassung

Heute habe ich Ihnen erklärt, was Streams sind, und Ihnen die globalen Objekte cout und cin beschrieben. Das Ziel der istream- und ostream-Objekte ist es, die Arbeit, die beim Schreiben an die Gerätetreiber und beim Puffern der Ein- und Ausgabe anfällt, zu kapseln.

In jedem Programm werden vier Standardstreamobjekte erzeugt: cout, cin, cerr und clog. Diese Objekte können von vielen Betriebssystemen »umgeleitet« werden.

Das istream-Objekt cin wird für die Eingabe verwendet und meist zusammen mit dem überladenen Eingabe-Operator (>>) eingesetzt. Das ostream-Objekt cout wird für die Ausgabe verwendet und meist zusammen mit dem überladenen Ausgabe-Operator (<<) eingesetzt.

Jedes dieser Objekte verfügt über eine Vielzahl von Elementfunktionen wie get() und put(). Da die am häufigsten verwendete Version dieser Methoden eine Referenz auf ein Streamobjekt zurückliefert, lassen sich diese Operatoren und Funktionen problemlos miteinander verketten.

Der Status der Streamobjekte kann mit Hilfe von Manipulatoren verändert werden. Diese können die Einstellungen für die Formatierung und Anzeige verändern und verschiedene andere Attribute der Streamobjekte setzen.

Für die Ein- und Ausgabe in und aus Dateien sind die fstream-Klassen verantwortlich, die sich von den Stream-Klassen ableiten. Dabei unterstützen diese Objekte nicht nur die normalen Eingabe- und Ausgabe-Operatoren, sondern auch read() und write() zum Speichern und Zurückholen großer binärer Objekte.

Fragen und Antworten

Frage:
Woher wissen Sie, wann Sie den Ausgabe- oder den Eingabe-Operator verwenden müssen und wann die anderen Elementfunktionen der stream-Klassen zum Einsatz kommen?

Antwort:
Im allgemeinen ist es einfacher den Eingabe- und den Ausgabe-Operator zu verwenden. Man gibt ihnen den Vorzug, wenn genau ihr Verhalten gewünscht wird. In den eher seltenen Fällen, wo diese Operatoren nicht ausreichen (wie z.B. einen String aus mehreren Wörtern einzulesen), können die anderen Funktionen eingesetzt werden.

Frage:
Was ist der Unterschied zwischen cerr und clog?

Antwort:
cerr wird nicht gepuffert. Alles, was an cerr geschrieben wird, wird direkt ausgegeben. cerr bietet sich vor allem bei Fehlern an, die auf dem Bildschirm ausgegeben werden. Allerdings kann es etwas auf Kosten der Leistung gehen, Protokolle auf die Festplatte zu schreiben. clog puffert seine Ausgabe und kann deshalb effizienter eingesetzt werden.

Frage:
Wozu hat man das Stream-Konzept entwickelt, wo doch printf() so gut funktioniert hat?

Antwort:
printf() unterstützt leider weder das strenge Typensystem von C++ noch benutzerdefinierte Klassen.

Frage:
In welchem Fall kommt putback() zum Einsatz?

Antwort:
Wenn man mit einer Lese-Operation prüfen möchte, ob ein Zeichen gültig ist, das Zeichen selbst aber für eine andere Lese-Operation (vielleicht ein anderes Objekt) im Puffer belassen werden soll. Am häufigsten wird dies beim Parsen einer Datei der Fall sein. So ist es beispielsweise gut möglich, daß der C++- Compiler putback() verwendet.

Frage:
In welchen Fällen kommt ignore() zum Einsatz?

Antwort:
ignore() wird meist nach get() verwendet. Da get() das Terminierungszeichen im Puffer läßt, ist es nicht unüblich, direkt an den Aufruf von get() einen Aufruf von ignore (1, '\1') anzuschließen. Auch dies ist häufig beim Parsen der Fall.

Frage:
Meine Freunde verwenden printf() in ihren C++-Programmen. Kann ich das auch?

Antwort:
Aber sicher. Dadurch wird einiges einfacher, leider aber zu Lasten der Typensicherheit.

Workshop

Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, und Übungen, die Sie anregen sollen, das eben Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Versuchen Sie, das Quiz und die Übungen zu beantworten und zu verstehen, bevor Sie die Lösungen in Anhang D lesen und zur Lektion des nächsten Tages übergehen.

Quiz

  1. Was ist der Ausgabe-Operator, und wozu dient er?
  2. Was ist der Eingabe-Operator, und wozu dient er?
  3. Wie lauten die drei Formen von cin.get(), und wo liegen die Unterschiede?
  4. Was ist der Unterschied zwischen cin.read() und cin.getline()?
  5. Wie groß ist die Standardbreite für die Ausgabe eines Integers vom Typ long mit Hilfe des Ausgabe-Operators?
  6. Wie lautet der Rückgabewert des Ausgabe-Operators?
  7. Welche Parameter übernimmt der Konstruktor eines ofstream-Objekts?
  8. Was bewirkt das Argument ios::ate?

Übungen

  1. Schreiben Sie ein Programm, das die vier Standardstreamobjekte cin, cout, cerr und clog verwendet.
  2. Schreiben Sie ein Programm, das den Anwender auffordert, seinen vollständigen Namen einzugeben, und diesen dann auf dem Bildschirm ausgibt.
  3. Schreiben Sie eine Neufassung von Listing 16.9, die zwar das gleiche bewirkt, jedoch ohne putback() und ignore() auskommt.
  4. Schreiben Sie ein Programm, das einen Dateinamen als Parameter übernimmt und die Datei zum Lesen öffnet. Lesen Sie jedes Zeichen der Datei und lassen Sie nur die Buchstaben und Zeichensetzungssymbole auf dem Bildschirm ausgeben. (Ignorieren Sie alle nicht-druckbaren Zeichen.) Schließen Sie dann die Datei und beenden Sie das Programm.
  5. Schreiben Sie ein Programm, das seine Befehlszeilenargumente in umgekehrter Reihenfolge und den Programmnamen überhaupt nicht anzeigt.



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH