C / FILES AND ERRORS
fread and fwrite for binary data
Move structs and arrays between memory and a file with fwrite and fread, read their element counts correctly, and tell a short read from a real error.
What you will learn
- Round-trip an array of structs through a file with fwrite and fread using sizeof
- Treat fread's return as a count of complete elements, not a byte count
- Use feof and ferror to tell a truncated file from a genuine read error
- Spot why padding, int width and byte order make dumped structs non-portable
Understanding fread and fwrite for binary data
fwrite and fread perform no conversion: they copy the object representation, the exact bytes an object occupies in memory, out to a stream and back. fprintf(f, "%g", 21.5) puts the four characters 21.5 in the file, while fwrite(&d, sizeof d, 1, f) puts the eight bytes that the double physically occupies. That is why binary I/O is fast and why the file looks like noise in a text editor: there is nothing to parse on the way back, only bytes to copy into an object of the same type.
Both functions take a size and a count rather than one total byte count, and the reason shows up in the return value, which is the number of complete elements transferred and never a number of bytes. fread(buf, sizeof buf[0], 8, f) returning 5 means five whole records arrived and the request came up short; if the leftover bytes cannot fill one more element, that partial element is not counted and its contents are indeterminate. A short fread is ambiguous until you ask feof and ferror which one happened, whereas a short fwrite is never ambiguous, because there is no end of file when writing, so it always means failure.
Since the bytes are a memory image, the file inherits everything about the machine that wrote it. A struct holding an int and a double is 16 bytes on x86-64, four of them padding your code never assigned; the int's bytes are stored least significant first; a char * member is stored as an address rather than the characters it points at. fread copies all of that straight back, so an fwrite dump is only trustworthy when read by the same program on the same ABI, which is why durable formats specify fixed-width fields and a byte order instead of dumping structs.
<stdio.h>
struct Reading {
int sensor_id;
double celsius;
};
int main(void)
{
struct Reading out[3] = { { 11, 21.5 }, { 12, -3.25 }, { 13, 100.0 } };
struct Reading in[3];
struct Reading extra;
FILE *f;
size_t n, m, i;
f = fopen("readings.bin", "wb");
if (f == NULL) {
perror("readings.bin");
return 1;
}
n = fwrite(out, sizeof out[0], 3, f);
if (fclose(f) != 0) {
perror("closing readings.bin");
return 1;
}
printf("fwrite returned %zu of 3\n", n);
if (n != 3) {
fprintf(stderr, "short write, file is incomplete\n");
return 1;
}
f = fopen("readings.bin", "rb");
if (f == NULL) {
perror("readings.bin");
return 1;
}
n = fread(in, sizeof in[0], 3, f);
printf("fread returned %zu of 3\n", n);
m = fread(&extra, sizeof extra, 1, f);
printf("second fread returned %zu (feof=%d, ferror=%d)\n",
m, feof(f) != 0, ferror(f) != 0);
fclose(f);
for (i = 0; i < n; i++)
printf("sensor %d -> %.2f C\n", in[i].sensor_id, in[i].celsius);
printf("one struct = %zu bytes, file = %zu bytes\n",
sizeof(struct Reading), 3 * sizeof(struct Reading));
return 0;
}
fread and fwrite copy raw object representations between memory and a stream, and their return value counts complete elements, not bytes.
Worked examples
A truncated record disappears
Shows that fread reports only whole elements, so a file whose length is not a multiple of the record size loses its tail.
<stdio.h>
int main(void)
{
int values[2] = { 1000, 2000 };
unsigned char tail[2] = { 0xAA, 0xBB };
FILE *f;
int v;
size_t n;
long count = 0;
f = fopen("stream.bin", "wb");
if (f == NULL) {
perror("stream.bin");
return 1;
}
fwrite(values, sizeof values[0], 2, f);
fwrite(tail, 1, sizeof tail, f); /* two stray bytes: half a record */
if (fclose(f) != 0) {
perror("closing stream.bin");
return 1;
}
f = fopen("stream.bin", "rb");
if (f == NULL) {
perror("stream.bin");
return 1;
}
while ((n = fread(&v, sizeof v, 1, f)) == 1)
printf("record %ld = %d\n", ++count, v);
printf("loop stopped with fread returning %zu\n", n);
printf("feof=%d ferror=%d\n", feof(f) != 0, ferror(f) != 0);
fclose(f);
return 0;
}
Example explained
Line 1fwrite(tail, 1, sizeof tail, f) appends two bytes, so the file is 10 bytes: two whole ints plus a record that is two bytes short.
Line 2The loop condition fread(&v, sizeof v, 1, f) == 1 accepts nothing but a full element, which is the only safe test for fixed-size records.
Line 3The third call finds two bytes, cannot complete an int, and returns 0; v must not be printed after that, since a partially filled element holds an indeterminate value.
Line 4feof=1 with ferror=0 proves the stop was a short file rather than a device error, so this is a format problem to report, not an I/O failure.
What the bytes really look like
Writes one unsigned int and reads it back byte by byte to expose the in-memory layout that fwrite copied.
<stdio.h>
int main(void)
{
unsigned int value = 0x12345678u;
unsigned char bytes[sizeof value];
FILE *f;
size_t i, n;
f = fopen("word.bin", "wb");
if (f == NULL) {
perror("word.bin");
return 1;
}
if (fwrite(&value, sizeof value, 1, f) != 1) {
fprintf(stderr, "write failed\n");
fclose(f);
return 1;
}
if (fclose(f) != 0) {
perror("closing word.bin");
return 1;
}
f = fopen("word.bin", "rb");
if (f == NULL) {
perror("word.bin");
return 1;
}
n = fread(bytes, 1, sizeof bytes, f);
fclose(f);
printf("wrote 0x%X as %zu bytes\n", value, n);
for (i = 0; i < n; i++)
printf("byte %zu = 0x%02X\n", i, bytes[i]);
return 0;
}
Example explained
Line 1fwrite(&value, sizeof value, 1, f) copies the four bytes of the unsigned int exactly as the CPU stores them; no digit characters are produced.
Line 2Reading with size 1 and count sizeof bytes asks for four one-byte elements, so here the return value happens to equal a byte count: the split between size and count is yours to choose.
Line 30x78 appears first because x86 is little-endian, so the same file interpreted as an unsigned int on a big-endian machine reads as 0x78563412.
Line 4Nothing in the file records the width or byte order, which is why the reader must already agree with the writer about both.
Important notes
The sizes in the output are for x86-64 Linux: 4-byte int, a struct padded to 16 bytes, little-endian. Another ABI gives different numbers and a file the first machine cannot read back.
The b in "wb" and "rb" is not decoration on Windows: in text mode a 0x0A byte inside a double becomes 0x0D 0x0A, so the file grows and the values come back wrong.
Common mistakes
Writing a struct through a pointer with fwrite(p, sizeof p, 1, f): sizeof p is the pointer size, so only 8 bytes of a 16-byte record reach the file and every later record is misaligned.
Comparing fread's result to a byte count, as in if (fread(buf, sizeof buf[0], 4, f) != sizeof buf), which makes a perfectly complete read look like a failure and throws away good data.
Dumping a struct that contains a char *name: the file stores an address, so after the program restarts the reloaded record points at memory that no longer exists and dereferencing it crashes or prints garbage.
Try it yourself
Change, predict, then run
Write the ints 1 through 5 to a file with a single fwrite, then reopen it and read three at a time in a loop, printing each fread return value until it returns 0. Confirm you see 3, then 2, then 0 with feof set, and that the second call's two elements are still valid data.
Open the C workspaceCheck your understanding
A file contains exactly 10 bytes. With 4-byte ints, the code runs int v[3]; size_t n = fread(v, sizeof v[0], 3, f); What happens?
- n is 2, the two complete ints are valid, and feof(f) becomes true
- n is 3, with the two leftover bytes zero-padded into v[2]
- n is 10, because fread returns the number of bytes it transferred
- n is 0 and ferror(f) is set, because the file size is not a multiple of the element size
Show answer
fread transfers whole elements only, so 10 bytes yields two complete 4-byte ints, a return of 2, and the end-of-file indicator set. The tempting answer is 3 with a padded last element, but nothing pads a partial element: the two remaining bytes cannot form an int, no third element is reported, and v[2] may hold indeterminate bytes. A truncated tail is end of file, not an error, so ferror stays clear.