C++ File I/O: Read & Write Files with fstream Complete Guide 2026
Table of Contents
How C++ File I/O Works
C++ handles files through stream objects defined in the <fstream> header. The three main types are std::ifstream for reading (input file stream), std::ofstream for writing (output file stream), and std::fstream for both. These work exactly like std::cin and std::cout — they use the same << and >> operators and support the same formatting.
The fundamental pattern is simple: create a stream object, check that it opened successfully, use it for reading or writing, and let RAII close it automatically when the object goes out of scope. If you’ve learned about move semantics and RAII in earlier lessons, file streams are a textbook example of that pattern in action.
Writing Files with ofstream
#include <fstream>
#include <iostream>
#include <string>
int main() {
// Create and write to a file
std::ofstream out("output.txt");
if (!out) {
std::cerr << "Failed to open file for writing
";
return 1;
}
// Write using << operator (just like cout)
out << "Hello, World!
";
out << "The answer is " << 42 << "
";
out << "Pi = " << 3.14159 << "
";
// Write a string
std::string message = "This is line 4
";
out << message;
// Write raw characters
out.put('A');
out.put('
');
// Write a block of characters
const char* data = "Block write
";
out.write(data, 12);
// File closes automatically when 'out' goes out of scope
// But you can close manually:
out.close();
std::cout << "File written successfully
";
}
Reading Files with ifstream
Reading Line by Line
#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ifstream in("output.txt");
if (!in) {
std::cerr << "Failed to open file
";
return 1;
}
// Read line by line — the most common pattern
std::string line;
int line_num = 1;
while (std::getline(in, line)) {
std::cout << line_num++ << ": " << line << "
";
}
}
Reading Word by Word
#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ifstream in("output.txt");
// >> skips whitespace and reads tokens
std::string word;
int count = 0;
while (in >> word) {
++count;
}
std::cout << "Word count: " << count << "
";
}
Reading Entire File into String
#include <fstream>
#include <sstream>
#include <string>
#include <iostream>
int main() {
std::ifstream in("output.txt");
if (!in) return 1;
// Method 1: stringstream (common)
std::stringstream buffer;
buffer << in.rdbuf();
std::string content1 = buffer.str();
// Method 2: iterator-based (more C++)
in.seekg(0); // Reset to beginning
std::string content2(
(std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>()
);
std::cout << "File size: " << content1.size() << " bytes
";
std::cout << content1;
}
File Modes
#include <fstream>
#include <iostream>
int main() {
// Default: ofstream truncates (overwrites), ifstream reads
std::ofstream out1("data.txt"); // Truncate (destroy old content)
// Append mode: add to end of file
std::ofstream out2("log.txt", std::ios::app);
out2 << "New log entry
";
// Binary mode: no text translations
std::ofstream out3("data.bin", std::ios::binary);
// Combine modes with |
std::ofstream out4("data.bin", std::ios::binary | std::ios::app);
// Read and write with fstream
std::fstream both("data.txt", std::ios::in | std::ios::out);
// Available modes:
// std::ios::in — Open for reading
// std::ios::out — Open for writing
// std::ios::app — Append to end
// std::ios::trunc — Truncate file (default for ofstream)
// std::ios::binary — Binary mode (no text translations)
// std::ios::ate — Seek to end after opening
}
Error Handling
#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ifstream in("nonexistent.txt");
// Method 1: Bool conversion (simplest)
if (!in) {
std::cerr << "Cannot open file
";
}
// Method 2: Check specific states
if (in.good()) std::cout << "All good
";
if (in.fail()) std::cout << "Logical error (format mismatch or open fail)
";
if (in.bad()) std::cout << "Read/write error (disk failure)
";
if (in.eof()) std::cout << "End of file reached
";
// Method 3: is_open() — specifically checks if file opened
if (!in.is_open()) {
std::cerr << "File is not open
";
}
// Method 4: Enable exceptions
std::ifstream strict("data.txt");
strict.exceptions(std::ios::failbit | std::ios::badbit);
try {
std::string line;
while (std::getline(strict, line)) {
std::cout << line << "
";
}
} catch (const std::ios_base::failure& e) {
// This catches fail and bad states
if (strict.eof()) {
// Normal end of file — not really an error
} else {
std::cerr << "I/O error: " << e.what() << "
";
}
}
// Clear error state to reuse stream
in.clear();
in.open("another_file.txt");
}
Binary Files
#include <fstream>
#include <iostream>
#include <vector>
#include <cstdint>
struct Record {
int32_t id;
float value;
char name[32];
};
int main() {
// Write binary data
{
std::ofstream out("records.bin", std::ios::binary);
Record r1{1, 3.14f, "Alice"};
Record r2{2, 2.72f, "Bob"};
out.write(reinterpret_cast<const char*>(&r1), sizeof(Record));
out.write(reinterpret_cast<const char*>(&r2), sizeof(Record));
// Write a vector of ints
std::vector<int> data = {10, 20, 30, 40, 50};
int32_t size = data.size();
out.write(reinterpret_cast<const char*>(&size), sizeof(size));
out.write(reinterpret_cast<const char*>(data.data()),
data.size() * sizeof(int));
}
// Read binary data
{
std::ifstream in("records.bin", std::ios::binary);
Record r;
while (in.read(reinterpret_cast<char*>(&r), sizeof(Record))) {
std::cout << r.id << ": " << r.name
<< " (" << r.value << ")
";
if (in.peek() == EOF) break;
}
// Read vector size then data
int32_t size;
in.read(reinterpret_cast<char*>(&size), sizeof(size));
std::vector<int> data(size);
in.read(reinterpret_cast<char*>(data.data()),
size * sizeof(int));
for (int v : data) std::cout << v << " ";
}
}
Binary files are smaller and faster to read/write than text, but they’re not portable across different platforms (endianness, struct padding) and aren’t human-readable. Use binary for performance-critical data; use text for configuration and logs.
File Position and Seeking
#include <fstream>
#include <iostream>
int main() {
std::fstream file("data.txt", std::ios::in | std::ios::out);
// Get current read position
auto pos = file.tellg(); // tellg for input, tellp for output
// Seek to specific position
file.seekg(0); // Beginning
file.seekg(0, std::ios::end); // End
file.seekg(-10, std::ios::end); // 10 bytes before end
file.seekg(5, std::ios::cur); // 5 bytes forward from current
// Get file size
file.seekg(0, std::ios::end);
auto size = file.tellg();
file.seekg(0); // Reset to beginning
std::cout << "File size: " << size << " bytes
";
}
Working with CSV Files
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <iostream>
struct Student {
std::string name;
int age;
double gpa;
};
// Write CSV
void write_csv(const std::string& filename,
const std::vector<Student>& students) {
std::ofstream out(filename);
out << "Name,Age,GPA
"; // Header
for (const auto& s : students) {
out << s.name << "," << s.age << "," << s.gpa << "
";
}
}
// Read CSV
std::vector<Student> read_csv(const std::string& filename) {
std::vector<Student> students;
std::ifstream in(filename);
std::string line;
std::getline(in, line); // Skip header
while (std::getline(in, line)) {
std::istringstream iss(line);
Student s;
std::string age_str, gpa_str;
std::getline(iss, s.name, ',');
std::getline(iss, age_str, ',');
std::getline(iss, gpa_str, ',');
s.age = std::stoi(age_str);
s.gpa = std::stod(gpa_str);
students.push_back(s);
}
return students;
}
int main() {
std::vector<Student> data = {
{"Alice", 20, 3.8},
{"Bob", 22, 3.5},
{"Charlie", 21, 3.9}
};
write_csv("students.csv", data);
auto loaded = read_csv("students.csv");
for (const auto& s : loaded) {
std::cout << s.name << " (age " << s.age
<< ", GPA " << s.gpa << ")
";
}
}
RAII and File Safety
#include <fstream>
#include <string>
#include <stdexcept>
// RAII: files close automatically when stream objects are destroyed
void process_file(const std::string& path) {
std::ifstream in(path);
if (!in) throw std::runtime_error("Cannot open: " + path);
// Process file...
// Even if an exception is thrown here,
// the destructor of 'in' closes the file.
}
// 'in' goes out of scope here — file is closed automatically
// DON'T do this (C-style file handling):
// FILE* f = fopen("data.txt", "r");
// // If you forget fclose(f), the file handle leaks
// fclose(f);
// Anti-pattern: unnecessary manual close
void bad_style(const std::string& path) {
std::ifstream in(path);
// ... do work ...
in.close(); // Unnecessary — destructor does this
// close() is only needed if you want to reopen or check for write errors
}
// When manual close IS useful: checking write success
void write_important_data() {
std::ofstream out("critical.dat");
out << "important data";
out.close();
if (!out) {
// The close flushed the buffer — check if that succeeded
throw std::runtime_error("Write failed!");
}
}
std::filesystem Integration
#include <filesystem>
#include <fstream>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path filepath = "data/output.txt";
// Create directories if they don't exist
fs::create_directories(filepath.parent_path());
// Write file
std::ofstream out(filepath);
out << "Hello from filesystem!
";
out.close();
// Check file properties
if (fs::exists(filepath)) {
std::cout << "Size: " << fs::file_size(filepath) << " bytes
";
std::cout << "Path: " << fs::absolute(filepath) << "
";
}
// Iterate directory
for (const auto& entry : fs::directory_iterator(".")) {
if (entry.is_regular_file()) {
std::cout << entry.path().filename() << " ("
<< entry.file_size() << " bytes)
";
}
}
// Copy, rename, delete
fs::copy_file(filepath, "backup.txt", fs::copy_options::overwrite_existing);
fs::rename("backup.txt", "backup_v2.txt");
fs::remove("backup_v2.txt");
}
Common Patterns
#include <fstream>
#include <string>
#include <iostream>
#include <sstream>
// Pattern 1: Config file parser
#include <map>
std::map<std::string, std::string> read_config(const std::string& path) {
std::map<std::string, std::string> config;
std::ifstream in(path);
std::string line;
while (std::getline(in, line)) {
if (line.empty() || line[0] == '#') continue; // Skip empty/comments
auto eq = line.find('=');
if (eq != std::string::npos) {
std::string key = line.substr(0, eq);
std::string val = line.substr(eq + 1);
config[key] = val;
}
}
return config;
}
// Pattern 2: Atomic write (write to temp, then rename)
#include <filesystem>
void safe_write(const std::string& path, const std::string& content) {
std::string tmp = path + ".tmp";
{
std::ofstream out(tmp);
out << content;
}
std::filesystem::rename(tmp, path); // Atomic on most filesystems
}
// Pattern 3: Process large file without loading into memory
void count_lines_and_words(const std::string& path) {
std::ifstream in(path);
std::string line;
int lines = 0, words = 0;
while (std::getline(in, line)) {
++lines;
std::istringstream iss(line);
std::string word;
while (iss >> word) ++words;
}
std::cout << lines << " lines, " << words << " words
";
}
Practice Exercises
Exercise 1: Write a program that reads a text file and outputs the number of lines, words, and characters (a simplified wc command).
Exercise 2: Create a simple log class that appends timestamped messages to a file. Include log levels (INFO, WARN, ERROR).
Exercise 3: Write a program that reads a CSV file of products (name, price, quantity), computes the total inventory value, and writes a summary to a new file.
Exercise 4: Implement a binary file format for storing a list of high scores (name + score). Write functions to save and load the list, and to add a new score if it’s in the top 10.
File I/O is one of those skills you’ll use in virtually every real C++ project. The stream model is consistent across files, strings, and console I/O, so patterns you learn here apply everywhere. Next, we’ll look at namespaces — how C++ organizes code to prevent name collisions in large projects.