Binary files change the rules of IO in one important way: the bytes are written exactly as they are, with no encoding and no newline translation. That makes them smaller, faster to read and exact, but it also means the file is only meaningful to a program that knows the format it was written in. A text file can be read with any editor; a binary file needs the code that wrote it.
Writing raw bytes
A stream in binary mode writes the bytes you give it, unchanged.
#include
#include
int main() {
std::ofstream file("readings.bin", std::ios::binary);
if (!file) {
std::cerr << "could not open readings.bin\n";
return 1;
}
const int values[3] = {42, 91, 12};
file.write(reinterpret_cast(values), sizeof(values));
file.close();
std::cout << "wrote " << sizeof(values) << " bytes\n";
return 0;
}The output shows the twelve bytes written — three integers of four bytes each:
wrote 12 bytesThe reinterpret_cast is what turns an array of int into something the stream can write. A stream writes characters, so a pointer to the raw bytes is what it needs.
Reading raw bytes
The mirror image is .read(), which fills a buffer and reports how many bytes it actually got.
#include
#include
int main() {
std::ifstream file("readings.bin", std::ios::binary);
if (!file) {
std::cerr << "could not open readings.bin\n";
return 1;
}
int values[3] = {0, 0, 0};
file.read(reinterpret_cast(values), sizeof(values));
std::cout << "bytes read: " << file.gcount() << '\n';
for (int value : values) {
std::cout << value << ' ';
}
std::cout << '\n';
return 0;
}The output shows the three values recovered exactly:
bytes read: 12
42 91 12.gcount() tells you how many bytes the last read actually produced, which is how you detect a file that is shorter than you expected.
Writing a struct
A struct of plain data can be written directly, which makes a binary format very easy to define.
#include
#include
#include
struct Reading {
std::int32_t sensor_id;
std::int32_t value;
double timestamp;
};
int main() {
std::ofstream file("reading.bin", std::ios::binary);
if (!file) {
std::cerr << "could not open reading.bin\n";
return 1;
}
Reading reading{7, 91, 12345.5};
file.write(reinterpret_cast(&reading), sizeof(reading));
file.close();
std::ifstream check("reading.bin", std::ios::binary);
Reading loaded{0, 0, 0.0};
check.read(reinterpret_cast(&loaded), sizeof(loaded));
std::cout << "sensor: " << loaded.sensor_id << '\n';
std::cout << "value: " << loaded.value << '\n';
std::cout << "time: " << loaded.timestamp << '\n';
return 0;
}The output shows the struct recovered field by field:
sensor: 7
value: 91
time: 12345.5Writing a struct directly is convenient and dangerous. The file format is now tied to the compiler's layout of that struct: the padding, the size and the member order are all part of the format, and a different compiler may lay it out differently.
Writing a portable format
The safe alternative is to define the format explicitly: fixed-width integers, a documented field order, and no padding.
#include
#include
#include
void write_u32(std::ofstream& file, std::uint32_t value) {
unsigned char bytes[4] = {
static_cast((value >> 24) & 0xFF),
static_cast((value >> 16) & 0xFF),
static_cast((value >> 8) & 0xFF),
static_cast(value & 0xFF)
};
file.write(reinterpret_cast(bytes), 4);
}
std::uint32_t read_u32(std::ifstream& file) {
unsigned char bytes[4] = {0, 0, 0, 0};
file.read(reinterpret_cast(bytes), 4);
return (static_cast(bytes[0]) << 24) |
(static_cast(bytes[1]) << 16) |
(static_cast(bytes[2]) << 8) |
static_cast(bytes[3]);
}
int main() {
std::ofstream file("portable.bin", std::ios::binary);
write_u32(file, 4821);
file.close();
std::ifstream check("portable.bin", std::ios::binary);
std::cout << "value: " << read_u32(check) << '\n';
return 0;
}The output shows the value surviving the round trip:
value: 4821The bytes are written most significant first, which is called big endian. Most modern hardware is little endian, so writing the bytes explicitly is what makes the file portable between machines.
What binary files are good for
- Compact storage. A double takes eight bytes instead of the dozen or so a text representation needs.
- Exact values. Floating point survives a round trip without the precision loss of decimal text.
- Fast reading. One read of a thousand bytes beats a thousand formatted extractions.
- Random access. A fixed-size record sits at a known offset, so you can seek straight to it.
#include
#include
int main() {
std::ofstream file("table.bin", std::ios::binary);
for (int i = 0; i < 10; ++i) {
int value = i * i;
file.write(reinterpret_cast(&value), sizeof(value));
}
file.close();
std::ifstream check("table.bin", std::ios::binary);
check.seekg(4 * sizeof(int), std::ios::beg); // the fifth record
int value = 0;
check.read(reinterpret_cast(&value), sizeof(value));
std::cout << "fifth value: " << value << '\n';
return 0;
}The output shows the fifth record read directly by seeking to its offset:
fifth value: 16What binary files cost you
- No human readability. Inspecting the file needs a hex editor or the code that wrote it.
- No self-description. The reader must know the format, the field order and the types.
- Fragile against change. Adding a field to the struct changes the layout and breaks every existing file.
- Padding surprises. A struct may contain padding bytes that are never initialised, so writing the struct writes garbage into the file.
Quick reference
- Open with
std::ios::binaryto disable newline translation. .write(ptr, size)and.read(ptr, size)move raw bytes;.gcount()reports what was read.- Writing a struct directly ties the file to the compiler's layout, padding included.
- A portable format uses fixed-width integers and a documented byte order.
- Binary files are compact, exact and fast, but only readable by the program that understands them.
- Fixed-size records can be seeked to directly.
One more habit: document the format of a binary file next to the code that reads it. Field order, byte order and the version of the layout are all part of the format, and the only place they can be recorded is the source.
Try it yourself: write an array of ten doubles to a binary file, read it back and confirm the values are identical. Then write the same values as text and compare the file sizes. Finally, write a struct with a [[char]] and an [[int]] and print its [[sizeof]] to see how much padding the compiler inserted.