Ein- und Ausgabe in C++ - IO-Streams



  • Vorbemerkungen

    In C werden Daten über die Funktionsfamilie um printf() und scanf() ein- bzw. ausgegeben. Dieser Ansatz hat einige Probleme und Nachteile:

    • Die nötigen Formatierungscodes sind recht komplex
    • Niemand kann überprüfen, ob die Daten zum Formatstring passen
    • Es ist nicht möglich, die Ein-/Ausgabe für eigene Datentypen zu erweitern
    • Der Platzbedarf der Zieldaten (besonders bei char*) kann nur schwer überwacht werden

    Mit den neuen Möglichkeiten von C++ gibt es deshalb einen anderen Ansatz, der diese Probleme weitgehend umschifft - Streams. Die Ein- und Ausgabe über C++-Streams ist typsicher, fehlertolerant und erweiterbar. Dank eines gemeinsamen Interfaces ist es möglich, unabhängig voneinander sowohl neue Datentypen zu verarbeiten als auch eigene Datenquellen einzubinden.

    Inhalt

    1. Basisklassen
    2. Ein- und Ausgabeformatierung
    3. Allgemeine Steuerfunktionen
    4. File-Streams
    5. String-Streams
    6. Stream-Puffer
    7. Erweiterungen

    1 Basisklassen

    Alle Streamklassen der C++-Bibliothek stammen von einem kleinen Satz an Grundklassen ab, die alle elementaren Funktionen zur Verfügung stellen.

    Anmerkung: Alle Klassen, die mit "basic_" beginnen, sind Templates. Ihre Parameter sind der verwendete Zeichentyp und seine Character-Traits. Letzteres ist eine Hilfsklasse, die Operationen zum Vergleichen, Kopieren und Suchen von Zeichenketten bereitstellt. Der Defaultwert für die Traits-Klasse verarbeitet Zeichenketten im C-Stil - indem die Methoden zurückgeführt werden auf die C-Funktionen memcmp(), memcpy(),... bzw. ihre wchar_t-Äquivalente.
    Von jeder dieser Klassen existiert eine Spezialisierung für char (diese hat denselben Namen ohne das "basic_"-Präfix) und für wchar_t (deren Name hat das Präfix "w").

    Die Character-Traits werden intensiver von den String-Klassen genutzt, deshalb werde ich bei der Behandlung von Strings auch näher auf sie eingehen. Die (für die Stream-Bibliothek) wichtigsten Elemente der Traits-Klassen sind die Typdefinitionen char_type (der verwendete Zeichentyp) und int_type (ein Hilfstyp, der alle Zeichen und einen zusätzlichen Wert als End-of-File Zeichen enthält) sowie die Methode eof(), die diesen Spezialwert zurückliefert. Alle Methoden der Stream-Bibliothek, die Einzelzeichen als Rückgabe liefern sollen, haben den Rückgabetyp traitT::int_type und geben im Fehlerfall den Wert traitT::eof() zurück.

    1.1 Grundklassen

    class ios_base;
    template<typename charT,typename traitT=char_traits<charT> >
    class basic_ios : public ios_base;
    
    typedef basic_ios<char>    ios;
    typedef basic_ios<wchar_t> wios;
    

    Diese beiden Klassen stellen die grundlegenden Definitionen für alle Streams zur Verfügung. ios_base enthält alle Definitionen, die unabhängig vom verwendeten Zeichensatz sind (z.B. die Modi, mit denen eine Datei geöffnet werden kann, oder Statusflags für einen Stream), basic_ios und seine Spezialisierungen enthalten alles, was vom Zeichentyp abhängig ist (z.B. der verwendeten Stream-Puffer).

    1.2 Ein- bzw. Ausgabestreams

    template<typename charT,typename traitT=char_traits<charT> >
    class basic_istream : public virtual basic_ios;
    template<typename charT,typename traitT=char_traits<charT> >
    class basic_ostream : public virtual basic_ios;
    template<typename charT,typename traitT=char_traits<charT> >
    class basic_iostream : public basic_istream<charT,traitT>, public basic_ostream<charT,traitT>;
    

    Diese Klassen definieren die nötigen Operationen für die Eingabe (istream) bzw. die Ausgabe (ostream) von Daten. Die Klasse iostream fasst Ein- und Ausgabeoperationen zu einem Stream zusammen, der in beide Richtungen genutzt werden kann.

    Eingabe-Operationen:

    • strm>>var;
      parst den Stream entsprechend seinem Parametertyp und schreibt das Ergebnis in die übergebene Variable.
      Es exisitieren passende Operatoren für alle eingebauten Typen von C++ sowie für etliche Klassen der STL - per Operator-Überladung kann jede Klasse ihre Eingabeoperation selber kontrollieren.
    • get() und get(c)
      lesen ein einzelnes Zeichen vom Stream (im Gegensatz zu operator>> werden Whitespaces mitgelesen). Die erste Version gibt das gelesene Zeichen (oder EOF) als Rückgabewert aus, die zweite speichert es in 'c'.
    • get(s,ct,t='\n') und getline(s,ct,t='\n')
      lesen maximal 'ct' Zeichen aus, bis sie auf das Zeichen 't' (Standardwert ist New-Line) treffen, und speichern sie im char-Array s (operator>> liest Strings bis zum ersten Whitespace). get() lässt das Trennzeichen im Stream stehen, getline() liest und verwirft es.
    • read(s,ct) und readsome(s,ct)
      lesen einen Block binäre Daten aus dem Stream ein (readsome() kann weniger Zeichen lesen, wenn es auf das Dateiende stößt). read() liefert als Rückgabe den Stream, readsome() die Anzahl der tatsächlich gelesenen Zeichen.
      getline(istr,s,t='\n') (globale Funktion)
      liest so lange Zeichen aus dem Stream 'istr' und hängt sie an den String 's' an, bis es auf das Zeichen 't' trifft
    • ignore(ct=1,t='\0')
      liest und verwirft 'ct' Zeichen aus dem Stream (wenn der zweite Parameter angegeben wird, werden maximal solange Zeichen ignoriert, bis der Stream das angegebene Zeichen findet)
      Um alle Zeichen bis zum angegebenen Trennzeichen zu ignorieren, muss als erster Parameter der Wert "numeric_limits<streamsize>::max()" übergeben werden.
    • peek()
      liest das nächste Zeichen im Stream und gibt es zurück - im Gegensatz zu get() wird die Leseposition nicht weitergeschoben, so dass die nächste Lese-Operation dasselbe Zeichen erhält
    • unget() und put_back(c)
      schieben das letzte gelesene Zeichen in den Eingabestream zurück (put_back() setzt badbit, wenn c nicht das letzte gelesene Zeichen war)
    • gcount()
      gibt die Anzahl an Zeichen zurück, die vom letzten get(), getline() oder read() Aufruf gelesen wurden.

    Ausgabe-Operationen:

    • strm<<var;
      gibt den Inhalt der gegebenen Variablen in Textform auf dem Stream aus (das genaue Format hängt vom Datentyp und dem Status des Streams ab).
      Es existieren passende Operatoren für alle eingebauten Typen von C++ sowie für etliche Klassen der STL - per Operator-Überladung kann jede Klasse ihre Ausgabeoperation selber kontrollieren
    • put(c)
      schreibt ein einzelnes Zeichen in den Stream
    • write(s,ct)
      schreibt einen Block binäre Daten in den Stream
    • flush()
      flusht den Stream (dies bewirkt, dass bisher geschriebene Daten aus dem Puffer in den Ausgabekanal geschrieben werden)
      Anmerkung: Der Puffer kann auch aus eigenem Antrieb seine Daten flushen, wenn sein Zwischenspeicher keinen Platz mehr zur Verfügung hat.

    Anmerkung: Die Stream-Operatoren >> und << geben als Ergebnis den verarbeiteten Stream zurück. Auf diese Weise lassen sich mehrere Ein- bzw. Ausgabeoperationen zu einer Operator-Kette koppeln.

    1.3 Stream-Puffer

    template<typename charT,typename traitT=char_traits<charT> >
    class basic_streambuf;
    

    Ein Stream-Puffer wird verwendet, um eine Verbindung zwischen dem Stream und seiner unterliegenden Datenrepräsentation herzustellen. Die Streamklassen formatieren ihre Eingaben zu einer charT-Folge und schreiben sie in den Puffer bzw. parsen die vom Puffer erhaltenen charT's, um ihre Ausgabedaten zu erzeugen. Der Puffer wiederum leitet die Anfragen z.B. an die Festplatte, den Monitor oder ein Datenbank-System weiter.

    Ich werde den Einsatz von Stream-Puffern im Kapitel 6 ausführlicher beschreiben.

    1.4 Standard-Stream-Objekte

    Analog zu den IO-Objekten aus C - stdin, stdout und stderr - gibt es auch in der Stream-Bibliothek einige spezielle Objekte, die mit den Standard-IO-Kanälen verknüpft sind:

    • cin - Standard-Eingabe (entspricht stdin)
    • cout - Standard-Ausgabe (entspricht stdout)
    • cerr - Fehler-Ausgabe (entspricht stderr)
    • clog - Logging-Ausgabe (hat kein C-Äquivalent)

    clog schreibt normalerweise auf denselben Kanal wie cerr, puffert seine Daten jedoch zwischen (cerr arbeitet ungepuffert - jede Ausgabe landet sofort auf dem Ausgabekanal).

    Für den Umgang mit Multibyte-Zeichensätzen exisiteren außerdem die wchar_t-Versionen dieser Standard-Streams wcin, wcout, wcerr und wclog.

    1.5 Stream-Header

    Die Stream-Bibliothek wurde aus verschiedenen Gründen auf mehrere Header verteilt. Auf diese Weise müssen nur die Teile eingebunden werden, die tatsächlich benötigt werden:

    • <iosfwd>
      Forward-Deklarationen für die Streamklassen
    • <streambuf>
      die Klasse basic_streambuf
    • <istream>
      die Klassen basic_istream und basic_iostream
    • <ostream>
      die Klasse basic_ostream
    • <iostream>
      die Standard-Streams (siehe Kapitel 1.4)
    • <fstream>
      File-Streams (siehe Kapitel 4)
    • <sstream>
      String-Streams (siehe Kapitel 5)
    • <strstream>
      char*-Streams (siehe Kapitel 5.1)
    • <iomanip>
      parametrisierte Manipulatoren (siehe Kapitel 2)

    2 Ein- und Ausgabeformatierung

    Die Arbeit der Stream-Operatoren >> und << kann angepasst werden, indem man vorher den Status des Streams geeignet setzt. Neben den Formatierungsmethoden, die von den Stream-Klassen bereitgestellt werden, können fast alle Formate auch über Manipulatoren gesetzt werden - das sind spezielle "Objekte", die in die Operatorkette eingeschoben werden können, um den verwendeten Stream anzupassen:

    //Anpassung über Methoden:
    double val=3.14;
    cout.precision(5);
    cout<<val;
    
    //Anpassung über Maipulator:
    double val=3.14;
    cout<<setprecision(5)<<val;
    

    Manipulatoren, die einen Parameter benötigen - wie das oben gegebene setprecision() - sind im Header <iomanip> definiert. Parameterlose Manipulatoren werden zusammen mit den Streamklassen definiert, die sie benötigen.

    2.1 allgemeine Flags

    Die meisten Formatflags des Streams sind zu einem Flag-Feld zusammengefasst. Dieses kann per Bit-Operatoren komplett oder teilweise angepasst werden:

    • setf(flags);
      ergänzt den aktuellen Status um die angegebenen Flags
    • setf(flags,mask);
      löscht die Flags der Gruppe "mask" und setzt anschließend die angegebenen Flags (normalerweise kann nur ein Flagwert pro Gruppe gesetzt sein)
    • unsetf(flags);
      resettet die angegebenen Flags
    • flags(flags)
      ersetzt den Status durch die angegebenen Flags (auf diese Weise kann recht schnell der Status rekonstruiert werden)
    ios:fmtflags oldf = cout.flags();     //alte Flags merken
    cout.setf(ios::showpos | ios:showbase);//Plus-Zeichen und 0x schreiben
    cout.setf(ios::left, ios::adjustfield);//Ausrichtung linksbündig
    cout<<hex<<val;
    
    cout.flags(oldf);                      //alte Flags zurücksetzen
    
    • copyfmt(strm)
      kopiert alle Formatinformationen vom Stream 'strm' (neben den Flags auch die Feldbreite, Präzision, Füllzeichen und iword()-Werte)
    • flags()
      gibt den aktuellen Wert der Formatflags zurück

    Zur Flag-Bearbeitung gibt es die Manipulatoren "setiosflags(f)" (ruft setf(f) auf) und "resetiosflags(m)" (verwendet setf(0,m)). Außerdem haben viele der Flag-Werte einen eigenen Manipulator, mit dem sie gesetzt bzw. zurückgesetzt werden können.

    Flag        Maske        Wirkung                       setzen     rücksetzen
                                                              (Manipulator)
    
    boolalpha   -            bool als "true" bzw. "false"  boolapha   noboolalpha
                             ein/ausgeben
    showpos     -            '+'-Vorzeichen ausgeben       showpos    noshowpos
    uppercase   -            'E' und Hex-Ziffern in        uppercase  nouppercase
                             Großbuchstaben
    showpoint   -            Dezimalpunkt immer anzeigen   showpoint  noshowpoint
    showbase    -            Zahlenpräfix (0 bzw. 0x)      showbase   noshowbase
                             anzeigen
    skipws      -            Whitespaces überspringen      skipws     noskipws
    unitbuf     -            Ausgabe ungepuffert           unitbuf    nounitbuf
    
    left        adjustfield  linksbündige Ausgabe          left
    right,0     adjustfield  rechtsbündige Ausgabe         right
    internal    adjustfield  interne Ausgabe (Vorzeichen   internal
                             links, Zahl rechts)
    
    oct         basefield    Oktale Ein-/Ausgabe           oct
    dec         basefield    Dezimale Ein-/Ausgabe         dec
    hex         basefield    Hexadezimale Ein-/Ausgabe     hex
    0           basefield    Ausgabe dezimal, Eingabe je
                             nach Präfix (0 oder 0x)
    
    fixed       floatfield   dezimale Notation             fixed
    scientific  floatfield   wissenschaftliche Notation    scientific
    0           floatfield   "beste" Variante ausgesucht
    

    2.2 Feldbreite und Füllzeichen

    Die Feldbreite des Streams bestimmt die Mindestgröße für die nächste Ausgabeoperation oder die Maximalgröße für die nächste Eingabe (besonders wichtig für die Eingabe in char-Arrays). Ihr aktueller Wert kann über die Methode strm.width() abgefragt und über die Methode strm.width(w) oder den Manipulator setw(w) gesetzt werden.

    Das Füllzeichen wird verwendet, wenn eine Ausgabeoperation weniger Zeichen benötigen würde als die Feldbreite angibt. In dem Fall wird die Ausgabe je nach Einstellungen der adjustfield-Flags vorne oder hinten mit dem gegebenen Zeichen gefüllt. Das aktuelle Füllzeichen kann über die Methode strm.fill() abgefragt und über die Methode strm.fill(c) oder den Manipulator setfill(c) gesetzt werden.

    Die Feldbreite gilt nur für den nächsten Aufruf von operator<< oder operator>> und wird anschließend auf 0 zurückgesetzt; das Füllzeichen gilt so lange, bis es neu gesetzt wird. Standardwert ist 0 für die Feldbreite und Space als Füllzeichen.

    2.3 Gleitkomma-Präzission

    Im Gegensatz zu den printf()-Formatstrings gilt die Präzissions-Angabe des Streams nur für Gleitkomma-Ausgaben. Sie bestimmt (je nach Belegung der floatfield-Flags), wie viele signifikante Ziffern bzw. Nachkommastellen der Zahl ausgegeben werden:

    Einstellung  Präzision  421           0.0123456789
    Normal       2          4.2e+02       0.012
                 6          421           0.0123457
    showpoint    2          4.2e+02       0.012
                 6          421.000       0.0123457
    fixed        2          421.00        0.01
                 6          421.000000    0.012346
    scientific   2          4.21e+02      1.23e-02
                 6          4.210000e+02  1.234568e-02
    

    Die Präzision kann über die Methode strm.precision() abgefragt und über die Methode strm.precision(p) oder den Maipulator setprecision(p) gesetzt werden. Sie bleibt so lange gültig, bis sie mit einem neuen Wert überschrieben wird.

    2.4 weitere Manipulatoren

    Es gibt noch einige weitere Manipulatoren, die auf die Streams angewendet werden können:

    • flush - führt die flush()-Methode eines Ausgabestreams aus
    • endl - schreibt ein '\n' und führt anschließend die flush-Methode aus
    • ends - schreibt ein '\0' (wird v.a. im Umgang mit char*-Streams benötigt
    • ws - liest und ignoriert Whitespaces

    Für eigene (parameterlose) Manipulatoren kann jede Funktion verwendet werden, die einen entsprechenden Stream übernimmt und zurückgibt. Der entsprechende Ein-/Ausgabe-Operator ruft diese Funktion auf und gibt den von ihr zurückgegebenen Stream weiter.

    Der folgende Manipulator kann z.B. verwendet werden, um eine Eingabezeile zu ignorieren:

    template<typename charT,typename traitT>
    inline basic_istream<charT,traitT>& ignoreLine(basic_istream<charT,traitT>& strm)
    {
      strm.ignore(std::numeric_limits<int>::max(),strm.widen('\n'));
      return strm;
    }
    
    ...
    cin>>ignoreLine;//ignoriere eine Zeile der Eingabe
    

    Parametrisierte Manipulatoren sind etwas komplizierter aufgebaut. Hierfür sieht der C++-Standard keine einheitliche Funktionsweise vor - Hauptsache es funktioniert. Eine Möglichkeit wäre es, eine Klasse zu defnieren, deren Konstruktor die notwendigen Parameter entgegennimmt und deren operator<< bzw. operator>> die nötigen Manipulationen vornimmt.

    2.5 erweiterte Flags

    Eigene Datentypen benötigen mitunter eigene Formatbeschreibungen. Um diese nutzen zu können, besitzt ein Stream ein Array von Hilfsflags, deren Bedeutung vom Programmierer festgelegt werden kann:

    • xalloc()
      gibt einen freien Index im Flag-Array zurück

    • iword(ind) und pword(ind)
      geben den Inhalt des Feldes 'ind' im Flag-Array als long-Wert bzw. als void-Pointer zurück

    • register_callback(func,arg)
      registiert eine Funktion, die bei Stream-Änderungen (Destruktor-Aufruf, copyfmt() oder neue Locale-Zuordnung) aufgerufen wird - diese kann z.B. genutzt werden, um eine tiefe Kopie des pword()-Blockes durchzuführen.
      Die zu übergebende Funktion erhält drei Parameter:

    • evt - das eingetretene Ereignis

    Event                    Bedeutung
    ios_base::erase_event    Destruktor-Aufruf oder Aufräumarbeiten vor copyfmt()
    ios_base::imbue_event    neues Locale gesetzt
    ios_base::copyfmt_event  Format übergeben per copyfmt()
    
    • strm - der betroffene Stream
    • arg - der registrierte Argumentwert (in der Regel der pword()-Index)

    Anmerkung: Der gesamte Mechanismus um xalloc() und register_callback() ist nicht bis zum Ende gedacht worden. Dazu gehört, dass es nicht möglich ist, Callbacks zu "unregistrieren", und dass xalloc()-Indizes in Zusammenarbeit mit DLLs Fehler produzieren können. Als Alternativmöglichkeit bietet es sich an, die existierenden Formatflags für eigene Datentypen neu zu interpretieren oder die Darstellung über globale Variablen zu steuern.

    Weitere Informationen zum Umgang mit den iword()-Flags und zum Aufbau von Manipulatoren überlasse ich einem Kollegen.

    2.6 Spezialbehandlung bestimmter Typen

    Einige der eingebauten Datentypen werden gesondert behandelt, wenn sie über die Stream-Operatoren gelesen bzw. geschrieben werden:

    bool

    Standardmäßig werden bool-Werte numerisch behandelt - 'false' wird als 0 gelesen und geschrieben, 'true' als 1. Alle anderen Eingabewerte gelten als Fehler.
    Alternativ kann das Flag 'boolalpha' gesetzt werden, was bewirkt, dass bool-Werte als "true" bzw. "false" behandelt werden (bzw. die lokalisierte Version davon, z.B. "wahr" und "falsch" in einem deutschen Locale).

    char* und std::string

    Ein char-Pointer wird als Beginn eines Nullterminierten C-Strings interpretiert. Bei Eingaben wird so lange gelesen, bis der Stream auf ein Whitespace trifft. Der Anwender ist selber dafür verantwortlich, dass hinter dem Zeiger genug Platz bereitsteht - notfalls kann die Eingabelänge per width() beschränkt werden. Im Gegensatz dazu wächst ein String mit, um genug Platz für die Eingabe zur Verfügung zu stellen.

    Wenn der einzugebende Text auch Leerzeichen enthalten darf, können die Memberfunktionen get() und getline() zur Eingabe in char-Arrays bzw. die globale Funktion getline() für Strings verwendet werden.

    void*

    Alle anderen Pointer-Typen werden nach void* interpretiert. Bei der Ein-/Ausgabe von Zeigern wird deren Adresse in einem Implementationsabhängigen Format geschrieben bzw. interpretiert.

    Achtung: Eine Adresse ist normalerweise nur gültig, solange das dazugehörige Objekt nicht freigegeben wurde.

    Stream-Puffer

    Stream-Puffer können auch direkt auf einen Stream umgeleitet werden. In dem Fall werden alle verfügbaren Daten aus dem Puffer geschrieben bzw. alle vorhandenen Daten in den Puffer weitergeleitet. Das ist vermutlich die schnellste Methode, eine Datei zu kopieren:

    fin >> noskipws >> fout.rdbuf();
    //oder
    fout << fin.rdbuf();
    

    stream& f(stream&)

    Funktionen, die auf der entsprechenden Streamklasse ausgeführt werden können, werden als Manipulator interpretiert. Der zugehörige Stream-Operator ruft die angegebene Funktion mit seinem linken Operand auf und gibt den Rückgabewert dieses Aufrufes zurück.

    eigene Typen

    Benutzerdefinierte Typen können die Stream-Operatoren überschreiben, um die eigene Ausgabe zu kontrollieren. Diese Arbeit ist im Grunde recht primitiv, allerdings sind einige Stolpersteine zu beachten.

    Bei der Ausgabe könnte es Probleme mit der width()-Angabe des Streams geben, wenn die Daten aus mehreren Einzelteilen bestehen, die hintereinander geschrieben werden müssen. Da die Feldbreite nur für die nächste formatierte Ausgabe gilt, würde das erste Element des Ausgabewertes mit dieser Breite ausgegeben und alle übrigen Elemente anschließend lückenlos angehängt werden. Als Lösung kann ein Stringstream als Zwischenspeicher genutzt werden:

    template<typename charT,typename traitT>
    basic_ostream<charT,traitT>& operator<<(basic_ostream<charT,traitT>& strm, const Data& val)
    {
      //Stringstream - übernimmt Formate vom Ausgabestream
      basic_ostringstream<charT,traitT> s;
      s.copyfmt(strm);
      s.width(0);
    
      //alle Elemente von val nach s schreiben
    
      strm<<s.str();
      return strm;
    }
    

    Eingabe-Operatoren haben vor allem das Problem, wie die Eingaben kontrolliert werden müssen. Außerdem sollten sie Fehleingaben an den Stream weitermelden, indem sie notfalls das failbit setzen. Sie sollten ihr Zielargument erst mit Werten füllen, wenn alle Eingabewerte fehlerfrei eingelesen werden konnten.

    template<typename charT,typename traitT>
    basic_istream<charT,traitT>& operator>>(basic_istream<charT,traitT>& strm, Data& val)
    {
      //alle wichtigen Elemente aus strm lesen und merken
      //Fehlerbehandlung
      if(/*ungültiger Wert*/)
      {
        strm.setstate(ios::failbit);
        return strm;
      }
    
      //Wertzuweisung (nur bei fehlerfreier Ausführung)
      if(strm)
        val = Data(...);
      return strm;
    }
    

    Beide Operatoren müssen als globale Funktionen geschrieben werden, da der linke Operand vom vorgegebenen Typ basic_ios<> (bzw. abgeleiteten Klassen) ist und deshalb nicht unter der Kontrolle des Programmierers liegt. Um eine komplette Klassenhierarchie typabhängig lesen/schreiben zu können, ist der Einsatz von virtuellen Hilfsfunktionen hilfreich:

    class base
    {
    public:
      virtual void print(ostream& strm) const;
      virtual void scan(istream& strm);
    }
    ostream& operator<<(ostream& strm, const base& val)
    {val.print(strm);return strm;}
    istream& operator>>(istream& strm, base& val)
    {val.scan(strm);return strm;}
    

    3 Allgemeine Steuerfunktionen

    3.1 Stream-Status

    Der Status des Streams gibt an, in welchem Zustand er sich intern befindet. Er kann durch verschiedene Operationen auf dem Stream geändert werden. Der Status setzt sich aus vier möglichen Werten zusammen:

    • goodbit - alles in Ordnung (keines der anderen Flags ist gesetzt)
    • eofbit - der Stream hat das Eingabeende erreicht (EOF setzt auch das failbit)
    • failbit - Fehler in der Ein-/Ausgabe (z.B. Formatfehler), der Stream ist noch intakt
    • badbit - schwerer Fehler, der Stream ist möglicherweise beschädigt

    Diese Flags sind Werte des Typs ios_base::iostate und können über Bit-Operationen miteinander verknüpft werden (ob hinter iostate ein enum, ein Ganzzahltyp oder z.B. ein bitset<> steht, legt der Standard nicht fest).

    Der Status eines Streams kann mit verschiedenen Memberfunktionen kontrolliert oder gesetzt werden:

    • good()
      gibt "true" zurück, wenn der Stream in Ordnung ist (goodbit "gesetzt")
      Äquivalent dazu kann auch die Typumwandlung über void* nach bool genutzt werden (der Umwandlungsoperator gibt den NULL-Zeiger (=false) zurück, wenn ein Fehler aufgetreten ist):
    if(cin) cout<<"Alles in Ordnung\n";
    
    • eof()
      gibt "true" zurück, wenn der Stream das Dateiende erreicht hat (eofbit gesetzt)
    • fail() oder !strm
      gibt "true" zurück, wenn ein Fehler aufgetreten ist (failbit oder badbit gesetzt)
    • bad()
      gibt "true" zurück, wenn ein schwerer Fehler aufgetreten ist (badbit gesetzt)
    • rdstate()
      gibt den aktuellen Status des Streams zurück
    • clear(state=goodbit)
      löscht den bisherigen Status des Threads und setzt ihn in Status 'state' (ohne Parameter wird der Stream auf "intakt" gesetzt)
    • setstate(state)
      setzt das Statusbit 'state' (zusätzlich zum bisherigen Status)

    Solange ein Stream ein Fehlerflag gesetzt hat, ignoriert er alle weiteren Zugriffsversuche. Deshalb muss der Status per clear() zurückgesetzt werden, bevor weitere Ein- oder Ausgabeoperationen aufgerufen werden können.

    Exceptions

    Normalerweise wird bei Fehlern nur der Stream-Status gesetzt, der nach der kritischen Operation abgefragt und zurückgesetzt werden muss. Alternativ dazu können Streams auch so konfiguriert werden, dass sie bei Fehlern eine Exception vom Typ ios_base::failure werfen.

    Die Methode exceptions() gibt die Statusflags zurück, die eine Exception auslösen, die Methode exceptions(state) aktiviert Exception-Verarbeitung für alle in 'state' enthaltenen Statusbits. Dabei kann auch sofort eine Exception geworfen werden, wenn sich der Stream bereits in einem Fehlerstatus befindet.

    3.2 Positionierung

    In einigen Stream-Klassen ist es möglich, beliebig durch die Daten zu navigieren. Dafür bieten die Basisklassen geeignete Methoden zur Positionierung:

    • tellg() und tellp()
      geben die Position des Lese- bzw. Schreibzeigers eines Streams zurück (bezogen auf den Dateianfang)
    • seekg(diff,base=ios::beg) und seekp(diff,base=ios::beg)
      setzen den Lese- bzw. Schreibzeiger des Streams relativ zum Anfang (ios::beg), zur aktuellen Position (ios::cur) oder zum Ende (ios::end) der Daten

    ((die ...g-Funktionen beziehen sich auf die Lese-Position (get), die ...p-Funktionen auf die Schreibposition (put) des Streams - auch wenn nicht jeder Stream beide Positionen unabhängig voneinander einstellen kann)

    3.3 Verbindung von Streams

    Es gibt zwei Möglichkeiten, zwei bestehende Streams miteinander zu koppeln. Eine lose Kopplung erfolgt über die Methode tie() - diese übernimmt einen Ausgabestream und bewirkt, dass vor jeder Lese- oder Schreiboperation der übergebene Stream geflusht wird. Auf diese Weise kann die Arbeit von zwei Streams miteinander synchronisiert werden.

    Eine enge Kopplung ist möglich, indem mehrere Streams denselben Puffer verwenden. Auf diese Weise ist es auch möglich, die Standard-Streams cin und cout auf einen internen Stream umzubiegen. Dazu besitzen alle Streams die Methode rdbuf(), die in Kapitel 6 angesprochen wird.

    3.4 Kopplung mit C-Streams

    Per Default arbeiten die Standard-Streams cin, cout etc. synchron mit den entsprechenden Standardkanälen der C-Bibliothek (stdin, stdout und stderr). In Programmen, in denen die Streams mit den C-Funktionen printf() und scanf() kombiniert werden, kann das durchaus erwünscht sein. Da diese Synchronisation jedoch Zusatzaufwand bedeutet, kann sie bei Bedarf abgeschaltet werden, indem am Programmanfang die statische Methode ios_base::sync_with_stdio(false) aufgerufen wird.

    Achtung: Dieser Aufruf muss erfolgen, bevor das Programm andere Ein-/Ausgabe-Operationen verwendet.

    3.5 Internationalisierung

    Um die Ein- und Ausgaben an die nationalen Formate anzupassen, verwenden die Streams Locales, die wiederum aus einzelnen Facetten zusammengesetzt werden. Die Methode getloc() gibt das Locale zurück, das gerade von einem Stream verwendet wird, die Methode imbue(loc) setzt ein neues Locale.

    Locales können auf verschiedenen Wegen aus vorgegebenen Locales zusammengesetzt werden:

    • locale::classic();
      gibt das "klassische" C-Locale zurück
    • locale()
      erzeugt eine Kopie des global gesetzten Locale
    • locale(name);
      erzeugt ein Locale aus dem übergebenen Namen - im Standard ist nur der Name "C" (für das C-Locale) definiert, andere Namen wie "de_DE" (Deutsch), "de_CH" (schweizer Deutsch) oder "en_US" (amerikanisches Englisch) können vom Compiler definiert werden
    • locale(loc,loc2,cat) oder locale(loc,name,cat)
      erzeugt eine Kopie von 'loc' und übernimmt die Facetten der Kategorie 'cat' aus 'loc2' bzw. 'locale(name)'
    • locale(loc,fac)
      erzeugt eine Kopie von 'loc' und fügt die Facette 'fac' ein
    • locale::global(loc)
      legt 'loc' als globales Locale für das Programm fest

    Das globale Locale des Programms wird als Defaultwert verwendet, wenn irgendwo im Programm ein Locale benötigt wird. Außerdem steuert es auch die Arbeitsweise von C-Funktionen wie printf() oder toupper().

    Kategorie  Facette       Verwendung
    
    numeric    num_get<>     Zahleneingabe
               num_put<>     Zahlenausgabe
               numpunct<>    Symbole für Zahlen (Vorzeichen etc.)
    time       time_get<>    Zeiteingabe
               time_put<>    Zeitausgabe
    monetary   money_get<>   Währungseingabe
               money_put<>   Währungsausgabe
               moneypunct<>  Symbole für Währungen
    ctype      ctype<>       Zeicheninformationen (isalpha, toupper etc.)
               codecvt<>     Zeichenumwandlung
    collate    collate<>     Stringvergleiche
    messages   messages<>    Fehlermeldungen
    

    Zur leichteren Verarbeitung von Zeichen besitzen Streams die Methoden widen(c) und narrow(c,def), mit denen einzelne Zeichen zwischen char und dem verwendeten Zeichensatz umgewandelt werden könnnen. widen(c) konvertiert c in den Zeichensatz des Streams, narrow(c,def) konvertiert c nach char (def wird zurückgegeben, wenn es keinen passenden char-Wert gibt).

    Anmerkung: Locales überladen auch den operator(), der zwei Strings mit den Methoden der collate<>-Facette vergleicht. Auf diese Weise kann ein Locale als Vergleichskriterium an Container oder Algorithmen übergeben werden.

    4 File-Streams

    template<typename charT,typename traitT=char_traits<charT> >
    class basic_ifstream : public basic_istream<charT,traitT>;
    template<typename charT,typename traitT=char_traits<charT> >
    class basic_ofstream : public basic_ostream<charT,traitT>;
    template<typename charT,typename traitT=char_traits<charT> >
    class basic_fstream : public basic_iostream<charT,traitT>;
    
    template<typename charT,typename traitT=char_traits<charT> >
    class basic_filebuf : public basic_streambuf<charT,traitT>;
    

    File-Streams dienen zur Arbeit mit Dateien. Sie werden beim Öffnen mit einer bestehenden oder neu angelegten Datei im unterliegenden Betriebssystem verknüpft und lesen/schreiben anschließend von bzw. auf die Festplatte.

    • open(name,modus=default)
      öffnet die angegebene Datei (Achtung: der Name muss als char* angegeben werden) im angegebenen Modus:
    Modus         Bedeutung         fopen-Flag
    in            Lesen             "r"
    out           Schreiben         "w"
    out|trunc     Schreiben         "w"
    out|app       Anhängen          "a"
    in|out        Lesen/Schreiben   "r+"
    in|out|trunc  Schreiben/Lesen   "w+"
    
    ...|ate       Position am Ende
    ...|binary    Binärmodus        "..b"
    

    (der Default-Modus ist ios::in für ifstream und ios::out für ofstream)

    • close()
      schließt die Datei
    • is_open()
      testet, ob die Datei geöffnet ist

    Es ist durchaus möglich, dass die Filestreams sich intern auf FILE* zurückführen lassen, die für die C-Dateiarbeit verwendet wurden. Allerdings sollte man sich nicht darauf verlassen.

    5 String-Streams

    template<typename charT,typename traitT=char_traits<charT> >
    class basic_istringstream : public basic_istream<charT,traitT>;
    template<typename charT,typename traitT=char_traits<charT> >
    class basic_ostringstream : public basic_ostream<charT,traitT>;
    template<typename charT,typename traitT=char_traits<charT> >
    class basic_stringstream : public basic_iostream<charT,traitT>;
    
    template<typename charT,typename traitT=char_traits<charT> >
    class basic_stringbuf : public basic_streambuf<charT,traitT>;
    

    String-Streams verwenden einen String (std::string bzw. std::wstring) als internen Puffer und können deshalb genutzt werden, um Zahlen in eine Textform umzuwandeln, die anschließend intern weiterverwendet werden kann, oder um eingegebene Zeichenfolgen nach Zahlen zu parsen. Auch die Boost-Funktion lexical_cast<>() verwendet String-Streams, um Datentypen ineinander umzuwandeln.

    Ein String-Stream bietet direkten Zugriff auf den verarbeiteten String über die Methode str() (Ausgabe des Puffers) und str(val) (Setzen des Puffers). Indem ein leerer String übergeben wird, kann der Puffer auch komplett gelöscht werden.

    Achtung: Im Gegensatz zu den STL-Containern löscht die Methode clear() NICHT den Inhalt des Pufferstrings, sondern setzt nur eventuelle Fehlerflags zurück.

    5.1 char*-Streams

    class istrstream : public istream;
    class ostrstream : public ostream;
    class strstream : public iostream;
    
    class strstreambuf : public streambuf;
    

    Die char*-Streams verwenden ein char-Array als internen Puffer und können theoretisch genauso verwendet werden wie die Stringstreams. Da der direkte Umgang mit "nackten" Pointern extrem unsicher ist, sollte jeder vernünftige Programmierer einen großen Bogen um die char*-Streams machen - Stringstreams leisten dasselbe und sind wesentlich problemloser zu verwenden.

    6 Stream-Puffer

    Stream-Puffer bilden die Schnittstelle zwischen einem Stream und seinen externen Daten. Ein Stream wandelt die übergebenen Werte in eine Folge von chars um (dazu nutzt er die Methoden seines Locale), die er auf den Puffer schreibt. In der Gegenrichtung erhält er eine Zeichenfolge, die er passend parsen kann. Der Puffer überträgt diese Zeichenfolgen wiederum in eine externe Datendarstellung.

    Von jedem Stream kann man über die Methode rdbuf() auf seinen zugeordneten Streambuffer zugreifen bzw. ihn mit rdbuf(buffer) an einen existierenden Puffer koppeln. Auf diese Weise können mehrere Streams ihre Ausgaben auf denselben Kanal leiten - da Formateinstellungen an den Stream gebunden sind, ermöglicht diese Methode es auch, schnell zwischen verschiedenen Formaten zu wechseln:

    ostream hexout(cout.rdbuf());//zweiten Stream auf cout "aufschalten"
    hexout.copyfmt(cout);
    hexout<<hex<<showbase;//Format anpassen
    
    for(int i=0;i<100;++i)
    {
      cout<<i<<'\t';//dezimale Ausgabe
      hexout<<i<<endl;//hex-Ausgabe
    }
    

    Eine andere Anwendungsmöglichkeit der Stream-Buffer ist es, die Standardkanäle im Programm umzuleiten:

    //Achtung: alten Puffer aufheben!!
    streambuf* outbuf = cout.rdbuf();
    ofstream file("test.txt");
    cout.rdbuf(file.rdbuf());
    
    //Ausgaben nach 'cout' landen nun in der Datei
    
    //Puffer wiederherstellen
    cout.rdbuf(outbuf);
    

    Achtung: Lagern Sie unbedingt den alten Stream-Puffer zwischen - sobald 'file' aus dem Scope fällt, wird dessen Puffer gelöscht und cout wird unbrauchbar.

    6.1 Schnittstelle zum Stream

    Aus Sicht des Streams bietet der Puffer eine Sammlung von Methoden zur Ein- und Ausgabe von Zeichensequenzen:

    • sputc(c)
      schreibt ein Zeichen in den Puffer
    • sputn(s,n)
      schreibt n Zeichen ab Position s in den Puffer
    • in_avial()
      gibt die Anzahl Zeichen im Puffer zurück
    • sgetc()
      gibt das aktuelle Zeichen zurück
    • sbumpc()
      gibt das aktuelle Zeichen zurück und schiebt den Lesezeiger vorwärts
    • snextc()
      schiebt den Lesezeiger vor und gibt das (neue) aktuelle Zeichen zurück
    • sgetn(s,n)
      liest n Zeichen und schreibt sie nach s
    • sputbackc(c)
      schreibt das Zeichen c zurück in den Eingabepuffer
    • sungetc()
      schiebt den Lesezeiger rückwärts
    • pubseekpos(p,d=ios::in|ios::out)
      setzt den Schreib- (d=ios::out) bzw. Lesezeiger (d=ios::in) des Puffers auf eine absolute Position
    • pubseekoff(o,r,d=ios::in|ios::out)
      setzt den Schreib- bzw. Lesezeiger des Puffers auf eine relative Position ('o' Bytes von 'r' (ios::beg = Anfang, ios::cur = aktuelle Position oder ios::end = Ende) entfernt)
    • pubseekoff(0,ios::cur)
      liefert die aktuelle Streamposition zurück
    • pubsetbuf(b,n)
      steuert die Pufferstrategie (ob und wie der Puffer darauf reagiert, hängt vom verwendeten Puffer ab)

    Im Gegensatz zu einem Stream haben die Puffer keinen eigenen Fehlerstatus. Ihre Methoden geben allerdings jeweils den EOF-Wert ihrer Traits-Klasse zurück, wenn etwas schief gegangen ist.

    6.2 Ausgabesteuerung

    Für die Ausgabe verwendet der Puffer einen Zwischenspeicher, den er mit drei Hilfszeigern kontrolliert:

    • pbase() - markiert den Anfang des Puffers
    • pptr() - markiert die aktuelle Schreibposition
    • epptr() - markiert das Ende des Puffers

    Jede Schreiboperation bewegt pptr() um einen Schritt nach vorne. Wenn er das Ende des Speichers erreicht, wird die virtuelle Methode overflow() aufgerufen, die den Zwischenspeicher in die externen Daten überträgt.
    Der Default-Konstruktor von basic_streambuf initialisiert alle drei Lesezeiger mit NULL, was bewirkt, dass jede Schreiboperation direkt an overflow() weitergeleitet wird.
    Zusätzlich kann auch die virtuelle Hilfsmethode xsputn() überschrieben werden, die von sputn() aufgerufen wird, um einen kompletten Datenblock zu schreiben. Die vorgegebene Version schreibt jedes Zeichen einzeln über sputc() (und overflow()) in den Puffer, aber eine optimierte Version könnte diesen Zwischenschritt umgehen.

    6.3 Eingabesteuerung

    Auch für die Eingabe wird ein Zwischenspeicher verwendet, der von drei Hilfszeigern verwaltet wird:

    • eback() - markiert den Anfang des Puffers (Ende des Putback-Bereiches)
    • gptr() - markiert die aktuelle Leseposition
    • egptr() - markiert das Ende des Puffers

    Allerdings ist die Steuerung der Eingabe etwas komplizierter aufgebaut, da mehr Sonderfälle beachtet werden müssen.

    Die Leseoperationen sgetc(), snextc() und sbumpc() lesen jeweils ein Zeichen aus und schieben gptr() entsprechend weiter und rufen die virtuelle Methode underflow() auf, die den Lesepuffer neu auffüllen muss. Diese Methode kümmert sich jedoch nicht um das Weiterschieben der Lesezeiger - dafür benötigt man entweder einen echten Zwischenspeicher oder die Hilfsmethode uflow().
    Die "Unlese"-Operationen sungetc() und sputbackc() schreiben je ein Zeichen zurück in den Puffer und schieben gptr() einen Schritt nach vorne. Wenn er den Anfang des Speichers erreicht, wird die virtuelle Methode pbackfail() aufgerufen, die möglicherweise ältere Eingaben rekonstruieren könnte (oder einfach nur einen Fehler zurückmeldet).

    7 Erweiterungen

    Wie bei allen Bestandteilen der STL ist es problemlos möglich, eigene Erweiterungen mit den IO-Streams zu kombinieren.

    7.1 neue Datentypen

    Um einen eigenen Datentyp einzubinden, müssen lediglich die Stream-Operatoren >> und << geeignet überladen werden. Beispiele dazu finden sich im Kapitel 2.6. Zusätzlich können auch spezielle Manipulatoren definiert werden, um die Verarbeitung der Daten im Detail zu kontrollieren.

    7.2 neue Datenquellen

    Für eine eigene Datenquelle benötigt man eine Ableitung von basic_streambuf<>, die die Methoden overflow() bzw. underflow() (siehe Kapitel 6.2 bzw. 6.3) geeignet überlädt, um die externe Datenrepräsentation anzusteuern. Anschließend kann man entweder die passend konstruierten Stream-Puffer an einen basic_istream<>, basic_ostream bzw. basic_iostream<> ankoppeln oder eigene Streamklassen definieren, die von diesen Klassen abgeleitet werden und die Pufferverwaltung selber übernehmen:

    class mybuf : public std::streambuf
    {
    public:
      mybuf(const std::string& name);
      ...
    };
    
    class omystream : public std::ostream
    {
    protected:
      mybuf buf;
    public:
      omystream(const std::string& name) : buf(name), std::ostream(&buf) {}
    };
    


  • Guter Artikel 👍

    Du hast aber nicht reinzufällig mehr Informationen zu 7.2? Mich würde das interessieren.

    MfG SideWinder



  • @Side
    Die umfangreichste Behandlung findest du in dem Klassiker "Standard C++ IOStreams and Locales" von Langer und Kreft.

    Ein schöner Auszug:
    http://www.angelikalanger.com/IOStreams/Excerpt/excerpt.htm



  • Im Moment nicht verfügbar, aber ich setze es mal auf meine Todo-Liste.



  • @Hume: Danke, das Buch empfiehlst du mir sowieso immer wenn ich zu dem Thema frage 😉 Kann und will aber dezreit meine Buchsammlung derzeit nicht aufstocken und eventl. ist es doch etwas "zu viel" für die paar Basics 🙂

    @CStoll: Wenn da noch etwas drüber kommen würde 👍

    MfG SideWinder



  • Hallo,

    das mit den Puffern würde mich auch mal interessieren. Ich habe mal folgendes nachprogrammiert:

    http://www.edm2.com/0604/introcpp9.html

    Da wird ein beliebig großes Array in einem File behandelt. Z.B. wird der Operator [] auf entsprechende Streambefehle umgeleitet. Leider war das Ergebnis nicht sonderlich schnell. Aber wenn man den Puffer vergrößert und evtl. ein wenig anpaßt, könnte das sehr nützlich sein. Man könnte z.B. große WAV-Dateien auf Platte bearbeiten und sowas.

    Also ich fänd's Klasse, wenn's dahingehend mal ein Update gäbe... 😉

    Viele Grüße,
    Stimpy



  • Das mit der Geschwindigkeit ist kein Wunder - auch wenn du dich noch mehr verrenkst, ist Festplattenzugriff deutlich langsamer als RAM-Zugriff. Da müsstest du vermutlich mit einer eigenen Zwischenpufferung arbeiten, um größere Teile der Datei zwischenzulagern.
    (ich hab dein Beispiel jetzt nur überflogen, aber offenbar rennst du sehr viel per seek() durch deine Datei, um die Index-Zugriffe zu realisieren)

    Stream-Puffer gehen übrigens in eine andere Richtung: Du nimmst dir irgendeine externe Datenstruktur (z.B. ein Array im Speicher oder eine Datenbank) und liest/schreibst diese mit Hilfe der Stream-Methoden.



  • Und ich dachte, das ginge mit einem angepaßten Stream-Puffer eleganter.
    Eine eigene Pufferung hatte ich auch schon angedacht.

    Danke,
    Stimpy



  • Klar geht das schneller, aber auch aufwendiger - der Stream-Puffer könnte große Teile der Datei auf Vorrat lesen und dann durch die interne Kopie navigieren. Aber das erfordert einiges an Verwaltungsaufwand und es dürfte wohl einfacher sein, den Zwischenschritt über den Stream wegzulassen und die Daten direkt in großen Blöcken einzulesen und zu verwalten.

    (bzw der Stream-Puffer steht für dein Problem am falschen Ende der Datenstruktur)



  • nur ein kleiner Hinweis auf Boost.IOstream, damit kann man relativ leicht Filter oder eigene Quellen in einen Standard-Stream einfügen. Gibt auch schon einige fertige Filter, zB für gzip, bzip2, und fertige Quelle zB für UNIX-Filehandles.



  • mir fällt grad auf,
    Bei Exceptions:
    ios_base::exceptions() ist der Name der Funktion



  • Danke für den Hinweis - ich habe den Funktionsnamen oben korrigiert (btw, es ist basic_ios<>::exceptions() 😉 - zumindest laut MSDN).



  • Du hast "in_avial" statt "in_avail" geschrieben


Anmelden zum Antworten