C++ File Handling: ifstream, ofstream and Open Modes
How to read and write files in C++ with ifstream, ofstream and fstream: checking the open, truncate vs append modes, RAII closing, and reading a whole file.
- Course: C++ study plan
- Module: Streams and files
- Kind: Lesson
- Reading time: 14 min
- Runtime: Clang 18 · C++20
How do you read and write a file in C++?
In C++ you write a file with std::ofstream and read one with std::ifstream, both from <fstream>. Construct the stream with a path to open it, check it with if (!stream), then use <<, >> and std::getline exactly as with std::cout and std::cin. The stream closes the file in its destructor, and a missing input file sets failbit rather than throwing.
Lesson
A file stream is std::cin or std::cout with a file behind the buffer. Everything from lessons 1 and 2 — >>, getline, the state bits, the recovery loop — works unchanged; what is new is opening, the modes that decide whether a file is truncated or appended to, the fact that a stream object is the open handle (so its destructor closes the file), and a handful of failure modes that a console program never meets: the file is missing, the directory does not exist, the data was written but never flushed before another stream tried to read it. The study judge lets a program create, write and read files in its working directory, so this lesson's programs write a file and read it straight back.
The three classes
<fstream> declares std::ofstream (output), std::ifstream (input) and std::fstream (both). The constructor takes a path — a std::string, a C string or a std::filesystem::path (lesson 5) — and opens it at once:
#include <fstream>
#include <iostream>
#include <string>
int main() {
{
std::ofstream out("notes.txt");
if (!out) { std::cout << "cannot create notes.txt\n"; return 0; }
out << "first line\n" << "second line\n";
} // out is destroyed here: flushed and closed
std::ifstream in("notes.txt");
std::string line;
int count = 0;
while (std::getline(in, line)) ++count;
std::cout << count << " lines\n";
return 0;
}
A path is resolved against the process's working directory — where the program was run from, not where the source file lives. "notes.txt" in the judge is a file beside the compiled program; the same string on your machine is a file in whatever directory your terminal was in.
Did it open?
The one check every file program needs. is_open() reports the handle; operator bool (that is, !fail()) reports the same thing right after construction and is the idiom, because it also catches a later failure. An ifstream for a file that does not exist does not throw — it sets failbit and every read silently fails, which is how a program prints 0 lines for a typo in the filename. An ofstream fails when the directory does not exist or is not writable; it creates the file when only the file is missing.
Open modes
The second constructor argument is a bit mask from std::ios:
| Mode | Meaning |
|---|---|
in | open for reading (the default for ifstream) |
out | open for writing; on its own, truncates to empty (the default for ofstream) |
app | append: every write goes to the end, whatever the position |
trunc | discard existing contents (implied by out alone) |
ate | start positioned at the end, but later writes may seek elsewhere |
binary | no newline translation (lesson 5) |
Combine them with |. The three that matter in practice: std::ofstream out("log.txt") starts a fresh file every run; std::ofstream out("log.txt", std::ios::app) keeps what is there and adds to it — a second open with app continues after what the first open wrote, which is how a ledger accumulates across runs; and std::fstream io("data.txt", std::ios::in | std::ios::out) opens for both directions without truncating, but fails if the file does not already exist — add std::ios::trunc or create it first.
RAII: the object is the open file
A file stream closes itself in its destructor. That is why the writing code above sits in its own block: when out goes out of scope, its buffer is flushed and the descriptor released before in opens the same file. Without that block the write might still be sitting in out's buffer when in reads, and the count comes back short. The explicit calls exist for the cases the scope cannot express — out.close() to finish a file and then reopen the same object with out.open("other.txt"), or out.flush() to push the bytes without closing — but the scope is the idiom, exactly as Module 7 lesson 3 described: the resource's lifetime is the object's lifetime, and an early return or an exception still closes the file.
Writes are not on disk until flushed. std::endl flushes after every line, which is the reason it is slow in loops and also the reason logs written with it survive a crash; a judged program writes with '\n' and lets the destructor flush once.
Reading a file
Every reading pattern from lesson 2 applies, with in in place of std::cin: while (std::getline(in, line)) for lines, while (in >> x) for tokens, in >> n; in.ignore(...) before switching to lines. Two whole-file idioms are worth knowing:
std::ifstream in("notes.txt");
std::string all((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>()); // the whole file
The extra parentheses around the first argument stop the compiler reading the line as a function declaration (the "most vexing parse"). For a file too large to hold, stream it line by line instead — a 2 GB log does not need 2 GB of memory to be counted.
The position is queryable: out.tellp() is how many bytes have been written so far, in.tellg() where the next read starts, and in.seekg(0) rewinds — the basis of lesson 5's record file. A read that ran off the end leaves the stream with eofbit and failbit set, so to read a file twice you clear() before the seekg.
Deleting and checking
A program that writes a scratch file should remove it: std::remove("notes.txt") from <cstdio> returns 0 on success, or std::filesystem::remove("notes.txt") (lesson 5) returns whether anything was deleted. Closing the stream first is essential — on some platforms an open file cannot be removed, and on all of them a buffered write may not have happened yet.
Error handling in a file program
The stream never throws unless asked (exceptions(), lesson 1). The checkable moments are: after opening (if (!out)), after the reading loop (in.bad() distinguishes a disk error from a plain end of file), and after closing (out.close(); if (out.fail()) — the flush can fail on a full disk). A judged program that reads its own freshly written file will not hit these, but a tool that does not check its open is one typo away from computing statistics on an empty stream and reporting them with confidence.
Pitfalls
| Mistake | What happens |
|---|---|
| Not checking the open | Every read fails silently; the program reports zeros. |
| Reading a file another stream is still writing | The writer's buffer has not been flushed: the reader sees a short file. Close or scope the writer first. |
std::ofstream out("f.txt") to add to a file | The default truncates. Use std::ios::app. |
std::ios::in | std::ios::out on a missing file | The open fails; fstream does not create it. |
| A relative path "to the source file" | Paths are relative to the working directory. |
std::endl on every line of a big file | A flush per line; use '\n' and let the close flush. |
Key takeaways
std::ofstream,std::ifstreamandstd::fstreamarecout/cinwith a file behind the buffer; every reading pattern carries over.- Check the open with
if (!stream); a missing input file is a silentfailbit, not an exception. outtruncates,appappends across opens,in | outneeds an existing file,binarydisables newline translation.- The destructor flushes and closes: scope the writer so it finishes before the reader opens the same file.
- Paths are relative to the working directory; remove scratch files after closing them.
Common questions
How do I append to a file in C++?
Open it in append mode: std::ofstream out("log.txt", std::ios::app); and every write goes to the end of the file. The default mode of std::ofstream is out, which truncates the file to empty each time it is opened.
How do I check if a file opened successfully in C++?
Test the stream: if (!in) right after construction, or in.is_open(). An ifstream for a missing file does not throw; it sets failbit and every read then fails silently, so an unchecked open usually shows up as a program that reports zero lines.
Do I need to close a file in C++?
Usually not: a file stream flushes and closes itself in its destructor, so giving it its own scope is the idiom, and an early return or exception still closes it. Call close() explicitly to reuse the object, to finish a file before another stream reads it in the same scope, or to check that the final flush succeeded.
How do I read a whole file into a string in C++?
Construct a string from stream-buffer iterators: std::string all((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());. The extra parentheses around the first argument avoid the most vexing parse. For a file too large to hold in memory, read it line by line instead.
Why is my file empty when I read it right after writing?
The writer's data is still in its buffer. Nothing is on disk until the stream flushes, so a second stream opened on the file while the first is still alive can see a short or empty file. Let the writer go out of scope, or call close() or flush(), before opening the reader.
Exercises
Write it down, read it back
Read every line of standard input until it ends and write each one, followed by a newline, to a file named notes.txt through a std::ofstream. Just before that stream closes, print written=<bytes> using tellp(). Then open notes.txt with a std::ifstream — the writer must be closed or out of scope first — and read it line by line with std::getline, counting lines, whitespace-separated words, and characters (the sum of the line lengths, newlines excluded), and tracking the longest line. Print lines=<l> words=<w> chars=<c> longest=<length>. Delete the file at the end (std::remove from <cstdio> or std::filesystem::remove).
Input: any number of lines. Output: two lines.
the quick brown
fox
prints
written=20
lines=2 words=4 chars=18 longest=15A ledger appended across two opens
The input holds two sessions of ledger entries. First an integer n and n lines, then an integer m and m lines; each entry line is <label> <amount> with an integer amount that may be negative. Write the first session's lines to ledger.txt with a fresh std::ofstream (the default mode, which truncates) and close it. Then open ledger.txt a second time with std::ios::app and write the second session's lines after the first. Finally read the whole file back and print every line as <i>: <line> with a 1-based number, then entries=<count> and balance=<sum of the amounts> (a long long). Delete the file at the end.
Use std::getline for the entry lines (remember the ignore after each count) and write each line back exactly as read.
Input: n, n lines, m, m lines. Output: the numbered lines, then two summary lines.
2
opening 100
coffee -3
1
salary 250
prints
1: opening 100
2: coffee -3
3: salary 250
entries=3
balance=347In this module: Streams and files
← Reading structured input — the pattern catalogue · String streams and formatting — building text in memory →