C++ • Input and Output

File I/O with fstream — Read and write text files using fstream (intro)

Master persistent data storage by reading from and writing to text files using the C++ fstream library.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

Historical Context & Motivation

Before high-level languages abstracted away the details, programmers interacted with storage devices through low-level system calls — platform-specific routines that differed between Unix, VMS, and early DOS environments. The C language introduced a portable file interface through stdio.h with functions like fopen, fprintf, and fclose, which became the standard for a generation of systems programmers. However, this C-style interface relied on raw pointers and manual resource management, which were error-prone and incompatible with the object-oriented paradigm that C++ was designed to promote. Bjarne Stroustrup and the early C++ standardization committee recognized that a type-safe, RAII-compliant stream abstraction was essential for robust file handling, leading to the development of the fstream library as part of the C++ Standard Library's I/O streams hierarchy.

1972 • C's stdio.h Emerges

Dennis Ritchie's C language introduces FILE pointers and buffered I/O functions, establishing the first portable file abstraction layer for Unix systems.

1985 • C++ Streams Concept

Bjarne Stroustrup's early C++ releases introduce the iostream class hierarchy, applying operator overloading (« and ») to create a type-safe, extensible I/O model.

1998 • C++98 Standardization

The first ISO C++ standard (ISO/IEC 14882:1998) formalizes the <fstream> header, defining ifstream, ofstream, and fstream classes with RAII semantics and locale support.

2011 • C++11 Move Semantics

C++11 adds move constructors to stream objects, enabling efficient transfer of file stream ownership between scopes without copying internal buffers.

2017 • C++17 std::filesystem

The <filesystem> library complements fstream by providing portable path manipulation, directory traversal, and file metadata queries — completing the file management story.

The central question that fstream answers is deceptively simple: how can a C++ program persist data beyond the lifetime of a single execution, and how can it retrieve that data later, all while respecting the language's commitment to type safety, deterministic resource cleanup, and operator-based syntax? Understanding fstream is the first step toward configuration file parsing, log output, data serialization, and every application that must survive a power cycle.

SECTION 2

Core Principles & Definitions

The C++ file stream library is built on a class hierarchy rooted in std::ios_base, with each derived class specializing the stream's directionality and buffering behavior. Three primary classes reside in the <fstream> header: std::ifstream for input, std::ofstream for output, and std::fstream for bidirectional access. Each of these classes wraps a std::filebuf object that manages the low-level buffer associated with the operating system's file descriptor, abstracting platform-specific system calls behind a uniform interface.

1

ifstream — Input File Stream

Derived from std::istream, ifstream opens a file for reading. It supports extraction operators (>>), getline(), and read() for character-level access. The default open mode is ios::in.
2

ofstream — Output File Stream

Derived from std::ostream, ofstream opens a file for writing. It supports insertion operators (<<) and write(). By default it truncates the target file; use ios::app to append instead.
3

fstream — Bidirectional Stream

Derived from std::iostream, fstream supports both reading and writing on the same file. You must manage the file position indicator carefully when switching between input and output operations.
4

RAII & Deterministic Cleanup

File streams follow the RAII idiom: the constructor acquires the resource (opens the file), and the destructor releases it (flushes buffers and closes the file). This eliminates the leak-prone manual close pattern common in C.
5

Open Modes (Bitmask Flags)

Modes like ios::in, ios::out, ios::app, ios::trunc, and ios::binary can be combined with bitwise OR to customize file access behavior at open time.
✦

KEY TAKEAWAY

Think of an fstream object like a librarian who not only fetches books from the archive (reading) but also files them back (writing). When you create the librarian (construct the stream), you hand over the archive key (filename); when the librarian goes off duty (destructor runs), the archive is locked automatically. You never have to remember to lock up yourself — that is the power of RAII-based resource management.

SECTION 3

Visual Explanation — The fstream Class Hierarchy

ios_baseiosistreamostreamiostreamifstreamofstreamfstreamread-onlywrite-onlyread + writeAll file stream classes reside in the <fstream> header. Highlighted boxes are the classes you use directly.
The class hierarchy shows how ifstream inherits from istream, ofstream from ostream, and fstream from iostream (which itself uses diamond inheritance from both istream and ostream). The highlighted boxes at the bottom are the three classes you instantiate directly for file operations.

The diagram above illustrates the multiple-inheritance hierarchy that underpins all C++ stream operations. Because ifstream is a specialization of istream, every extraction technique you already know from std::cin — the >> operator, getline(), get() — works identically when reading from a file. Similarly, ofstream inherits from ostream, so << and manipulators like std::endl behave exactly as they do with std::cout. This polymorphic design is the reason file I/O in C++ feels immediately familiar to anyone who has used console I/O.

SECTION 4

How fstream Works — Open, Operate, Close

Every file stream interaction follows a three-phase lifecycle: open the file, perform I/O operations, and close the file. In C++, the open and close phases can be handled implicitly through RAII: passing a filename to the constructor opens the file, and the destructor closes it when the stream object goes out of scope. Alternatively, you can call .open() and .close() explicitly, which is useful when you need to reuse a single stream object for multiple files sequentially.

Writing to a File

To write data, construct an std::ofstream object with the target filename. If the file does not exist, the operating system creates it; if it does exist, the default mode (ios::out | ios::trunc) truncates it to zero length before writing begins. The insertion operator << streams formatted data into the file buffer, and the buffer is flushed to disk either when the buffer fills, when you explicitly flush, or when the stream is closed.

💡

Code Pattern — Writing to a File

#include <fstream> #include <string> int main() { std::ofstream outFile("output.txt"); if (!outFile) { // handle error: file could not be opened return 1; } outFile << "Name: Alice" << '\n'; outFile << "Score: " << 95 << '\n'; // outFile.close() is called automatically by destructor return 0; }

Reading from a File

To read data, construct an std::ifstream object. The extraction operator >> reads whitespace-delimited tokens, while std::getline() reads entire lines including embedded spaces. A common idiom is to use while (std::getline(inFile, line)) to process a file line by line until EOF is reached. The stream's boolean conversion operator returns false when any error flag is set — including the eofbit — which makes it safe and idiomatic as a loop condition.

💡

Code Pattern — Reading from a File

#include <fstream> #include <iostream> #include <string> int main() { std::ifstream inFile("output.txt"); if (!inFile) { std::cerr << "Cannot open file.\n"; return 1; } std::string line; while (std::getline(inFile, line)) { std::cout << line << '\n'; } return 0; }

Error Checking and Stream State

Every stream maintains a set of state flags: goodbit (no errors), eofbit (end-of-file reached), failbit (a logical error such as type mismatch), and badbit (an irrecoverable I/O error). You should always check whether a file was successfully opened before proceeding with operations, either by testing the stream in a boolean context (if (!file)) or by calling file.is_open(). Ignoring stream state is a common source of subtle bugs — for example, reading from a failed stream yields default-initialized or stale values without any runtime exception.

SECTION 5

Open Modes & File Positioning

The behavior of a file stream is configured at open time through a bitmask of open mode flags defined in std::ios_base. These flags can be combined using the bitwise OR operator to achieve different effects. Understanding which combinations are valid — and what default modes each class applies — is essential for avoiding accidental data loss or silent failures.

FlagConstantEffect
inios::inOpen for reading. Default for ifstream.
outios::outOpen for writing. Default for ofstream.
appios::appAppend mode — all writes go to the end of the file. Does not truncate.
truncios::truncTruncate the file to zero length on open. Default when ofstream opens without ios::app.
ateios::ateSeek to end immediately after opening. Unlike app, subsequent writes can be repositioned.
binaryios::binaryDisable newline translation. Important for non-text data on Windows.

Standard open mode flags defined in <ios>

File Stream Lifecycle: Open → Operate → ClosePHASE 1: OPENofstream f("a.txt");f.open("a.txt");Check: if (!f)or f.is_open()Default modes:ofstream → out|truncifstream → infilebuf acquires OSfile descriptorPHASE 2: OPERATEf << data;f >> variable;getline(f, str);State flags:good|eof|fail|badData flows throughinternal filebuf.Buffer → OS → DiskPHASE 3: CLOSEf.close();Or: destructor~ofstream() called1. Buffer flushed2. FD released to OS3. Stream reusableRAII guarantee:No leaks if scope exits
The three-phase lifecycle of a file stream object: open (acquire resource), operate (read or write through the buffer), and close (flush and release). RAII ensures the close phase happens automatically if the stream leaves scope, even during an exception.
⚠

Append vs. Truncate

A frequent mistake is opening an ofstream without ios::app when you intend to add data to an existing file. The default behavior of ofstream is to truncate — silently erasing all previous content. Always double-check your open mode when working with files that already contain data.

SECTION 6

Worked Example — Student Grade Logger

The following worked example demonstrates a complete program that writes student records to a text file and then reads them back, parsing each line. This covers the full lifecycle of both ofstream and ifstream in a realistic context.

Write and Read Student Records

  1. 1

    Step 1 — Include Headers and Declare Data

    We need <fstream> for file streams, <iostream> for console output, <string> for string handling, and <sstream> for parsing individual fields from a line. We define a simple struct to hold a student's name and grade.
    struct Student { std::string name; int grade; };
  2. 2

    Step 2 — Open ofstream and Write Records

    Construct an std::ofstream with the filename "grades.txt". Verify it opened successfully. Then write each student record as a comma-separated line.
    std::ofstream outFile("grades.txt"); if (!outFile) { std::cerr << "Error opening file.\n"; return 1; } for (const auto& s : students) { outFile << s.name << "," << s.grade << "\n"; }
  3. 3

    Step 3 — Close the Output Stream

    Explicitly call outFile.close() or let the destructor handle it. Closing flushes the buffer, ensuring all data reaches the disk before we attempt to read the file. In this example we close explicitly to clearly separate the write and read phases within the same function.
    outFile.close();
  4. 4

    Step 4 — Open ifstream and Read Records

    Construct a std::ifstream for the same file. Use std::getline() in a while loop to read each line. For each line, create a std::istringstream to extract the name (delimited by comma) and the grade (an integer).
    std::ifstream inFile("grades.txt"); std::string line; while (std::getline(inFile, line)) { std::istringstream iss(line); std::string name; int grade; std::getline(iss, name, ','); iss >> grade; std::cout << name << " scored " << grade << "\n"; }
  5. 5

    Step 5 — Verify Output

    If the students vector contained { {"Alice", 92}, {"Bob", 85}, {"Carol", 97} }, the program writes three lines to grades.txt and then reads them back, producing the console output shown.
    Console output: Alice scored 92 Bob scored 85 Carol scored 97

SECTION 7

fstream vs. C-Style I/O vs. Other Approaches

C++ programmers often encounter multiple file I/O strategies. The C-style FILE* API is still available through <cstdio>, and some projects use platform-specific system calls for performance-critical paths. Understanding the trade-offs helps you choose the right tool for a given context.

CriterionC++ fstreamC stdio (FILE*)POSIX open/read/write
Type safetyStrong — operator overloading for each typeWeak — format strings (%d, %s) can mismatchNone — raw byte buffers
Resource managementRAII — destructor closes fileManual — must call fclose()Manual — must call close(fd)
PerformanceGood — buffered; slight overhead from virtual dispatchExcellent — minimal abstractionFastest — direct syscalls, no buffering layer
ExtensibilityHigh — custom types via operator<< / operator>>Low — limited to printf format specifiersN/A — no formatted I/O
PortabilityFully portable (ISO C++)Fully portable (ISO C)POSIX only — not Windows-native

Comparing C++ fstream with C-style and POSIX file I/O

✦

KEY TAKEAWAY

For most application-level C++ code, fstream is the default choice. Its RAII semantics prevent resource leaks, its type-safe operators prevent format-string vulnerabilities, and its performance is more than adequate for configuration files, logs, and data import/export. Reserve C-style or POSIX I/O for performance-critical inner loops or when interfacing with legacy C libraries.

SECTION 8

Connection to Advanced File I/O

The introductory fstream techniques covered here form the foundation for a rich landscape of advanced file I/O topics. As your programs grow in complexity, you will encounter scenarios that demand binary file processing, custom stream buffers, memory-mapped files, and asynchronous I/O. The table below previews how each introductory concept extends into more sophisticated territory.

Introductory ConceptAdvanced Extension
Text-mode read/write with << and >>Binary I/O with read()/write() and ios::binary for struct serialization
Sequential line-by-line readingRandom access with seekg()/seekp()/tellg()/tellp() for database-like patterns
Stream state flags (eof, fail, bad)Exception-based error handling via stream.exceptions() bitmask
Default filebuf bufferingCustom streambuf subclasses for compression, encryption, or network streams
Filename as string literalstd::filesystem::path objects for platform-safe, Unicode-aware path handling (C++17)

From introductory fstream to advanced I/O patterns

One particularly important direction is the combination of fstream with serialization libraries such as Boost.Serialization, Protocol Buffers, or JSON parsers like nlohmann/json. In these frameworks, fstream provides the transport layer while the serialization library handles the encoding and decoding of complex object graphs. Mastering the basics of fstream now ensures that you can integrate seamlessly with any serialization framework later.

ℹ

Looking Ahead: C++23 and Beyond

Recent C++ standards continue to improve the I/O landscape. C++20 introduced std::format for type-safe formatting (an alternative to the << chain), and C++23 adds std::print which can target file streams directly. These features build on — not replace — the fstream foundation you are learning here.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain why std::ofstream truncates a file by default when opened, and describe two different ways to prevent this behavior while still writing to the file.
Show answer
The default open mode for ofstream is ios::out | ios::trunc, which erases existing content to provide a clean slate for new output — this mirrors the common use case of generating a fresh output file. To prevent truncation, you can (1) use ios::app mode, which forces all writes to the end of the file, or (2) use ios::in | ios::out (via std::fstream), which opens for both reading and writing without truncation, allowing you to position the write pointer wherever needed.

Question 2: BASIC CALCULATION

Write a short C++ program that creates a file called "numbers.txt" containing the integers 1 through 10, each on its own line. Then read the file back and compute their sum, printing it to the console.
Show answer
First, open an ofstream and write integers 1–10 in a for loop, each followed by '\n'. Close the stream. Then open an ifstream on the same file and use a while (inFile >> n) loop to extract each integer, accumulating into a sum variable. The expected output is 55 (the sum 1 + 2 + … + 10). Key points: always check that both streams opened successfully, and note that the extraction operator >> naturally skips whitespace/newlines between integers.

Question 3: INTERMEDIATE

A CSV file "inventory.csv" has the format: item_name,quantity,price (one record per line, no header row). Write a C++ function that reads this file, computes the total inventory value (sum of quantity × price for each item), and writes a summary line to a new file "summary.txt" in the format: "Total value: $XXXX.XX". Handle the case where the input file does not exist.
Show answer
Open an ifstream for "inventory.csv" and check if (!inFile) to handle the missing-file case. Read each line with std::getline(), then parse it with an istringstream: use getline(iss, name, ',') to extract the item name, then iss >> qty followed by ignoring a comma with iss.ignore(1, ','), then iss >> price. Accumulate qty × price into a double. After the loop, open an ofstream for the summary file and use std::fixed and std::setprecision(2) to format the output as currency.

Question 4: APPLIED

You are building a simple logging system. Write a C++ class Logger whose constructor takes a filename and opens an ofstream in append mode. Provide a method void log(const std::string& message) that writes a timestamped entry. Ensure that the file is properly closed when the Logger is destroyed, and explain how RAII guarantees this.
Show answer
The Logger class stores a std::ofstream as a member variable, opened in the constructor with ios::app to preserve existing log entries. The log() method uses std::time() and std::ctime() (or C++20's std::chrono) to prepend a timestamp, then writes the message. The destructor does not need an explicit close() call because the ofstream member's own destructor flushes and closes the file when the Logger object is destroyed. This RAII guarantee means that even if a function throws an exception after constructing a Logger, the log file is still properly closed during stack unwinding.

Question 5: CRITICAL THINKING

Consider a program that opens a file with std::fstream (bidirectional) and alternates between reading a record, modifying it in memory, and writing it back to the same position. What problems might arise if you do not call seekp() or seekg() between read and write operations? Discuss both the C++ standard's requirements and practical consequences.
Show answer
The C++ standard (inherited from C's fseek requirement) mandates that when switching between input and output on a bidirectional stream, you must perform an intervening seek or flush operation — even a no-op seek like f.seekg(0, ios::cur). Without this, the behavior is undefined. In practice, the internal filebuf maintains separate get and put pointers, and the buffer may not be synchronized between read and write modes. Consequences can include writing to the wrong file position (overwriting the wrong record), reading stale buffered data, or corrupting the file entirely. The underlying issue is that the single operating-system file offset must be reconciled with two logical pointers, and only a seek forces that reconciliation. This is why for most text-file use cases, separate ifstream and ofstream objects are preferred over a single bidirectional fstream.

SUMMARY

Lesson Summary

Summary

The C++ <fstream> header provides three core classes for file I/O: ifstream for reading, ofstream for writing, and fstream for bidirectional access. These classes inherit from the iostream hierarchy, which means every operator<< and operator>> technique you know from console I/O transfers directly to file operations. The RAII pattern ensures files are automatically closed when stream objects leave scope, preventing resource leaks even in the presence of exceptions.

Key practices include always checking stream state after opening a file, understanding the difference between truncation and append modes, and using getline() for line-oriented input and >> extraction for token-oriented input. These fundamentals prepare you for advanced topics including binary I/O, random file access, and integration with modern serialization frameworks.

Varsity Tutors • C++ • File I/O with fstream — Read and write text files using fstream (intro)