Skip to content

File I/O and Serialization — std::fstream, Binary vs Text I/O, JSON, Protocol Buffers, Boost.Serialization

DodaTech Updated 2026-06-28 10 min read

In this tutorial, you will learn about File I/O and Serialization. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ std::fstream provides text and binary file I/O through stream classes, with serialization libraries (JSON, Protocol Buffers, Boost.Serialization) handling structured data persistence and interchange.

What You'll Learn

You will read and write text files with std::ifstream and std::ofstream, perform binary I/O with read() and write(), handle errors using stream state flags and exceptions, serialize and deserialize structs to JSON using a modern C++ library, understand binary serialization formats, and use std::stringstream for in-memory I/O.

Why It Matters

File I/O is essential for configuration files, data persistence, logging, and inter-Process communication. While raw binary I/O works for simple structures, modern C++ applications use structured serialization formats for type safety, versioning, and cross-language interoperability. Understanding both approaches lets you choose the right tool for each scenario.

Learning Path

graph LR
    A["63: Atomics & Synchronization"] --> B["64: File I/O & Serialization"]
    B --> C["65: Build Systems (CMake)"]
    C --> D["66: Unit Testing"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Text File I/O

Reading and writing text files with std::fstream.

#include <iostream>
#include <fstream>
#include <string>
#include <vector>

// Write text file
void writeText(const std::string& path, const std::vector<std::string>& lines) {
    std::ofstream file(path);
    if (!file.is_open()) {
        throw std::runtime_error("Cannot open file: " + path);
    }

    for (const auto& line : lines) {
        file << line << "\n";
    }

    if (file.fail()) {
        throw std::runtime_error("Write failed");
    }
    // File automatically closed by destructor (RAII)
}

// Read text file
std::vector<std::string> readText(const std::string& path) {
    std::ifstream file(path);
    if (!file.is_open()) {
        throw std::runtime_error("Cannot open file: " + path);
    }

    std::vector<std::string> lines;
    std::string line;
    while (std::getline(file, line)) {
        lines.push_back(line);
    }

    return lines;
}

int main() {
    std::vector<std::string> data = {"Hello", "World", "File I/O in C++"};

    writeText("output.txt", data);

    auto readBack = readText("output.txt");
    for (const auto& line : readBack) {
        std::cout << line << "\n";
    }
}

Binary File I/O

Reading and writing raw bytes.

#include <iostream>
#include <fstream>
#include <vector>
#include <cstring>

// Binary write
void writeBinary(const std::string& path, const int* data, size_t count) {
    std::ofstream file(path, std::ios::binary);
    if (!file) throw std::runtime_error("Cannot open");

    file.write(reinterpret_cast<const char*>(data), count * sizeof(int));

    if (!file) throw std::runtime_error("Write failed");
}

// Binary read
std::vector<int> readBinary(const std::string& path) {
    std::ifstream file(path, std::ios::binary);
    if (!file) throw std::runtime_error("Cannot open");

    // Get file size
    file.seekg(0, std::ios::end);
    std::streampos size = file.tellg();
    file.seekg(0, std::ios::beg);

    size_t count = size / sizeof(int);
    std::vector<int> data(count);

    file.read(reinterpret_cast<char*>(data.data()), size);
    if (!file) throw std::runtime_error("Read failed");

    return data;
}

struct Header {
    uint32_t magic;
    uint32_t version;
    uint64_t numRecords;
};

struct Record {
    int64_t id;
    double value;
    char name[32];
};

void writeRecords(const std::string& path, const Header& hdr, const Record* records) {
    std::ofstream file(path, std::ios::binary);

    file.write(reinterpret_cast<const char*>(&hdr), sizeof(hdr));
    file.write(reinterpret_cast<const char*>(records), hdr.numRecords * sizeof(Record));
}

int main() {
    // Simple binary read/write
    int numbers[] = {1, 2, 3, 4, 5};
    writeBinary("data.bin", numbers, 5);
    auto result = readBinary("data.bin");

    for (int x : result) std::cout << x << " ";
    std::cout << "\n";  // 1 2 3 4 5
}

Stream State Checking

Proper error handling with streams.

#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ifstream file;

    // Check before using
    std::cout << "File state (not opened):\n";
    std::cout << "  good: " << file.good() << "\n";   // false
    std::cout << "  fail: " << file.fail() << "\n";   // false
    std::cout << "  bad:  " << file.bad() << "\n";    // false
    std::cout << "  eof:  " << file.eof() << "\n";    // false

    file.open("nonexistent.txt");
    std::cout << "After opening nonexistent file:\n";
    std::cout << "  good: " << file.good() << "\n";   // false
    std::cout << "  fail: " << file.fail() << "\n";   // true
    std::cout << "  bad:  " << file.bad() << "\n";    // true or false

    // Clear error state
    file.clear();

    // Enable exceptions
    file.exceptions(std::ios::failbit | std::ios::badbit);

    try {
        file.open("also_missing.txt");
    } catch (const std::ios_base::failure& e) {
        std::cout << "Exception: " << e.what() << "\n";
    }

    // Reading with validation
    std::ifstream config("config.txt");
    int port;
    std::string host;

    if (config >> port >> host) {
        std::cout << "Config: " << port << " " << host << "\n";
    } else {
        std::cout << "Failed to parse config\n";
    }
}

std::stringstream — In-Memory I/O

String streams work like file streams but use memory buffers.

#include <iostream>
#include <sstream>
#include <string>
#include <iomanip>

int main() {
    // Write to string stream
    std::ostringstream oss;
    oss << "Value: " << 42 << ", Pi: " << std::fixed
        << std::setprecision(4) << 3.14159;
    std::string result = oss.str();
    std::cout << result << "\n";  // Value: 42, Pi: 3.1416

    // Parse from string stream
    std::istringstream iss("123 456.789 hello");
    int x;
    double y;
    std::string z;

    if (iss >> x >> y >> z) {
        std::cout << "Parsed: " << x << ", " << y << ", " << z << "\n";
        // Parsed: 123, 456.789, hello
    }

    // CSV parsing
    std::string csv = "Alice,30,Engineer\nBob,25,Designer";
    std::istringstream csvStream(csv);
    std::string line;

    while (std::getline(csvStream, line)) {
        std::istringstream lineStream(line);
        std::string name, age, role;

        std::getline(lineStream, name, ',');
        std::getline(lineStream, age, ',');
        std::getline(lineStream, role, ',');

        std::cout << name << " (" << age << ") - " << role << "\n";
    }
}

JSON Serialization (using nlohmann/json)

Modern C++ projects often use the nlohmann/json library.

#include <iostream>
#include <fstream>
#include <string>
#include <vector>

// Assuming nlohmann/json.hpp is available
// #include <nlohmann/json.hpp>
// using json = nlohmann::json;

// Simulated JSON-like class for illustration
// In real projects: use nlohmann/json (single-header library)

struct Person {
    std::string name;
    int age;
    std::vector<std::string> tags;
};

// JSON serialization (conceptual — requires nlohmann/json)
void toJsonFile(const Person& person, const std::string& path) {
    // json j;
    // j["name"] = person.name;
    // j["age"] = person.age;
    // j["tags"] = person.tags;
    //
    // std::ofstream file(path);
    // file << j.dump(4);  // Pretty print with 4-space indent

    std::cout << "Would write JSON to " << path << "\n";
}

Person fromJsonFile(const std::string& path) {
    // std::ifstream file(path);
    // json j;
    // file >> j;
    //
    // return {
    //     j["name"].get<std::string>(),
    //     j["age"].get<int>(),
    //     j["tags"].get<std::vector<std::string>>()
    // };

    std::cout << "Would read JSON from " << path << "\n";
    return {"Alice", 30, {"student", "engineer"}};
}

int main() {
    Person alice{"Alice", 30, {"developer", "manager"}};
    toJsonFile(alice, "alice.json");

    auto loaded = fromJsonFile("alice.json");
    std::cout << "Loaded: " << loaded.name << ", " << loaded.age << "\n";
}

Binary Serialization with FlatBuffers (Conceptual)

FlatBuffers and Protocol Buffers provide cross-language binary serialization.

#include <iostream>
#include <vector>
#include <cstdint>

// Simplified binary serialization for illustration
// Real projects use FlatBuffers, Protocol Buffers, or Cap'n Proto

class BinarySerializer {
    std::vector<uint8_t> buffer_;
public:
    template <typename T>
    void write(const T& value) {
        const auto* bytes = reinterpret_cast<const uint8_t*>(&value);
        buffer_.insert(buffer_.end(), bytes, bytes + sizeof(T));
    }

    void writeString(const std::string& str) {
        uint32_t len = static_cast<uint32_t>(str.size());
        write(len);
        buffer_.insert(buffer_.end(), str.begin(), str.end());
    }

    const std::vector<uint8_t>& data() const { return buffer_; }

    void save(const std::string& path) const {
        std::ofstream file(path, std::ios::binary);
        file.write(reinterpret_cast<const char*>(buffer_.data()), buffer_.size());
    }
};

class BinaryDeserializer {
    const uint8_t* data_;
    size_t size_;
    size_t pos_ = 0;
public:
    BinaryDeserializer(const std::vector<uint8_t>& buf)
        : data_(buf.data()), size_(buf.size()) {}

    template <typename T>
    T read() {
        if (pos_ + sizeof(T) > size_) throw std::runtime_error("EOF");
        T value;
        std::memcpy(&value, data_ + pos_, sizeof(T));
        pos_ += sizeof(T);
        return value;
    }

    std::string readString() {
        uint32_t len = read<uint32_t>();
        if (pos_ + len > size_) throw std::runtime_error("EOF");
        std::string str(reinterpret_cast<const char*>(data_ + pos_), len);
        pos_ += len;
        return str;
    }
};

int main() {
    // Serialize
    BinarySerializer ser;
    ser.write<int32_t>(42);
    ser.write<double>(3.14);
    ser.writeString("Hello, Binary!");

    std::cout << "Serialized " << ser.data().size() << " bytes\n";

    // Deserialize
    BinaryDeserializer deser(ser.data());
    int32_t i = deser.read<int32_t>();
    double d = deser.read<double>();
    std::string s = deser.readString();

    std::cout << i << ", " << d << ", " << s << "\n";  // 42, 3.14, Hello, Binary!
}

File System Operations (C++17)

#include <iostream>
#include <filesystem>
#include <fstream>

namespace fs = std::filesystem;

int main() {
    // Check if file exists
    fs::path dataDir = "data";
    fs::path configFile = dataDir / "config.txt";

    if (!fs::exists(dataDir)) {
        fs::create_directory(dataDir);
        std::cout << "Created directory: " << dataDir << "\n";
    }

    // Write to file
    std::ofstream(configFile) << "version=1\n";

    // Check file info
    if (fs::exists(configFile)) {
        std::cout << "File: " << configFile << "\n";
        std::cout << "  Size: " << fs::file_size(configFile) << " bytes\n";
        std::cout << "  Modified: " << fs::last_write_time(configFile) << "\n";
    }

    // List directory
    std::cout << "Contents of " << dataDir << ":\n";
    for (const auto& entry : fs::directory_iterator(dataDir)) {
        std::cout << "  " << entry.path().filename()
                  << (entry.is_directory() ? " [dir]" : "") << "\n";
    }

    // Copy and remove
    fs::path backup = dataDir / "config_backup.txt";
    fs::copy_file(configFile, backup, fs::copy_options::overwrite_existing);
    fs::remove(backup);  // Clean up
}

Common Mistakes

Mistake 1: Not checking if file opened successfully

std::ofstream file("path.txt");
file << "data";  // Silent failure if file didn't open

Always check file.is_open() or use RAII with exceptions.

Mistake 2: Binary mode for binary data

std::ofstream file("data.bin");
file.write(data, size);  // Missing std::ios::binary — newlines may be translated!

Mistake 3: Reading past EOF without checking

while (!file.eof()) {  // Wrong: eof() is set after a read fails
    file >> value;
}

Check after reading: while (file >> value).

Mistake 4: Forgetting that sizeof includes padding

struct Record { char a; int b; };  // sizeof is 8, not 5 (padding)

Use packed structs or explicit serialization for portable binary format.

Mistake 5: Not flushing after critical writes

file << "important data";  // May be buffered!
file.flush();

Or use std::endl (flushes) vs "\n" (doesn't flush).

Practice Questions

  1. What is the difference between text and binary file modes? Answer: Binary mode (std::ios::binary) disables newline translation and reads/writes raw bytes. Text mode converts platform-specific line endings.

  2. How do you handle file open failures? Answer: Check file.is_open(), or enable exceptions: file.exceptions(std::ios::failbit).

  3. What is std::stringstream used for? Answer: In-memory I/O — building strings from formatted data (ostringstream) or Parsing strings (istringstream).

  4. What is the advantage of JSON over binary serialization? Answer: Human-readable, self-describing, widely supported across languages. Binary is smaller and faster for machine processing.

  5. How does std::filesystem help with I/O? Answer: It provides portable path manipulation, directory listing, file metadata, and copy/move operations without platform-specific code.

FAQ

What file I/O classes does C++ provide

std::ifstream (reading), std::ofstream (writing), std::fstream (both), std::stringstream (in-memory), and std::filesystem (C++17, file system operations).

What is the difference between serialization and file I/O

File I/O reads/writes bytes. Serialization converts structured data (objects, structs) to a byte stream or text format (JSON, XML, binary).

Should I use text or binary format for configuration

Text (JSON, YAML, TOML) for configuration — human-readable and editable. Binary for performance-critical serialization or when data size matters.

What is the best C++ JSON library

nlohmann/json is the most popular single-header JSON library. Boost.JSON is a close alternative with official Boost support.

How do I handle file errors properly

Use RAII (files close automatically), check is_open(), enable exceptions for failbit/badbit, or check stream state after each operation.

Mini Project

Build a simple key-value store that persists to a binary file:

#include <iostream>
#include <fstream>
#include <map>
#include <string>
#include <vector>

// Your persistent key-value store

int main() {
    KVStore store("data.kv");

    store.set("name", "Alice");
    store.set("score", "95");
    store.set("grade", "A");

    std::cout << "name: " << store.get("name") << "\n";   // Alice
    std::cout << "score: " << store.get("score") << "\n";  // 95
    std::cout << "exists(grade): " << store.exists("grade") << "\n"; // 1

    store.flush();  // Write to disk

    // Load from existing file
    KVStore loaded("data.kv");
    std::cout << "Loaded score: " << loaded.get("score") << "\n";  // 95

    store.remove("grade");
    std::cout << "exists(grade): " << store.exists("grade") << "\n"; // 0
}

This project demonstrates how C++ file I/O and serialization apply to real-world data persistence, similar to key-value stores like Redis or RocksDB but at a simpler level.

What's Next

You now master file I/O and serialization in C++. Next, you will learn build systems (CMake), which orchestrate compilation, linking, and dependency management for C++ projects.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro