Ein- und Ausgabe

Für die Ein- und Ausgabe sollten nicht die stdio-Funktionen wie putchar(), printf() etc. genutzt werden sondern die im folgenden beschriebene Bibliothek.

Für Ein- und Ausgabe bietet C++ die sogenannten Streams an. Ein großer Vorteil der Streams ist, dass die Ein- und Ausgabe über die überladenen Operatoren >> und << realisiert wird, womit eine übersichtliche Programmierung und eine einfache Erweiterung für benutzerdefinierte Datentypen ermöglicht wird. Im einfachsten Fall werden Sie die Ausgabe auf den Bildschirm und die Eingabe von der Tastatur benötigen. Dazu stellt C++ die Klassen istream und ostream sowie die Objekte

Objekt Verwendung
cin Standardeingabe (Tastatur)
cout Standardausgabe (Bildschirm)
cerr Fehlerausgabe (Bildschirm)

zur Verfügung. Um die IO-Streams zu benutzen, müssen Sie die Bibliothek über

    #include <iostream>

einbinden (ohne Endung .h).

Ausgabe

Eine Ausgabe erfolgt durch die Anweisung:

    std::cout << object;

Mehrere Ausgaben können auch direkt miteinander verbunden werden:

    std::cout << object << object << ... << object;

Der Operator << ist bereits für alle eingebauten Datentypen definiert, die Ausgabe erfolgt immer entsprechend des jeweiligen Objekts.

Beispiel:

#include <iostream>
#include <iomanip>
#include <fstream>
double x = 25.391;
int i = 10;
char s[] = "Test";

std::cout << "Ausgabe im Terminal:\n";

std::cout << "Dies ist die Zahl x: " << x << std::endl;
std::cout << "Dies ist ein " << s << " mit i = " << i << std::endl;

std::endl steht für neue Zeile, stattdessen kann auch \n innerhalb eines Strings verwendet werden. Bei Verwendung von \n werden Ausgaben erst angezeigt, wenn das Programm endet oder std::flush aufgerufen wird.

Formatierte Ausgabe

Die Stream-Klassen bieten folgende Methoden zur Manipulation der Ausgabe:

Methode Verwendung
width() Lesen/Setzen der Feldbreite für Ausgabe
fill() Lesen/Setzen des Füllzeichens für Ausgabe
flags() Lesen/Setzen der Flags für Ausgabe
setf() Setzen von einzelnen Flags
unsetf() Löschen von einzelnen Flags
precision() Nachkommastellen bei Fließkommaausgabe

Beachte: Das Setzen der Feldbreite hat nur Einfluss auf das nächste Ausgabeelement. Ohne Angabe eines Parameters liefern width(), fill(), flags() und precision() jeweils den aktuellen Wert.

Für die Ausgabeformatierung stehen folgende Flags zu Verfügung:

Flag Verwendung
ios::left Linksbündige Ausgabe
ios::right Rechtsbündige Ausgabe
ios::internal Vorzeichen linksbündig, Wert rechtsbündig
ios::dec Dezimale Ausgabe
ios::oct Oktale Ausgabe
ios::hex Hexadezimale Ausgabe
ios::showbase Ausgabe der Zahlenbasis
ios::showpoint Ausgabe aller Nachkommazahlen, auch wenn 0
ios::showpos Ausgabe von + vor positiven Werten
ios::uppercase "E" und "X" statt "e" und "x"
ios::scientific Darstellung: 1.2345e67
ios::fixed Darstellung: 1234.56
ios::unitbuf Ausgabe nach jeder Operation
ios::stdio Ausgabe nach jedem Zeichen

Als Masken für setf() stehen außerdem zur Verfügung:

Flag Verwendung
ios::basefield Alle Zahlenbasis-Flags
ios::adjustfield Alle Ausrichtungs-Flags
ios::floatfield Alle Fließkomma-Flags

Beispiele für die Anwendung der Methoden:

    double x = 19.275;
    int i = 125;
    std::cout.width(6);
    std::cout.precision(3);
    std::cout.setf(std::ios::fixed, std::ios::floatfield);
    std::cout << x << std::endl;
    std::cout.width(10);
    std::cout.precision(5);
    std::cout.setf(std::ios::fixed, std::ios::floatfield);
    std::cout << x << std::endl;
    std::cout.width(10);
    std::cout << i << std::endl;
    std::cout.width(6);
    std::cout << i << std::endl;

Um das Ganze etwas zu vereinfachen, existieren die sogenannten IO-Manipulatoren, die das Setzen der Ausgabeparameter direkt als Ausgabeelemente über << ermöglichen. Sie sind in iomanip definiert. Um sie zu verwenden, muss

    #include <iomanip>

eingebunden werden. Die zur Verfügung stehenden IO-Manipulatoren sind:

Flag Verwendung
setbase() Setzen der Zahlenbasis
setfill() Setzen des Füllzeichens für die Ausgabe
setprecision() Nachkommastellen bei Fließkommaausgabe
setw() Setzen der Feldbreite für die Ausgabe
setiosflags() Setzen der ios-Flags
resetiosflags() Rücksetzen der ios-Flags

Die Parameter entsprechen denen der IO-Methoden. Am Beispiel ist nochmals die obige Ausgabe realisiert:

double x = 19.275;
    int i = 125;
    std::cout << std::setw(6) << std::setprecision(3)
         << std::setiosflags(std::ios::fixed) << x << std::endl;
    std::cout << std::setw(10) << std::setprecision(5)
         << std::setiosflags(std::ios::fixed) << x << std::endl;
    std::cout << std::setw(10) << i << std::endl;
    std::cout << std::setw(6) << i << std::endl;

Die Parameter sind voneinander unabhängig . Das bedeutet zum Beispiel, dass man für einen Wechsel von links- auf rechtsbündige Ausgabe left zurücksetzen und right setzen muss. Ansonsten ist das Resultat nicht vorhersehbar.

Eingabe

Möchte man den Wert einer Variablen einlesen, so existiert dafür der >>-Operator. Dieser funktioniert analog zur Ausgabe:

    cin >> variable;

Die Eingabe muss mit der Taste Enter abgeschlossen werden. Ausgewertet wird die Eingabe für jede Variable soweit, wie die gelesenen Zeichen zum Typ der Variable passen: z.B. endet die Eingabe für int beim ersten Zeichen das keine Ziffer ist, bei string bei einem Leerzeichen. Alle Leerzeichen innerhalb der Eingabe werden als Trennzeichen behandelt, soweit nicht das skipws-Flag gesetzt ist.

Ein als enum definierter Datentyp kann nicht einfach mit dem entsprechenden Operator eingelesen oder ausgegeben werden. Stattdessen muss eine Variable (z.B. ein int) eingelesen werden und dann konvertiert werden. Mit einem static_cast kann der Wert ohne Überprüfung konvertiert werden. Besser ist es, mit einem switch die Eingabe zu prüfen:


    enum class Farbe { Rot = 1, Blau = 2 };
    std::cout << "Farbe? (1=Rot, 2=Blau, sonst=Rot)" << std::endl;
    int i;
    std::cin >> i;
    Farbe f;
    // oder ungeprüft: f = static_cast<Farbe>(i);
    switch (i) {
        case int(Farbe::Blau):  
            f = Farbe::Blau;
            break;
        case int(Farbe::Rot):  
        default:
            f = Farbe::Rot;
            break;
    }
    std::cout << "Farbe: " << int(f) << std::endl;

Formatierte Eingabe

Analog zur Ausgabe, lässt sich auch die Eingabe durch Flags beeinflussen.

Flag Verwendung
ios::skipws führende Leerzeichen beim Einlesen ignorieren (default)
ios::noskipws führende Leerzeichen beim Einlesen nicht ignorieren

Ein- und Ausgabe mit Dateien

C++ unterstützt zwei Arten von Dateien: sequentielle Dateien und Dateien mit wahlfreiem Zugriff. In diesem Praktikum werden nur erstere behandelt.

Mit sequentiellen Dateien kann auf drei Arten interagiert werden: Daten einlesen, in eine neue Datei schreiben oder an eine existierende Datei anhängen. Bei einem sequentiellen File können die Daten nicht umgeordnet werden, ohne das ganze File zu löschen.

Die Standardbibliothek zum Lesen und Schreiben von Dateien kann mit

    #include <fstream>

eingebunden werden.

Schreiben von Daten in eine Datei

Die Ausgabe von Daten in ein File erfolgt analog zur Ausgabe von Daten auf dem Bildschirm. Betrachten Sie zunächst folgendes Beispielprogramm:


        // Testvariablen
        std::string name = "Arthur";
        int semester = 42;
        double schnitt = 4.2;
        const std::string& dateiname = "Ausgabedatei.dat";

        // Öffnen des Files für die Ausgabe
        std::ofstream outfile(dateiname);

        // Schreibe die Daten in das File
        outfile << "Name: " << name << std::endl;
        outfile << "Semester: " << semester << std::endl;
        outfile << "Notenschnitt: " << std::setprecision(1)
                << std::setiosflags(std::ios::fixed) << schnitt << std::endl;
    

Nach der Initialisierung einiger Testvariablen erzeugt das Programm ein Objekt mit Namen outfile, welches den Datentyp ofstream (für output file stream) hat. ofstream ist eine Unterklasse von ostream, der allgemeinen Klasse, für die der <<-Operator definiert wird. Als Parameter erhält der Konstruktor eines ofstream-Objektes den Namen des zu erzeugenden Files (hier: Ausgabedatei.dat). Das Objekt outfile repräsentiert nun das File, in das die Ausgabe erfolgen soll. Selbstverständlich können Sie auch einen beliebigen anderen Namen für das Objekt verwenden. Ganz analog zur Ausgabe auf dem Bildschirm können die Daten nun mit Hilfe des Ausgabeoperators << in das File geschrieben werden.

Beachten Sie, dass das File beim Aufruf des Konstruktors automatisch geöffnet wird und der Destruktor das File wieder schließt, wenn der Gültigkeitsbereich des Objektes verlassen wird.

Das Programm erzeugt bei seiner Ausführung die Datei Ausgabedatei.dat mit folgendem Inhalt:

Name: Arthur
Semester: 42
Notenschnitt: 4.2

Lesen von Daten aus einer Datei

Das folgende Programm liest die Daten aus der Datei Ausgabedatei.dat wieder ein und gibt sie auf dem Bildschirm aus:


        std::string name;
        int semester;
        double schnitt;
        std::string info[3];
        const std::string& dateiname = "Ausgabedatei.dat";

        // Öffnen des Files zum Lesen
        std::ifstream infile(dateiname);

        // Lese die Daten aus dem File
        infile >> info[0] >> name;
        // info[0] = "Name:"; name = "Arthur";

        infile >> info[1] >> semester;
        // info[1] = "Semester:"; semester = 42;

        infile >> info[2] >> schnitt;
        // info[2] = "Notenschnitt:"; schnitt = 4.2;

        // Schreiben auf den Bildschirm
        std::cout << info[0] << name << std::endl;
        std::cout << info[1] << semester << std::endl;
        std::cout << info[2] << schnitt << std::endl;
    

Nach der Definition der Variablen, welche die einzulesenden Daten aufnehmen sollen, wird ein Objekt vom Typ ifstream (für input file stream) erzeugt, welches automatisch das gewünschte File zum Lesen öffnet. ifstream ist eine Unterklasse von istream, der allgemeinen Klasse, für die der >>-Operator definiert wird. Der überladene Eingabeoperator >> wird dann verwendet um die Daten aus dem File in die Variablen einzulesen. Zu beachten ist hierbei, dass Leerzeichen, Tabulatoren und Zeilenumbrüche als Trennzeichen dienen um die einzelnen Daten im File voneinander abzugrenzen. Ein in der Datei gespeicherter String darf also keine Leerzeichen enthalten, da er beim Lesen sonst als zwei (oder mehr) Strings interpretiert wird. Dieses Verhalten kann durch spezielle Funktionen geändert werden. Auch ist darauf zu achten, dass die Datentypen der Variablen mit den Datentypen der gelesenen Werte übereinstimmen, da es ansonsten zu einem Lesefehler kommt. Das Programm gibt folgendes auf dem Bildschirm aus:

Name:Arthur
Semester:42
Notenschnitt:4.2

Ist die Menge der einzulesenden Daten nicht von vornherein bekannt, so muss es eine Möglichkeit geben zu erkennen, wann das Dateiende erreicht ist. Dies kann mittels der Funktion eof() getestet werden.

// Variable zum Einlesen
int zahl;
// Öffnen der Datei
std::ifstream fin("Testdatei.dat"); // Lies , bis Dateiende erreicht fin >> zahl;
fin >> zahl; // Einlesen vor erstem eof Test
while (!fin.eof())
{
    std::cout << "Gelesene Zahl : " << zahl << std::endl;
    fin >> zahl; // Versuch, nächste Zahl zu lesen 
}

Beachten Sie, dass das Dateiende nicht beim Lesen der letzten Zeile, sondern erst beim darauffolgenden (erfolglosen) Versuch, eine Zeile zu lesen, erkannt wird. Bevor auf eof() getestet werden kann, muss also eingelesen werden.

Zur Verdeutlichung der Zusammenhänge sei hier noch einmal die Klassenstruktur der IO dargestellt.

image.png

Abfangen von Fehlern

Beim Lesen von Files und Schreiben in Files können Fehler auftreten, die abgefangen werden müssen. Beispielsweise könnte man versuchen, aus einem nichtexistierenden File zu lesen, das [usb]{acronym-label="usb" acronym-form="singular+short"}-Laufwerk könnte nicht verfügbar sein oder man könnte für die Datei nicht genügend Rechte haben. Ein ifstream- oder ofstream-Objekt besitzt vier Funktionen, über die der Zustand abgefragt werden kann: good() (Operation erfolgreich), eof() (Ende der Datei erreicht), fail() (Fehler z.B. Formatfehler) und bad() (Gegenteil von good()). Dieser Zustand bezieht sich immer auf die letzte ausgeführte Leseoperation.

Fehler bei Streams können auf zwei Weisen behandelt werden. Die eine Möglichkeit ist, nach jeder Operation abzufragen, ob der Stream noch good() ist. Falls ein Fehlerzustand vorliegt, kann der Fehler durch eine entsprechende Aktion bearbeitet werden oder eine Exception geworfen werden.

Die Stream-Klassen unterstützen das Werfen der Exception auch direkt: Mit der exceptions-Methode wird aktiviert, dass der Stream Exceptions wirft, wenn eine Operation fehlschlägt. Diese Methode nimmt eine Bitmaske entgegen und wird wie folgt aufgerufen:

    ifstream f("dateiname");
    f.exceptions(ios_base::eofbit | ios_base::failbit | ios_base::badbit);

ios_base ist dabei die Basisklasse aller Eingabe- und Ausgabe-Streams. Die Zustände, die zu Exceptions führen sollen, werden mit einem bitweisen Oder (|) verknüpft. Bei der Ausführung werden dann bei einem Fehler Ausnahmen vom Typ ios::failure -- einer Unterklasse von std::exception -- geworfen. Falls schon ein Fehler existiert, wenn diese Methode aufgerufen wird, wird sofort eine Ausnahme geworfen.


    int liesFile(std::string dateiname) {
        // Öffne die Datei zum Lesen
        std::ifstream fin(dateiname);
        fin.exceptions(std::ios::eofbit | std::ios::failbit | std::ios::badbit);
        // Exceptions definieren. 

        // Lies den Wert ein
        int i;
        fin >> i;
        // Exception geworfen, falls Einlesen fehlschlug
        return i;
    }

Der Unterschied zwischen den Zuständen fail und bad ist klein, aber durchaus von Interesse: fail wird i.A. gesetzt, wenn ein Vorgang nicht korrekt durchgeführt werden konnte, der Stream aber prinzipiell in Ordnung ist. Typisch dafür sind Formatfehler beim Einlesen: Wenn z.B. ein Integer eingelesen werden soll, das nächste Zeichen aber ein Buchstabe ist, kommt der Stream in diesen Zustand. bad wird gesetzt, wenn der Stream prinzipiell nicht mehr in Ordnung ist oder Zeichen verlorengegangen sind. Dann ist der Stream nicht mehr benutzbar. Dieses Flag wird z.B. beim Positionieren vor einen Dateianfang gesetzt.

Datei-Flags

Für den genauen Modus der Dateibearbeitung existieren einige Flags, die in der Klasse ios_base wie folgt definiert werden:

Flag Verwendung
ios_base::in Lesen (Default bei ifstream)
ios_base::out Schreiben (Default bei ofstream)
ios_base::app Anhängen
ios_base::ate ans Ende Positionieren ("at end")
ios_base::trunc alten Dateiinhalt löschen
ios_base::nocreate Datei muss existieren
ios_base::noreplace Datei darf nicht existieren

Die Flags können mit dem |-Operator verknüpft werden. Übergeben werden sie dann dem Konstruktor als optionales zweites Argument. Die folgende Anweisung öffnet z.B. eine Datei, die bereits existieren muss, zum Schreiben und sorgt dafür, dass der geschriebene Text an das Ende der Datei angehängt wird:

    fstream datei("xyz.out",
                  ios_base::out | ios_base::app | ios_base::nocreate);

Strings

Mit einem String der Standardbibliothek von wird die Bearbeitung von Zeichenketten gegenüber C-Strings (char[]) wesentlich erleichtert. Voraussetzung für die Benutzung ist die Einbindung der Bibliothek <string> durch

    #include <string>

Es können dann Elemente der Klasse string mit verschiedenen Konstruktoren erstellt werden:

  1. string myString; erstellt einen leeren String.

  2. string myString(10, 'A');

    erstellt einen String mit 10 Zeichen und dem Inhalt "AAAAAAAAAA". Üblicher ist natürlich eine Initialisierung mit Leerzeichen.

  3. string myString("Dies ist ein String");

    erstellt einen String mit 19 Zeichen und dem Inhalt "Dies ist ein String".

Die Ein-/Ausgabe der Strings erfolgt über die entsprechenden << bzw. >>-Operatoren.


    std::string s1(10, 'A');
    std::string s2("Dies ist ein String.");
    std::string s3;

    std::cout << s1 << std::endl;
    std::cout << s2 << std::endl;
    std::cout << "Geben Sie einen String ein:" << std::endl;
    std::cin >> s3;
    std::cout << "Der String " << s3  << " wurde eingegeben." << std::endl;

Auch die Vergleichsoperatoren, sowie der + - und der += -Operator sind intuitiv für Strings definiert. Die Vergleichsoperatoren liefern das Ergebnis des zeichenweisen Vergleichs entsprechend der zugrundeliegenden Codierung. Da die Codierung von Buchstaben alphabetisch ist, sind auch die Vergleichsoperatoren für Strings alphabetisch. Die + -Operatoren dienen zur Aneinanderreihung (Konkatenation) zweier Strings.

Beispiel:

        std::string s1("ABC");
        std::string s2;
        std::string s3;

        s2 = "AC";
        s3 = s1 + s2;
        std::cout << s3 << std::endl;
        std::cout << "Maximum von: " << s1 << " und " << s2 << ": ";
        if (s1 > s2)
            std::cout << s1;
        else
            std::cout << s2;

Der Zugriff auf die einzelnen Zeichen eines Strings erfolgt mit dem []-Operator. Auch hier wird mit dem Index 0 auf das erste Zeichen zugegriffen. Im Gegensatz zu den C-Strings wird string nicht durch ein Null-Byte (\0) beendet. Das Ende wird durch die aktuelle Länge des Strings, die mitgespeichert wird, bestimmt. Die Länge kann durch die Methode length() abgefragt werden.

Beispiel:


        std::string s1("ABC");

        // [] fängt bei 0 an
        std::cout << "Zweites Zeichen: " << s1[1] << std::endl;
        // length() gibt die Anzahl der Zeichen an
        std::cout << "Länge: " << s1.length() << std::endl;
    

Beachte: s1[s1.length() - 1] ist das letzte Zeichen des Strings, s1[s1.length()] erzeugt einen Zugriffsfehler.

Neben diesem direkten Zugriff auf die einzelnen Elemente kann auch sequentiell auf die Zeichen eines Strings zugegriffen werden.

Beispiel:


    std::string s1("ABCD");

    for (auto c : s1)
        std::cout << c << ' ';

Für die komfortable Bearbeitung von Stringobjekten stehen diverse Methoden zur Verfügung. Die in aufgeführte Auswahl ist nicht vollständig und für einige Methoden gibt es neben den dargestellten auch noch weitere Aufrufvarianten mit anderen Parametertypen (siehe dazu die Literatur oder Online-Hilfe).

Methode Funktionalität
length(), size() Liefert die aktuelle Länge des Strings
empty() Liefert true, falls leer
insert(pos, str) Fügt strvor pos im Strin ein
erase(pos, anzahl) Löscht im String ab pos anzahl Zeichen
replace(pos, anzahl, str) Ersetzt ab pos bis zu anzahl Zeichen durch die Zeichen von str
find(str, pos) Liefert die Position des ersten Auftretens von str im String ab pos, oder -1 falls nicht vorhanden
rfind(str, pos) Liefert die Position des letzten Auftretens von str im String ab pos, oder -1 falls nicht vorhanden
find_first_of(str, pos) Liefert die Position des ersten Auftretens eines Zeichens von str im String ab pos, oder -1 falls nicht vorhanden
find_last_of(str, pos) Liefert die Position des letzten Auftretens eines Zeichens von str im String ab pos, oder -1 falls nicht vorhanden
find_first_not_of(str, pos) Liefert die erste Position des Zeichens im String, das nicht in str vorkommt, oder -1 falls alle vorkommen
find_first_not_of(str, pos) Liefert die letzte Position des Zeichens im String, das nicht in str vorkommt, oder -1 falls alle vorkommen
substr(pos, laenge) Liefert einen Substring ab pos im String mit der angegebenen laenge
compare(str)
compare(pos, anz, str)
compare(pos, anz, str, pos1, anz1
Vergleicht den String zeichenweise mit str und liefert als Ergebnis:
-1 falls str < aktuellem String
0 falls str = aktuellem String
1 falls str > aktuellem String
Durch die übrigen Parameter kann der Vergleich auf einen Teilstring des aktuellen Strings (pos, anz) bzw. des Vergleichsstrings (pos1, anz1) eingeschränkt werden.

Es ist zu beachten, dass Stringobjekte und C-Strings nicht verwechselt oder gemischt werden dürfen, auch wenn für die meisten obigen Stringmethoden auch entsprechende überlagerte Methoden für C-Strings als Parameter existieren. Für Funktionen, die C-Strings erwarten (z.B. Funktionen der Windowsbibliothek) existieren keine entsprechenden Aufrufe für Stringobjekte. Allerdings können sie in beide Richtungen umgewandelt werden:

String-Streams

Mit den String-Stream-Klassen kann man unter Verwendung der Formatierungsmöglichkeiten der IO-Streams aus einem String lesen (wie z.B. von der Standardeingabe cin) oder in einen String schreiben (wie auf die Standardausgabe cout). Solche Streams werden String-Streams (Klasse stringstream) genannt. Sie werden in <sstream> definiert.

Wie bei den Stream-Klassen für Dateien gibt es analog Stream-Klassen für Strings:

Die Klassen erben von iostream und übernehmen daher auch den Funktionsumfang der Oberklasse, wie z.B. die Ein-/Ausgabeoperatoren. Weiterhin stellt stringstream folgende Funktionen zur Verfügung:

Beispiel:

    #include <sstream>
    using namespace std;

    string namen[3];
    for (int i = 0; i < 3; i++) {
        // Beachte: stringstream lokal, keine Datei
        stringstream s;
        // Ausgabe "Objekt" + Nummer, z.B. "Objekt1"
        s << "Objekt" << i + 1;
        namen[i] = s.str();
    }

    // Kontrollausgabe
    for (const auto& name : namen){
        cout << name << endl;
    }