iostream formatting
A binary file stores bytes, not lines. The stream functions write and read take a pointer and a count, and the program decides what the bytes mean: an array of ints, a struct, a compressed block.
Binary is compact and fast and not portable: the bytes of an int depend on its size and byte order, and the bytes of a struct depend on padding. The last section writes a struct and says exactly why the file only makes sense to the program that wrote it.
The two ends of a binary file are the program's responsibility: the writer decides the layout and the reader has to use the same one. A version number at the start of the file is the cheapest way to notice that the layout changed.
Writing bytes
std::ios::binary turns off the text translation of newline characters. write takes a const char* and a byte count, so the address of an array is cast to that pointer type.
The program writes three ints and prints tellp(), the number of bytes written. On this build an int is four bytes, so the answer is 12.
The cast with reinterpret_cast is the point where the program says the bytes are the representation and not a text. It is also the point where the file stops being portable, which the last section explains.
#include <fstream>
#include <iostream>
int main()
{
std::ofstream out{"values.bin", std::ios::binary};
int values[]{10, 20, 30};
out.write(reinterpret_cast<const char*>(values), sizeof(values));
std::cout << out.tellp() << '\n';
}
g++ -std=c++23 -Wall -Wextra write.cpp -o write
./write
12
Reading bytes back
read takes a char* and a byte count and copies that many bytes from the file into the buffer. The buffer has to be large enough, and its type has to match what was written.
The program writes three ints, closes the stream and opens the file for reading. The zeros in the array initialisation make it visible that read filled every element.
read does not check that the file has that many bytes. After the call, gcount() tells how many bytes were actually read, and the stream's fail bit is set when the count is short.
#include <fstream>
#include <iostream>
int main()
{
{
std::ofstream out{"values.bin", std::ios::binary};
int values[]{10, 20, 30};
out.write(reinterpret_cast<const char*>(values), sizeof(values));
}
std::ifstream in{"values.bin", std::ios::binary};
int values[3]{};
in.read(reinterpret_cast<char*>(values), sizeof(values));
for (int value : values) {
std::cout << value << ' ';
}
std::cout << '\n';
}
g++ -std=c++23 -Wall -Wextra read.cpp -o read_binary
./read_binary
10 20 30
Seeking to a record
A binary file of fixed-size records can be read in any order. seekg moves the read position, and the position is a byte offset, so record n starts at n * sizeof(Record).
The program writes five ints and reads the fourth by seeking to byte 12. The read position can be moved backwards as well, which is what makes a file with an index practical.
tellg returns the current position and -1 at the end of the file. A common way to get the size of a file is to seek to the end and ask for the position, which is the next section's job in lesson 069 with file_size.
#include <fstream>
#include <iostream>
int main()
{
{
std::ofstream out{"records.bin", std::ios::binary};
int values[]{1, 2, 3, 4, 5};
out.write(reinterpret_cast<const char*>(values), sizeof(values));
}
std::ifstream in{"records.bin", std::ios::binary};
in.seekg(3 * sizeof(int));
int fourth = 0;
in.read(reinterpret_cast<char*>(&fourth), sizeof(fourth));
std::cout << fourth << ' ' << in.tellg() << '\n';
}
g++ -std=c++23 -Wall -Wextra seek.cpp -o seek
./seek
4 16
A struct as a record
A plain struct with fixed-size members can be written as one block. The program writes a record with an int and two shorts, reads it back and prints the fields.
sizeof(record) is eight bytes even though the members add up to eight: the int plus two shorts, with the padding the compiler chose. The file contains that padding too, which is why the format is the struct's memory layout.
A format read by another program should write the fields one by one with fixed-width types, not the struct as a block. The block form is fine for a cache file that the same build reads back.
#include <fstream>
#include <iostream>
struct record
{
int id;
short x;
short y;
};
int main()
{
record written{7, 3, 4};
{
std::ofstream out{"point.bin", std::ios::binary};
out.write(reinterpret_cast<const char*>(&written), sizeof(written));
}
record read_back{};
std::ifstream in{"point.bin", std::ios::binary};
in.read(reinterpret_cast<char*>(&read_back), sizeof(read_back));
std::cout << read_back.id << ' ' << read_back.x << ' ' << read_back.y << ' '
<< sizeof(record) << '\n';
}
g++ -std=c++23 -Wall -Wextra struct.cpp -o struct_binary
./struct_binary
7 3 4 8
read on an output stream
An ofstream writes; it has no read member. Calling read on one is a type error, and the compiler lists the members it does have, none of which match.
The program opens a binary file for writing and calls read. The message says there is no matching function and shows the declaration, which takes a char* and a count.
The fix is an ifstream, or an fstream opened with both std::ios::in and std::ios::out. The two-directional stream is the third type in <fstream> and the one to use when a file is updated in place.
#include <fstream>
int main()
{
std::ofstream out{"data.bin", std::ios::binary};
int value = 0;
out.read(reinterpret_cast<char*>(&value), sizeof(value));
}
g++ -std=c++23 -Wall -Wextra read_on_output.cpp -o read_on_output
read_on_output.cpp: In function 'int main()':
read_on_output.cpp:7:9: error: 'std::ofstream' {aka 'class std::basic_ofstream<char>'} has no member named 'read'
7 | out.read(reinterpret_cast<char*>(&value), sizeof(value));
| ^~~~
Exercises
- Write a vector of doubles to a binary file and read it back into a vector of the same size.
- Write five records, then read only the third one with seekg.
- Compare the size of a text file of the numbers 1 to 100 with a binary file of the same ints.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
writetake?Answer
A pointer to bytes and a count
The function copies the raw bytes, and the program decides what they mean.
-
Why is a binary file less portable than a text file?
Answer
The bytes of an int depend on its size and byte order
Two machines can disagree about the representation of a number.
-
What does
seekgdo?Answer
It moves the read position
The g stands for get, the read position; seekp is the write position.
-
What does
reinterpret_cast<char*>do in a write call?Answer
It views the object's bytes as characters
The stream interface takes a byte pointer, and the cast names that view.
-
What does this program print?
#include <fstream> #include <iostream> int main() { int values[]{10, 20, 30}; { std::ofstream out{"data.bin", std::ios::binary}; out.write(reinterpret_cast<char*>(values), sizeof(values)); } std::ifstream in{"data.bin", std::ios::binary}; in.seekg(0, std::ios::end); std::cout << in.tellg(); }Answer
12
Three ints are four bytes each, so the file is twelve bytes long.
-
What does this program print?
#include <fstream> #include <iostream> int main() { { std::ofstream out{"data.bin", std::ios::binary}; int values[]{10, 20, 30}; out.write(reinterpret_cast<char*>(values), sizeof(values)); } int values[3]{}; std::ifstream in{"data.bin", std::ios::binary}; in.read(reinterpret_cast<char*>(values), sizeof(values)); std::cout << values[0] << ' ' << values[1] << ' ' << values[2]; }Answer
10 20 30
The bytes are read back into the array in the same order.
-
What does this program print?
#include <fstream> #include <iostream> int main() { { std::ofstream out{"rec.bin", std::ios::binary}; int values[]{10, 20, 30, 40}; out.write(reinterpret_cast<char*>(values), sizeof(values)); } std::ifstream in{"rec.bin", std::ios::binary}; int value = 0; in.seekg(3 * sizeof(int)); in.read(reinterpret_cast<char*>(&value), sizeof(value)); std::cout << (in.tellg() / sizeof(int)) << ' ' << value; }Answer
4 40
The seek moves past three ints, the read gets 40, and the position is four ints into the file.
-
What does this program print?
#include <cstring> #include <fstream> #include <iostream> struct record { int id; char name[8]; double score; }; int main() { record item{7, "ada", 3.5}; { std::ofstream out{"rec.bin", std::ios::binary}; out.write(reinterpret_cast<char*>(&item), sizeof(item)); } record back{}; std::ifstream in{"rec.bin", std::ios::binary}; in.read(reinterpret_cast<char*>(&back), sizeof(back)); std::cout << back.id << ' ' << back.name << ' ' << back.score << ' ' << sizeof(record); }Answer
7 ada 3.5 24
The whole struct is written and read back, and its size includes the padding after the char array.
-
What does the compiler report for this line?
#include <fstream> int main() { std::ofstream out{"x.bin", std::ios::binary}; int value = 1; out.read(reinterpret_cast<char*>(&value), sizeof(value)); }Answer
'std::ofstream' has no member named 'read'
An output stream has write but not read; the input stream is the one with read.
-
Why can a binary file be written faster than a text file?
Answer
The bytes are copied without formatting
There is no conversion to digits and separators, just a memory copy.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev