Skip to Content
Course content

199: Binary File I/O in C++

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 ScoreEntry struct. Since it contains a std::string, you must implement custom saveEntry(std::ofstream&, const ScoreEntry&) and loadEntry(std::ifstream&): ScoreEntry functions.
  • The Workflow:
    1. Create a std::vector<ScoreEntry> with at least three different players.
    2. Save the number of entries in the vector first, then loop through and save each entry using your custom function.
    3. Clear your vector.
    4. 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.
  • Constraint: Do not use reinterpret_cast on the ScoreEntry struct itself. You must handle the string serialization manually as described in the lesson.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.