CodeGym /Courses /C++ SELF /Text and Binary Formats

Text and Binary Formats

C++ SELF
Level 63, Lesson 2
Available

1. A file is bytes, a format is an agreement

When you first hear “text” and “binary” file, it's easy to imagine two different worlds: one for humans, one for machines. But in reality what’s “on disk” is almost always the same: a sequence of bytes. The difference begins when we agree how to interpret those bytes and how to read/write them via streams.

Imagine a file is a box of LEGO pieces but without instructions. Bytes are the pieces themselves. A “format” is the instruction: how to assemble meaning from the pieces. You can pack pieces into a bag with a little paper saying "2x4 red, 2x2 blue..." — that resembles text. Or you can pack pieces into bags with codes and sizes — that resembles a binary format.

For this agreement to work we need two things: first, decide whether we write text or “raw bytes”, and second, open the file with the correct mode so the stream behaves as we expect.

Text format: easy to read, easy to break

Text format is usually chosen not because it's “cool” but because it's practical: you can open it in a text editor, look with your eyes, quickly fix it and send it to a friend (or your future self) in a messenger. In early learning stages this is a lifesaver: you see data, not “magic goo”.

But text has a downside: it requires parsing and strict discipline. If you agreed that a task is written as id done title, then one extra “pipe” |, an extra space or an empty line can make reading difficult. So text is easy to debug, but also easy to corrupt.

Let's continue with our learning app. Let it be a small console task tracker TaskBook. For now it lives in memory (in std::vector), but now we want to learn how to save it to a file.

Mini task model:

#include <string>

struct Task {
    int id = 0;
    bool done = false;
    std::string title;
};

Serialization to a single text line (the simplest “agreement”):

#include <string>

std::string to_line(const Task& t) {
    // Format: id done title
    // Example: 7 1 Buy_milk
    return std::to_string(t.id) + " " + (t.done ? "1" : "0") + " " + t.title;
}

Note a small trick: I used Buy_milk, not Buy milk. Because “words with spaces” in such a primitive format immediately force us to think about quotes or reading a whole line. That's fine, but today we focus on open modes, not a full-featured format.

2. Binary format: compact and fast

Binary format is when you store data not as characters '1', '2', 'a', 'b', but as “how it sits in memory” (or close to that). Usually it's more compact and faster for the machine, but completely unreadable for humans. Open it in a text editor — you'll see “garbage”, and that's normal.

At the same time binary format requires an even stricter agreement: “which fields in which order”, “what sizes they have”, “how to store a string”, “what encoding”. And the most important: portability. The same binary file may be read differently on different platforms if you haven't carefully designed the format.

We won't dive into full binary serialization today (that's a separate story), but we need to understand one key thing: to work “as bytes” you usually open the file with the std::ios::binary flag. This flag affects how the library does (or doesn't do) text transformations, especially on platforms where there's a difference between "\n" and the platform's file end-of-line representation.

4. Open modes: std::ios::openmode

When you open a file you kind of sign a contract: “I want to read”, “I want to write”, “I want to overwrite the file completely”, “I want to append to the end”, “I want to work with bytes”.

In the standard library this is expressed with open mode flags. Historically they're called openmode, and they live alongside IO streams. In drafts and the standard documents the concept appears as ios::openmode and is related to the family of ios-types.

Let's review the main flags as the programmer's intent:

Flag Idea Typical stream
std::ios::in
open for reading
std::ifstream
std::ios::out
open for writing
std::ofstream
    
std::ios::trunc
truncate the file on open (overwrite) often with
out
std::ios::app
always write at the end (log/history) often with
out
std::ios::binary
work “as bytes”, without text transformations any streams

A practical thought: trunc and app are not “decorations”. They are two completely different scenarios. trunc says: “I create a new version of the file.” app says: “I keep a journal, I don't touch the old data.”

5. Why flags combine with |, not with ||

At this point one of the most common newbie mistakes usually happens: the hand reaches for || because it seems like OR. That's human-logical, but wrong in C++.

The operator || is logical OR. It works with boolean expressions, like x > 0 || y > 0, and its result is a bool.

But std::ios::openmode is a set of “bit flags”. So it's not truth/falsehood, it's about specific flags being on/off. To combine such flags you use bitwise OR |.

Minimal example “how to”:

#include <fstream>

int main() {
    std::ofstream log("taskbook.log", std::ios::out | std::ios::app);
    log << "App started\n";
}

Minimal example “how not to” (and why):

#include <fstream>

int main() {
    // ERROR: || returns bool, but open expects openmode.
    std::ofstream log("taskbook.log", std::ios::out || std::ios::app);
}

If the compiler complains here — it's not being annoying. It's genuinely saving you from a situation where the open mode suddenly becomes “true/false”, and then you open the file in a way you didn't intend.

6. Practice: save, log and open in binary

Overwrite the tasks file (trunc) — “save state”

When we click “Save”, most often we want to overwrite the file entirely: the old version is no longer needed, we create a new snapshot of the current memory.

This scenario in text format looks like: open the file for writing with trunc, iterate tasks, write each task as a line.

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

bool save_tasks_text(const std::string& filename, const std::vector<Task>& tasks) {
    std::ofstream out(filename, std::ios::out | std::ios::trunc);
    if (!out) return false;

    for (const Task& t : tasks) {
        out << to_line(t) << '\n';
    }
    return static_cast<bool>(out);
}

Note: std::ofstream usually opens a file “for writing”, and in many implementations it truncates by default. But we explicitly write std::ios::trunc because it makes the intent clear: “yes, I want to destroy the old content”. It's not about speed, it's about readability and safety.

Append a log (app) — “event journal”

Another frequent need is logging. For example, we want to write to a file on every app start, every task addition and every mark of "done". This is not a snapshot, this is a history of events. And here app is your best friend.

#include <fstream>
#include <string>

void append_log(const std::string& filename, const std::string& message) {
    std::ofstream log(filename, std::ios::out | std::ios::app);
    if (!log) return;

    log << message << '\n';
}

One subtlety: with app writes go to the end even if you tried to reposition the file pointer. We don't use seeking today, but it's useful to remember: app is “only add to the tail”.

Text vs binary: when std::ios::binary really matters

People often ask: “But I'm already writing bytes, why do I need binary at all?”

In terms of portability binary matters because in text mode the library may perform end-of-line transformations. The most famous example is the historical difference between "\n" in program and how “end of line” is stored in a text file on a given platform. If you write and read as “text” that's usually fine — the library gives you convenience. If you write and read as “data” — any transformations are dangerous because you expect bytes to match 1-to-1.

So the rule is simple: if you store “human text” open as text (i.e., without binary). If you store a “data format where every byte matters” open with binary.

Compare in a table:

Question Text format Binary format
Can you open and understand it by eye? yes almost never
Easy to debug manually? yes no
Compactness usually worse usually better
Requires a strict schema moderately very much
Need std::ios::binary? usually no almost always yes

Mini open “for binary format” (no write, just demonstrating the mode):

#include <fstream>
#include <string>

bool open_binary_for_write(const std::string& filename) {
    std::ofstream out(filename, std::ios::out | std::ios::binary | std::ios::trunc);
    return static_cast<bool>(out);
}

The point here is not to “hurry to write binary”, but to see: binary is just another open mode flag like app or trunc, and it combines with | the same way.

7. Buffering and flush: why std::endl is sometimes slow

Now add one more practical thing: file output is buffered. So when you do out << "hello\n";, bytes may not immediately go to disk. They may wait in the buffer until it's full or until the stream is explicitly flushed.

An explicit buffer flush is called flush. In standard library terms it's related to flush() and manipulators like std::flush. Notes and discussions in editorial material emphasize that flush() behaves as an “unformatted output function” (i.e., it's a separate stream action, not about formatting text).

For a programmer this means: if you flush too often you can slow your program, because every flush is an attempt to push data out right now.

Compare '\n' and std::endl in action:

#include <fstream>

int main() {
    std::ofstream out("demo.txt", std::ios::out | std::ios::trunc);

    out << "Line 1\n";            // just a line break
    out << "Line 2" << std::endl; // line break + flush (usually)
}

A practical default rule: write '\n' to files and console, and use std::endl when you really need flush. For example, you write a log and after the line "CRITICAL ERROR" you want to be sure it reached the file before a possible crash.

If you need a flush without a line break, use std::flush or out.flush():

#include <fstream>

int main() {
    std::ofstream log("taskbook.log", std::ios::out | std::ios::app);

    log << "Before critical step...";
    log.flush(); // we want to push this out right now
}

8. Mini-app TaskBook: save + log

Let's assemble the pieces into a more coherent picture. Suppose we have a simple menu: add tasks, show, save, exit. UI beauty doesn't matter here; what matters are open modes and the chosen format.

Example “skeleton” main (shortened to avoid drowning in details):

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

int main() {
    std::vector<Task> tasks;
    tasks.push_back(Task{1, false, "Buy_milk"});

    if (!save_tasks_text("tasks.txt", tasks)) {
        std::cerr << "Cannot save tasks.txt\n";
        return 1;
    }

    append_log("taskbook.log", "Saved 1 task to tasks.txt");
    std::cout << "Saved!\n"; // Saved!
}

If you open tasks.txt, you'll see a perfectly human-readable:

1 0 Buy_milk

And taskbook.log will grow, not destroying old entries:

Saved 1 task to tasks.txt

And that's already two different “contracts” for working with files: one file is a “snapshot”, the other is a “history”.

To reinforce this mentally, it's useful to picture the process as a simple diagram:

flowchart TD
    A[Tasks in memory: vector<Task>] --> B{What kind of file?}
    B -->|Snapshot| C["open: out | trunc"]
    B -->|Event journal| D["open: out | app"]
    C --> E[Write all tasks]
    D --> F[Append 1 line]

9. fstream and the “read + write” mode

When you see std::fstream there's a temptation: “Oh! I'll open one file and both read and write.” That's possible, but in practice it's safer for a beginner to split scenarios: load separately (with std::ifstream), save separately (with std::ofstream). That reduces the chance of accidentally opening the file with the wrong mode and getting strange behavior.

If you still want to show how the in | out combination looks, here's a minimal “just open” example:

#include <fstream>
#include <string>

bool open_for_read_write(const std::string& filename) {
    std::fstream io(filename, std::ios::in | std::ios::out);
    return static_cast<bool>(io);
}

What's important here is not that we “can do everything”, but that you see the same mechanics: flags combine with |, and the set of flags describes your intent.

10. Common mistakes

Mistake #1: using || instead of | when combining flags.
It looks logical until you remember that || is a logical operator that yields a bool, while the open mode is a set of bit flags. At best you'll get a compile error (and that's good). At worst you'll pass true somewhere a mode was expected and spend a long time debugging why the file opened “incorrectly”.

Mistake #2: confusing trunc and app and accidentally losing data.
If you intended to keep a log and opened the file with trunc, you'll erase the history on every run. If you intended to save a snapshot and opened with app, duplicates will accumulate and the loader may later read 17 versions of the same task. This mistake is especially sneaky because the compiler doesn't complain: it doesn't know what you intended.

Mistake #3: treating std::endl as just a “line break”.
Yes, std::endl looks like '\n', but it usually also flushes. If you put std::endl inside a loop for a thousand log lines you may suddenly get slowdowns. Keep '\n' as the default and use flush as a deliberate action: “I need to push data now.”

Mistake #4: writing “binary data” in text mode (or vice versa) and hoping “it'll be fine”.
Sometimes it seems: “I'm writing bytes anyway, why this binary?” The problem is that text mode may apply transformations convenient for text but destructive for binary formats. So if you agreed that the file is a data format where bytes matter, open with std::ios::binary from the start.

Mistake #5: not making the open mode part of code readability.
Technically you can often rely on defaults, but for educational and team projects it's useful to write the intent explicitly: out | trunc for “save fresh”, out | app for “append log”, in | binary for “read as bytes”. This reduces surprises and makes code self-explanatory — almost like a comment, but the compiler also checks types.

1
Task
C++ SELF, level 63, lesson 2
Locked
Append to log
Append to log
1
Task
C++ SELF, level 63, lesson 2
Locked
Snapshot of notes
Snapshot of notes
1
Task
C++ SELF, level 63, lesson 2
Locked
Binary capsule
Binary capsule
1
Task
C++ SELF, level 63, lesson 2
Locked
Double saving
Double saving
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION