Streams in C++

Introduction to Streams

In C++, a stream is an abstraction that represents a device on which input and output operations are performed. It can be thought of as a flow of data between a program and an external device such as a keyboard, screen, file, or network connection.

Streams provide a consistent interface for handling various types of input and output operations, regardless of the specific device involved. This abstraction simplifies I/O operations and makes them more portable.

The Stream Classes Hierarchy

C++ streams are implemented through a hierarchy of classes in the Standard Library:

                ios_base

                   |
                  ios

                   |
          ┌───────┴───────┐
          |               |
       istream         ostream
          |               |
          |               |
    ifstream         ofstream
          |               |
          |               |
          └───→ iostream ←┘
                   |
                   |
               fstream

The key classes in this hierarchy are:

  • ios_base: The base class for the entire stream hierarchy, containing basic functions and flags
  • ios: Adds formatting state and error state functionality
  • istream: For input operations (derived from ios)
  • ostream: For output operations (derived from ios)
  • iostream: For both input and output operations (derived from both istream and ostream)
  • ifstream: For file input (derived from istream)
  • ofstream: For file output (derived from ostream)
  • fstream: For file input and output (derived from iostream)

Standard Input/Output Streams

C++ provides several predefined standard stream objects:

  • cin: Standard input stream (keyboard by default)
  • cout: Standard output stream (console by default)
  • cerr: Standard error output stream (console by default, unbuffered)
  • clog: Standard error output stream (console by default, buffered)

These objects are defined in the <iostream> header.

Basic Input/Output Operations

Output with cout

#include <iostream>
using namespace std;

int main() {
    int age = 25;
    string name = "John";
    
    // Output simple values
    cout << "Hello, World!" << endl;
    
    // Output variables
    cout << "Name: " << name << endl;
    cout << "Age: " << age << endl;
    
    // Multiple items with a single statement
    cout << "My name is " << name << " and I am " << age << " years old." << endl;
    
    return 0;
}

Input with cin

#include <iostream>
using namespace std;

int main() {
    int age;
    string name;
    
    // Input for basic types
    cout << "Enter your name: ";
    cin >> name;
    
    cout << "Enter your age: ";
    cin >> age;
    
    // Multiple inputs in a single statement
    int x, y, z;
    cout << "Enter three integers: ";
    cin >> x >> y >> z;
    cout << "Sum: " << x + y + z << endl;
    
    return 0;
}

Stream Manipulation

Output Formatting

C++ provides several ways to format output:

1. Using Manipulators

Manipulators are functions that can be inserted into streams to modify their behavior:

#include <iostream>
#include <iomanip>  // Required for many manipulators
using namespace std;

int main() {
    double pi = 3.14159265359;
    
    // Set precision (total digits)
    cout << "Default: " << pi << endl;
    cout << "With precision(4): " << setprecision(4) << pi << endl;
    
    // Fixed-point notation
    cout << "Fixed with precision(4): " << fixed << setprecision(4) << pi << endl;
    
    // Scientific notation
    cout << "Scientific: " << scientific << pi << endl;
    
    // Field width
    cout << "Width(10): |" << setw(10) << 42 << "|" << endl;
    
    // Fill character
    cout << "Fill with stars: |" << setfill('*') << setw(10) << 42 << "|" << endl;
    
    // Alignment
    cout << "Left aligned: |" << left << setw(10) << 42 << "|" << endl;
    cout << "Right aligned: |" << right << setw(10) << 42 << "|" << endl;
    
    // Numerical base
    cout << "Decimal: " << dec << 42 << endl;
    cout << "Hexadecimal: " << hex << 42 << endl;
    cout << "Octal: " << oct << 42 << endl;
    
    // Show/hide base prefix
    cout << "With showbase: " << showbase << hex << 42 << endl;
    
    // Booleans as true/false instead of 1/0
    cout << "Boolean (default): " << (5 > 3) << endl;
    cout << "Boolean (with boolalpha): " << boolalpha << (5 > 3) << endl;
    
    return 0;
}

2. Using Stream Member Functions

#include <iostream>
using namespace std;

int main() {
    // Using member functions for formatting
    cout.width(10);
    cout.fill('-');
    cout << 42 << endl;
    
    // Precision
    cout.precision(4);
    cout << 3.14159265359 << endl;
    
    // Format flags
    cout.setf(ios::fixed);
    cout << 3.14159265359 << endl;
    
    // Reset flags
    cout.unsetf(ios::fixed);
    
    return 0;
}

Input Operations and Challenges

Reading Whole Lines

cin with the >> operator stops at whitespace, which can be problematic for reading names or sentences:

// Problem: Reading "John Doe" will only get "John"
string name;
cout << "Enter your full name: ";
cin >> name;

Solution - use getline():

string fullName;
cout << "Enter your full name: ";
cin.ignore();  // Skip any leftover newline character
getline(cin, fullName);

Input Validation

#include <iostream>
using namespace std;

int main() {
    int age;
    cout << "Enter your age: ";
    
    // Check if input is valid
    if (cin >> age) {
        cout << "Your age is: " << age << endl;
    } else {
        cout << "Invalid input!" << endl;
        // Clear error flags
        cin.clear();
        // Discard invalid input
        cin.ignore(numeric_limits<streamsize>::max(), '\n');
    }
    
    return 0;
}

File Streams

File streams allow reading from and writing to files:

Writing to a File

#include <iostream>
#include <fstream>
using namespace std;

int main() {
    // Create an output file stream
    ofstream outFile("example.txt");
    
    // Check if file opened successfully
    if (!outFile) {
        cerr << "Error opening file!" << endl;
        return 1;
    }
    
    // Write to the file
    outFile << "This is line 1." << endl;
    outFile << "This is line 2." << endl;
    outFile << "The value of pi is approximately " << 3.14159 << endl;
    
    // Close the file
    outFile.close();
    
    cout << "Data has been written to the file." << endl;
    return 0;
}

Reading from a File

#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main() {
    // Create an input file stream
    ifstream inFile("example.txt");
    
    // Check if file opened successfully
    if (!inFile) {
        cerr << "Error opening file for reading!" << endl;
        return 1;
    }
    
    // Read line by line
    string line;
    while (getline(inFile, line)) {
        cout << line << endl;
    }
    
    // Close the file
    inFile.close();
    
    return 0;
}

Reading and Writing Binary Data

#include <iostream>
#include <fstream>
using namespace std;

struct Person {
    char name[50];
    int age;
    double salary;
};

int main() {
    Person person = {"John Doe", 30, 50000.50};
    
    // Write binary data
    ofstream outBinary("person.dat", ios::binary);
    outBinary.write(reinterpret_cast<char*>(&person), sizeof(Person));
    outBinary.close();
    
    // Read binary data
    Person readPerson;
    ifstream inBinary("person.dat", ios::binary);
    inBinary.read(reinterpret_cast<char*>(&readPerson), sizeof(Person));
    
    cout << "Name: " << readPerson.name << endl;
    cout << "Age: " << readPerson.age << endl;
    cout << "Salary: " << readPerson.salary << endl;
    
    inBinary.close();
    
    return 0;
}

File Opening Modes

When opening a file, you can specify different modes:

ofstream file("example.txt", ios::out | ios::app);

Common file modes:

  • ios::in: Open for reading (default for ifstream)
  • ios::out: Open for writing (default for ofstream)
  • ios::app: Append to end of file
  • ios::ate: Seek to end of file after opening
  • ios::trunc: Delete contents if file exists
  • ios::binary: Open in binary mode

String Streams

String streams allow you to treat strings as streams, useful for formatting or parsing:

#include <iostream>
#include <sstream>
#include <string>
using namespace std;

int main() {
    // String output stream
    ostringstream oss;
    
    // Format data into the string stream
    int id = 101;
    string name = "Alice";
    double score = 95.5;
    
    oss << "ID: " << id << ", Name: " << name << ", Score: " << score;
    
    // Get the resulting string
    string result = oss.str();
    cout << "Formatted string: " << result << endl;
    
    // String input stream
    istringstream iss("123 45.67 Hello");
    
    // Parse data from the string stream
    int num;
    double dbl;
    string word;
    
    iss >> num >> dbl >> word;
    
    cout << "Parsed integer: " << num << endl;
    cout << "Parsed double: " << dbl << endl;
    cout << "Parsed string: " << word << endl;
    
    return 0;
}

Using String Streams for Conversions

#include <iostream>
#include <sstream>
#include <string>
using namespace std;

// Convert any type to string
template<typename T>
string toString(T value) {
    ostringstream oss;
    oss << value;
    return oss.str();
}

// Convert string to any type
template<typename T>
T fromString(const string& str) {
    T value;
    istringstream iss(str);
    iss >> value;
    return value;
}

int main() {
    // Number to string
    int num = 42;
    string numStr = toString(num);
    cout << "Number as string: " << numStr << endl;
    
    // String to number
    string str = "123";
    int parsedNum = fromString<int>(str);
    cout << "Parsed number: " << parsedNum << endl;
    
    return 0;
}

Stream States and Error Handling

Streams maintain state flags that indicate their current status:

  • good(): No errors
  • eof(): End-of-file reached
  • fail(): Logical error on i/o operation
  • bad(): Read/writing error
#include <iostream>
#include <fstream>
using namespace std;

int main() {
    ifstream file("nonexistent.txt");
    
    if (file.good()) {
        cout << "File opened successfully." << endl;
    } else {
        cout << "File state flags:" << endl;
        cout << "eof(): " << file.eof() << endl;
        cout << "fail(): " << file.fail() << endl;
        cout << "bad(): " << file.bad() << endl;
    }
    
    // Reset error state
    file.clear();
    
    return 0;
}

Custom Stream Operators

You can overload the << and >> operators for your custom types:

#include <iostream>
using namespace std;

class Point {
private:
    int x, y;
    
public:
    Point(int x = 0, int y = 0) : x(x), y(y) {}
    
    friend ostream& operator<<(ostream& os, const Point& p);
    friend istream& operator>>(istream& is, Point& p);
};

// Output stream operator overload
ostream& operator<<(ostream& os, const Point& p) {
    os << "(" << p.x << ", " << p.y << ")";
    return os;
}

// Input stream operator overload
istream& operator>>(istream& is, Point& p) {
    // Expected format: (x,y)
    char ch;
    is >> ch >> p.x >> ch >> p.y >> ch;
    return is;
}

int main() {
    Point p1(10, 20);
    cout << "Point: " << p1 << endl;
    
    Point p2;
    cout << "Enter a point (format: (x,y)): ";
    cin >> p2;
    cout << "You entered: " << p2 << endl;
    
    return 0;
}

Buffering and Flushing

Streams use buffers to improve I/O performance by reducing the number of actual device operations:

#include <iostream>
using namespace std;

int main() {
    // Output without flushing
    cout << "This might not appear immediately";
    
    // Force flush
    cout << "This will appear immediately" << flush;
    
    // endl both inserts a newline and flushes the stream
    cout << "This includes a newline and flush" << endl;
    
    // Disable buffering entirely
    cout.setf(ios::unitbuf);
    
    return 0;
}

Stream Redirection

You can redirect standard streams:

#include <iostream>
#include <fstream>
using namespace std;

int main() {
    // Save original stream buffer
    streambuf* coutBuffer = cout.rdbuf();
    
    // Create file stream
    ofstream file("output.txt");
    
    // Redirect cout to file
    cout.rdbuf(file.rdbuf());
    
    // This will go to the file, not console
    cout << "This text goes to the file." << endl;
    
    // Restore original stream
    cout.rdbuf(coutBuffer);
    
    // This will go to the console
    cout << "Back to console output." << endl;
    
    return 0;
}

Best Practices for Stream Usage

  1. Always check stream states after operations to ensure they succeeded
  2. Close file streams when done with them
  3. Use appropriate manipulators for formatting instead of custom parsing
  4. Handle input errors gracefully by checking stream state and clearing errors
  5. Use string streams for string parsing and formatting
  6. Prefer C++ streams over C-style I/O functions for type safety and extensibility
  7. Be mindful of buffer flushing for interactive applications

Common Stream Pitfalls

  1. Mixing cin and getline: After using cin >>, there’s often a newline left in the buffer that getline() will read. Use cin.ignore() to clear it.

  2. Not checking file open success: Always verify that files opened successfully.

  3. Ignoring stream state: Check for errors after operations, especially with file I/O.

  4. Forgetting to close files: Always close files when done to free resources and ensure data is written.

  5. Incorrect format specifiers: Make sure your format matches what you’re trying to read/write.

Conclusion

Streams in C++ provide a powerful, flexible, and consistent way to handle input and output operations. By understanding their behavior and proper usage patterns, you can create robust and maintainable code for a wide range of I/O requirements.