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/fstream with 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 like cout does.
  • The buffer is flushed to disk automatically when it fills, when endl or flush is 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) or if (file.is_open())) before using it.
  • Use the extraction/read call itself as the loop condition (while (file >> x) or while (getline(file, line))) instead of eof().
  • 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::app when you want to add data without destroying what’s already in the file, and plain ios::out (the default) when you intentionally want to overwrite it.
  • Use ios::binary when 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.txt each time it runs (so the file grows with every run instead of being overwritten).
  • Write a program that reads log.txt from 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> provides ofstream (write), ifstream (read), and fstream (both), all built on the same stream interface as cin/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, and ios::binary control 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.