Skip to content
C++

File I/O with std::fstream

Programs that read from and write to files can process data sets far larger than what a user can type interactively, and they can persist results so the next run can pick up where the last one left off. C++ gives you three stream types for this — ifstream for reading, ofstream for writing, and fstream for both — all declared in <fstream>. Once a file stream is open, you use it exactly like std::cin or std::cout: the same << and >> operators, the same getline, the same manipulators. The only new steps are opening the stream and checking whether the open succeeded.

The three file stream types

All three types live in <fstream> and inherit the same formatting and extraction machinery from the base stream classes. The choice between them comes down to what direction data flows:

std::ifstream

Input (read from file)

Opens a file for reading. Think of it as a file-backed std::cin.

std::ofstream

Output (write to file)

Opens a file for writing, creating it if it does not exist and truncating it if it does. Think of it as a file-backed std::cout.

std::fstream

Both (read and write)

Opens a file for both reading and writing. You must supply open-mode flags to specify what you actually want.

#include <fstream>
#include <string>
#include <iostream>

int main() {
    std::ifstream in_file;     // read-only stream (not yet associated with a file)
    std::ofstream out_file;    // write-only stream
    std::fstream  rw_file;     // read-write stream
    // All three: same >> / << / getline interface once open
}

Opening a file and checking for failure

A stream by itself is not connected to anything. Calling .open(filename) is what associates the stream object with an actual file on disk. The filename argument is a string holding a path — relative to the working directory of the program, or an absolute path. If the open fails (file does not exist, wrong permissions, bad path), the stream enters a failed state and every subsequent read or write silently does nothing. This means you must check for failure immediately after opening; there is no exception thrown by default. The idiomatic check is if (stream.fail()) or equivalently if (!stream), because a stream in failed state converts to false.

#include <fstream>
#include <string>
#include <iostream>

int main() {
    std::ifstream in_file;
    in_file.open("data.txt");

    if (in_file.fail()) {          // or: if (!in_file)
        std::cerr << "Error: could not open data.txt\n";
        return 1;
    }

    // safe to read here...
    std::string word;
    in_file >> word;
    std::cout << word << "\n";
    // in_file closes automatically when it goes out of scope
}

You can also construct the stream and open the file in a single step by passing the filename directly to the constructor. The behaviour is identical — the constructor calls .open() for you — so the failure check is still necessary afterwards:

std::ifstream in_file{"data.txt"};   // open in constructor
if (!in_file) { return 1; }          // same check, shorter spell

std::ofstream out_file{"results.txt"};
if (!out_file) { return 1; }

When the stream object is destroyed — at the end of its scope, or when the function it lives in returns — the file is closed automatically. This is RAII in action: the lifetime of the stream object guarantees the lifetime of the file handle. You rarely need to call .close() explicitly; let the destructor do it.

Reading from a file: >>, getline, and .get()

There are three main ways to pull data out of a file stream, and they differ in how much they consume with each call. The extraction operator >> reads one whitespace-delimited token at a time — ideal for structured data where each field is a word, number, or identifier separated by spaces or newlines. getline(stream, string) reads until and including the next newline, storing everything except the newline — ideal for reading complete lines of text or CSV records. .get(ch) reads exactly one character, whitespace included — useful when you need to process every byte, such as when encrypting or counting particular characters.

// Reading structured records with >>
// File: scores.txt
// Alice 95
// Bob   87
// Carol 72

std::ifstream in{"scores.txt"};
std::string name;
int score;
while (in >> name >> score) {      // loop ends when >> fails (EOF or bad data)
    std::cout << name << ": " << score << "\n";
}

// Reading lines with getline
// File: poem.txt  (multiple words per line)
std::ifstream poem{"poem.txt"};
std::string line;
while (std::getline(poem, line)) { // loop ends at EOF
    std::cout << line << "\n";
}

// Reading characters with .get()
std::ifstream raw{"file.bin"};
char ch;
while (raw.get(ch)) {              // processes every byte, including spaces and newlines
    process(ch);
}

All three forms use the stream itself as the loop condition: a stream converts to true while it is in a good state and to false once it reaches end-of-file or encounters a read error. The loop therefore stops naturally when the file is exhausted.

The >> / getline mixing pitfall

One subtle trap catches many beginners: >> reads a token but does not consume the newline character that follows it. That newline stays in the input buffer. If you immediately call getline afterwards, it reads that leftover newline and returns an empty string — not the next meaningful line. The fix is to skip past the remainder of the current line (including the newline) before switching to getline.

// File: inventory.txt
// 42
// Widget A
// 17
// Gadget B

std::ifstream in{"inventory.txt"};
int count;
std::string name;

// WRONG — getline reads the leftover newline after >>
in >> count;
std::getline(in, name);   // name is "" because getline consumed just the newline

// CORRECT — skip the rest of the line before calling getline
in >> count;
std::string remainder;
std::getline(in, remainder);  // discards the newline after the number
std::getline(in, name);       // now reads "Widget A"
std::cout << count << " × " << name << "\n";  // → 42 × Widget A

A practical alternative is to read everything with getline and then parse numbers from the line using std::stoi or std::istringstream. This avoids the mixed-mode problem entirely and gives you more control over malformed lines.

Writing to a file

Writing to an ofstream is identical to writing to std::cout: use << with values, strings, and manipulators. By default, opening an ofstream truncates any existing file at that path to zero length before writing. If you want to append to an existing file rather than overwrite it, pass the std::ios::app flag when opening.

#include <fstream>
#include <iomanip>   // setw, setprecision, fixed, left, right

int main() {
    std::ofstream out{"report.txt"};
    if (!out) { return 1; }

    out << "Name              Score\n";
    out << "-----------------------------\n";
    out << std::left  << std::setw(18) << "Alice"
        << std::right << std::setw(5)  << 95 << "\n";
    out << std::left  << std::setw(18) << "Bob"
        << std::right << std::setw(5)  << 87 << "\n";
    // report.txt is closed automatically here

    // Append to the file instead of overwriting it:
    std::ofstream log{"events.log", std::ios::app};
    log << "[2026-05-24] Program started\n";
}

The formatting manipulators from <iomanip> work the same way on file streams as on std::cout. The most useful ones for producing aligned output are:

ManipulatorEffectSticky?
setw(n)Set minimum field width for the next value onlyNo
setfill(c)Fill character used to pad fields (default: space)Yes
left / rightAlign value within the field widthYes
fixedFloating-point in decimal notationYes
setprecision(n)Number of digits after decimal point (with fixed)Yes
scientificFloating-point in scientific notationYes

“Sticky” means the manipulator stays in effect for all subsequent output on that stream until you explicitly change it. setw is the exception — it resets to zero after each value, so you must repeat it for every field.

Always pass streams by reference

Stream objects track state: the current read position, whether end-of-file has been reached, whether an error has occurred. When you read one token, the stream advances its internal cursor so the next read picks up after it. This state is non-copyable — the stream classes deliberately delete their copy constructors, which means you cannot pass a stream by value to a function. The compiler will refuse. Instead, always pass stream parameters as references: std::ifstream& for read-only functions and std::ofstream& for write-only ones. That way the function works on the original stream object and any changes to its position or error state are visible to the caller.

// Correct — stream passed by reference so the caller's position advances
void read_header(std::ifstream& in) {
    std::string title, date;
    in >> title >> date;
    std::cout << "File: " << title << " (" << date << ")\n";
}

void write_row(std::ofstream& out, const std::string& name, double value) {
    out << std::left << std::setw(20) << name
        << std::right << std::fixed << std::setprecision(2) << value << "\n";
}

int main() {
    std::ifstream in{"data.txt"};
    if (!in) { return 1; }
    read_header(in);             // advances in past the header line

    std::string item; double val;
    while (in >> item >> val)    // continues reading from where read_header left off
        std::cout << item << " " << val << "\n";

    std::ofstream out{"out.txt"};
    write_row(out, "Widget A", 3.14);
    write_row(out, "Gadget B", 99.0);
}

Complete example: reading a data file, writing results

The following program reads a text file of student names and scores, one record per line, and writes a formatted report to a second file. It also prints a summary to the console. This illustrates the complete pattern: open both files, check both opens, process in a loop, and let RAII close both streams on exit.

// grades.txt (input):
//   Alice 95
//   Bob 87
//   Carol 72
//   Dave 91

#include <fstream>
#include <iomanip>
#include <iostream>
#include <string>

int main() {
    std::ifstream in{"grades.txt"};
    if (!in) {
        std::cerr << "Error: cannot open grades.txt\n";
        return 1;
    }

    std::ofstream out{"report.txt"};
    if (!out) {
        std::cerr << "Error: cannot create report.txt\n";
        return 1;
    }

    out << std::left << std::setw(16) << "Name"
        << std::right << std::setw(6)  << "Score" << "\n";
    out << std::string(22, '-') << "\n";

    std::string name;
    int score;
    int total{0}, count{0};

    while (in >> name >> score) {
        out << std::left  << std::setw(16) << name
            << std::right << std::setw(6)  << score << "\n";
        total += score;
        ++count;
    }

    if (count > 0) {
        double average = static_cast<double>(total) / count;
        out << std::string(22, '-') << "\n";
        out << std::left  << std::setw(16) << "Average"
            << std::right << std::fixed << std::setprecision(1)
            << std::setw(6) << average << "\n";
        std::cout << "Processed " << count << " students. "
                  << "Average score: " << average << "\n";
    }

    // in and out close automatically here
    return 0;
}

// report.txt produced:
//   Name            Score
//   ----------------------
//   Alice              95
//   Bob                87
//   Carol              72
//   Dave               91
//   ----------------------
//   Average          86.2

Key rules to remember

Include <fstream> and check for failure immediately after opening

A failed open does not throw by default — it silently puts the stream in a failed state. Every read and write thereafter does nothing. Always test if (!stream) right after open.

Use ifstream for reading, ofstream for writing — fstream when you need both

Choosing the specific type documents intent and prevents accidentally writing to an input stream. Use fstream with explicit ios::in | ios::out flags only when bidirectional access is genuinely needed.

Loop condition on the stream: while (in >> x) or while (getline(in, line))

Both >> and getline return the stream itself, which converts to false at EOF or on error. This gives you a clean, idiomatic loop that stops naturally at end of file.

After >>, skip the rest of the line before calling getline

>> leaves the newline in the buffer. If you switch to getline immediately, it reads an empty string. Consume the remainder of the line with an extra getline before you start reading full lines.

Always pass streams by reference, never by value

Streams are non-copyable by design. Passing by value won't compile. Pass as ifstream& or ofstream& so the function operates on the caller's stream and advances its position.

RAII closes the file — you rarely need to call .close() manually

The stream destructor closes the file when the stream object goes out of scope. Explicitly calling .close() is only necessary if you need to reuse the same object for a different file, or if you need to check for write errors before the destructor runs.

Sign in to track progress