C++ / CONSOLE AND FILE INPUT-OUTPUT
Binary file I/O and positioning within a stream
Read and write raw bytes with std::ios::binary, and use seekg, seekp, tellg and gcount to treat a file of fixed-size records as a random-access array.
What you will learn
- Copy object bytes with write/read plus reinterpret_cast<char*> and sizeof.
- Jump to record n with seekg(n * sizeof(Record), std::ios::beg), no scanning.
- Measure a file with seekg(0, std::ios::end) followed by tellg().
- Call gcount() after read() to learn how many bytes actually arrived.
Understanding Binary file I/O and positioning within a stream
Streaming with << converts a value into characters: the int 300 becomes the three bytes '3', '0', '0'. write() does the opposite; it copies the object representation, so an std::int32_t always occupies exactly 4 bytes no matter how large the number is. That fixed width is the whole point, because when every record has the same size the byte offset of record n is just n * sizeof(Record), and you can land on any record in one seek instead of parsing everything before it.
std::ios::binary matters because in text mode the library is allowed to translate newlines. On Windows a written 0x0A byte becomes 0x0D 0x0A, and an int whose value happens to be 10 or 2570 contains such a byte, so the file silently grows and every offset after it is wrong. Positioning is done with seekg/seekp, each taking either an absolute position or an offset plus a direction (std::ios::beg, cur, end); tellg/tellp report the current byte offset. An ifstream has only a get pointer and an ofstream only a put pointer, while fstream has both and they move independently, which is why an in-place update needs a seekp for the write and a separate seekg before reading back.
Only trivially copyable types survive this treatment, and even then only within one build. Struct padding, integer width, and endianness are properties of the compiler and CPU, so the bytes are a memory dump, not a file format. Writing a struct that holds a std::string or a pointer stores a heap address rather than the characters, and reading it back in another process gives you a dangling pointer. If the file must be read by anything other than the program that wrote it, spell the layout out: fixed-width types from <cstdint>, a chosen byte order, and no reliance on the compiler's padding.
<cstdint>
<fstream>
<iostream>
int main() {
const std::int32_t temps[6] = {12, 19, 23, 21, 17, 9};
std::ofstream out("temps.bin", std::ios::binary);
out.write(reinterpret_cast<const char*>(temps), sizeof temps);
out.close();
std::ifstream in("temps.bin", std::ios::binary);
if (!in) { std::cerr << "cannot open temps.bin\n"; return 1; }
in.seekg(0, std::ios::end);
const std::streamoff size = in.tellg();
std::cout << size << " bytes = " << size / sizeof(std::int32_t) << " records\n";
in.seekg(3 * sizeof(std::int32_t), std::ios::beg);
std::int32_t one = 0;
in.read(reinterpret_cast<char*>(&one), sizeof one);
std::cout << "record 3 = " << one << ", gcount = " << in.gcount() << "\n";
in.seekg(-2 * static_cast<std::streamoff>(sizeof(std::int32_t)), std::ios::end);
std::int32_t tail[2] = {0, 0};
in.read(reinterpret_cast<char*>(tail), sizeof tail);
std::cout << "last two = " << tail[0] << " " << tail[1] << ", tellg = " << in.tellg() << "\n";
}Binary mode plus explicit byte offsets turn a file into a random-access array of raw bytes, where read/write copy object representations and seekg/seekp choose the position.
Worked examples
Overwrite one record in place
Patching a single record with seekp on an fstream, without rewriting or resizing the file.
<cstdint>
<fstream>
<iostream>
int main() {
const std::int32_t data[4] = {100, 200, 300, 400};
{
std::ofstream create("scores.bin", std::ios::binary);
create.write(reinterpret_cast<const char*>(data), sizeof data);
}
std::fstream f("scores.bin", std::ios::in | std::ios::out | std::ios::binary);
if (!f) { std::cerr << "cannot open scores.bin\n"; return 1; }
const std::int32_t patched = 999;
f.seekp(2 * sizeof(std::int32_t));
f.write(reinterpret_cast<const char*>(&patched), sizeof patched);
f.seekg(0);
std::int32_t back[4] = {0, 0, 0, 0};
f.read(reinterpret_cast<char*>(back), sizeof back);
for (std::int32_t v : back) std::cout << v << " ";
std::cout << "\n";
f.seekg(0, std::ios::end);
std::cout << "size still " << f.tellg() << " bytes\n";
}Example explained
Line 1std::fstream with in | out | binary opens the existing file without truncating it; an ofstream would have discarded the 16 bytes on open.
Line 2seekp(2 * sizeof(std::int32_t)) places the put pointer at byte 8, so the 4-byte write lands exactly on record 2 and no other record moves.
Line 3seekg(0) is a separate call because the get and put pointers are independent, and repositioning is also what makes switching from writing to reading safe.
Line 4The file is still 16 bytes: writing at an offset overwrites, it never inserts.
Detecting a truncated final record
How read() reports a partial record through gcount() and the failbit.
<cstdint>
<fstream>
<iostream>
int main() {
{
const char bytes[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::ofstream out("odd.bin", std::ios::binary);
out.write(bytes, sizeof bytes);
}
std::ifstream in("odd.bin", std::ios::binary);
std::int32_t rec = 0;
int n = 0;
while (in.read(reinterpret_cast<char*>(&rec), sizeof rec)) ++n;
std::cout << "full records: " << n << "\n";
std::cout << "leftover bytes: " << in.gcount() << "\n";
std::cout << "eof=" << in.eof() << " fail=" << in.fail() << "\n";
}Example explained
Line 1The loop tests the stream itself, so it stops the first time read() cannot deliver all four requested bytes.
Line 210 bytes hold two complete records; gcount() reports the 2 bytes of the third, which are sitting in rec as an incomplete value that must not be used.
Line 3read() sets failbit alongside eofbit on a short read, so fail() is 1 even though the file itself is intact.
Line 4A file length that is not a multiple of sizeof(rec) means the file is truncated or the record size assumption is wrong.
Seeing the byte order of a dumped integer
Reading back an int32 as individual bytes shows that write() stores the CPU's in-memory layout.
<cstdint>
<fstream>
<iostream>
int main() {
const std::uint32_t magic = 0x41424344u;
{
std::ofstream out("magic.bin", std::ios::binary);
out.write(reinterpret_cast<const char*>(&magic), sizeof magic);
}
std::ifstream in("magic.bin", std::ios::binary);
unsigned char b[4] = {0, 0, 0, 0};
in.read(reinterpret_cast<char*>(b), sizeof b);
std::cout << "as text: ";
for (unsigned char c : b) std::cout << static_cast<char>(c);
std::cout << "\nas hex:";
for (unsigned char c : b) std::cout << ' ' << std::hex << static_cast<int>(c);
std::cout << "\n";
}Example explained
Line 10x41424344 spells the bytes 41 42 43 44 ('A' 'B' 'C' 'D') in the literal, but write() copies them in the order the CPU stores them.
Line 2On a little-endian machine the low byte 0x44 ('D') sits first in memory, so the file, and therefore the printed text, is reversed relative to the literal.
Line 3static_cast<int> is required for the hex line, because streaming an unsigned char prints a character rather than a number.
Line 4std::hex is sticky, so it stays in effect for the rest of the stream once set.
Important notes
seekg clears eofbit but not failbit, so after a short read at the end of a file you must call clear() before repositioning, otherwise the seek is ignored.
The seekg(0, std::ios::end) plus tellg() trick only works on seekable files; on a pipe or on std::cin the seek fails and tellg returns -1, and for a real file std::filesystem::file_size is the more direct answer.
Common mistakes
Omitting std::ios::binary: on Windows every 0x0A byte inside the data becomes 0x0D 0x0A, so the file is longer than count * sizeof(record) and all later offsets point into the middle of a record, producing garbage values rather than an error.
Mixing << with read(): out << 300 stores the characters "300", so a later read() of 4 bytes reinterprets ASCII digits as an integer and returns a huge unrelated number instead of 300.
Dumping a struct that contains a std::string or pointer: the bytes written are a heap address and internal bookkeeping, so reading them back in a new process yields a dangling pointer and usually a crash.
Try it yourself
Change, predict, then run
Write the values 1 through 10 as std::int32_t into nums.bin, then reopen it in binary and print only records 0, 5 and 9 by seeking to each computed byte offset instead of reading the whole file. Finish by seeking to the end and confirming tellg() reports 40.
Open the C++ workspaceCheck your understanding
A program writes five std::int32_t values with write(), then does seekg(0, std::ios::end) and tellg(), and on Windows it prints 25 instead of 20. What is the most likely cause?
- seekg(0, std::ios::end) leaves the position one byte past the last byte, so tellg always reports size + 1
- sizeof(std::int32_t) is 5 in that compiler's ABI
- The file was opened without std::ios::binary, so a 0x0A byte inside one of the integers was stored as 0x0D 0x0A
- tellg() counts the end-of-file marker byte that every file carries
Show answer
In text mode a newline byte is expanded on write, and an int32 holding a value such as 10 or 2570 contains the byte 0x0A, so the file gains a byte and every offset after it shifts. Option 0 is tempting because off-by-one at the end is a common bug, but std::ios::end with offset 0 is exactly the byte count; if it were wrong it would be wrong for every file, not only this one, and it would not depend on the values written.