Files & Streams
Reviewed & published by Brayan K
By the end of this lesson you'll be able to write data to a file, read it back line by line or token by token, choose the right file mode, and parse text in memory with std::stringstream — the same stream skills power files, the console, and strings in C++.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Write text files with std::ofstream and the << operator
- Read files back with std::ifstream, >>, and std::getline
- Use std::fstream and file modes (ios::app, ios::binary)
- Always check is_open() and detect end-of-file (EOF) correctly
- Parse strings in memory with std::stringstream
- Close files properly and rely on RAII
💡 Real-World Analogy
Think of a stream as a conveyor belt for characters. cout is a belt running out to your screen; cin is a belt running in from the keyboard. A file stream is the exact same belt — it just connects to a file on disk instead. Because every belt works the same way, the << ("push onto the belt") and >> ("take off the belt") operators you already know for the console work unchanged on files and on strings. Learn one, and you've learned all three.
1. Writing a File with ofstream
std::ofstream (output file stream) opens a file for writing. You give it a filename, then push data onto it with << — exactly like cout. The golden rule: always check is_open() first, because a wrong path or a read-only folder fails silently otherwise. Read this worked example, then run it locally and open scores.txt to see the result.
#include <iostream>
#include <fstream> // file streams live here: ofstream, ifstream, fstream
using namespace std;
int main() {
// ofstream = "output file stream" -> opens a file for WRITING.
// If the file does not exist it is created; if it exists it is OVERWRITTEN.
ofstream out("scores.txt");
// ALWAYS check the file actually opened before you use it.
if (!out.is_open()) {
cout << "Could not open scores.txt for writing" << endl;
return 1; // bail out instead of writing into the void
}
// Use << exactly like cout — the file is just another stream.
out << "Alice 92" << endl; // writes the line + a newline
out << "Bob 87" << endl;
out << "Cara " << 95 << endl; // numbers are converted to text for you
out.close(); // flush the buffer + release the file
cout << "Wrote scores.txt" << endl;
return 0;
}
// ✅ Expected output:
// Wrote scores.txt
//
// The file it just wrote, scores.txt, now contains:
// Alice 92
// Bob 87
// Cara 952. Reading a File with ifstream
std::ifstream (input file stream) opens a file for reading. You have two tools: std::getline(in, line) grabs a whole line at a time, while in >> value reads one whitespace-separated token and converts its type for you. Both return the stream itself, which becomes "false" at end-of-file (EOF) — so they're perfect loop conditions that stop on their own.
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
// Write the file first, so this snippet runs on its own. Reading a file
// the example never created is the commonest way a "working" sample fails
// the moment somebody actually runs it.
{
ofstream seed("scores.txt");
seed << "Alice 92\n" << "Bob 87\n" << "Cara 95\n";
} // the ofstream closes here, flushing what we wrote
// ifstream = "input file stream" -> opens a file for READING.
ifstream in("scores.txt");
if (!in.is_open()) {
cout << "scores.txt not found" << endl;
return 1;
}
// Pattern 1: read WHOLE LINES with getline.
// getline(stream, line) returns the stream, which is "false" at end-of-file,
// so the loop stops automatically when there is nothing left to read.
string line;
cout << "--- line by line ---" << endl;
while (getline(in, line)) {
cout << line << endl;
}
in.clear(); // clear the end-of-file flag
in.seekg(0); // rewind to the start so we can read again
// Pattern 2: read TOKEN BY TOKEN with >>.
// >> skips whitespace and reads one "word" at a time, converting types for you.
string name;
int score;
cout << "--- as name + number ---" << endl;
while (in >> name >> score) { // stops on EOF or a bad conversion
cout << name << " scored " << score << endl;
}
in.close();
return 0;
}
// ✅ Expected output:
// --- line by line ---
// Alice 92
// Bob 87
// Cara 95
// --- as name + number ---
// Alice scored 92
// Bob scored 87
// Cara scored 953. File Modes: app and binary
By default ofstream overwrites a file. Pass a mode flag as the second argument to change that. std::ios::app means append — new writes go to the end and existing content is kept (ideal for log files). std::ios::binary writes raw bytes with no text formatting, which you need for .write()/.read() on structs. std::fstream opens a file for reading and writing at once.
#include <iostream>
#include <fstream>
using namespace std;
struct Point { int x; int y; }; // a fixed-size record (8 bytes here)
int main() {
// ios::app -> APPEND. Writes go to the END; existing content is kept.
ofstream log("log.txt", ios::app);
log << "user logged in" << endl; // added after whatever was already there
log.close();
// ios::binary -> write RAW BYTES, no text formatting / newline translation.
Point p{3, 4};
ofstream bout("point.bin", ios::binary);
// reinterpret_cast<char*> = "treat these bytes as a char buffer".
bout.write(reinterpret_cast<const char*>(&p), sizeof(Point));
bout.close();
// Read the raw bytes straight back into a struct.
Point q{};
ifstream bin("point.bin", ios::binary);
bin.read(reinterpret_cast<char*>(&q), sizeof(Point));
bin.close();
cout << "Read back: (" << q.x << ", " << q.y << ")" << endl;
return 0;
}
// ✅ Expected output:
// Read back: (3, 4)
//
// Note: ios::binary skips newline translation; reinterpret_cast lets you copy
// the struct's bytes wholesale. (Binary files are not portable across machines.)Heads-up: binary files are not portable across machines — endianness and struct padding differ between architectures. For data you'll share, prefer a text format (CSV, JSON) or a serialization library.
4. Parsing in Memory with stringstream
A std::stringstream is a stream backed by a std::string instead of a file. std::istringstream lets you read a string with getline and >> (perfect for splitting a CSV line into fields); std::ostringstream lets you build a string with << and pull the result out with .str(). Because it's all in memory, it runs anywhere — so the exercises below work right here in the editor.
Your turn. The program below is almost complete — fill in the blanks marked ___ using the // 👉 hints, then run it.
#include <iostream>
#include <sstream> // istringstream = "read a string as if it were a file"
#include <string>
using namespace std;
int main() {
// 🎯 YOUR TURN — fill in the blanks marked ___ then press "Try it Yourself".
// No real file needed: istringstream lets us PRACTISE parsing in memory.
string csv = "Alice,London,30";
// istringstream wraps the string so we can use getline / >> on it.
istringstream iss(csv);
string name, city, ageText;
// getline(stream, target, ',') reads up to the next comma (the delimiter).
// 1) Read the name (up to the first comma)
getline(iss, name, ___); // 👉 the delimiter is a comma in 'single quotes'
// 2) Read the city (up to the next comma)
getline(iss, city, ',');
// 3) Read the rest (no more commas) into ageText
getline(iss, ___); // 👉 the variable that should hold "30"
cout << name << " lives in " << city << " and is " << ageText << endl;
// ✅ Expected output:
// Alice lives in London and is 30
return 0;
}One more. This time you'll read numbers with >> and build a report string with ostringstream. Fill in the two blanks:
#include <iostream>
#include <sstream> // ostringstream builds a string; istringstream reads one
#include <string>
#include <vector>
using namespace std;
int main() {
// 🎯 YOUR TURN — parse some numbers, then BUILD a report string.
string data = "10 25 5"; // three numbers separated by spaces
istringstream in(data);
// Read tokens with >> (it splits on whitespace for you).
int total = 0, n;
vector<int> nums;
while (in >> n) {
nums.push_back(n);
total += n;
}
// ostringstream lets you assemble text with <<, then call .str() for the result.
ostringstream report;
report << "count=" << nums.size();
// 1) Append the total to the report
report << " total=" << ___; // 👉 the variable holding the sum
// 2) Pull the finished string out of the stream
string line = report.___; // 👉 the method that returns the string
cout << line << endl;
// ✅ Expected output:
// count=3 total=40
return 0;
}Common Errors (and the fix)
- Not checking is_open(): if the path is wrong or the folder is read-only, the open fails silently and your writes vanish. Always guard with if (!stream.is_open()) { /* handle it */ } before reading or writing.
- Mixing >> and getline: >> leaves the trailing newline in the buffer, so the next getline returns an empty line. After reading a number with >>, call in.ignore() (or std::ws) before getline.
- Forgetting ios::binary: calling .write()/.read() in text mode lets newline translation corrupt your bytes on Windows. Open with ofstream out(name, ios::binary) for any non-text data.
- Not closing / not relying on RAII: data sits in a buffer until the stream is flushed. If you read the file before the writer's close() (or before it goes out of scope), it can look empty. Call .close() or let the stream's scope end first.
- Looping on !in.eof(): EOF is set after a failed read, so this processes the last line twice. Loop on the read itself instead: while (getline(in, line)) or while (in >> x).
📋 Quick Reference
| Task | Code |
|---|---|
| Open file to write | ofstream out("f.txt"); |
| Open file to read | ifstream in("f.txt"); |
| Read & write | fstream io("f.txt"); |
| Append mode | ofstream(f, ios::app) |
| Binary mode | ifstream(f, ios::binary) |
| Check it opened | if (in.is_open()) |
| Read a line | getline(in, line) |
| Read a token | in >> word |
| Split on a char | getline(ss, f, ',') |
| Build a string | oss << x; oss.str() |
| Close the file | stream.close(); |
Mini-Challenge: Average a Row of Scores
No blanks this time — just a brief and an outline. You'll parse a comma-separated line entirely in memory with istringstream, so it runs right here. Build it, run it, and check your output against the example in the comments.
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: average a row of scores
// The data is a comma-separated line (pretend it came from a CSV file):
string row = "Maths,72,68,90,85";
//
// 1. Wrap "row" in an istringstream.
// 2. getline(..., ',') the first field into a "subject" string.
// 3. Loop getline(..., token, ',') over the rest; convert each with stoi(token),
// add to a running sum and count how many there were.
// 4. Print: "Maths average: 78.75" (sum / count as a double)
//
// Hints: include <sstream>; double avg = (double)sum / count;
//
// ✅ Expected output:
// Maths average: 78.75
// your code here
return 0;
}Pro Tips
- 💡 Prefer the read as your loop test: while (getline(in, line)) beats checking eof() — it never processes a phantom last line.
- 💡 Read a line, then parse it: getline the whole line from the file, then feed it to an istringstream to split the fields. Clean and robust.
- 💡 Let RAII help, but flush on purpose: the stream closes itself at scope end, but call .close() when another reader needs the data now.
🎉 Lesson Complete
- ✅ ofstream writes, ifstream reads, fstream does both — all use << / >>
- ✅ Always check is_open(); loop on the read so EOF stops you cleanly
- ✅ getline grabs whole lines; >> grabs tokens and converts types
- ✅ ios::app appends, ios::binary writes raw bytes
- ✅ stringstream parses and builds text in memory — same stream skills, no file needed
- ✅ Close files (or rely on RAII) so the buffer is flushed
Practice quiz
Which stream type opens a file for WRITING?
- std::ifstream
- std::istringstream
- std::ofstream
- std::cin
Answer: std::ofstream. std::ofstream (output file stream) opens a file for writing; ifstream is for reading.
What should you always do right after opening a file stream?
- Check is_open() before reading or writing
- Call .flush()
- Call .seekg(0)
- Set ios::binary mode
Answer: Check is_open() before reading or writing. A wrong path or read-only folder fails silently, so always guard with is_open() first.
When should you use getline() instead of >> ?
- When you want one whitespace-separated token
- getline() and >> are identical
- Only when reading numbers
- When you want a whole line, including the spaces inside it
Answer: When you want a whole line, including the spaces inside it. >> reads one token (a word or number); getline() grabs a whole line, and with a delimiter it splits CSV fields.
What does the file mode std::ios::app do?
- Opens the file in binary mode
- Appends new writes to the end, keeping existing content
- Truncates the file to empty
- Opens the file for reading only
Answer: Appends new writes to the end, keeping existing content. ios::app appends: writes go to the end and existing content is kept — ideal for log files.
Why is looping on while (!in.eof()) a common bug?
- EOF is set AFTER a failed read, so it processes the last line twice
- eof() does not exist on ifstream
- It is too slow
- It only works in binary mode
Answer: EOF is set AFTER a failed read, so it processes the last line twice. Loop on the read itself — while (getline(in, line)) or while (in >> x) — so EOF stops you cleanly.
What does std::ios::binary change?
- It compresses the file
- It makes the file read-only
- It turns off text translation so bytes are written exactly as-is
- It appends to the file
Answer: It turns off text translation so bytes are written exactly as-is. Binary mode skips newline translation; you need it for .write()/.read() on structs and other non-text data.
How do you read up to the next comma in a CSV field with getline?
- getline(iss, field, comma)
- getline(iss, field, ',')
- iss >> field >> ','
- getline(iss.comma(), field)
Answer: getline(iss, field, ','). getline(stream, target, ',') reads characters up to the next comma delimiter.
How do you get the finished string out of a std::ostringstream named report?
- report.get()
- report.string()
- report >> result
- report.str()
Answer: report.str(). You build text with << then call .str() to pull the result out of the ostringstream.
Why do the 'Your Turn' exercises in this lesson use std::stringstream instead of real files?
- stringstream is faster than files
- Online compilers often have no writable filesystem, but stringstream works entirely in memory
- Files cannot hold text
- stringstream is required by C++20
Answer: Online compilers often have no writable filesystem, but stringstream works entirely in memory. Sandboxed online compilers may lack a writable filesystem, so in-memory stringstream runs anywhere.
Do you strictly need to call .close() on a file stream?
- Yes, or the file is corrupted
- Only for ifstream, never ofstream
- No — RAII closes it at scope end, but .close() flushes immediately and frees the file for others
- Only in binary mode
Answer: No — RAII closes it at scope end, but .close() flushes immediately and frees the file for others. File streams are RAII, so the destructor closes them; calling close() explicitly flushes the buffer and releases the file now.
Continue this course
- Previous: Memory Allocation Internals: new/delete, allocators & custom allocators
- Next: Exception Safety Levels (Basic, Strong, No-Throw Guarantees) — Design functions with the basic, strong, and no-throw safety guarantees
- Quick reference: C++ cheat sheet
Frequently asked questions
Why does my online program say it can't open or find the file?
Many online compilers (including this one) run in a sandbox with no writable or persistent filesystem, so ofstream may fail and a file you 'wrote' won't exist on the next run. That's exactly why the practice exercises here use std::stringstream, which works entirely in memory. The file examples run perfectly on your own machine — install a compiler like g++ and run them locally.
When should I use getline() instead of >> ?
Use >> to read one whitespace-separated token at a time (a word or a number) with automatic type conversion. Use getline() when you want a whole line, including the spaces inside it — for example a full name or address. getline() also takes an optional delimiter, so getline(s, field, ',') is the easy way to split CSV data.
Do I really need to call .close()?
Not strictly — file streams use RAII, so the destructor closes the file automatically when the stream goes out of scope. But calling .close() explicitly flushes the buffer to disk immediately and frees the file so another part of your program (or another program) can open it. It also makes your intent obvious to the next reader.
What does ios::binary actually change?
It turns off text translation. On Windows, text mode rewrites ' ' as ' ' on write and back on read; binary mode leaves every byte exactly as-is. You need ios::binary whenever you call .write()/.read() with reinterpret_cast on structs or non-text data, otherwise stray byte translation can corrupt it.
Why is istringstream useful if it's not a file?
Because the stream interface is the same. Once you can parse an istringstream you can parse a file — the only difference is where the characters come from. It's also perfect for splitting one line you've already read (with getline) into fields, and for converting text to numbers safely.