C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
199: Binary File I/O in C++
I've seen a lot of developers transition from text-based files to binary files and make the exact same mistake. They assume that because they are writing "raw data," they can just take any object—like a std::string or a std::vector—cast it to a char*, and dump it into a file. It feels intuitive. You're telling the computer, "Just take this chunk of memory and put it on the disk," right?
The "Dump Everything" Delusion
Here is why that approach will crash your program (or worse, silently corrupt your data). Let's look at a simple Player struct:
struct Player {
int id;
std::string name;
};
Player p1 = {1, "Hero"};
std::ofstream outFile("save.bin", std::ios::binary);
outFile.write(reinterpret_cast<char*>(&p1), sizeof(Player));
You might think you've saved "Hero" to the disk. You haven't. A std::string doesn't actually store the characters inside the struct itself; it stores a pointer to a heap-allocated buffer where the characters live. When you run outFile.write, you aren't saving the text "Hero"—you're saving the memory address (the pointer) where that text happened to be sitting in RAM at that exact microsecond. When you restart your program and read that pointer back, it's pointing to a memory location that no longer contains your string. It's a dangling pointer, and accessing it is a one-way ticket to a Segfault.
Writing Plain Old Data (POD) Correctly
Binary I/O is fantastic for "Plain Old Data"—types that don't use pointers or dynamic memory. Think int, float, bool, or structs containing only these types. For these, write() and read() are incredibly efficient because they bypass the overhead of converting numbers to text.
To do this right, you need to open the file with the std::ios::binary flag. If you don't, the runtime might try to be "helpful" and translate line endings (like converting \n to \r\n on Windows), which will absolutely mangle your binary data.
struct GameStats {
int level;
float health;
int gold;
};
GameStats stats = {10, 85.5f, 1200};
// Writing
std::ofstream outFile("stats.dat", std::ios::binary);
outFile.write(reinterpret_cast<char*>(&stats), sizeof(GameStats));
outFile.close();
// Reading
GameStats loadedStats;
std::ifstream inFile("stats.dat", std::ios::binary);
inFile.read(reinterpret_cast<char*>(&loadedStats), sizeof(GameStats));
Handling Variable-Length Data Without the Crash
So, how do we actually handle a std::string? You have to be explicit. I usually recommend a "length-prefix" strategy: write the size of the string first, then write the actual characters. This tells the reader exactly how much memory to allocate before reading the content.
I'll show you how I typically handle this in a production setting:
std::string playerName = "DragonSlayer_99";
size_t nameLen = playerName.size();
// 1. Write the length
outFile.write(reinterpret_cast<char*>(&nameLen), sizeof(nameLen));
// 2. Write the actual character array (using .data())
outFile.write(playerName.data(), nameLen);
// To read it back:
size_t readLen;
inFile.read(reinterpret_cast<char*>(&readLen), sizeof(readLen));
std::string readName(readLen, ' '); // Pre-allocate space
inFile.read(&readName[0], readLen);
It's a bit more verbose, but it's the only way to ensure your files are portable and stable. You're essentially building your own mini-protocol for how the data is laid out on the disk. Just remember: if it has a pointer, you can't write() the object; you have to write() the data the pointer points to.
📋 Practical Task
Project: Binary High-Score Table System
Your task is to create a system that saves and loads a high-score leaderboard to a binary file. A leaderboard consists of multiple entries, where each entry contains a player's name (variable length) and their score (integer).
- The Requirement: Create a
ScoreEntrystruct. Since it contains astd::string, you must implement customsaveEntry(std::ofstream&, const ScoreEntry&)andloadEntry(std::ifstream&): ScoreEntryfunctions. - The Workflow:
- Create a
std::vector<ScoreEntry>with at least three different players. - Save the number of entries in the vector first, then loop through and save each entry using your custom function.
- Clear your vector.
- Read the number of entries from the file, loop that many times to load the entries back into the vector, and print them to the console to verify the data survived the trip.
- Create a
- Constraint: Do not use
reinterpret_caston theScoreEntrystruct itself. You must handle the string serialization manually as described in the lesson.
There are no comments for now.