Module 8.2

C++ Stream Operations

Dive deep into C++ stream operations - from string streams for in-memory parsing to stream states for error handling. Master formatting with manipulators to create professional, polished output!

40 min read
Intermediate
Hands-on Examples
What You'll Learn
  • String streams (stringstream, istringstream, ostringstream)
  • Stream states and error detection
  • Output formatting with iomanip
  • Stream manipulators (setw, setprecision, fixed)
  • Custom stream operators
Contents
01

String Streams

String streams allow you to perform I/O operations on strings just like you would on files or the console. They are incredibly powerful for parsing, type conversion, and building formatted strings in memory without the overhead of actual file operations.

Understanding String Streams

Think of a string stream as a virtual file that exists only in your computer's memory. You can write to it, read from it, and manipulate its contents - all without touching the disk. This makes string streams perfect for parsing text data, converting between types, and constructing complex strings.

String Stream Classes

C++ provides three string stream classes in the <sstream> header, each designed for specific use cases:

  • stringstream: For both reading and writing - the most versatile option
  • istringstream: For reading (input) only - when you need to parse a string
  • ostringstream: For writing (output) only - when building strings
stringstream

Read and write operations. Use for general-purpose string manipulation when you need both input and output capabilities.

istringstream

Input only. Perfect for parsing existing strings, extracting values, or tokenizing text data.

ostringstream

Output only. Ideal for constructing formatted strings, concatenating values, or building complex output.

Basic String Stream Usage

String streams allow you to perform input/output operations on strings as if they were files or console streams. A stringstream acts like an in-memory buffer - you write to it with << and read from it with >>, just like cout and cin. The key advantage is that everything stays in memory, making it fast and convenient for string building, parsing, and type conversions without the complexity of manual string manipulation.

#include <sstream>
#include <iostream>
#include <string>
Include <sstream> for string stream classes like stringstream, istringstream, and ostringstream. The <iostream> header is needed for console output with cout, while <string> provides the std::string type. These three headers together give you everything needed for string stream operations - you can process strings in memory just like you would read from a file or the console.
int main() {
    // Creating a stringstream
    std::stringstream ss;
Create a stringstream object named ss - it works like a virtual file that exists only in memory. You can both read from and write to it, making it the most versatile of the three string stream types. Think of it as an in-memory buffer where you can format text, perform type conversions, and parse data without touching the disk. This is perfect when you need to build complex strings or parse text data without the overhead of actual file operations.
    // Writing to the stream (like cout)
    ss << "Hello, ";
    ss << "World! ";
    ss << 42;
Use the << insertion operator to write data to the stream, exactly like cout. You can chain multiple values together - strings, integers, floats, and other types are automatically converted to their string representations. Each << operation appends to the stream's internal buffer without overwriting previous content. This is much cleaner than using multiple string concatenations with +, and it handles different data types seamlessly without manual conversion.
    // Get the result as a string
    std::string result = ss.str();
    std::cout << result << std::endl;  // Hello, World! 42
    
    return 0;
}
The str() method returns the entire accumulated contents of the stream as a std::string object. All the data you've written - the text and converted numbers - comes out as one complete string: "Hello, World! 42". If you need to reuse the stream, you can call str("new content") to replace its contents entirely, or use str("") to empty it. This makes string streams incredibly flexible for building formatted output piece by piece.

Parsing Strings with istringstream

The istringstream (input string stream) class is designed specifically for reading and parsing data from strings. It treats a string as if it were an input source like cin, allowing you to extract typed data using the >> operator. This is invaluable for parsing configuration files, processing CSV data, converting strings to numbers, or extracting multiple values from user input without complex string manipulation.

#include <sstream>
#include <iostream>
#include <string>

int main() {
    std::string data = "Alice 25 3.8";
    std::istringstream iss(data);
Create an istringstream object initialized with the string you want to parse. The "i" stands for input - this stream is read-only and designed specifically for extracting data from strings. You pass the source string in the constructor, and then you can read from it using extraction operators just like cin. This is ideal for parsing configuration data, processing CSV lines, or extracting multiple values from a single string without complex string manipulation code.
    std::string name;
    int age;
    double gpa;
    
    // Extract values (like cin)
    iss >> name >> age >> gpa;
Use the >> extraction operator to pull values from the stream, exactly like reading from cin. The operator automatically skips leading whitespace (spaces, tabs, newlines) and converts the text to the target variable's type. Here it reads "Alice" as a string, "25" gets converted to an integer, and "3.8" becomes a double. The type conversion happens automatically - if conversion fails, the stream enters an error state that you can check with fail().
    std::cout << "Name: " << name << std::endl;   // Alice
    std::cout << "Age: " << age << std::endl;     // 25
    std::cout << "GPA: " << gpa << std::endl;     // 3.8
    
    return 0;
}
The extracted values are now stored in their respective variables with proper types - name is a string, age is an integer, and gpa is a double. The parsing and type conversion all happened automatically. This technique is perfect for processing space-separated data like CSV files, parsing configuration file entries, or extracting multiple fields from user input. Instead of manually searching for spaces and calling conversion functions, you let the stream do all the work.

Building Strings with ostringstream

The ostringstream (output string stream) class is optimized specifically for building and constructing strings. It's write-only - you can only insert data with <<, not read from it. This specialization makes it more efficient than stringstream when you only need to assemble strings. It's ideal for creating formatted reports, log messages, error descriptions, or any scenario where you need to combine text, numbers, and formatting into a single cohesive string.

#include <sstream>
#include <iostream>
#include <iomanip>

int main() {
    std::ostringstream oss;
Create an ostringstream object for output operations. The "o" stands for output - this stream is write-only and specifically optimized for constructing strings. You can't read from it, but you can write to it repeatedly using <<, applying formatting as you go. It's perfect for building complex formatted strings like reports, log messages, or error descriptions where you need to combine text with numbers and apply precise formatting. Think of it as a string builder with built-in formatting capabilities.
    // Build a formatted report
    oss << "=== Sales Report ===" << std::endl;
    oss << "Product: Widget" << std::endl;
    oss << "Quantity: " << 150 << std::endl;
Write strings and values to the stream using the << operator. Each piece - whether it's a string literal, a number, or std::endl for newlines - is appended to the stream's internal buffer. Nothing gets printed to the screen yet; it's all stored in memory. You're building the string piece by piece, which is much more efficient than repeatedly concatenating with +. This approach also makes your code more readable when constructing multi-line or complex formatted output.
    oss << "Price: $" << std::fixed << std::setprecision(2) << 29.99 << std::endl;
    oss << "Total: $" << std::fixed << std::setprecision(2) << (150 * 29.99) << std::endl;
Apply formatting manipulators like std::fixed and std::setprecision(2) to control how numbers appear in the output. std::fixed ensures fixed-point notation (no scientific notation like 1.23e+10), while std::setprecision(2) sets exactly 2 decimal places. Together they format 29.99 and 4498.50 as currency with proper decimal places. These manipulators work on streams just like they do with cout, giving you precise control over number formatting in your built string.
    std::string report = oss.str();
    std::cout << report;
    
    return 0;
}
Call str() to retrieve the complete built string with all your accumulated content and formatting. The method returns a std::string containing everything you've written to the stream. This approach is significantly more efficient than using repeated string concatenation with +, especially when building long strings, because the stream manages a buffer optimally. Plus, it's more readable and handles automatic type conversion without manual std::to_string() calls.

Type Conversion with String Streams

One of the most practical uses of string streams is converting between strings and numbers. Before C++11's std::stoi() and friends, this was the standard approach.

// String to number conversion
template<typename T>
T stringToNumber(const std::string& str) {
    std::istringstream iss(str);
    T value;
    iss >> value;
    return value;
}
This template function converts any string to a numeric type like int, double, or float. It creates an istringstream from the input string, then uses the extraction operator >> to read and convert the text to the requested type T. The template parameter makes it work with any numeric type - just specify what you want when calling it. The conversion happens automatically through the stream's type conversion system, handling whitespace and validating format.
// Number to string conversion
template<typename T>
std::string numberToString(T value) {
    std::ostringstream oss;
    oss << value;
    return oss.str();
}
This template function converts any numeric value to its string representation. It creates an ostringstream, writes the value to it using <<, then calls str() to extract the string result. The template works with integers, floating-point numbers, or any type that supports stream output. This was the standard way to convert numbers to strings before C++11 introduced std::to_string(), and it still offers advantages when you need custom formatting during conversion.
int main() {
    // String to int
    int num = stringToNumber<int>("42");
    std::cout << "Number: " << num << std::endl;  // 42
    
    // String to double
    double pi = stringToNumber<double>("3.14159");
    std::cout << "Pi: " << pi << std::endl;  // 3.14159
Call the template function with the desired target type specified in angle brackets: stringToNumber<int> converts to integers, stringToNumber<double> converts to double-precision floating-point. The template parameter tells the function what type to extract from the string. The stream automatically handles the conversion - "42" becomes integer 42, "3.14159" becomes double 3.14159. If conversion fails (like "abc" to int), the stream enters a failed state that you can check.
    // Number to string
    std::string str = numberToString(123.456);
    std::cout << "String: " << str << std::endl;  // 123.456
    
    return 0;
}
While C++11 introduced convenient functions like std::to_string() and std::stoi()/stod()/stof(), the stringstream approach still has advantages. It works with any custom type that defines stream operators, not just built-in types. You can apply formatting manipulators during conversion (like setting precision or base). It also provides better error handling through stream state flags. For simple conversions, use the C++11 functions; for complex formatting or custom types, use stringstreams.

Practice: String Streams

Given:

std::vector<std::string> fruits = {"apple", "banana", "cherry"};

Task: Use an ostringstream to join these words with commas into a single string.

Expected output: apple, banana, cherry

Hint: Use a loop with a condition to avoid trailing comma.

Show Solution
#include <sstream>
#include <iostream>
#include <vector>
#include <string>

int main() {
    std::vector<std::string> fruits = {"apple", "banana", "cherry"};
    std::ostringstream oss;
    
    for (size_t i = 0; i < fruits.size(); i++) {
        if (i > 0) oss << ", ";
        oss << fruits[i];
    }
    
    std::cout << oss.str() << std::endl;  // apple, banana, cherry
    return 0;
}

Given:

std::string data = "John,25,Engineer";

Task: Parse this CSV-style string to extract name, age, and profession using getline with a comma delimiter.

Expected output:

Name: John
Age: 25
Profession: Engineer

Hint: Use std::getline(stream, variable, ',') to read until comma.

Show Solution
#include <sstream>
#include <iostream>
#include <string>

int main() {
    std::string data = "John,25,Engineer";
    std::istringstream iss(data);
    
    std::string name, ageStr, profession;
    
    std::getline(iss, name, ',');
    std::getline(iss, ageStr, ',');
    std::getline(iss, profession, ',');
    
    int age = std::stoi(ageStr);
    
    std::cout << "Name: " << name << std::endl;
    std::cout << "Age: " << age << std::endl;
    std::cout << "Profession: " << profession << std::endl;
    
    return 0;
}

Given:

std::string text = "The quick brown fox jumps over the lazy dog";

Task: Write a function countWords() that uses istringstream to count the number of words in the sentence.

Expected output: Word count: 9

Hint: The extraction operator >> automatically skips whitespace between words.

Show Solution
#include <sstream>
#include <iostream>
#include <string>

int countWords(const std::string& sentence) {
    std::istringstream iss(sentence);
    std::string word;
    int count = 0;
    
    while (iss >> word) {
        count++;
    }
    
    return count;
}

int main() {
    std::string text = "The quick brown fox jumps over the lazy dog";
    int words = countWords(text);
    std::cout << "Word count: " << words << std::endl;  // 9
    return 0;
}
02

Stream States

Every C++ stream maintains internal state flags that indicate whether operations have succeeded or failed. Understanding these states is crucial for writing robust code that handles errors gracefully and avoids silent failures.

Understanding Stream State Flags

Streams track their status using four boolean flags. When an operation fails, one or more of these flags are set, allowing you to detect and respond to problems. Learning to check these flags is essential for professional-quality C++ code.

The Four Stream State Flags

  • goodbit: No errors - everything is working correctly
  • eofbit: End of file/input has been reached
  • failbit: Operation failed (wrong format, logical error)
  • badbit: Serious error (stream corrupted, I/O failure)
Method Returns true when Use case
good() No flags set - stream is healthy Check before operations
eof() End of file reached Loop termination condition
fail() failbit OR badbit is set Detect any operation failure
bad() badbit is set (serious error) Detect critical failures
operator bool() Stream is usable (!fail()) if (stream) checks

Checking Stream States

#include <iostream>
#include <fstream>

int main() {
    std::ifstream file("data.txt");
    
    // Method 1: Boolean conversion
    if (!file) {
        std::cerr << "Failed to open file!" << std::endl;
        return 1;
    }
The boolean conversion (if (!file)) is the most common way to check stream status. It returns true if the stream is usable (no failbit or badbit set).
    // Method 2: Explicit state check
    if (file.good()) {
        std::cout << "File is ready for reading" << std::endl;
    }
The good() method returns true only when the stream is in a completely healthy state with no error flags set - not eofbit, not failbit, not badbit. It's the most strict check, confirming the stream is ready for I/O operations. Use this when you need to verify the stream is perfect before proceeding, especially after opening a file or performing critical operations. It's essentially equivalent to checking that all flags are clear and the stream is ready for the next read or write.
    int number;
    file >> number;
    
    // Check if read succeeded
    if (file.fail()) {
        std::cerr << "Failed to read integer!" << std::endl;
    }
The fail() method returns true if either failbit OR badbit is set, making it a general-purpose error check. Failbit is set for logical errors like trying to read "abc" as an integer, or reading past end-of-file. Badbit indicates more serious errors like stream corruption or hardware failure. Use fail() after read/write operations to detect whether they succeeded. If fail() returns true, you should investigate the error and possibly clear it before continuing.
    // Check for end of file
    if (file.eof()) {
        std::cout << "Reached end of file" << std::endl;
    }
    
    return 0;
}
The eof() method returns true when the end-of-file flag is set, meaning you've attempted to read past the last character in the stream. Note that eof() is only set AFTER a read operation fails due to reaching the end - it's not set by simply being at the last character. This makes it useful for loop termination conditions when reading files: keep reading while !eof() or until a read fails. Don't check eof() before reading; check it after to see why a read failed.

Clearing Stream Errors

When a stream enters a failed state, it stays that way until you explicitly clear the error flags. This is essential for recovery scenarios where you want to try again after an error.

#include <iostream>
#include <sstream>
#include <limits>

int main() {
    std::istringstream iss("abc 123");
    
    int num;
    iss >> num;  // Fails - "abc" is not a number
Attempting to read the string "abc" into an int variable fails because "abc" is not a valid number. When the extraction operator >> can't convert the input to the target type, it sets the stream's failbit. At this point, the stream enters an error state and all subsequent read operations will fail automatically until you clear the error. The bad input ("abc") remains in the stream buffer, unconsumed. This is why both clearing the error AND removing the bad data are necessary for recovery.
    if (iss.fail()) {
        std::cout << "Read failed, clearing error..." << std::endl;
        
        // Clear the error flags
        iss.clear();
The clear() method resets all error state flags (eofbit, failbit, badbit) back to goodbit, making the stream usable again for I/O operations. However, it's crucial to understand that clear() only resets the flags - it doesn't fix the underlying problem or remove bad data from the stream buffer. Think of it like turning off a "check engine" light without actually fixing the engine. You still need to skip or remove the problematic input manually before attempting to read again.
        // Skip the bad input
        std::string garbage;
        iss >> garbage;  // Reads "abc"
After clearing the error flags, you must manually consume the problematic input that caused the failure. Here we read the bad input ("abc") into a string variable, which successfully extracts it from the stream buffer. Any variable type that can accept "abc" works - string is convenient because it accepts any text. Once the bad data is consumed, the stream position advances past it to "123", allowing subsequent reads to succeed. This two-step process (clear + skip) is essential for robust error recovery in stream operations.
        // Try again
        iss >> num;  // Now reads 123
        
        if (iss.good()) {
            std::cout << "Successfully read: " << num << std::endl;
        }
    }
    
    return 0;
}
Now that the error flags are cleared and the bad input has been skipped, the stream can successfully read the next value "123" into the integer variable num. The stream position has moved past "abc" to the valid integer, and the stream is in a good state. This demonstrates the complete error recovery pattern: detect the error, clear the flags, skip the bad input, and retry. Always verify success after recovery by checking good() or testing the stream's boolean state before using the read value.

Safe Input with Error Recovery

#include <iostream>
#include <limits>

int getValidInteger(const std::string& prompt) {
    int value;
    
    while (true) {
        std::cout << prompt;
        std::cin >> value;
Loop indefinitely using while (true) until valid input is received. On each iteration, prompt the user and attempt to read an integer from cin using the extraction operator. This pattern ensures the user can't proceed with invalid data - they must enter a correct integer to break out of the loop. The loop will handle any number of invalid attempts gracefully, clearing errors and prompting again until success. This is a standard pattern for bulletproof user input in production code.
        if (std::cin.good()) {
            return value;  // Success!
        }
If cin.good() returns true, the read operation succeeded and value contains a valid integer. The stream has no error flags set, meaning the user entered valid numeric input. At this point, we can safely return the value and exit both the loop and the function. Using good() is stricter than just checking !fail() - it ensures no flags are set at all. This gives you confidence the value is reliable before using it in calculations or logic.
        // Handle error
        std::cout << "Invalid input! Please enter a number." << std::endl;
        
        // Clear error flags
        std::cin.clear();
If the read failed (user entered text like "abc" instead of a number), provide helpful feedback by printing an error message. Then call cin.clear() to reset the error flags on the stream, putting it back into a usable state. Without clearing, cin would remain in a failed state permanently, and all subsequent input operations would silently fail. This is a common mistake - always clear the error state before attempting to read again. The message helps users understand what went wrong and what they need to do.
        // Discard bad input (up to newline)
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    }
}
The ignore() method discards characters from the input buffer without storing them anywhere. Using std::numeric_limits<std::streamsize>::max() as the count parameter means "discard up to the maximum possible number of characters". The second parameter '\n' is the delimiter - ignore() stops when it encounters a newline. This effectively clears all bad input from the current line, including any trailing garbage after the invalid characters. Without this, the bad input would still be in the buffer and interfere with the next read attempt.
int main() {
    int age = getValidInteger("Enter your age: ");
    std::cout << "Your age is: " << age << std::endl;
    return 0;
}
The function keeps looping and re-prompting until valid input is received, then returns the validated integer. If the user types "abc", they'll see "Invalid input!" and get prompted again. If they type "3.14", the integer part (3) is read and the decimal part remains in the buffer (a potential issue). Only a valid integer like "25" will satisfy the validation and allow the program to continue. This pattern ensures your program never crashes or produces garbage values due to invalid user input - essential for production-quality software.

Practice: Stream States

Given:

std::string filename = "test.txt";

Task: Write code that attempts to open the file and prints different messages based on stream state: "File opened successfully!" for success, "Failed to open file." for failure, and "Critical I/O error!" if bad() is set.

Expected output (if file exists): File opened successfully!

Hint: Use is_open(), good(), and bad() to check different states.

Show Solution
#include <fstream>
#include <iostream>

int main() {
    std::ifstream file("test.txt");
    
    if (file.is_open() && file.good()) {
        std::cout << "File opened successfully!" << std::endl;
        file.close();
    } else {
        std::cerr << "Failed to open file." << std::endl;
        if (file.bad()) {
            std::cerr << "Critical I/O error!" << std::endl;
        }
    }
    
    return 0;
}

Given:

std::string inputs[] = {"3.14159", "abc", "12.5abc"};

Task: Write a function parseDouble(const std::string& str, double& result) that reads a double from a string and returns true if conversion was fully successful (no trailing characters), false otherwise.

Expected output:

Parsed: 3.14159
Failed to parse 'abc'
Failed to parse '12.5abc' (trailing chars)

Hint: Check both fail() and eof() to ensure complete conversion.

Show Solution
#include <sstream>
#include <iostream>
#include <string>

bool parseDouble(const std::string& str, double& result) {
    std::istringstream iss(str);
    iss >> result;
    
    // Check if we read something and reached end of string
    return !iss.fail() && iss.eof();
}

int main() {
    double value;
    
    if (parseDouble("3.14159", value)) {
        std::cout << "Parsed: " << value << std::endl;
    }
    
    if (!parseDouble("abc", value)) {
        std::cout << "Failed to parse 'abc'" << std::endl;
    }
    
    if (!parseDouble("12.5abc", value)) {
        std::cout << "Failed to parse '12.5abc' (trailing chars)" << std::endl;
    }
    
    return 0;
}

Given:

int minValue = 1;
int maxValue = 100;

Task: Write an input validation loop that continuously asks the user to enter an integer between 1 and 100. If non-numeric input is detected, use clear() to reset the error state and ignore() to discard bad input. Continue prompting until valid input is received.

Expected behavior:

Enter a number between 1 and 100: abc
Error: Invalid input. Please enter a number.
Enter a number between 1 and 100: 150
Error: Number out of range. Must be 1-100.
Enter a number between 1 and 100: 50
Valid input received: 50

Hint: Use std::numeric_limits<std::streamsize>::max() to ignore all characters until newline.

Show Solution
#include <iostream>
#include <limits>

int main() {
    int number;
    bool validInput = false;
    
    while (!validInput) {
        std::cout << "Enter a number between 1 and 100: ";
        std::cin >> number;
        
        if (std::cin.fail()) {
            // Clear error state
            std::cin.clear();
            // Discard bad input
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            std::cout << "Error: Invalid input. Please enter a number.\n" << std::endl;
        } else if (number < 1 || number > 100) {
            std::cout << "Error: Number out of range. Must be 1-100.\n" << std::endl;
        } else {
            validInput = true;
            std::cout << "Valid input received: " << number << std::endl;
        }
    }
    
    return 0;
}

Given:

// File: numbers.txt contains:
// 10 20 30 invalid 50

Task: Write a program that reads integers from "numbers.txt" and calculates their sum. Handle all errors: file not found (!is_open()), critical I/O errors (bad()), and format errors (fail() without eof()). Display appropriate messages and partial results.

Expected output (with invalid data):

Error: Invalid data format in file (non-numeric content)
Successfully read 3 numbers before error

Hint: After the read loop, check eof() for normal completion vs fail() for format errors.

Show Solution
#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ifstream file("numbers.txt");
    
    // Check if file opened successfully
    if (!file.is_open()) {
        std::cerr << "Error: Could not open file 'numbers.txt'" << std::endl;
        return 1;
    }
    
    int number, sum = 0, count = 0;
    bool errorOccurred = false;
    
    while (file >> number) {
        sum += number;
        count++;
        
        // Check for critical I/O errors after read
        if (file.bad()) {
            std::cerr << "Error: Critical I/O error while reading file" << std::endl;
            errorOccurred = true;
            break;
        }
    }
    
    // Check why loop ended
    if (!errorOccurred) {
        if (file.eof()) {
            // Normal end - all data read successfully
            std::cout << "Successfully read " << count << " numbers" << std::endl;
            std::cout << "Sum: " << sum << std::endl;
        } else if (file.fail()) {
            // Format error - non-numeric data encountered
            std::cerr << "Error: Invalid data format in file (non-numeric content)" << std::endl;
            std::cerr << "Successfully read " << count << " numbers before error" << std::endl;
            errorOccurred = true;
        }
    }
    
    file.close();
    return errorOccurred ? 1 : 0;
}
03

Output Formatting

Professional applications require polished, well-formatted output. C++ provides powerful formatting tools through the <iomanip> header that let you control precision, alignment, padding, and number representation.

The iomanip Header

The <iomanip> header provides manipulators - special functions that modify how streams format output. Unlike regular functions, manipulators are inserted directly into the stream using <<.

Common Format Manipulators

  • setw(n): Set minimum field width for next output
  • setprecision(n): Set decimal precision for floating-point
  • setfill(c): Set padding character (default is space)
  • fixed: Use fixed-point notation for decimals
  • scientific: Use scientific notation (e.g., 1.23e+10)
  • left/right: Set alignment within field width

Controlling Field Width with setw

The setw() manipulator sets the minimum field width for the very next output item. If the content is shorter than the specified width, it's padded with the fill character (space by default). If it's longer, the content is printed in full without truncation. This is the primary tool for creating aligned columns in text-based tables and reports. Unlike most manipulators, setw() is not sticky - it only affects one output operation.

#include <iostream>
#include <iomanip>

int main() {
    // setw only affects the NEXT output item
    std::cout << std::setw(10) << "Name" << std::setw(8) << "Age" << std::setw(10) << "Salary" << std::endl;
Print a header row with setw() setting minimum field widths for each column. The setw(10) means "make this at least 10 characters wide", padding with spaces if necessary. Each column is right-aligned by default with the specified width, creating visually aligned columns when you print multiple rows. Remember that setw() affects only the very next output item, so you must repeat it for every column in every row. This is the foundation of creating professional-looking text tables in console applications.
    std::cout << std::setw(10) << "Alice" << std::setw(8) << 28 << std::setw(10) << 75000 << std::endl;
    std::cout << std::setw(10) << "Bob" << std::setw(8) << 34 << std::setw(10) << 82000 << std::endl;
    std::cout << std::setw(10) << "Charlie" << std::setw(8) << 22 << std::setw(10) << 55000 << std::endl;
Each data row uses the exact same width values (10, 8, 10) as the header to maintain column alignment throughout the table. The consistency is what creates the aligned appearance - if you used different widths, the columns would be misaligned. setw() must be repeated for every single output item because it's not "sticky" - unlike manipulators like setprecision(), it resets after each use. This may seem tedious, but it gives you precise control over each column's width.
    return 0;
}
setw() only affects the very next output operation - after that item prints, the width automatically resets to 0 (no minimum width). This "one-shot" behavior is unique among manipulators. Most other manipulators like setprecision(), fixed, or hex are "sticky" - they stay in effect until explicitly changed. The non-sticky nature of setw() prevents accidental formatting of subsequent output, but requires you to be explicit about every field width. This is actually a safety feature to avoid unexpected padding.

Formatting Numbers with setprecision

The setprecision() manipulator controls how many digits are displayed for floating-point numbers. Its behavior changes dramatically depending on whether you use it with fixed or scientific flags. Without any flags, it controls significant digits (total meaningful digits). With fixed, it controls decimal places (digits after the decimal point). Understanding this distinction is crucial for proper number formatting.

setprecision() Behavior

  • Default mode: setprecision(n) → n significant digits total
  • With std::fixed: setprecision(n) → n digits after decimal point
  • With std::scientific: setprecision(n) → n digits in mantissa after decimal
  • Stickiness: Once set, remains in effect until explicitly changed
#include <iostream>
#include <iomanip>
#include <cmath>

int main() {
    double pi = 3.14159265358979;
    
    // Default precision (usually 6 significant digits)
    std::cout << "Default: " << pi << std::endl;  // 3.14159
Without any manipulators, C++ streams use default precision which is typically 6 significant digits total. This means the entire number (including digits before and after the decimal point) is limited to 6 meaningful digits. For pi (3.14159265...), this produces "3.14159". The trailing digits are rounded, not truncated. Default precision works in "significant digits" mode, not "decimal places" mode, which can be confusing at first. To control decimal places specifically, you need to combine fixed with setprecision().
    // setprecision without fixed = significant digits
    std::cout << "3 sig digits: " << std::setprecision(3) << pi << std::endl;  // 3.14
Without fixed or scientific, setprecision(n) sets the total number of significant digits displayed across the entire number. Significant digits count all non-zero digits from left to right, ignoring leading/trailing zeros. So setprecision(3) on 3.14159 shows "3.14" (3 digits total), while on 0.00456 it shows "0.00456" (3 significant digits: 4,5,6). This mode is called "default" or "general" format. It's useful for scientific notation but confusing for everyday output, which is why fixed is more common.
    // fixed + setprecision = decimal places
    std::cout << std::fixed;
    std::cout << "2 decimal places: " << std::setprecision(2) << pi << std::endl;  // 3.14
    std::cout << "5 decimal places: " << std::setprecision(5) << pi << std::endl;  // 3.14159
With fixed active, setprecision(n) changes behavior to control exactly how many digits appear after the decimal point, regardless of how many digits are before it. This is the intuitive behavior most people expect - setprecision(2) means "show 2 decimal places". This is the most common mode for displaying currency ($19.99), measurements (5.75 inches), and percentages (98.60%). The fixed manipulator is sticky, so once set, it stays until you change to scientific or reset the stream.
    // Scientific notation
    std::cout << std::scientific;
    std::cout << "Scientific: " << std::setprecision(3) << pi << std::endl;  // 3.142e+00
    
    return 0;
}
With scientific active, numbers display in exponential (scientific) notation with the format: mantissa × 10^exponent, like 3.142e+00. The mantissa is always normalized to have exactly one non-zero digit before the decimal point. When combined with setprecision(n), the precision controls how many digits appear after the decimal in the mantissa, not the exponent part. For example, setprecision(3) with 3.14159 produces "3.142e+00". This format is essential for displaying very large or very small numbers in scientific applications.

Alignment and Fill Characters

When using setw() to create fixed-width fields, you can control where content appears within that field and what character fills the empty space. C++ provides alignment manipulators (left, right, internal) to position content, and setfill() to customize the padding character. These tools are essential for creating professional-looking tables, reports, and formatted output.

#include <iostream>
#include <iomanip>

int main() {
    // Right alignment (default)
    std::cout << "Right aligned:" << std::endl;
    std::cout << std::right << std::setw(10) << 42 << std::endl;      // "        42"
std::right aligns content to the right edge of the field width, padding with the fill character (space by default) on the left side. This is the default alignment for streams, which is why numbers naturally align on their right edges in columns. For example, setw(10) << 42 produces " 42" (8 spaces, then 42). Right alignment is standard for numeric columns in tables because it aligns decimal points and makes number comparison easier. Left alignment is typically used for text columns.
    // Left alignment
    std::cout << "Left aligned:" << std::endl;
    std::cout << std::left << std::setw(10) << 42 << "|" << std::endl; // "42        |"
std::left aligns content to the left edge of the field width, padding with the fill character on the right side instead of the left. For example, setw(10) << 42 with left alignment produces "42 " (42, then 8 spaces). This is especially useful for text columns in tables where you want names or descriptions to start at the same horizontal position. Left-aligned text is easier to read than right-aligned text, making it the standard for string columns while keeping numbers right-aligned.
    // Custom fill character
    std::cout << "With fill character:" << std::endl;
    std::cout << std::right << std::setfill('0') << std::setw(5) << 42 << std::endl;  // "00042"
    std::cout << std::setfill('.') << std::setw(10) << "Price" << std::endl;  // ".....Price"
setfill() changes the padding character used by setw() from the default space to any character you specify. Common uses include: setfill('0') for leading zeros in numbers (00042), setfill('.') for menu items with dotted leaders ("Pizza.....\$12.99"), and setfill('-') for separators or borders. The fill character applies to whatever alignment is active - with right alignment, it fills on the left; with left alignment, it fills on the right. Always remember that setfill() is sticky and affects all subsequent setw() operations until you change it again.
    // Reset fill to space
    std::cout << std::setfill(' ');
    
    return 0;
}
setfill() is a "sticky" manipulator - once set, it remains in effect for the entire stream until explicitly changed again. This means if you set setfill('0') and forget to reset it, ALL subsequent uses of setw() will use zeros for padding, potentially causing bugs that are hard to trace. Always explicitly reset to setfill(' ') (space character) after you're done with a custom fill character. This is a defensive programming practice that prevents your formatting choices from leaking into other parts of the code.

Formatting Currency and Tables

Combining multiple formatting manipulators creates professional-looking tables and reports. For currency and financial data, you'll typically use fixed with setprecision(2) for exactly two decimal places, setw() for column alignment, and mix left/right alignment depending on content type. This real-world example demonstrates how all these techniques work together to produce polished, production-quality output.

#include <iostream>
#include <iomanip>
#include <string>
#include <vector>

struct Product {
    std::string name;
    double price;
    int quantity;
};
Define a simple struct to hold product data with three fields: name (string), price (double), and quantity (int). Using a struct (or class) to group related data is a fundamental programming pattern, much better than managing parallel arrays or separate variables. This makes the code more maintainable and type-safe. Structs work perfectly for "plain old data" types like this where all members can be public. This pattern is common for storing tabular data that needs to be formatted and displayed in reports, invoices, or inventory systems.
int main() {
    std::vector<Product> inventory = {
        {"Laptop", 999.99, 15},
        {"Mouse", 29.50, 150},
        {"Keyboard", 79.99, 75},
        {"Monitor", 349.00, 30}
    };
Create a vector of Product objects using C++11 initializer list syntax, where each set of inner braces {...} initializes one Product struct using aggregate initialization. The values inside each inner brace correspond to the struct members in order: name, price, quantity. This is a concise way to initialize a container with multiple objects without calling constructors explicitly. You could also populate the vector with push_back() in a loop, but initializer lists are cleaner for known, fixed data sets.
    // Print table header
    std::cout << std::left << std::setw(15) << "Product"
              << std::right << std::setw(12) << "Price"
              << std::setw(10) << "Qty"
              << std::setw(15) << "Total" << std::endl;
    std::cout << std::string(52, '-') << std::endl;
Print the table header with left alignment for the "Product" column (text reads better left-aligned) and right alignment for numeric columns ("Price", "Qty", "Total"). The column widths are 15, 12, 10, and 15 characters respectively, totaling 52. The constructor call std::string(52, '-') creates a string of exactly 52 dash characters, forming a separator line that perfectly matches the table width. This horizontal line visually separates the header from the data rows, making the table more readable and professional-looking.
    // Print each product
    double grandTotal = 0;
    for (const auto& p : inventory) {
        double total = p.price * p.quantity;
        grandTotal += total;
        
        std::cout << std::left << std::setw(15) << p.name
                  << std::right << "$" << std::fixed << std::setprecision(2)
                  << std::setw(10) << p.price
                  << std::setw(10) << p.quantity
                  << "$" << std::setw(13) << total << std::endl;
    }
Loop through all products using a range-based for loop with const auto& to avoid copying each Product object. For each row, calculate the total (price × quantity) and add it to the running grandTotal. The formatting combines multiple manipulators: left for the name, right for numbers, fixed to use decimal places mode, and setprecision(2) to show exactly 2 decimal places for currency. The dollar signs are manually placed before the numeric values. This multi-manipulator approach creates professional, aligned currency tables.
    std::cout << std::string(52, '-') << std::endl;
    std::cout << std::left << std::setw(37) << "Grand Total:"
              << "$" << std::right << std::setw(13) << grandTotal << std::endl;
    
    return 0;
}
Print another 52-character separator line to visually close the table, then display the grand total in a footer row. The "Grand Total:" label is left-aligned with setw(37) to push it to the appropriate position, then the dollar amount is right-aligned in a 13-character field to match the "Total" column above. Consistent column widths throughout all rows (header, data, footer) create a professional, aligned appearance that looks like a real business report. This attention to formatting detail is what separates amateur console output from production-quality applications.

Practice: Output Formatting

Given:

double price = 19.5;

Task: Display the price as currency with exactly 2 decimal places using fixed and setprecision.

Expected output: $19.50

Hint: Use std::fixed before setprecision(2) to ensure exactly 2 decimal digits.

Show Solution
#include <iostream>
#include <iomanip>

int main() {
    double price = 19.5;
    
    std::cout << "$" << std::fixed << std::setprecision(2) << price << std::endl;
    // Output: $19.50
    
    return 0;
}

Given:

int numbers[] = {5, 42, 389, 7, 1024};

Task: Print each number right-aligned in a 6-character wide field, one per line.

Expected output:

     5
    42
   389
     7
  1024

Hint: Use setw(6) with std::right for each number.

Show Solution
#include <iostream>
#include <iomanip>

int main() {
    int numbers[] = {5, 42, 389, 7, 1024};
    
    for (int num : numbers) {
        std::cout << std::right << std::setw(6) << num << std::endl;
    }
    
    return 0;
}

Given:

int employeeId = 42;

Task: Display the employee ID with leading zeros to make it 5 digits (format: EMP-00042).

Expected output: EMP-00042

Hint: Use setfill('0') with setw(5) to pad with zeros.

Show Solution
#include <iostream>
#include <iomanip>

int main() {
    int employeeId = 42;
    
    std::cout << "EMP-" << std::setfill('0') << std::setw(5) << employeeId << std::endl;
    // Output: EMP-00042
    
    // Reset fill character for subsequent output
    std::cout << std::setfill(' ');
    
    return 0;
}

Given:

struct Product { std::string name; double price; int qty; };
Product products[] = {
    {"Apple", 1.50, 100},
    {"Banana", 0.75, 250},
    {"Orange Juice", 3.99, 45}
};

Task: Display products in a formatted table with columns: Product (left-aligned, 15 chars), Price (right-aligned, 8 chars with $), Qty (right-aligned, 6 chars).

Expected output:

Product         |   Price|   Qty
--------------------------------
Apple           |   $1.50|   100
Banana          |   $0.75|   250
Orange Juice    |   $3.99|    45

Hint: Combine left, right, setw, fixed, and setprecision.

Show Solution
#include <iostream>
#include <iomanip>
#include <string>

struct Product { std::string name; double price; int qty; };

int main() {
    Product products[] = {
        {"Apple", 1.50, 100},
        {"Banana", 0.75, 250},
        {"Orange Juice", 3.99, 45}
    };
    
    // Header
    std::cout << std::left << std::setw(15) << "Product" << "|" 
              << std::right << std::setw(7) << "Price" << "|" 
              << std::setw(6) << "Qty" << std::endl;
    std::cout << std::string(32, '-') << std::endl;
    
    // Data rows
    std::cout << std::fixed << std::setprecision(2);
    for (const auto& p : products) {
        std::cout << std::left << std::setw(15) << p.name << "|" 
                  << std::right << "$" << std::setw(6) << p.price << "|" 
                  << std::setw(6) << p.qty << std::endl;
    }
    
    return 0;
}
04

Stream Manipulators

Stream manipulators modify how streams interpret and display data. Beyond the basic formatting manipulators, C++ provides specialized manipulators for different number bases, boolean representation, and more.

Number Base Manipulators

Sometimes you need to display numbers in different bases - hexadecimal for memory addresses, octal for file permissions, or binary for bit patterns. C++ makes this easy with base manipulators.

Manipulator Base Example (value: 255)
std::dec Base 10 (decimal) 255
std::hex Base 16 (hexadecimal) ff
std::oct Base 8 (octal) 377
#include <iostream>
#include <iomanip>

int main() {
    int value = 255;
    
    std::cout << "Decimal:     " << std::dec << value << std::endl;  // 255
    std::cout << "Hexadecimal: " << std::hex << value << std::endl;  // ff
    std::cout << "Octal:       " << std::oct << value << std::endl;  // 377
The manipulators dec, hex, and oct change the number base (radix) used for integer output, converting the same numeric value to different representations. In decimal (base 10), 255 uses digits 0-9. In hexadecimal (base 16), it becomes "ff" using digits 0-9 and letters a-f (where a=10, b=11, ..., f=15). The calculation: 15×16¹ + 15×16⁰ = 240+15 = 255. In octal (base 8), it becomes "377" using only digits 0-7: 3×8² + 7×8¹ + 7×8⁰ = 192+56+7 = 255. These bases are essential in systems programming.
    // With base prefix using showbase
    std::cout << std::showbase;
    std::cout << "Hex with prefix: " << std::hex << value << std::endl;  // 0xff
    std::cout << "Oct with prefix: " << std::oct << value << std::endl;  // 0377
The showbase manipulator adds standard C++ numeric base prefixes to output: 0x for hexadecimal values and 0 (zero) for octal values. Decimal has no prefix. For example, 255 becomes "0xff" in hex and "0377" in octal. These prefixes make the base immediately clear to anyone reading the output, preventing confusion between "255" (decimal) and "255" (which could be misread as octal). The prefixes match C++ literal syntax, so the output can often be copied directly into source code. Use noshowbase to turn off the prefix when you want just the digits.
    // Uppercase hex
    std::cout << std::uppercase;
    std::cout << "Uppercase hex: " << std::hex << value << std::endl;  // 0XFF
The uppercase manipulator affects two things in hexadecimal output: it makes the letter digits (A-F) uppercase instead of lowercase (a-f), and it capitalizes the 'X' in the 0X prefix when showbase is active. For example, 255 becomes "0XFF" instead of "0xff". Uppercase hex is often preferred for memory addresses, color codes in graphics programming, and hardware register values because it's more readable and matches industry conventions. Scientific notation (from scientific) also uses uppercase 'E' instead of 'e' with this manipulator. Use nouppercase to revert to lowercase.
    // Reset to decimal
    std::cout << std::dec << std::noshowbase << std::nouppercase;
    
    return 0;
}
Base manipulators like hex, oct, and dec are "sticky" - once set, they persist for all subsequent output operations on that stream until explicitly changed again. If you set hex to print a memory address, ALL integers after that will print in hexadecimal until you reset to dec. This is a common source of bugs where developers forget to reset and wonder why their numbers look strange. Similarly, showbase and uppercase are sticky. Always explicitly reset to dec, noshowbase, and nouppercase at the end of your formatting section to prevent these settings from leaking to other code.

Boolean Representation

By default, C++ streams output boolean values as integers (1 for true, 0 for false). While compact, this numeric representation isn't always intuitive. The boolalpha manipulator switches to textual output ("true"/"false"), making your output more readable and self-documenting. This is especially valuable in logs, debugging output, and user-facing messages where clarity matters more than brevity.

#include <iostream>
#include <iomanip>

int main() {
    bool flag = true;
    
    // Default: prints 1 or 0
    std::cout << "Default: " << flag << std::endl;  // 1
By default, C++ streams treat boolean values as integers: true prints as 1 and false prints as 0. This numeric representation is compact and matches the way booleans are stored in memory (as 1-byte integers), but it's not very readable or self-documenting in output. When debugging or logging, seeing "1" or "0" requires mental translation to understand what boolean condition they represent. For program output meant to be read by humans, the textual representation is almost always clearer and more professional.
    // With boolalpha: prints true or false
    std::cout << std::boolalpha;
    std::cout << "Boolalpha: " << flag << std::endl;  // true
    std::cout << "False: " << false << std::endl;     // false
The boolalpha manipulator changes boolean output to textual representation: true prints as the literal text "true" and false prints as "false". This is dramatically more readable in logs, debugging output, configuration dumps, and user-facing messages. Compare "User authenticated: 1" vs "User authenticated: true" - the latter is instantly clear. The manipulator is sticky (stays in effect until changed), and it also affects input: with boolalpha active, you can read the words "true" or "false" directly from a stream into a bool variable, which is perfect for configuration files and user input.
    // Turn off boolalpha
    std::cout << std::noboolalpha;
    std::cout << "Back to numeric: " << flag << std::endl;  // 1
    
    return 0;
}
The noboolalpha manipulator switches boolean output back to the default numeric representation (1 for true, 0 for false), reversing the effect of boolalpha. Since boolalpha is sticky and affects all subsequent boolean operations, you may need to explicitly turn it off if you need numeric output later. The boolalpha setting also affects input streams: with it active, cin >> boolVar will successfully read the words "true" or "false", but with noboolalpha, it expects 0 or 1. This bidirectional behavior makes boolalpha very useful for human-readable configuration files.

Positive Sign and Decimal Point

Fine-tune numeric output with showpos to display explicit plus signs for positive numbers, and showpoint to always show decimal points for floating-point values. The showpos manipulator is useful when sign information is meaningful (temperature changes, financial deltas, coordinates), while showpoint helps distinguish floating-point values from integers in scientific or technical output where type clarity is important.

#include <iostream>
#include <iomanip>

int main() {
    // Show positive sign
    std::cout << std::showpos;
    std::cout << 42 << " " << -42 << std::endl;  // +42 -42
    std::cout << std::noshowpos;
The showpos manipulator forces a plus sign (+) to be displayed before positive numbers, making the sign explicit for both positive and negative values. For example, 42 becomes "+42" while -42 stays "-42". This is particularly useful when displaying: temperature changes ("+5°C"), financial changes (profit: "+\$5000"), coordinate positions ("x: +15.3"), or any scenario where the sign carries meaning. Without showpos, positive numbers have no sign, which is ambiguous in some contexts. Use noshowpos to turn it off and return to the default behavior where positive numbers have no explicit sign.
    // Always show decimal point
    std::cout << std::showpoint;
    std::cout << 100.0 << std::endl;  // 100.000 (shows decimal)
    std::cout << std::noshowpoint;
    std::cout << 100.0 << std::endl;  // 100 (no decimal if not needed)
    
    return 0;
}
The showpoint manipulator forces the decimal point and trailing zeros to always display, even for floating-point values that are exact whole numbers. Without it, 100.0 might print as just "100" (the stream omits the ".0" as unnecessary), which can be confusing because it's unclear whether it's an integer or a float. With showpoint, 100.0 always prints as "100.000" (or however many digits setprecision specifies), making it clear it's a floating-point value. This is useful in scientific contexts where the distinction matters. Use noshowpoint to return to the default compact behavior.

Flushing Output

Stream output is normally buffered for efficiency - text accumulates in memory before being written to the destination. Flushing forces immediate output by emptying the buffer. Use endl to add a newline AND flush, or flush alone when you need immediate output without a newline. Flushing is essential for progress indicators, prompts, and critical messages, but overuse can impact performance since each flush is a system call.

#include <iostream>

int main() {
    // endl flushes the buffer AND adds newline
    std::cout << "Line 1" << std::endl;
The endl manipulator performs two distinct operations: it inserts a newline character ('\n') into the stream AND it flushes the output buffer, forcing all buffered text to be written immediately to the destination (console, file, etc.). The flush operation ensures the text appears right away rather than waiting for the buffer to fill. This makes endl safer for error messages and critical output, but also slower than just '\n'. For most line-ending needs, use '\n' (which is faster); reserve endl for when you need guaranteed immediate output, like before a long computation or crash.
    // flush only flushes, no newline
    std::cout << "Waiting..." << std::flush;
The flush manipulator forces the stream to write all buffered output immediately without adding any characters (unlike endl which adds a newline). This is perfect for scenarios where you want text to appear on the current line without moving to a new line - for example, prompts like "Enter password: " where the cursor should stay on the same line, or progress indicators. Without flush, the text might not appear until the buffer fills (typically 8KB) or a newline is written. Use flush when immediate visibility matters more than performance.
    // For debugging - output appears immediately
    for (int i = 0; i < 5; i++) {
        std::cout << "." << std::flush;
        // simulate work...
    }
    std::cout << " Done!" << std::endl;
    
    return 0;
}
Use flush in loops for progress indicators where you want each dot, percentage, or symbol to appear immediately as it's generated, giving users real-time feedback. Without flush, the output is buffered and all dots might appear together in a burst when the buffer fills or the program ends, defeating the purpose of a progress indicator. Be aware that excessive flushing (like in a tight loop processing millions of items) can significantly hurt performance because each flush is a system call. Balance responsiveness with performance - flush every N iterations, not every single iteration, for large-scale operations.

Practice: Stream Manipulators

Given:

int colorCode = 255;

Task: Display the color code in uppercase hexadecimal with "0x" prefix.

Expected output: 0xFF

Hint: Use std::hex, std::uppercase, and std::showbase.

Show Solution
#include <iostream>

int main() {
    int colorCode = 255;
    
    std::cout << std::hex << std::uppercase << std::showbase 
              << colorCode << std::endl;
    // Output: 0XFF
    
    // Reset to decimal
    std::cout << std::dec << std::nouppercase << std::noshowbase;
    
    return 0;
}

Given:

bool isActive = true;
bool isAdmin = false;

Task: Display these booleans as "true"/"false" text instead of 1/0.

Expected output:

Active: true
Admin: false

Hint: Use std::boolalpha manipulator.

Show Solution
#include <iostream>

int main() {
    bool isActive = true;
    bool isAdmin = false;
    
    std::cout << std::boolalpha;
    std::cout << "Active: " << isActive << std::endl;
    std::cout << "Admin: " << isAdmin << std::endl;
    
    // Reset to numeric display
    std::cout << std::noboolalpha;
    
    return 0;
}

Given:

int change1 = 50;
int change2 = -30;
int change3 = 0;

Task: Display each number with explicit sign (+ for positive, - for negative).

Expected output:

Change: +50
Change: -30
Change: +0

Hint: Use std::showpos to always display the sign.

Show Solution
#include <iostream>

int main() {
    int change1 = 50;
    int change2 = -30;
    int change3 = 0;
    
    std::cout << std::showpos;
    std::cout << "Change: " << change1 << std::endl;
    std::cout << "Change: " << change2 << std::endl;
    std::cout << "Change: " << change3 << std::endl;
    
    std::cout << std::noshowpos;  // Reset
    
    return 0;
}

Given:

int values[] = {10, 20, 30};

Task: Display each array element with its memory address in uppercase hex format (8 characters wide, zero-padded).

Expected output (addresses will vary):

[0x00C5F8A0] = 10
[0x00C5F8A4] = 20
[0x00C5F8A8] = 30

Hint: Cast pointer to uintptr_t and use hex, uppercase, setfill('0'), setw(8).

Show Solution
#include <iostream>
#include <iomanip>
#include <cstdint>

int main() {
    int values[] = {10, 20, 30};
    
    for (int i = 0; i < 3; i++) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(&values[i]);
        
        std::cout << "[0x" 
                  << std::hex << std::uppercase 
                  << std::setfill('0') << std::setw(8) 
                  << addr 
                  << "] = " 
                  << std::dec << values[i] 
                  << std::endl;
    }
    
    // Reset manipulators
    std::cout << std::setfill(' ') << std::nouppercase;
    
    return 0;
}
05

Custom Stream Operators

One of C++'s most powerful features is the ability to define custom stream operators for your own types. This lets you seamlessly integrate your classes with the stream I/O system, making them as easy to print and read as built-in types.

Overloading the Output Operator

To make your class work with cout and other output streams, you overload the << operator. The convention is to make it a friend function that returns a reference to the stream.

#include <iostream>
#include <string>

class Person {
private:
    std::string name;
    int age;
Define a simple Person class with two private data members: name (string) and age (integer). The members are private following encapsulation principles - external code shouldn't directly access them. However, to print these private members using cout, we need special access. We'll overload the stream insertion operator (<<) as a friend function, which is the standard pattern for making custom types work seamlessly with C++ streams. This lets Person objects be printed just like built-in types, maintaining the intuitive cout << person syntax everyone expects.
public:
    Person(const std::string& n, int a) : name(n), age(a) {}
The constructor uses an initializer list (the colon syntax) to set member variables name(n) and age(a) directly during object construction. Initializer lists are more efficient than assignment in the constructor body because they initialize members directly rather than default-constructing them first and then assigning. For string members especially, this avoids an extra copy operation. This constructor takes a const std::string& reference to avoid copying the string argument unnecessarily - another performance optimization that's considered best practice in modern C++.
    // Friend function for output
    friend std::ostream& operator<<(std::ostream& os, const Person& p) {
        os << "Person{name=\"" << p.name << "\", age=" << p.age << "}";
        return os;
    }
};
The friend function declaration allows this non-member function to access the private members of Person. It takes a reference to std::ostream (not specifically cout) as the first parameter, making it work with ANY output stream - cout, cerr, ofstream, ostringstream, etc. The Person parameter is const because printing shouldn't modify the object. Crucially, the function returns ostream& (a reference to the stream), enabling chaining like cout << p1 << p2. This signature is the standard pattern for output operator overloading in C++.
int main() {
    Person alice("Alice", 30);
    Person bob("Bob", 25);
    
    // Now works like any built-in type!
    std::cout << alice << std::endl;
    std::cout << "Meeting: " << alice << " and " << bob << std::endl;
    
    return 0;
}
With the operator<< overload defined, Person objects can now be streamed just like integers, strings, or any built-in type. The syntax cout << alice looks natural and intuitive, matching how you'd print any other value. The second line demonstrates chaining: cout << "text" << alice << " and" << bob works seamlessly because each << operation returns the stream reference, allowing the next operation. This is the power of operator overloading - your custom types integrate perfectly into the language's existing idioms, making code more readable and maintainable.

Overloading the Input Operator

Overloading the input operator (>>) allows custom classes to be read from input streams just like built-in types. This operator must modify the object being read into, so it takes a non-const reference to the class instance. It reads data from the stream in a specific format, extracts the values, and stores them in the object's member variables. Like the output operator, it returns a reference to the stream to enable chaining multiple input operations. This integration makes custom types work seamlessly with cin, file streams, and string streams, providing a consistent and intuitive interface for reading data.

#include <iostream>
#include <string>

class Point {
private:
    double x, y;
    
public:
    Point() : x(0), y(0) {}
    Point(double x, double y) : x(x), y(y) {}
The Point class represents a 2D point with x and y coordinates, both private. It provides two constructors: a default constructor that initializes to the origin (0, 0) using an initializer list, and a parameterized constructor that accepts specific coordinates. The default constructor is crucial for declarations like Point p; without arguments, and for creating arrays of points. To make this class fully integrated with C++ streams, we'll overload both input (>>) and output (<<) operators, allowing points to be both printed to and read from streams naturally.
    // Output operator
    friend std::ostream& operator<<(std::ostream& os, const Point& p) {
        os << "(" << p.x << ", " << p.y << ")";
        return os;
    }
The output operator formats the Point in a standard mathematical notation: (x, y) with parentheses and comma separation. The parameter is const Point& because output operations should never modify the object being printed - it's read-only. Using a const reference also allows printing temporary/rvalue Point objects like cout << Point(5, 10). The function returns ostream& to enable chaining, so you can write cout << p1 << " to " << p2 << endl. This formatting makes the output immediately recognizable as coordinate pairs, matching mathematical convention.
    // Input operator - expects format: x y
    friend std::istream& operator>>(std::istream& is, Point& p) {
        is >> p.x >> p.y;
        return is;
    }
};
The input operator reads two space-separated numeric values from the stream into the Point's x and y coordinates using the extraction operator >> twice. It takes a non-const Point& reference because it needs to modify the object's state (writing the input values). The stream parameter is also non-const because input operations modify the stream's internal state (position, flags). Returning istream& enables chaining like cin >> p1 >> p2. This expects whitespace-separated input like "3.5 4.2" - the stream automatically skips whitespace between values.
int main() {
    Point p1(3.5, 4.2);
    std::cout << "Point 1: " << p1 << std::endl;
    
    std::cout << "Enter a point (x y): ";
    Point p2;
    std::cin >> p2;
    std::cout << "You entered: " << p2 << std::endl;
    
    return 0;
}
Both operators work seamlessly together: output with << prints in the format "(x, y)", and input with >> reads space-separated coordinates directly into a Point object. The user types something like "3.5 4.2" (or "3.54.2") and the Point automatically extracts both values. This makes Point behave exactly like a built-in type for I/O purposes. You could even use these operators with file streams (filestream >> point) or string streams (istringstream("1.5 2.5") >> point), not just console I/O. This is the power of generic programming - write once, works everywhere.

Complete Example: Date Class

This comprehensive example demonstrates a complete implementation of both input and output stream operators for a Date class. The Date class showcases advanced formatting techniques using manipulators like setfill() and setw() to produce standardized ISO 8601 date format (YYYY-MM-DD). The input operator includes format validation by checking for the expected dash separators and setting the stream's fail state if the format is incorrect. This example illustrates how stream operators can implement robust, user-friendly I/O for complex data types, with proper error handling and formatting control that matches industry standards for date representation.

#include <iostream>
#include <iomanip>
#include <sstream>

class Date {
private:
    int year, month, day;
    
public:
    Date(int y = 2000, int m = 1, int d = 1) : year(y), month(m), day(d) {}
The Date class constructor uses default parameter values: y = 2000, m = 1, d = 1, making all three parameters optional. This means you can create dates in multiple ways: Date() creates January 1, 2000; Date(2025) creates January 1, 2025; Date(2025, 12) creates December 1, 2025; and Date(2025, 12, 25) creates December 25, 2025. Default arguments work from right to left - you can omit trailing parameters but not middle ones. This flexibility is very user-friendly and reduces the need for multiple constructor overloads.
    // Output: YYYY-MM-DD format
    friend std::ostream& operator<<(std::ostream& os, const Date& d) {
        os << d.year << "-"
           << std::setfill('0') << std::setw(2) << d.month << "-"
           << std::setw(2) << d.day
           << std::setfill(' ');  // Reset fill
        return os;
    }
The output operator formats the date in ISO 8601 standard format: YYYY-MM-DD (e.g., "2026-02-03"). It uses setfill('0') to ensure leading zeros, and setw(2) to guarantee two-digit month and day fields (01-12, 01-31 respectively). The year is printed as-is without width restriction. Critically, after formatting, the function calls setfill(' ') to reset the fill character back to space, preventing this "sticky" manipulator from affecting subsequent output elsewhere in the program. ISO 8601 format is internationally recognized, sorts correctly lexicographically, and is widely used in databases and APIs.
    // Input: expects YYYY-MM-DD
    friend std::istream& operator>>(std::istream& is, Date& d) {
        char dash1, dash2;
        is >> d.year >> dash1 >> d.month >> dash2 >> d.day;
The input operator expects the date in YYYY-MM-DD format and reads it piece by piece using the extraction operator. First it reads the year (an integer), then a dash character into dash1, then the month (integer), then another dash into dash2, and finally the day (integer). The dash characters are read into char variables simply to consume them from the stream - their purpose is to move the stream position past them. If the input doesn't match this format (like "2025/12/25" or "12-25-2025"), the dashes won't match and we can detect the error, which we handle in the next step.
        // Validate format
        if (dash1 != '-' || dash2 != '-') {
            is.setstate(std::ios::failbit);
        }
        
        return is;
    }
};
After reading the values, validate that both separator characters were actually dashes ('-'). If either dash1 or dash2 is not a dash, the input format was incorrect (maybe the user typed "2025/12/25" with slashes, or "20251225" with no separators). In this case, call is.setstate(std::ios::failbit) to manually set the stream's fail flag, signaling to the caller that input failed. The caller can then check with if (cin.fail()) or if (!cin) to detect the error and handle it appropriately - perhaps by displaying an error message and clearing the stream state for retry.
int main() {
    Date today(2026, 2, 3);
    std::cout << "Today: " << today << std::endl;  // 2026-02-03
    
    // Parse from string
    std::istringstream iss("2025-12-25");
    Date christmas;
    iss >> christmas;
    std::cout << "Christmas: " << christmas << std::endl;  // 2025-12-25
    
    return 0;
}
The Date class now has full stream integration: cout << today outputs "2026-02-03" in ISO format, and iss >> christmas parses "2025-12-25" from a string stream directly into a Date object. This works not just with cout and string streams, but with ANY stream - file streams, network streams, etc. The Date class behaves exactly like a built-in type for I/O purposes. You can even do complex operations like cout << "Deadline: " << myDate << " (" << daysRemaining << " days)" with perfect chaining. This is the ideal integration - your custom types work seamlessly with all of C++'s I/O facilities.

Practice: Custom Stream Operators

Given:

class Rectangle {
private:
    int width, height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    // Add friend operator<< here
};

Task: Overload the << operator to display the rectangle as "Rectangle(w x h)".

Expected output: Rectangle(10 x 5)

Hint: Declare the operator as a friend function that returns std::ostream&.

Show Solution
#include <iostream>

class Rectangle {
private:
    int width, height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    
    friend std::ostream& operator<<(std::ostream& os, const Rectangle& r) {
        os << "Rectangle(" << r.width << " x " << r.height << ")";
        return os;
    }
};

int main() {
    Rectangle rect(10, 5);
    std::cout << rect << std::endl;  // Rectangle(10 x 5)
    return 0;
}

Given:

class Temperature {
private:
    double celsius;
public:
    Temperature() : celsius(0) {}
    // Add friend operator>> and operator<< here
};

Task: Overload >> to read temperature (expects format like "25.5C") and << to display it. Set failbit if 'C' is missing.

Expected usage:

// Input: "25.5C"
// Output: 25.5°C

Hint: Read the number, then read a char and validate it's 'C' or 'c'.

Show Solution
#include <iostream>
#include <sstream>

class Temperature {
private:
    double celsius;
public:
    Temperature() : celsius(0) {}
    
    friend std::istream& operator>>(std::istream& is, Temperature& t) {
        char unit;
        is >> t.celsius >> unit;
        if (unit != 'C' && unit != 'c') {
            is.setstate(std::ios::failbit);
        }
        return is;
    }
    
    friend std::ostream& operator<<(std::ostream& os, const Temperature& t) {
        os << t.celsius << "°C";
        return os;
    }
};

int main() {
    std::istringstream input("25.5C");
    Temperature temp;
    
    if (input >> temp) {
        std::cout << temp << std::endl;  // 25.5°C
    } else {
        std::cout << "Invalid format" << std::endl;
    }
    
    return 0;
}

Given:

class Point3D {
private:
    double x, y, z;
public:
    Point3D(double x=0, double y=0, double z=0) : x(x), y(y), z(z) {}
};

Task: Overload both << and >> operators so they can be chained: cout << p1 << " to " << p2 and cin >> p1 >> p2.

Expected format: (x, y, z) for output, space-separated x y z for input.

Hint: Return stream reference to enable chaining.

Show Solution
#include <iostream>
#include <sstream>

class Point3D {
private:
    double x, y, z;
public:
    Point3D(double x=0, double y=0, double z=0) : x(x), y(y), z(z) {}
    
    friend std::ostream& operator<<(std::ostream& os, const Point3D& p) {
        os << "(" << p.x << ", " << p.y << ", " << p.z << ")";
        return os;  // Enable chaining
    }
    
    friend std::istream& operator>>(std::istream& is, Point3D& p) {
        is >> p.x >> p.y >> p.z;
        return is;  // Enable chaining
    }
};

int main() {
    std::istringstream input("1.0 2.0 3.0 4.0 5.0 6.0");
    Point3D p1, p2;
    
    input >> p1 >> p2;  // Chained input
    std::cout << p1 << " to " << p2 << std::endl;  // Chained output
    // Output: (1, 2, 3) to (4, 5, 6)
    
    return 0;
}

Given:

class Time {
private:
    int hours, minutes, seconds;
public:
    Time(int h=0, int m=0, int s=0) : hours(h), minutes(m), seconds(s) {}
};

Task: Overload << to display in "HH:MM:SS" format (zero-padded) and >> to read "HH:MM:SS" format with colon validation.

Expected:

// Input: "09:05:30"
// Output: 09:05:30

Hint: Use setfill('0') and setw(2) for output. Read colons into char variables and validate.

Show Solution
#include <iostream>
#include <iomanip>
#include <sstream>

class Time {
private:
    int hours, minutes, seconds;
public:
    Time(int h=0, int m=0, int s=0) : hours(h), minutes(m), seconds(s) {}
    
    friend std::ostream& operator<<(std::ostream& os, const Time& t) {
        os << std::setfill('0') 
           << std::setw(2) << t.hours << ":"
           << std::setw(2) << t.minutes << ":"
           << std::setw(2) << t.seconds
           << std::setfill(' ');  // Reset
        return os;
    }
    
    friend std::istream& operator>>(std::istream& is, Time& t) {
        char colon1, colon2;
        is >> t.hours >> colon1 >> t.minutes >> colon2 >> t.seconds;
        
        if (colon1 != ':' || colon2 != ':') {
            is.setstate(std::ios::failbit);
        }
        return is;
    }
};

int main() {
    std::istringstream input("09:05:30");
    Time t;
    
    if (input >> t) {
        std::cout << t << std::endl;  // 09:05:30
    } else {
        std::cout << "Invalid time format" << std::endl;
    }
    
    return 0;
}
06

Key Takeaways

String Streams

Use stringstream for in-memory I/O, istringstream for parsing, and ostringstream for building strings.

Check Stream States

Always verify stream status with good(), fail(), or boolean conversion. Use clear() to reset errors.

setw is Temporary

setw() only affects the next output item. Other manipulators like setprecision() and setfill() are sticky.

fixed for Decimals

Use fixed with setprecision() to control decimal places. Without fixed, it controls significant digits.

Custom Operators

Overload << and >> as friend functions to integrate your classes with the stream system.

Reset Manipulators

Always reset hex/oct to dec, and custom setfill() back to space to avoid surprises.

Knowledge Check

Quick Quiz

Test what you've learned about C++ stream operations

1 Which header is needed for string streams?
2 What does stream.clear() do?
3 Which manipulator only affects the next output item?
4 What is the result of: std::cout << std::hex << 255?
5 How do you make a custom class work with cout?
6 What does std::fixed do with setprecision(2)?
Answer all questions to check your score