AlgoMaster Logo

File Streams

Medium Priority36 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

C++ talks to files through the same <iostream> machinery it uses for the console. A file on disk is exposed as a stream of characters, and the << and >> operators work on it the same way they work on std::cout and std::cin. The <fstream> header is what makes that work: it provides three classes (std::ifstream, std::ofstream, std::fstream) that connect a stream interface to a file on disk. This lesson covers the header, the three classes, how they fit into the broader iostream hierarchy, the open/close lifecycle, the stream state flags, error-handling patterns, and how RAII ties it all together.

The fstream Library

Everything in this lesson lives behind a single include:

The header declares three concrete file-stream classes and pulls in the supporting types from <iostream> automatically. There's no separate library to link on most compilers; <fstream> is part of the C++ standard library that comes with the toolchain.

The three classes split the work by direction. std::ifstream is for input (reading from a file). std::ofstream is for output (writing to a file). std::fstream does both. Each one is a thin layer that takes the operating system's file handle and wraps it in the stream interface the language already uses for the console.

ClassDirectionDefault open mode
std::ifstreamInput (read)std::ios::in
std::ofstreamOutput (write)std::ios::out | std::ios::trunc
std::fstreamBothstd::ios::in | std::ios::out

The default open modes are worth paying attention to. Constructing a std::ofstream over an existing file truncates the file: the old contents are gone before any new write happens. That's almost always what you want for writing a fresh report, and almost never what you want when the goal is to append. For now, the default is fine: read-only for ifstream, write-and-truncate for ofstream, read-and-write for fstream.

Output:

(The file products.txt is written first, then read back. The first record's two-word name reads as two tokens because >> splits on whitespace; getline handles line-oriented reads.)

The same << and >> operators that work on std::cout and std::cin also work on std::ofstream and std::ifstream. That's not an accident; it's the whole point of the stream library. The program also never calls delete or any explicit cleanup. The ofstream and ifstream destructors close their files automatically when the objects go out of scope. The explicit close() after writing is optional, used here only to make the read sequence easier to follow.

The Stream Class Hierarchy

The reason << works the same way on the console as it does on a file is that both are subclasses of the same abstract type. The class hierarchy underneath <fstream> is small, and it explains where the shared functionality lives.

The diagram shows the inheritance chain from the abstract base classes down to the concrete file-stream types. The cyan boxes at the top are foundation classes: std::ios_base holds the format options (precision, hex vs decimal, width) and std::ios adds the state flags (good, bad, fail, eof) and a pointer to the underlying buffer. The orange middle layer is std::istream and std::ostream, which add the actual >> and << operators along with helpers like getline and flush. std::iostream inherits from both. The green concrete types in <fstream> extend those by knowing how to open a file: std::ifstream from std::istream, std::ofstream from std::ostream, std::fstream from std::iostream.

The same picture applies to std::cin, std::cout, and std::cerr. They are global objects of types std::istream and std::ostream, hooked up to the standard input and standard output of the process at program startup. A function that takes std::istream& accepts both std::cin and an std::ifstream, because the file stream inherits from std::istream. This is why functions that parse input can be written once and used with both sources.

Output:

The function readPrices doesn't know or care that the data came from a file. It takes a reference to the base class and works with whatever subclass shows up at runtime. That's polymorphism doing useful work: one function, multiple sources. The same function would work with std::cin if a user typed prices from the keyboard.

Quick Check: Which standard library class is the most-derived input file-stream type?

  • A) std::istream
  • B) std::ifstream
  • C) std::fstream

<details> <summary>Answer</summary>

B. std::ifstream is the concrete file-input class. It inherits from std::istream (the general input stream) and adds file-specific behaviour like open(), close(), and is_open(). std::fstream does both input and output, but it's not specifically the input one.

</details>

Opening Files

There are two ways to associate a file-stream object with an actual file on disk. The constructor takes a filename and opens the file immediately. The open() method does the same thing on an already-constructed stream. Both leave the stream in a known state afterwards, success or failure.

is_open() returns true if the underlying OS call succeeded and the stream is now associated with a real file. It returns false for any of the reasons opening can fail: the file doesn't exist (for input), the path is wrong, the process lacks permission, the disk is full (for output), the path is a directory. The constructor doesn't throw on failure; it leaves the stream in a fail state.

The two methods do the same job, so the constructor form is preferred when the filename is known. Use open() only when the filename comes from later code: a function argument, a config setting, a user prompt.

The filename here isn't known until the user types it, so the two-step form (default-construct, then open) reads cleanly. The same code could go in a function that takes the filename as a parameter, in which case the constructor form would work just as well.

A second overload of both the constructor and open() takes an open mode as a second argument. The default is whichever mode the class uses by default (read for ifstream, write+truncate for ofstream, read+write for fstream). For this chapter, the defaults are fine.

The Stream State Flags

Every stream carries four state flags that describe what happened on the most recent operation. They live in std::ios (the second box from the top of the hierarchy diagram), which means all stream types share them.

FlagMember functionMeaning
goodbits.good()No problems. The stream is ready for more I/O.
eofbits.eof()End-of-file was reached on the last read.
failbits.fail()The last operation didn't produce the expected value (parse error, EOF during a read, failed open).
badbits.bad()The stream is corrupted (rare: hardware I/O error).

The flags are not mutually exclusive. After a >> that runs into EOF mid-token, both eofbit and failbit are set. After a clean read that happens to be the last one, only eofbit is set (and even that depends on whether the read consumed the terminating whitespace).

Output:

After the loop, both eof and fail are set. The reason: the loop kept reading until >> couldn't extract another integer. That last failed read set failbit to mark the failure, and eofbit to mark why (the stream ran out of data). If the file had contained 10 20 banana 30, the loop would have exited on banana with failbit set but eofbit not set, because there was still data left to read; the data was the wrong type.

The class also supports a contextual conversion to bool (technically to void* in C++98 and to bool in C++11+). That's what makes while (in >> n) work. The expression in >> n returns a reference to the stream, and the stream is true exactly when neither failbit nor badbit is set. eofbit on its own does not make the stream evaluate to false; only a failed operation does.

This is the idiomatic check after opening: if (!in). It's equivalent to if (in.fail() || in.bad()). The opposite, if (in), evaluates to true only when the stream is in a good state. Use whichever reads more clearly.

Quick Check: What does this program print?

  • A) true
  • B) false
  • C) The program throws an exception.

<details> <summary>Answer</summary>

B. Opening a non-existent file does not throw; instead, the stream's failbit is set. The contextual bool conversion returns false because the stream is in a fail state.

</details>

Error Handling Patterns

Streams default to silent failure mode. An operation that can't succeed sets a flag and does nothing; it does not throw. That's a deliberate design choice (exceptions on every parse failure would be too heavy), but it puts the responsibility for checking on the programmer.

Three patterns cover almost every case.

Pattern 1: check after open. Always check is_open() (or the falsy conversion) right after a file is opened. If the open failed, return or take a fallback path immediately; don't try to read or write from a broken stream.

Pattern 2: check the read in the loop condition. The canonical loop is while (stream >> value) or while (std::getline(stream, line)). Both put the read in the condition, so the loop exits on the first failure (whether it's EOF or a parse error). This is the correct pattern and is much simpler than checking eof() separately.

Pattern 3: distinguish EOF from a parse error after the loop. Once the loop exits, the stream's flags tell you why. eof() true means the data ran out cleanly. fail() true with eof() false means the data was malformed. Both can be set if EOF was hit during a read that was also a parse error.

Output:

A fourth, less-used option is to turn on exceptions. The exceptions() member function takes a bitmask of state flags that should cause the stream to throw std::ios_base::failure when set. This is off by default. Turning it on can be useful when wrapping file I/O in a layer that wants to use exceptions for error handling, but most everyday code prefers the explicit-check patterns above.

Output (g++ on Linux, message wording varies):

With exceptions enabled on failbit, the failed open() throws instead of leaving the stream in a quiet fail state. The exception type is std::ios_base::failure, which inherits from std::system_error and ultimately from std::exception. The wording of what() is implementation-defined.

There's a trade-off. Exceptions make code shorter at the call site but can fire on routine conditions like end-of-file, which is not an error. Most codebases leave stream exceptions off and check flags explicitly. Either approach is valid.

RAII for File Streams

The std::ifstream, std::ofstream, and std::fstream destructors close their underlying files automatically. That's more than a convenience; it's the property that makes file streams safe to use in the presence of exceptions, early returns, and any other early exit from a function.

The function exportReport has no explicit cleanup. Whether the function returns normally, throws an exception, or returns early on a check, the local out variable's destructor runs as the stack unwinds. The destructor flushes any buffered data and closes the file. RAII at work.

The diagram traces the file's lifetime. The cyan box is the constructor opening the file. Writes happen during the green block. The orange diamond is the function's exit, however it happens. Both branches lead to the teal box: the destructor. The destructor flushes the buffer (so the bytes reach disk) and closes the file. No matter what path the function takes, the cleanup is the same.

Comparing this to the C approach (fopen and fclose) shows why RAII matters. In C, every early return or jump out of a function has to be paired with a fclose call, and any path that misses it leaks the file handle. In C++, opening the file once is enough; the destructor handles the rest.

Calling close() explicitly is still legal and sometimes useful: it releases the OS file handle earlier than the destructor would, which matters if the same scope opens many files in sequence. After close(), the stream is detached from the file and can be reopened via open(). The destructor on an already-closed stream is harmless.

That snippet writes to two different files using the same stream object. The close() between them is necessary because a stream can only be associated with one file at a time. Without it, the second open() would fail because the stream is already open.

Quick Check: Which of these reliably closes the file when a function returns or throws?

  • A) An explicit out.close() call at the end of the function.
  • B) Calling out.flush() before returning.
  • C) Letting the local std::ofstream go out of scope.

<details> <summary>Answer</summary>

C. The destructor of std::ofstream closes the file, and the destructor runs whenever the local variable goes out of scope, whether through a normal return or an exception. The explicit close() in A is fine but isn't reliable on its own; if an exception happens before the close() line is reached, the destructor still does the work. flush() writes pending data but doesn't close the file.

</details>

Relation to iostream

The exact same << and >> operators that work on the file streams also work on the global console streams. The reason is the hierarchy: std::ifstream inherits >> from std::istream, the same base class that std::cin is an instance of. The operators are not file-specific or console-specific; they're stream-specific.

This is why utility functions that take std::istream& or std::ostream& work uniformly with files and the console. It's also why std::cout and std::ofstream accept the same manipulators: std::endl, std::setprecision, std::hex, std::fixed, all of them.

The same std::fixed and std::setprecision(2) manipulators apply to both the file and the console. They live in std::ios_base (the topmost class in the hierarchy) and affect formatting on any stream they're applied to.

One thing the console streams have that file streams don't is automatic synchronisation with C's stdio library and a tie between std::cin and std::cout that flushes the output when input is requested. Those features exist to make cout/cin behave nicely as the program's standard channels. File streams skip them, which makes file I/O slightly faster but otherwise identical from the user's point of view.

Mixed reading and writing on a single std::fstream requires a seek between the two directions, because the file pointer is shared. std::fstream exists for cases where read-and-write on the same handle is needed (updating an in-place record in a binary file, for instance).

A larger example that pulls together everything from this lesson: open a file safely, write a small report, then read it back. Both directions use the same class hierarchy and the same operator overloads, and both rely on the destructor for cleanup.

Output:

The two helper functions are mirror images of each other. Both open a file, check for open failure with if (!stream), do their work using stream operators that came from the iostream hierarchy, and let the destructor close the file. The reading function adds a post-loop check on eof() to distinguish a clean end from a parse failure. The names use underscores because >> reads whitespace-separated tokens; getline handles multi-word names.

Interview Questions

Q1: What's the difference between `std::ifstream`, `std::ofstream`, and `std::fstream`?

All three are concrete file-stream classes declared in <fstream>. std::ifstream opens a file for input (reading) and inherits from std::istream, so it provides >> and getline but not <<. std::ofstream opens a file for output (writing), inherits from std::ostream, and provides << and related output functionality. std::fstream does both, inheriting from std::iostream. The default open mode also differs: ifstream opens read-only, ofstream opens write-and-truncate (destroying existing content), and fstream opens for read and write without truncation.

Q2: Why does `std::ifstream` work with the same `>>` operator as `std::cin`?

Both are subclasses of std::istream, which defines the >> operator overloads for the built-in types. The polymorphism here is static: >> is a non-member function that takes std::istream& as its first parameter, and any subclass (including ifstream) can be passed in. The same pattern applies to << and std::ostream. This shared base class is what lets utility functions like a "parse a list of doubles" routine accept any input source, including the console, a file, or a string stream.

Q3: What do the four stream state flags mean, and how are they different from each other?

goodbit is the no-errors state. eofbit is set when a read tries to go past the end of the file. failbit is set when an operation didn't produce the expected value, either because the data wasn't the right format (parse error) or because the stream ran out of data mid-read. badbit is set on a serious problem like a hardware error or a corrupt stream. The key distinction is between eofbit and failbit: end-of-file alone is not a failure, so a clean last read sets only eofbit, while a read that runs into EOF sets both. The contextual bool conversion looks only at failbit and badbit, which is why while (stream >> x) exits at EOF.

Q4: Why don't C++ file streams require an explicit `close()` call?

Because the destructor closes the file. The std::ofstream (and other file-stream) destructor flushes pending writes and releases the underlying OS file handle when the stream object goes out of scope. This is the RAII pattern: the resource (the open file) is bound to the lifetime of an object, so the cleanup happens automatically on any exit path, including exceptions. An explicit close() is allowed and useful for releasing the handle early (for instance, to reopen the same stream on a different file), but it's not required for correctness.

Q5: What's the safest way to check whether a file was opened successfully?

Right after constructing the stream (or calling open()), check the stream's state. The idiomatic way is if (!stream), which is true when failbit or badbit is set, exactly the state that an open failure leaves the stream in. An alternative is if (!stream.is_open()). Both work; the first is more general (it also catches a stream that's in a fail state for other reasons). Either way, the check has to happen, because opening a non-existent file does not throw by default. It leaves the stream in a fail state where every subsequent read or write does nothing.

Exercises

Exercise 1: Write a program that creates a file greetings.txt containing the lines Hello, World, Goodbye (one per line). Then open the file with std::ifstream and print each line with a line number.

Expected Output:

<details> <summary>Solution</summary>

The output file uses the default mode (write + truncate). The input file uses the default mode (read). Both are closed by their destructors.

</details>

Exercise 2: What does this program print?

Expected Output:

<details> <summary>Solution</summary>

A failed open leaves the stream in a fail state: failbit is set, goodbit is cleared, is_open() returns false. eofbit is not set because the stream never reached an end-of-file condition; the file was never opened in the first place.

</details>

Exercise 3: Fix the bug. The program below should write 3 lines to out.txt, but the file ends up empty.

Expected Output (after the fix):

(in the file out.txt)

<details> <summary>Solution</summary>

The original code constructs a new std::ofstream for each line. Every construction opens the file with ios::trunc (the default), which wipes the previous contents. By the time the program ends, only the last line was written, and even that line might be lost because the temporary objects are destroyed immediately. The fix is to construct the stream once and reuse it.

</details>

Exercise 4: Write a function lineCount(const std::string& path) that returns the number of lines in a text file. If the file can't be opened, return -1. Use the function in main to print the line count of a small test file.

Expected Output:

<details> <summary>Solution</summary>

The function uses RAII: the local ifstream closes automatically on return.

</details>

Exercise 5: Predict the output. Will the second open succeed?

Expected Output:

<details> <summary>Solution</summary>

A stream can only be associated with one file at a time. The second open() fails because out is already open. To switch files, call out.close() before calling open() again.

</details>

Exercise 6: Write a function appendLine(const std::string& path, const std::string& line) that uses an std::ofstream opened in append mode (so prior contents are kept). After three calls, the file should contain three lines. Hint: the second argument to the constructor is std::ios::app.

Expected Output (file contents):

<details> <summary>Solution</summary>

std::ios::app opens for writing without truncating: every write goes to the end of the file. The first std::ofstream(std::string("greetings.txt")) resets the file at the start of main so the test is repeatable.

</details>

Exercise 7: What does this program print?

Expected Output:

(and letters.txt contains abc)

<details> <summary>Solution</summary>

writeBytes takes a reference to std::ostream, the base class. Both std::ofstream and std::cout are derived from it, so the same function works on either. The file gets abc and the console also gets abc. Polymorphism through the iostream hierarchy.

</details>

Exercise 8: Modify this program so that a failed open prints an error message instead of producing garbage.

Expected Output:

<details> <summary>Solution</summary>

Two checks: one right after opening, one wrapping the read. Without them, a failed open leaves n uninitialised and the program prints garbage.

</details>

Quiz

File Streams Quiz

10 quizzes