<< Home

Chapter 23: I/O Streams and Files

C++ streams handle text, binary data, and filesystem work through a layered model built on buffers, formatters, and device abstractions. This chapter tours the core stream families, shows how to achieve high performance with buffering, covers simple serialization patterns, and finishes with modern filesystem utilities. The goal is command of the tools that move data between your program and the outside world.

Stream classes and formatting

Stream classes follow a family pattern. std::istream consumes characters, std::ostream produces characters, and std::iostream combines both. Derivatives such as ifstream, ofstream, stringstream, and ostringstream plug different data sources into the same interface. Formatting uses manipulators like std::setw, std::setprecision, and std::boolalpha which adjust output state until changed again.

Text formatting and manipulators

Manipulators work by toggling flags or updating width and precision in the underlying ios_base. Since stream state persists, compose formatting carefully and reset when needed. A few well placed manipulators can turn raw values into readable reports.

#include <iostream>
#include <iomanip>

int main() {
  double x = 3.14159265;
  std::cout << std::fixed << std::setprecision(3) << x << "\n";
}
πŸ’‘ Use std::format or std::format_to for structured output when streams become cluttered. Format strings keep layout in one place.

Error states and exception modes

Streams track failure through flags such as failbit, eofbit, and badbit. Check good() or inspect specific bits to distinguish end of file from invalid input. Enabling exceptions for failbit or badbit can simplify error paths for strict code.

Buffered I/O and performance

Every stream uses a buffer under the hood. The buffer absorbs small writes, reduces system calls, and improves throughput. std::filebuf handles OS level operations and exposes knobs for tuning. You can replace a buffer to redirect a standard stream or provide custom behavior for logging or filtering.

Understanding streambuf layers

A streambuf defines get and put areas that the stream uses during extraction and insertion. Overriding underflow or overflow allows full control of refills and flushes. This is an advanced path yet essential for specialized devices.

Manual buffering and large reads

When processing files in bulk, prefer read and write on istream and ostream with sizable buffers. Chunking reduces overhead and keeps inner loops tight, especially in binary workloads.

#include <fstream>
#include <vector>

int main() {
  std::ifstream in("data.bin", std::ios::binary);
  std::vector<char> buf(65536);
  while (in.read(buf.data(), buf.size()) || in.gcount() > 0) {
    // process buf.data() … in.gcount()
  }
}
⚠️ Flushing too often slows output. Let buffers flush naturally or call flush() only where correctness demands it.

Binary I/O and serialization basics

Binary I/O writes raw bytes without formatting. You can serialize structs, primitive values, or length prefixed sequences. Be cautious about alignment, padding, and endianness since layouts vary across platforms.

Reading and writing binary blocks

Use read and write for simple blocks. They exchange bytes exactly as they appear in memory. Consider explicit control of endianness when storing for long term or cross platform sharing.

#include <fstream>

struct record {
  int id;
  double value;
};

int main() {
  record r{5, 2.5};
  std::ofstream out("rec.bin", std::ios::binary);
  out.write(reinterpret_cast<char*>(&r), sizeof(r));
}

Simple tagged formats

A tagged format writes a type code or length before data. This makes decoding safer because readers know how many bytes to consume. Even a small tag system prevents misaligned reads and helps versioning.

Text based serialization

Text formats like JSON or CSV use streams easily. You can generate text with std::format or stream operators and parse with tokenized reads. Text is more verbose yet more portable and debuggable than raw binary.

Filesystem paths, iteration, and metadata

The std::filesystem library offers portable paths, directory traversal, and metadata queries. It replaces ad hoc path manipulation with clear operations that respect platform rules. Paths manage separators, encodings, and native conversions automatically.

Working with std::filesystem::path

A path stores components in a normalized form. Use / or operator/= to append segments. Query extensions, filenames, and parents with dedicated functions. Stay in the path domain until absolutely necessary to convert to strings.

#include <filesystem>
#include <iostream>

int main() {
  std::filesystem::path p = "logs";
  p /= "2025";
  p /= "run.txt";
  std::cout << p.string() << "\n";
}

Directory iteration

Use directory_iterator for one level, or recursive_directory_iterator for deep traversal. Entries expose type queries, sizes, timestamps, and permissions. This makes file utilities such as cleaners or indexers straightforward.

#include <filesystem>

int main() {
  for (const auto& e : std::filesystem::directory_iterator(".")) {
    // process e.path() …
  }
}
πŸ’‘ When walking large trees use options::skip_permission_denied to ignore inaccessible nodes cleanly.

Metadata and filesystem operations

Metadata queries such as file_size, last_write_time, and is_regular_file reveal structure without opening files. Operations such as copy, rename, and remove_all modify trees safely. Wrap calls that can fail with std::error_code overloads when you need to recover from errors without throwing.

Resource safety for I/O

I/O APIs interact with the operating system so resource safety matters. Streams own file descriptors or other handles and release them in destructors. Always check that a file opened successfully and fail fast when it does not. Use RAII wrappers for any raw file handles provided by external libraries.

Ensuring handles close correctly

A file closes automatically when its stream object goes out of scope. This behavior prevents many leaks. For custom handles, wrap the resource in a small struct whose destructor calls the correct close function. This keeps lifetimes aligned with scopes.

Recovering from failures

I/O errors happen for reasons outside your control. Handle them predictably by checking states, using error_code overloads, and validating assumptions such as file size or format signatures. Well structured error paths keep your program robust even in cluttered environments.

Streams, buffers, binary formats, and filesystem utilities form a steady toolkit. With careful handling of state and errors you can build pipelines that process data reliably and efficiently.