Search⌘ K
AI Features

Working with File Streams

Explore how to work with file streams in C++ to enable persistent data storage. Understand opening, verifying, and managing file streams, the difference between text and binary modes, and how RAII ensures automatic resource cleanup.

Up to this point, our programs have been transient. They run, produce output, then exit, and any state disappears with the process. Real software needs persistence so it can save configuration, store logs, and keep progress across runs. Files are one of the simplest ways to do that.

C++ keeps file I/O familiar by exposing files through streams, the same model you already used with std::cin and std::cout. Once a stream is connected to a file, reading and writing use the same tools you know. The key difference is that file streams must be opened successfully, and that can fail.

The file stream classes

In C++, file handling allows us to read from and write data to files using stream-based abstractions. File operations are provided by the <fstream> header. It defines three stream types:

  • std::ifstream is an input file stream. It inherits from std::istream and is used to read from files.

  • std::ofstream is an output file stream. It inherits from std::ostream and is used to write to files.

  • std::fstream is a combined stream. It inherits from std::iostream and can both read and write.

The practical takeaway is simple: after successfully opening a file stream, you interact with it using the same stream operations you already know.

Opening files

The most common way to open a file is in the constructor. This connects the stream to the file immediately.

std::ofstream file("data.txt"); // Opens for writing. Creates or overwrites

This pattern is compact and idiomatic. Creating an std::ofstream with a filename requests a writable handle from the operating system. If the file does not exist, it is created. If it already exists, its contents are erased by default.

Sometimes you want the stream object first, then open it later, for example, after deciding on the filename.

std::ofstream file; // Stream exists, but is not connected to a file
// ... heavy logic ...
file.open("data.txt"); // Now connected

This separates “the stream exists” from “the stream is connected.” It is useful when the filename is not known at the moment you declare the stream.

File opening behavior

When you open an output file stream like this:

std::ofstream file("data.txt");

The default behavior is:

  • If the file does not exist, it is created.

  • If the file exists, it is immediately truncated (wiped).

This default is great when you want a fresh output file each run, but it is not what you want for logs or histories.

Verifying the stream is open

Opening a file is a request to the operating system. The OS might refuse that request. The file might not exist, the path could be invalid, the file may be read-only, or permissions might prevent access.

If the connection fails and we continue using the stream, the program will not behave as expected. Reads will fail silently, and writes will not go anywhere meaningful. That is why we must verify that the stream successfully connected before performing any I/O.

The is_open() function tells us whether the stream currently holds a valid file handle.

The config.ini file content is as follows:

username=player1
difficulty=hard

You may verify the working below:

C++ 23
#include <iostream>
#include <fstream>
#include <string>
int main() {
std::string filename = "config.ini";
std::ifstream configFile(filename);
if (configFile.is_open()) {
std::cout << "File opened. Ready to read.\n";
std::string line;
std::getline(configFile, line);
std::cout << "First line: " << line << "\n";
} else {
std::cerr << "Critical Error: Could not open " << filename << "\n";
return 1;
}
return 0;
}

When we construct std::ifstream configFile(filename);, the program immediately asks the operating system for access to that file.

  • If the file exists and permissions allow access, the stream becomes connected.

  • If the file is missing or inaccessible, the stream fails to open.

The is_open() check ensures that we only proceed when the connection is valid. If the file cannot be opened, we stop early and report the issue instead of continuing with a broken state.

This pattern should become second nature whenever you work with files.

Buffering and destructors (RAII)

When we write to a file stream, the data is not necessarily written to the physical disk immediately. Disk operations are significantly slower than CPU operations, so C++ uses a buffer, a temporary region in memory, to accumulate output before writing it in larger chunks.

For example, when we execute:

file << "Hello";

The text is first stored in memory. The system decides when to flush that buffer to disk, typically when the buffer is full, when we explicitly flush it, or when the file is closed.

This introduces a risk: if a program crashes before the buffer is flushed, recently written data may be lost.

C++ addresses this problem using RAII. When a file stream object goes out of scope, its destructor is automatically invoked. That destructor performs two critical operations:

  • It flushes any remaining buffered data to disk.

  • It closes the file handle with the operating system.

This guarantees cleanup even if we return early from a function.

Let's say we have a blank file secure_data.txt.

Writing data to a file and relying on RAII to ensure it is safely flushed and closed

Write to a file and rely on RAII to flush data and close it automatically.

After running the program, verify the contents in the terminal:

cat secure_data.txt

You should see the lines written inside writeSensitiveData().

The important insight here is that we never manually called close(). The destructor handled everything automatically when the stream object went out of scope. This is one of the strongest advantages of C++ resource management and a key reason why RAII is the standard pattern for file handling.

Text vs. binary modes

Files can be opened in either text mode or binary mode.

By default, file streams use text mode, which is suitable for human-readable content such as configuration files, logs, and reports. In this mode, the system may perform minor platform-specific translations, such as handling newline characters appropriately for the operating system.

Binary mode, enabled using std::ios::binary, disables such translations and treats the file as a raw sequence of bytes. This is necessary when working with non-text data such as images or serialized objects.

For now, we'll continue using text mode. We'll explore binary files in detail in a later lesson.