Ströme und Dateien in C++



  • Da hier relativ häufig Fragen über die Dateibehandlung in C++
    auftauchen, habe ich mich dazu entschieden mal ein paar Sätze
    über das Thema zu schreiben. Ich will diesen Thread
    dann später den C++ FAQs hinzufügen und bitte deshalb jeden,
    der der Meinung ist, dass ich etwas wichtiges vergessen habe,
    dieses zu ergänzen.

    In C++ wird die Ein- und Ausgabe über sogenannte Ströme (engl. Streams)
    realisiert. Durch das Einbinden des Headers <iostream> erhält man vier
    bereits instanziierte Stromobjekte.
    Diese sind:

    • cout: ein gepufferter Stream der der Standardausgabe (i.A. Bildschirm)
      zugeordnet ist (in C: stdout).
    • cin: ein gepufferter Stream der der Standardeingabe (i.A. Tastatur)
      zugeordnet ist (in C: stdin)
    • cerr: ein ungepufferter Stream der der Standardfehlerausgabe (i.A. Bildschirm)
      zugeordnet ist (in C: stderr)
    • clog: gepufferter Stream der der Standardprotokollausgabe (i.A. Bildschirm)
      zugeordnet ist.

    Die Eingabestreams sind Instanzen der Klasse istream oder von Klassen
    die von istream abgeleitet wurden. Die Basisklasse der Ausgabestreams
    ist ostream.

    In Ausgabestreams schreibt man mittels des bitweise-linksshift "<<",
    aus Eingabeströmen ließt man mittels des bitweise-rechtsshift ">>" Operators.
    Diese beiden Operatoren sind für alle Standarddatentypen implementiert.
    Für alle nicht Standarddatentypen kann man diese beiden Operatoren überladen.
    Dies ermöglicht einem jeden Datentypen immer auf die gleiche Art und Weise ein-
    und auszugeben.
    Da die Ein-/Ausgabe über gepufferte Ströme abläuft, muß es eine Möglichkeit
    geben diese Puffer zu leeren (engl. to flush). Das leeren eines Ausgabepuffers
    erzielt man durch "flush". Mittels "endl" fügt man dem Puffer ein NewLine-Zeichen
    hinzu und leert danach den Puffer.
    Bsp:

    #include <iostream>
    using namespace std;
    int main()
    {
        cout << "Ich mag C++ " << 10 << " mal lieber als Paprika!" << endl;
        return 0;
    }
    

    Zur Navigation in Strömen gibt es die Methoden seekg und seekp. Seekg setzt
    den Lesezeiger in Eingabeströmen. Seekp setzt den Schreibzeiger in Ausgabeströmen.
    Beide Zeiger können relativ zu einem Offset positioniert werden.
    Gültige Angaben hierfür sind:

    • ios::beg: Suche vom Beginn des Streams
    • ios::curr: Suche von der aktuellen Position
    • ios:🔚 Suche vom Ende des Streams

    Die aktuelle Position im Stream erhält man über die Methoden tellg und tellp.

    Da die Dateibehandlung in C++ ebenfalls über Ströme realisiert ist, gilt das oben
    geschriebene hier ebenfalls. So läßt sich z.B. mit "<<" in eine Datei schreiben
    und mit ">>" kann aus einer Datei gelesen werden.
    Weitere wichtige Funktionen sind:

    • open(): Öffnet eine Datei und verbindet sie mit dem Strom
    • is_open(): liefert true falls der Dateistrom mit einer gültigen
      Datei verbunden ist.
    • close(): Trennt den Strom von einer Datei
    • read(char* puch, int nCount) : Ließt nCount Bytes aus dem Eingabestrom und
      schreibt sie nach puch.
    • gcount(): Liefert die Anzahl der beim letzten Aufruf von read gelesenen Zeichen.
    • write(const char* puch, int nCount): Schreibt nCount Bytes aus puch in den Stream

    Hier vier Beispiele die das hantieren mit Dateien ein wenig verdeutlichen.

    Das erste Beispiel liest eine Datei zeilenweise ein und speichert
    jede Zeile in einem string-Vector. Als letztes wird der Inhalt des
    Vektors (und damit der Inhalt der Datei) nach cout geschrieben.
    Benötigte Header: <iostream>, <fstream>, <vector>, <string> und <iterator>

    using namespace std;
    ifstream FileIn("Main.cpp");
    if (FileIn)     // Falls FileIn gültig ist.
    {
        vector<string> Contents;              // Container für die einzelnen Zeilen
    
        // Solange kein Fehler auftritt und nicht eof
        for (string ReadString; getline(FileIn, ReadString); )         
            Contents.push_back(ReadString);   // Aktuelle Zeile in den Vektor einfügen
    
        // Alle Elemente des Vektors ausgeben.
        ostream_iterator<string> Out(cout, "\n");
        copy(Contents.begin(), Contents.end(), Out);
    }
    

    Das zweite Beispiel öffnet eine Datei im binär-Modus und liest ihren Inhalt
    in einen Puffer. Nach dem Lesen werden zwei Zeichen ab Offset 7 durch die
    letzten beiden Zeichen der Datei ersetzt.
    Benötigte Header: <iostream>, <fstream>

    using namespace std;
    fstream FileBin("d:\\cdtemp\\streams\\test.dat", ios::in|ios::out|ios::binary);
    if (FileBin.is_open())
    {
        // 1. Dateigröße bestimmen.
        FileBin.seekg(0, ios::end);
        unsigned long FileSize = streamoff(FileBin.tellg());
        FileBin.seekg(0, ios::beg);
    
        // 2. Puffer anlegen und Datei einlesen.
        char* pBuffer = new char[FileSize];
        FileBin.read(pBuffer, FileSize);
    
        // 3. An Stelle 7 (relativ zum Beginn) zwei Bytes ersetzen.
        FileBin.seekp(7, ios::beg);
        FileBin.write(pBuffer+FileSize-2, 2);
    
        // Nachher wieder aufräumen.
        delete [] pBuffer;
    }
    

    Das dritte Beispiel zeigt wie man eine Datei kopieren kann.
    Erforderliche Header: <fstream>

    using namespace std;
    // Quelldatei
    ifstream FileInCopy("d:\\cdtemp\\uncle_kracker-follow_me.mp3", ios::binary);
    
    // Zieldatei
    ofstream FileOutCopy("d:\\cdtemp\\uncle_kracker-follow_me.mp3.bak", ios::binary);
    
    if (FileInCopy)
        FileOutCopy << FileInCopy.rdbuf();
    

    Das letzte Beispiel zeigt wie man Datensätze schreiben und lesen kann.
    Erforderliche Header: <iostream> und <fstream>

    using namespace std;
    // Einen Person-Datensatz definieren
    struct PERSON
    {
        char VorName[20];
        char NachName[20];
        int TelNr;
    };
    
    // fstream damit die Datei zuerst schreibend und dann lesend geöffnet werden kann.
    fstream File("Person.dat", ios::out|ios::binary);
    if (File.is_open())
    {
        // Nur zwei Datensätze schreiben.
        // Hier wäre eine Fehlerprüfung angebracht
        for (int i = 0; i < 2 ; ++i)
        {
            PERSON Pers;
            cout << "Vorname: ";
            cin >> Pers.VorName;
            cout << "Nachname: ";
            cin >> Pers.NachName;
            cout << "TelNr: ";
            cin >> Pers.TelNr;
    
            // Aktuelle Person in der Datei speichern.
            File.write((const char*)&Pers, sizeof(Pers));
        }
        File.close();   // Datei schließen
    }
    // Datei zum Lesen öffnen.
    File.open("Person.dat", ios::in|ios::binary);
    if (File.is_open())
    {
        PERSON Pers;
        while (File.read((char*)&Pers, sizeof(Pers)))
            cout << Pers.VorName << " " << Pers.NachName << " " << Pers.TelNr << endl;
    }
    

    Die Ströme in C++ sind ein sehr weitläufiges Thema. Das hier geschriebene
    zeigt deshalb nur einen winzig kleinen Ausschnitt.

    Jetzt seit ihr gefragt: Was soll hier unbedingt noch erwähnt werden?
    Wo habe ich was falsches gesagt?

    Update:
    Immer mal wieder taucht die Frage auf, warum die Angaben ios::nocreate bzw. ios::noreplace als Open-Flags auf Compiler A nicht aber auf Compiler B funktionieren. Die Antwort ist einfach:
    Weder ios::nocreate noch ios::noreplace sind Standard-C++. Diese beiden Open-Flags waren bei den veralteten <fstream.h>-Strömen weit verbreitet. Sie gehörten aber zu keiner Zeit zu den Standard-Strömen.

    Manchmal sind diese Flags aber ganz praktisch. Man kann sie wie folgt simulieren:

    int main()
    {
        // nocreate
        fstream f("Datei.txt", ios::in);
        if (f.good())
        {   // ok. Datei existiert.
            f.close();
            f.open("Datei.txt", ios::out);
            if (f.is_open())
            {
                // Mit Datei arbeiten
            }
        }
        else
        {
            cout << "Datei existiert nicht." << endl;
        }
    
        // noreplace
        fstream f2("Datei.txt", ios::in);
        if (!f2)
        {   // ok. Datei existiert nicht. Neu erstellen
            f.clear();
            f.open("Datei.txt", ios::out);
            if (f.is_open())
            {
                // mit Datei arbeiten
            }
        }
        else
        {
            cout << "Datei existiert bereits!" << endl;
        }
    
    }
    

    [ Dieser Beitrag wurde am 30.08.2002 um 01:12 Uhr von Dimah editiert. ]

    [ Dieser Beitrag wurde am 13.01.2003 um 18:31 Uhr von HumeSikkins editiert. ]

    [ Dieser Beitrag wurde am 30.03.2003 um 23:34 Uhr von HumeSikkins editiert. ]

    [ Dieser Beitrag wurde am 08.04.2003 um 14:37 Uhr von HumeSikkins editiert. ]



  • Ich trag mal was dazu bei:

    Man kann bei stream noch testen, ob die letzte Aktion geklappt hat. Das kann sehr nützlich sein, wenn man z.B. der Anwender bei

    int i;
    cin >> i;
    

    keine Zahl eingibt.

    Es gibt folgende Funktionen:
    ios::good // alles ok
    ios::bad // Stream ist 'kaputt' / grober Fehler
    ios::eof // Dateiende
    ios::fail // einlesen bzw. ausgeben hat nicht geklappt



  • Man kann den cout-stream auch leicht auf eine Datei umleiten. Das ist oft recht sinnvoll, wenn ein Consolenprojekt größer wird.

    ofstream file;
    cout.rdbuf(file.rdbuf())

    Die Methode rdbuf() liefert den aktuellen Buffer zurück und setzt den Stream auf den als Parameter übergebenen Buffer.

    Leider kann man das mit der Funktion rdbuf() eines ofstream-Objekts nicht machen, da die Methode überschrieben ist. Es geht aber auch irgendwie, weiß nur nicht genau wie. Vielleicht hat ja jemand ne Ahnung.



  • Jo, einfach nach ios casten, dann geht es. Ich habs aber auch erst durch die "StreamRedirect"-Klasse festgestellt, die ich wegen dieses Beitrags getestet habe: http://www.c-plusplus.net/ubb/cgi-bin/ultimatebb.cgi?ubb=get_topic&f=15&t=000850

    ios& stream = cout; // oder ein fstream etc.
    stream.rdbuf( another_stream.rdbuf() );