C++ File Handling
File handling in C++ lets a program read data from and write data to files stored on disk, so information can persist after the program exits. Instead of losing everything in variables once main() returns, you can save results to a file and load them back later. C++ provides this through the <fstream> library, which builds on the same stream concepts used by cin and cout. Once you understand streams, file I/O feels almost identical to console I/O.
Overview / How It Works
C++ treats files as streams of bytes. A stream is an abstraction that lets you push data out (output) or pull data in (input) without worrying about the low-level details of the operating system’s file API. The Standard Library gives you three main stream classes, all declared in <fstream>:
ofstream(output file stream) — used to write to files.ifstream(input file stream) — used to read from files.fstream(file stream) — can both read and write, depending on the mode you open it with.
All three inherit from the same stream hierarchy as cin/cout (specifically from istream and ostream), which is why the << and >> operators, getline(), and boolean checks all work the same way you already know.
When you open a file, the operating system hands your program a file handle and sets up an internal buffer. Writes with << don’t necessarily hit the disk immediately — they’re buffered in memory and flushed (physically written) when the buffer fills up, when you call .flush(), or when the stream is closed. Reads work the other way: the library reads a chunk of the file into a buffer and hands you data from that buffer as you request it, refilling from disk as needed. This buffering is what makes file I/O fast; touching the disk for every single character would be very slow.
Every file stream also keeps track of a file position indicator — essentially a cursor that marks where the next read or write will happen. Reading advances this cursor forward; you can also move it manually with seekg() (for input) and seekp() (for output).
Finally, every stream carries state flags that tell you whether the last operation succeeded: good(), eof(), fail(), and bad(). A stream object also converts implicitly to a boolean, which is true if the stream is in a usable (“good”) state — this is the idiomatic way to check for errors.
Syntax
#include <fstream>
ofstream outFile("filename.txt"); // open for writing (creates/truncates)
ifstream inFile("filename.txt"); // open for reading
fstream file("filename.txt", ios::in | ios::out); // open for both
outFile << data; // write to file
inFile >> data; // read formatted data from file
getline(inFile, line); // read a whole line
outFile.close(); // release the file handle
Key parts explained:
| Element | Meaning |
|---|---|
ofstream / ifstream / fstream |
The stream type: output-only, input-only, or both. |
"filename.txt" |
Path to the file, relative or absolute. |
| mode flags | Second constructor argument controlling how the file is opened (see table below). |
<< / >> |
Insert into / extract from the stream, same as with cout/cin. |
close() |
Flushes any buffered data and releases the OS file handle. |
Common file open modes (found in the ios namespace, combined with the bitwise OR operator |):
| Mode | Effect |
|---|---|
ios::in |
Open for reading (default for ifstream). |
ios::out |
Open for writing (default for ofstream); creates the file if missing, truncates if it exists. |
ios::app |
Append — all writes go to the end of the file, existing content is kept. |
ios::trunc |
Truncate — erase existing content when opening. |
ios::ate |
Open and immediately move the cursor to the end (but writes can still go anywhere). |
ios::binary |
Open in binary mode — no text translation of characters like newlines. |
Examples
Example 1: Writing to a file
#include <iostream>
#include <fstream>
using namespace std;
int main() {
ofstream outFile("scores.txt");
if (!outFile) {
cerr << "Error opening file for writing." << endl;
return 1;
}
outFile << "Alice 92" << endl;
outFile << "Bob 85" << endl;
outFile << "Charlie 78" << endl;
outFile.close();
cout << "Data written to scores.txt" << endl;
return 0;
}
Output:
Data written to scores.txt
Opening an ofstream with just a filename creates the file if it doesn’t exist, or erases its previous contents if it does (this is the default ios::out behavior). The if (!outFile) check relies on the implicit boolean conversion — it becomes true only if the stream failed to open, for example because of a permissions problem or an invalid path.
Example 2: Reading a file line by line
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ofstream outFile("scores.txt");
outFile << "Alice 92\n";
outFile << "Bob 85\n";
outFile << "Charlie 78\n";
outFile.close();
ifstream inFile("scores.txt");
if (!inFile) {
cerr << "Error opening file for reading." << endl;
return 1;
}
string line;
while (getline(inFile, line)) {
cout << "Line: " << line << endl;
}
inFile.close();
return 0;
}
Output:
Line: Alice 92
Line: Bob 85
Line: Charlie 78
getline(inFile, line) reads everything up to (and discarding) the next newline character and returns the stream itself. Because the stream converts to false once reading fails (which happens right after the last line is consumed), the loop stops naturally without an extra empty iteration.
Example 3: Formatted reading and appending
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ofstream outFile("scores.txt");
outFile << "Alice 92\n";
outFile << "Bob 85\n";
outFile << "Charlie 78\n";
outFile.close();
ifstream inFile("scores.txt");
string name;
int score;
int total = 0;
int count = 0;
while (inFile >> name >> score) {
cout << name << " scored " << score << endl;
total += score;
count++;
}
inFile.close();
double average = static_cast<double>(total) / count;
cout << "Average score: " << average << endl;
ofstream appendFile("scores.txt", ios::app);
appendFile << "Average " << average << "\n";
appendFile.close();
return 0;
}
Output:
Alice scored 92
Bob scored 85
Charlie scored 78
Average score: 85
The >> operator skips leading whitespace (including newlines) and stops at the next whitespace character, so alternating name >> score naturally parses each line’s two fields. The loop condition while (inFile >> name >> score) is the idiomatic pattern: it stops as soon as an extraction fails, which happens exactly once, at end of file. Opening a second stream with ios::app then adds a new line to the end of the file without erasing what’s already there.
How It Works Step by Step
- Constructing an
ofstream/ifstream/fstreamwith a filename asks the operating system to open that file, according to the requested mode. If it fails (bad path, missing permissions, disk full for writes), the stream is left in a failed state rather than throwing an exception by default. - Each
<<write appends formatted text into the stream’s internal output buffer, converting numbers and other types to their text representation, just likecoutdoes. - The buffer is flushed to disk automatically when it fills, when
endlorflushis used, or when the stream is closed or destroyed. - Each
>>read pulls characters from the input buffer (refilling it from disk as needed), skips leading whitespace, and stops at the next whitespace or type mismatch, converting the text back into the requested type. - Calling
close()flushes any pending writes and releases the OS file handle. If you never call it, the destructor of the stream object does this automatically when it goes out of scope — but it’s still good practice to close explicitly once you’re done, especially before reopening the same file with another stream.
Common Mistakes
Mistake 1: Not checking whether the file actually opened
If you skip the error check, a failed ifstream just silently returns empty or zeroed data instead of crashing, which can hide bugs for a long time.
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ifstream inFile("does_not_exist.txt");
string data;
inFile >> data;
cout << "Data: " << data << endl;
return 0;
}
Output:
Data:
The file doesn’t exist, so inFile never opens, and the extraction into data just fails silently, leaving data empty. Nothing warns you that the read never happened. Always check the stream before trusting the data it gives you:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ifstream inFile("does_not_exist.txt");
if (!inFile) {
cout << "Error: could not open file." << endl;
return 1;
}
string data;
inFile >> data;
cout << "Data: " << data << endl;
return 0;
}
Output:
Error: could not open file.
Mistake 2: Looping with while (!inFile.eof())
This is one of the most common C++ bugs. The end-of-file flag is only set after a read attempt fails, so checking eof() before reading causes one extra, bogus iteration.
#include <iostream>
#include <fstream>
using namespace std;
int main() {
ofstream outFile("numbers.txt");
outFile << "10 20 30";
outFile.close();
ifstream inFile("numbers.txt");
int num;
while (!inFile.eof()) {
inFile >> num;
cout << num << endl;
}
inFile.close();
return 0;
}
Output:
10
20
30
0
After reading 30, eof() is still false, so the loop runs one more time; the extraction then fails (nothing left to read), sets num to 0 (the standard-mandated behavior since C++11), and that stray 0 gets printed. The fix is to make the extraction itself the loop condition, so the loop stops the moment a read actually fails:
#include <iostream>
#include <fstream>
using namespace std;
int main() {
ofstream outFile("numbers.txt");
outFile << "10 20 30";
outFile.close();
ifstream inFile("numbers.txt");
int num;
while (inFile >> num) {
cout << num << endl;
}
inFile.close();
return 0;
}
Output:
10
20
30
Best Practices
- Always check that a file stream opened successfully (
if (!file)orif (file.is_open())) before using it. - Use the extraction/read call itself as the loop condition (
while (file >> x)orwhile (getline(file, line))) instead ofeof(). - Close files explicitly with
close()once you’re finished, even though destructors do it automatically — this makes resource release predictable and lets you safely reopen the same path. - Use
ios::appwhen you want to add data without destroying what’s already in the file, and plainios::out(the default) when you intentionally want to overwrite it. - Use
ios::binarywhen writing non-text data (raw structs, images, etc.) so the platform doesn’t translate line-ending characters and corrupt the data. - Prefer relative paths carefully — they’re resolved against the program’s current working directory, not the source file’s location, which can surprise beginners.
- Wrap file operations that must not fail silently with clear error messages so problems surface immediately instead of producing quietly wrong output.
Practice Exercises
- Write a program that asks the user for their name and a favorite number, then appends both to a file called
log.txteach time it runs (so the file grows with every run instead of being overwritten). - Write a program that reads
log.txtfrom the previous exercise and prints how many lines it contains. - Write a program that reads a list of integers from a file called
data.txt(one per line) and prints the largest, smallest, and average value. Handle the case where the file doesn’t exist by printing a clear error message instead of crashing.
Summary
<fstream>providesofstream(write),ifstream(read), andfstream(both), all built on the same stream interface ascin/cout.- File streams buffer data in memory and only touch the disk when the buffer fills, on
flush/endl, or when closed — this is what makes file I/O efficient. - Always verify a stream opened correctly before reading or writing — a failed open doesn’t throw by default, it just leaves the stream unusable.
- Loop on the read operation itself, never on
eof(), to avoid the classic duplicate-last-value bug. - Open modes like
ios::app,ios::trunc, andios::binarycontrol exactly how a file is created, overwritten, or interpreted. close()flushes and releases the file handle; it also happens automatically when the stream object is destroyed, but explicit closing keeps behavior predictable.
