File I/O in C++
Read and write files with fstream, manipulate paths with std::filesystem, and parse structured data.
File I/O in C++
C++ provides robust file I/O through the <fstream> library and, since C++17, convenient path and directory manipulation via <filesystem>. File I/O is a critical skill because almost every real program needs to read configuration, write logs, process data files, or walk directory trees. The <fstream> types are RAII wrappers — they open in the constructor and close in the destructor, so you never leak file handles even when exceptions are thrown.
Opening Files: ifstream and ofstream
std::ifstream is for reading, std::ofstream is for writing. Both follow RAII: the file is opened when the object is constructed and closed when it is destroyed. Always check the stream after opening — a failed open leaves the stream in a bad state, and subsequent reads or writes silently do nothing rather than reporting an error.
#include <fstream>
#include <iostream>
#include <string>
int main() {
// Write to a file — creates it if it doesn't exist, truncates if it does
std::ofstream out("output.txt");
if (!out) {
std::cerr << "Failed to open output.txt\n";
return 1;
}
out << "Hello, file!\n";
out << 42 << "\n";
out.close(); // optional: RAII closes on destruction anyway
// Read from a file
std::ifstream in("output.txt");
if (!in) {
std::cerr << "Failed to open output.txt\n";
return 1;
}
std::string line;
while (std::getline(in, line)) {
std::cout << line << "\n";
}
}
std::ifstream and std::ofstream are RAII wrappers — the file closes automatically when they go out of scope. Always check the stream after opening.
Open Modes
Open modes let you control how a file is opened: whether existing content is preserved or truncated, whether writes append or overwrite, and whether the file is treated as text or binary. Combining them with | gives you precise control.
#include <fstream>
// Append instead of truncating — log files, event streams
std::ofstream log("app.log", std::ios::app);
// Read and write simultaneously — useful for update-in-place operations
std::fstream rw("data.txt", std::ios::in | std::ios::out);
// Binary mode — no newline translation on Windows, exact byte representation
std::ofstream bin("data.bin", std::ios::binary);
// Truncate existing content explicitly
std::ofstream fresh("report.txt", std::ios::out | std::ios::trunc);
The most common combinations:
ios::in | ios::out— read/write an existing file without truncatingios::out | ios::app— always append, never overwriteios::binary— suppress newline translation on Windows
Reading Strategies
Different reading strategies suit different use cases. Line-by-line is good for log files and CSV. Word-by-word is good for tokenized text. Reading the entire file at once is good when you need the full content in memory for processing.
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
// Line by line — most common pattern for text files
void readLines(const std::string& path) {
std::ifstream in(path);
std::string line;
while (std::getline(in, line)) {
// process line
}
}
// Word by word — operator>> skips whitespace and reads tokens
void readWords(const std::string& path) {
std::ifstream in(path);
std::string word;
while (in >> word) {
// process word
}
}
// Entire file into a string — idiomatic one-liner, faster than looping
std::string readAll(const std::string& path) {
std::ifstream in(path);
std::ostringstream buf;
buf << in.rdbuf(); // rdbuf() streams the entire file buffer directly
return buf.str();
}
buf << in.rdbuf() is the idiomatic one-liner to slurp a whole file. It is faster than looping because it bypasses character-by-character formatting.
Parsing with std::stringstream
std::stringstream lets you treat a string like a stream, which is perfect for parsing structured lines. The std::getline overload with a delimiter is the key tool for CSV and similar formats — it reads until the delimiter rather than whitespace.
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
struct Record {
std::string name;
int age;
double score;
};
std::vector<Record> parseCSV(const std::string& path) {
std::ifstream in(path);
std::vector<Record> records;
std::string line;
std::getline(in, line); // skip header row
while (std::getline(in, line)) {
std::istringstream ss(line); // treat each line as a stream
Record r;
std::string token;
std::getline(ss, r.name, ','); // read until comma
std::getline(ss, token, ',');
r.age = std::stoi(token);
std::getline(ss, token, ',');
r.score = std::stod(token);
records.push_back(r);
}
return records;
}
// CSV content:
// name,age,score
// Alice,30,98.5
// Bob,25,87.0
Binary File I/O
Binary I/O is necessary for non-textual data: images, serialized structs, custom file formats, network packet captures. read() and write() transfer raw bytes, and reinterpret_cast tells the compiler to treat a struct as a byte sequence. Always open in ios::binary to prevent the OS from translating newline bytes.
#include <fstream>
#include <cstdint>
#include <vector>
struct Packet {
uint32_t id;
float value;
};
void writeBinary(const std::string& path, const std::vector<Packet>& packets) {
std::ofstream out(path, std::ios::binary);
uint32_t count = packets.size();
// Write count first so the reader knows how many packets to expect
out.write(reinterpret_cast<const char*>(&count), sizeof(count));
out.write(reinterpret_cast<const char*>(packets.data()),
count * sizeof(Packet));
}
std::vector<Packet> readBinary(const std::string& path) {
std::ifstream in(path, std::ios::binary);
uint32_t count = 0;
in.read(reinterpret_cast<char*>(&count), sizeof(count));
std::vector<Packet> packets(count);
in.read(reinterpret_cast<char*>(packets.data()), count * sizeof(Packet));
return packets;
}
Be aware that binary layouts are not portable across platforms with different endianness or struct padding. For portable serialization, consider a format like Protocol Buffers or MessagePack.
std::filesystem (C++17)
<filesystem> replaces ad-hoc POSIX/Win32 calls with a clean, cross-platform API. Before C++17, directory traversal required platform-specific code (opendir on POSIX, FindFirstFile on Windows). Now it is the same code everywhere. Path objects handle / concatenation, extension queries, stem extraction, and normalization across platforms.
#include <filesystem>
#include <iostream>
#include <system_error>
namespace fs = std::filesystem;
void filesystemDemo() {
fs::path p = "/tmp/demo";
// Create directory tree — creates intermediate directories too
fs::create_directories(p / "data" / "output");
// Query file info
fs::path file = p / "data" / "input.csv";
if (fs::exists(file)) {
std::cout << "Size: " << fs::file_size(file) << " bytes\n";
std::cout << "Last write: "
<< fs::last_write_time(file).time_since_epoch().count()
<< "\n";
}
// Copy and rename — cross-platform, handles overwrite options
fs::copy(file, p / "data" / "output" / "input.csv",
fs::copy_options::overwrite_existing);
fs::rename(p / "data" / "output" / "input.csv",
p / "data" / "output" / "processed.csv");
// Walk a directory tree recursively
for (const auto& entry : fs::recursive_directory_iterator(p)) {
std::cout << entry.path() << "\n";
}
}
// Error handling without exceptions — use error_code overloads when failure is expected
void safeDelete(const fs::path& p) {
std::error_code ec;
fs::remove_all(p, ec);
if (ec) {
std::cerr << "Error: " << ec.message() << "\n";
}
}
Use std::error_code overloads when you want to handle errors without exceptions.
Putting It Together: File Processor
This example combines directory iteration, path manipulation, and stream I/O into a complete file processing pattern. It demonstrates how the pieces fit together in real-world code.
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
namespace fs = std::filesystem;
// Process all .txt files in inputDir, adding line numbers, writing to outputDir
void processDirectory(const fs::path& inputDir, const fs::path& outputDir) {
fs::create_directories(outputDir); // ensure output directory exists
for (const auto& entry : fs::directory_iterator(inputDir)) {
if (entry.path().extension() != ".txt") continue; // skip non-.txt files
std::ifstream in(entry.path());
std::ofstream out(outputDir / entry.path().filename());
std::string line;
int lineCount = 0;
while (std::getline(in, line)) {
out << ++lineCount << ": " << line << "\n"; // prefix with line number
}
std::cout << "Processed " << entry.path().filename()
<< " (" << lineCount << " lines)\n";
}
}
int main() {
processDirectory("input", "output");
}
This pattern — iterate a directory, filter by extension, transform line-by-line, write to a parallel output directory — covers a large class of file processing tasks cleanly.