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
- Always check stream states after operations to ensure they succeeded
- Close file streams when done with them
- Use appropriate manipulators for formatting instead of custom parsing
- Handle input errors gracefully by checking stream state and clearing errors
- Use string streams for string parsing and formatting
- Prefer C++ streams over C-style I/O functions for type safety and extensibility
- Be mindful of buffer flushing for interactive applications
Common Stream Pitfalls
-
Mixing cin and getline: After using
cin >>, there’s often a newline left in the buffer thatgetline()will read. Usecin.ignore()to clear it. -
Not checking file open success: Always verify that files opened successfully.
-
Ignoring stream state: Check for errors after operations, especially with file I/O.
-
Forgetting to close files: Always close files when done to free resources and ensure data is written.
-
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.