Zum Inhalt springen

E/A: Streams, Formatierung & std::print

Meistere die Ein- und Ausgabe in C++ vom klassischen iostream bis zu modernem std::format und std::print — samt Stream-Zuständen, Datei-E/A und typischen Pufferfallen.

Die Entwicklung der C++-E/A

Die Ein- und Ausgabe in C++ hat sich im Lauf der Sprachgeschichte stark gewandelt. Die klassische iostream-Bibliothek (std::cin, std::cout) ist seit C++98 der Standard, hat aber bekannte Schwächen bei Ergonomie und Performance. C++20 hat std::format eingeführt, eine typsichere Formatierungsbibliothek nach dem Vorbild von Pythons str.format(). C++23 kam std::print hinzu, das Formatierung und Ausgabe in einem einzigen Aufruf vereint.

Alle drei Ansätze zu kennen ist wichtig: iostream steckt in unzähligen bestehenden Codebasen, std::format ist der moderne Weg, Strings zu formatieren, und std::print ist die Zukunft der Ausgabe in C++.

Klassische E/A: cout, cin, cerr

Die iostream-Bibliothek stellt Stream-Objekte für die Standardeingabe, die Standardausgabe und die Fehlerausgabe bereit:

io_basics.cppc++23
#include <iostream>
#include <string>
#include <limits>    // For numeric_limits

int main() {
    // Output with cout
    std::cout << "Hello, " << "World!" << '\n';      // Chained insertion
    std::cout << "The answer is: " << 42 << '\n';    // Automatic type formatting

    // Error output — cerr is unbuffered (immediate), clog is buffered
    std::cerr << "Error: something went wrong\n";

    // Input with cin
    std::cout << "Enter your name: ";
    std::string name;
    std::getline(std::cin, name);         // Read entire line (including spaces)

    std::cout << "Enter your age: ";
    int age{};
    std::cin >> age;                       // Read a single value (stops at whitespace)

    // Check if input succeeded
    if (std::cin.fail()) {
        std::cerr << "Invalid input!\n";
        std::cin.clear();                  // Reset error flags
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');  // Discard bad input
    }

    // The getline-after-cin trap:
    // After cin >> age, a '\n' remains in the buffer.
    // A subsequent getline() would read an empty string!
    // Fix: call cin.ignore() before getline():
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

    std::string city;
    std::cout << "Enter your city: ";
    std::getline(std::cin, city);          // Now works correctly

    std::cout << name << " (" << age << ") from " << city << '\n';
}
Ausgabe

Klicke auf Ausführen, um dieses Beispiel zu kompilieren und auszuführen.

Zustandsflags von Streams

Jeder Stream führt eine Reihe von Zustandsflags, die seine aktuelle Lage anzeigen:

- goodbit — Keine Fehler. Der Stream ist bereit für E/A-Operationen.
- eofbit — Das Ende der Datei bzw. Eingabe ist erreicht. Wird gesetzt, wenn der Stream über das Ende hinaus zu lesen versucht.
- failbit — Ein logischer Fehler ist aufgetreten (z. B. der Versuch, eine Ganzzahl zu lesen, wo Text steht). Nach einem Aufruf von clear() ist der Stream weiter benutzbar.
- badbit — Ein nicht behebbarer E/A-Fehler ist aufgetreten (z. B. ein Plattenfehler). Der Stream ist in der Regel unbrauchbar.

Du prüfst sie mit stream.good(), stream.eof(), stream.fail(), stream.bad() oder indem du den Stream in einem booleschen Kontext verwendest: if (std::cin) entspricht if (!std::cin.fail()).

Ein entscheidendes Muster: Prüfe den Stream-Zustand immer nach einer Eingabeoperation, nicht davor. Das verbreitete Idiom while (std::cin >> value) liest und prüft in einem Schritt — es bricht ab, sobald das Lesen fehlschlägt.

Stolperfalle

Der häufigste E/A-Fehler in C++ entsteht beim Mischen von >> (formatierte Extraktion) mit std::getline(). Der Operator >> liest einen Wert, lässt den abschließenden Zeilenumbruch aber im Puffer zurück. Ein anschließendes std::getline() liest dann genau diesen übrig gebliebenen Zeilenumbruch als leeren String, sodass es scheint, als würde die Eingabe übersprungen.

Die Lösung ist, zwischen >> und std::getline() den Aufruf std::cin.ignore(std::numeric_limits::max(), '\n') einzufügen, um den übrigen Zeilenumbruch zu verwerfen.

Ein anderer Ansatz ist, sämtliche Eingaben mit std::getline() zu lesen und die Strings selbst mit std::istringstream oder std::stoi/std::stod auszuwerten. Das umgeht das Problem vollständig und gibt dir eine bessere Fehlerbehandlung.

  • Nach cin >> x bleibt ein Zeilenumbruch im Puffer — das nächste getline() liest ihn als leeren String
  • Lösung: Rufe zwischen >> und getline() cin.ignore(numeric_limits::max(), '\n') auf
  • Alternative: Lies ALLE Eingaben mit getline() und werte die Strings mit stoi/stod oder istringstream aus
  • Prüfe nach jeder Eingabe den Stream-Zustand: if (cin >> x), statt blind zu lesen

Datei-E/A

Die Datei-E/A nutzt dieselbe Stream-Schnittstelle wie die Konsolen-E/A, mit std::ifstream zum Lesen und std::ofstream zum Schreiben:

file_io.cppc++23
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

int main() {
    // Writing to a file
    {
        std::ofstream out("scores.txt");  // Opens for writing (creates or truncates)
        if (!out) {
            std::cerr << "Failed to open file for writing\n";
            return 1;
        }
        out << "Alice 95\n";
        out << "Bob 87\n";
        out << "Charlie 92\n";
    }  // File is automatically closed when 'out' goes out of scope (RAII!)

    // Reading from a file
    std::ifstream in("scores.txt");
    if (!in) {
        std::cerr << "Failed to open file for reading\n";
        return 1;
    }

    std::string name;
    int score{};
    std::vector<std::pair<std::string, int>> results;

    while (in >> name >> score) {           // Read until failure (EOF or bad data)
        results.emplace_back(name, score);
    }

    for (const auto& [n, s] : results) {
        std::cout << n << ": " << s << '\n';
    }

    // Appending to a file
    std::ofstream append("scores.txt", std::ios::app);  // Open in append mode
    append << "Diana 98\n";

    // Reading entire file line by line
    std::ifstream in2("scores.txt");
    std::string line;
    while (std::getline(in2, line)) {
        std::cout << "Line: " << line << '\n';
    }
}
Ausgabe

Klicke auf Ausführen, um dieses Beispiel zu kompilieren und auszuführen.

Moderne Formatierung: std::format & std::print

std::format aus C++20 und std::print aus C++23 bieten eine typsichere, Python-ähnliche Formatierung, die sicherer und besser lesbar ist als die Manipulatoren von iostream:

format_print.cppc++23
#include <format>   // C++20
#include <print>    // C++23
#include <string>
#include <vector>
#include <numbers>  // C++20: mathematical constants

int main() {
    // std::format (C++20) — returns a formatted std::string
    std::string msg = std::format("Hello, {}!", "World");
    // msg == "Hello, World!"

    // Positional arguments
    std::string pos = std::format("{1} comes before {0}", "second", "first");
    // pos == "first comes before second"

    // Formatting specifiers
    std::string nums = std::format(
        "int: {:>10d}\n"    // Right-aligned, width 10, decimal
        "hex: {:#06x}\n"    // '0x' prefix, width 6, zero-padded hex
        "float: {:.3f}\n"   // 3 decimal places, fixed notation
        "sci: {:.2e}\n"     // 2 decimal places, scientific notation
        "pi: {:.10f}",      // 10 decimal places
        42, 255, 3.14159, 12345.6789, std::numbers::pi
    );

    // std::print (C++23) — format + output in one step
    std::print("Name: {}, Age: {}\n", "Alice", 30);
    std::print("Pi is approximately {:.6f}\n", std::numbers::pi);

    // std::println (C++23) — like print but adds a newline
    std::println("Score: {:>5}", 95);       // "Score:    95"
    std::println("Score: {:>5}", 100);      // "Score:   100"

    // Formatting containers — print each element
    std::vector<int> values = {10, 20, 30, 40, 50};
    for (std::size_t i = 0; i < values.size(); ++i) {
        std::println("[{:2}] = {:>4}", i, values[i]);
    }

    // Why prefer std::format/print over iostream?
    // 1. Type-safe (unlike printf)
    // 2. Readable format strings (unlike iostream manipulators)
    // 3. No state leakage (iostream manipulators are sticky!)
    // 4. Better performance in many cases
}
Ausgabe

Klicke auf Ausführen, um dieses Beispiel zu kompilieren und auszuführen.

Best Practice

Modernes C++ bietet dir drei Ansätze für die Ein- und Ausgabe. So wählst du den passenden:

Verwende std::print/std::println (C++23) für neuen Code, sofern dein Compiler das unterstützt. Das ist die sauberste Syntax, sie ist typsicher und vermeidet die Probleme der zustandsbehafteten Manipulatoren von iostream.

Verwende std::format (C++20), wenn du einen formatierten String brauchst (statt direkter Ausgabe), oder wenn dein Compiler C++20, aber noch nicht C++23 beherrscht.

Verwende iostream, wenn du mit Code zusammenarbeitest, der Stream-Objekte erwartet, wenn du die Stream-Abstraktion brauchst (z. B. beim Lesen von cin oder bei Datei-E/A) oder wenn du an einer alten Codebasis arbeitest.

Vermeide printf/scanf aus C — sie sind nicht typsicher (ein falscher Formatbezeichner ist undefiniertes Verhalten) und funktionieren nicht mit C++-Typen wie std::string.

  • Bevorzuge std::print/println (C++23) für neuen Ausgabecode — am saubersten und sichersten
  • Verwende std::format (C++20), wenn du einen formatierten String brauchst statt direkter Ausgabe
  • iostream wird weiterhin für cin, Datei-E/A und streambasierte Abstraktionen gebraucht
  • Vermeide printf/scanf — sie sind nicht typsicher und kommen mit std::string nicht direkt zurecht
Wichtigste Erkenntnisse
  • Prüfe den Stream-Zustand immer nach einer Eingabeoperation — verwende 'if (cin >> x)' oder 'while (getline(in, line))'
  • Das Mischen von cin >> und getline() verursacht den Zeilenumbruch-im-Puffer-Fehler — rufe dazwischen cin.ignore() auf
  • Dateistreams nutzen RAII — sie schließen sich automatisch, sobald sie ihren Gültigkeitsbereich verlassen
  • std::format (C++20) bietet typsichere, Python-ähnliche String-Formatierung
  • std::print/println (C++23) vereint Formatierung und Ausgabe — der bevorzugte Weg für neuen Code
  • Die Manipulatoren von iostream sind zustandsbehaftet (wirken fort) — std::format umgeht dieses Problem vollständig

Quiz — Teste dein Wissen

(10 XP)

1. Was passiert, wenn du direkt nach `std::cin >> number` ein `std::getline(std::cin, str)` aufrufst?

2. Was ergibt `std::format("{:#06x}", 255)`?

3. Warum schließen sich Dateistreams (ifstream/ofstream) automatisch, sobald sie ihren Gültigkeitsbereich verlassen?