File I/O and Serialization — std::fstream, Binary vs Text I/O, JSON, Protocol Buffers, Boost.Serialization
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
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.
How do you handle file open failures? Answer: Check
file.is_open(), or enable exceptions:file.exceptions(std::ios::failbit).What is
std::stringstreamused for? Answer: In-memory I/O — building strings from formatted data (ostringstream) or Parsing strings (istringstream).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.
How does
std::filesystemhelp with I/O? Answer: It provides portable path manipulation, directory listing, file metadata, and copy/move operations without platform-specific code.
FAQ
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