File Handling · intermediate · ~10 min
- Call `fread(buf, sz, n, f)` correctly and interpret its return value as an *item count*, not a byte count. - Read raw bytes and fixed-size records (structs, arrays of numbers) from a file opened in binary mode. - Distinguish a clean end-of-file from a real read error using `feof` and `ferror`. - Read a file in a loop, processing each chunk safely until EOF. - Allocate buffers that are large enough and bounds-safe for the data you read. - Know when `fread` is the right tool and when `fgets`/`fscanf` fit better.
In the previous lesson, fopen and the stdio model, you learned how to open a FILE * stream and why C buffers I/O for you. fopen gives you the door into a file; fread is one of the main ways to actually pull data through that door.
fread is the C standard library's general-purpose binary read function. "Binary" here simply means raw bytes — fread does not interpret newlines, does not split on whitespace, and does not stop at any delimiter. You tell it exactly how many bytes you want, it copies that many (or as many as exist) straight into a buffer you provide, and it tells you how many it got.
This makes fread ideal for two situations:
ints, or a sequence of identical struct records, written out earlier by fwrite (the next lesson in this category).The function signature is:
size_t fread(void *buf, size_t sz, size_t n, FILE *f);
It reads up to n items, each sz bytes long, into buf, and returns the number of complete items it actually read. That return value — items, not bytes — is the single most important thing to understand about fread, and the most common source of bugs. The rest of this lesson builds your mental model around it.
Almost every program that touches stored data eventually reads bytes the way fread does. Image loaders, audio decoders, game save files, database page caches, compression tools, network protocol parsers, and file-copy utilities all read fixed-size chunks or records from a stream. fgets and fscanf are convenient for human-readable text, but the moment your data is structured binary — a header followed by records, a pixel buffer, a serialized struct — fread is the correct primitive.
It also matters for robustness. A surprising number of real bugs come from assuming a read "just worked": treating a short read as success, forgetting that EOF and error look identical in the return value, or writing past the end of a too-small buffer. These are not academic concerns — a short read mishandled in a binary parser is a classic source of crashes and corrupted data. Learning fread properly means learning to check what you got every single time.
fread contract: items, not bytesDefinition. size_t fread(void *buf, size_t sz, size_t n, FILE *f) attempts to read n items of sz bytes each from stream f into memory at buf, advancing the file position by the number of bytes actually read, and returns the count of complete items read.
Plain language. You describe the data as "n things, each sz bytes big." fread copies up to that many bytes and reports back in things, not bytes. If you asked for 10 items and got 7, exactly 7 complete items are in your buffer.
How it works internally. fread pulls bytes from the stream's internal buffer (filled by the OS in big reads). It copies sz * n bytes if available; if the stream hits EOF partway, it copies whatever complete items it could and stops. A partial trailing item (fewer than sz bytes left) is not counted, and those leftover bytes stay unread in the stream.
When to use / when not. Use it for raw bytes and fixed-size records. Do not use it to read text line-by-line (fgets is built for that) or to parse formatted text like "42 3.14" (fscanf is built for that).
Pitfall. Reading the return value as a byte count. With sz = sizeof(int) and n = 5, a return of 5 means 5 ints (20 bytes), not 5 bytes.
fread(buf, sz=4, n=5, f) request: 5 items * 4 bytes = 20 bytes
file has only 14 bytes left:
[item0][item1][item2][lefto]
4 4 4 2 <- partial, NOT counted
returns 3 (3 complete items copied; 12 bytes consumed)
the 2 leftover bytes remain unread in the stream
Knowledge check. You call
fread(buf, sizeof(int), 4, f)and it returns2. How many bytes were copied intobuf, assumingsizeof(int)==4?
Definition. A return value smaller than n means "I could not give you everything." It does not tell you why. The two reasons are: the file ended (EOF), or a hardware/OS read error occurred.
How to tell them apart. After a short read, ask the stream:
feof(f) is non-zero if the end of the file was reached.ferror(f) is non-zero if a read error occurred.These are two independent flags stored on the FILE object. A normal end-of-input gives feof true and ferror false. A disk error gives ferror true.
When to check. Always check after the loop or after a read that returned fewer items than requested. For a clean byte-streaming loop, the usual idiom checks ferror once after the loop ends.
Pitfall. Calling feof(f) before reading to decide whether to read. feof only becomes true after a read attempt fails to find more data. Looping with while (!feof(f)) and reading inside typically processes the last chunk twice or reads garbage. Drive the loop with the fread return value instead.
short read (n_read < n)
|
v
ferror(f) ? --yes--> real I/O error: report/handle, do NOT trust buffer
|
no
v
feof(f) ? --yes--> normal end of file: process what you got, stop
Knowledge check. Why is
while (!feof(f)) { fread(...); use(...); }a buggy pattern? What should drive the loop instead?
Definition. The buf you pass must point to writable memory of at least sz * n bytes. fread does no allocation and no bounds checking — it trusts your sizes completely.
Binary mode. Open the file with "rb" (from the fopen lesson). On Windows, plain "r" (text mode) translates \r\n to \n and may stop early at a Ctrl-Z byte, corrupting binary data. On Linux/macOS "r" and "rb" behave the same, but always write "rb" for binary reads so the code is portable.
Choosing a chunk size. For bulk copying, a stack buffer like unsigned char buf[4096] is a good default — large enough to amortize call overhead, small enough not to risk a stack overflow. For records, size the buffer to the record type: struct Rec recs[64]; then fread(recs, sizeof(struct Rec), 64, f).
Pitfall. Multiplication overflow in sz * n. If both come from untrusted input, sz * n can wrap around and you allocate too little. Validate sizes before allocating.
memory you must provide:
buf ->|<------------- sz * n bytes (at least) -------------->|
[ item 0 ][ item 1 ][ item 2 ] ... [ item n-1 ]
fread writes left to right, never past sz*n, never allocates for you
Knowledge check. You declare
int data[3];and callfread(data, sizeof(int), 10, f). What is the bug, and what undefined behavior can it cause?
#include <stdio.h>
size_t fread(void *buf, // destination: writable memory >= sz*n bytes
size_t sz, // size in bytes of ONE item
size_t n, // number of items you want
FILE *f); // stream opened for reading (binary: "rb")
// returns: number of COMPLETE items read (0..n)
Two common ways to call it, depending on intent:
// (a) Byte-oriented: treat each byte as one item.
// Return value == number of bytes read. Handy for bulk copying.
size_t bytes = fread(buf, 1, sizeof buf, f);
// (b) Record-oriented: each item is a whole struct/value.
// Return value == number of records read.
struct Rec recs[64];
size_t got = fread(recs, sizeof(struct Rec), 64, f);
After any short read, classify it:
if (got < want) {
if (ferror(f)) { /* I/O error */ }
else if (feof(f)) { /* clean end of file */ }
}
fread doessize_t fread(void *buf, size_t sz, size_t n, FILE *f);
fread reads up to n items, where each item is sz bytes. It stores them in the buffer buf.
It returns the number of items it actually read. This can be fewer than n if the file reaches its end (EOF) or if an error occurs.
When fread returns fewer items than requested, you cannot tell why from the return value alone. Check which happened:
feof(f) is true if the end of the file was reached.ferror(f) is true if a read error occurred.Use fread for binary data or fixed-size records. For reading text one line at a time, use fgets instead.
#include <stdio.h>
#include <stdlib.h>
/* Read a whole file in 4 KB chunks, count the bytes, and report cleanly. */
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "usage: %s <file>\n", argv[0]);
return 1;
}
FILE *f = fopen(argv[1], "rb"); /* binary mode for portable byte reads */
if (!f) {
perror("fopen");
return 1;
}
unsigned char buf[4096];
size_t total = 0;
size_t n;
/* Item size is 1 byte, so the return value is a byte count.
Loop continues while we keep getting bytes. */
while ((n = fread(buf, 1, sizeof buf, f)) > 0) {
total += n;
/* ...process the first n bytes of buf here... */
}
/* A return of 0 ends the loop. Find out why before trusting the result. */
if (ferror(f)) {
perror("fread");
fclose(f);
return 1;
}
/* Otherwise feof(f) is true: we reached the end normally. */
printf("read %zu bytes\n", total);
if (fclose(f) != 0) { /* closing can fail; check it */
perror("fclose");
return 1;
}
return 0;
}
What it does. The program opens the file named on the command line in binary mode, then repeatedly reads up to 4096 bytes at a time. Because sz is 1, each fread returns the number of bytes read, which it adds to total. The loop ends when fread returns 0. It then checks ferror to separate a real I/O failure from a normal end-of-file, prints the total, and closes the file (checking that the close itself succeeds).
Expected output. For a file of 10000 bytes, two full 4096-byte reads plus a final 1808-byte read give:
read 10000 bytes
Edge cases. An empty file makes the very first fread return 0, the loop body never runs, feof is true, and it prints read 0 bytes. A file that disappears mid-read or lives on failing media trips ferror, and the program reports the error instead of a (wrong) byte count.
Walkthrough of the key loop and its aftermath, assuming a 10000-byte file:
fopen(argv[1], "rb") opens the file for binary reading and returns a FILE *. The if (!f) guard catches a missing or unreadable file and reports via perror.unsigned char buf[4096] reserves a 4096-byte stack buffer — the destination for each chunk. total accumulates the byte count.fread(buf, 1, 4096, f). The stream has 10000 bytes, so all 4096 are copied; the call returns 4096. n = 4096 > 0, so the body runs and total becomes 4096.4096; total becomes 8192. The file position is now at byte 8192.10000 - 8192 = 1808 bytes remain. fread copies all 1808 (they form 1808 one-byte items) and returns 1808. total becomes 10000.fread returns 0, the stream's EOF flag is set, and the while condition is false, so the loop exits.ferror(f) is 0 (no I/O error), so the error branch is skipped. Implicitly feof(f) is true — a clean end.printf("read %zu bytes\n", total) prints read 10000 bytes. The %zu specifier matches size_t.fclose(f) flushes/releases the stream; the return check catches a rare close failure (e.g. on networked filesystems).Trace of how state changes:
iter | bytes available | fread returns n | total after
-----+-----------------+-----------------+------------
1 | 10000 | 4096 | 4096
2 | 5904 | 4096 | 8192
3 | 1808 | 1808 | 10000
4 | 0 | 0 | 10000 (loop ends)
Mistake 1 — Treating the return value as a byte count when sz != 1.
/* WRONG: ints, but counting the return value as bytes */
int nums[5];
size_t got = fread(nums, sizeof(int), 5, f);
printf("read %zu bytes\n", got); /* prints 5, not 20 */
fread returns items. With sz = sizeof(int), a return of 5 means 5 ints. Fix: either multiply, or read with sz = 1.
size_t got = fread(nums, sizeof(int), 5, f);
printf("read %zu ints (%zu bytes)\n", got, got * sizeof(int));
Recognize it: your byte counts are suspiciously small (off by a factor of sizeof(item)).
Mistake 2 — Driving the loop with feof.
/* WRONG: feof is true only AFTER a failed read */
while (!feof(f)) {
fread(buf, 1, sizeof buf, f);
process(buf, sizeof buf); /* may process stale/garbage bytes */
}
The last iteration reads 0 bytes but still calls process on whatever was in buf. Fix: let the return value drive the loop, as in the main example (while ((n = fread(...)) > 0)), and only ever process the n bytes you were told you got.
Mistake 3 — Assuming a full read.
/* WRONG: ignores short reads */
struct Header h;
fread(&h, sizeof h, 1, f);
use(&h); /* h may be partially/never filled if file too short */
If the file is shorter than sizeof h, fread returns 0 and h is garbage. Fix: check the count.
if (fread(&h, sizeof h, 1, f) != 1) {
fprintf(stderr, "file too short or read error\n");
return 1;
}
Mistake 4 — Buffer too small for the request.
/* WRONG: buffer holds 3 ints, asks for 10 */
int data[3];
fread(data, sizeof(int), 10, f); /* writes past data -> UB */
Fix: make n no larger than the buffer's capacity in items: fread(data, sizeof(int), 3, f).
Compiler warnings. Build with -Wall -Wextra. A common one here is -Wformat complaining that %zu doesn't match the argument — that usually means you passed an int where a size_t is expected, or vice versa. Match size_t with %zu.
"It reads garbage / wrong numbers." Most often this is a byte-order or mode issue, not an fread bug:
"rb"? On Windows, "r" mangles binary data.int size, alignment/padding, or endianness? Binary records are only portable across identical layouts."It stops early." Print the return value and check the flags right where the short read happens:
size_t got = fread(buf, sz, n, f);
fprintf(stderr, "got=%zu eof=%d err=%d\n", got, feof(f), ferror(f));
If err is set, the underlying read failed (permissions, removed media, networked FS); perror("fread") prints the OS reason. If only eof is set, the file really was that short.
"It crashes." Run under a memory checker — valgrind ./prog file on Linux, or compile with -fsanitize=address (Clang/GCC). An out-of-bounds write from too-small a buffer, or reading from a NULL FILE * after a failed fopen, shows up immediately.
Questions to ask when it doesn't work. Did fopen actually succeed? Is sz * n what I think it is? Am I processing exactly got items, never n? Did I check ferror after the loop, not just feof?
fread is a thin, trusting wrapper over an OS read — it performs no bounds checking and no allocation. The safety burden is entirely on the caller.
buf points to at least sz * n writable bytes. The classic bug is a buffer declared for K items while n > K is passed, causing an out-of-bounds write (undefined behavior, often a crash or silent corruption). Keep n ≤ buffer capacity in items.fread returned. For a single record, treat fread(&rec, sizeof rec, 1, f) != 1 as failure and do not use rec.sz * n is computed in size_t. If sz and n come from a file header or other untrusted source, the product can wrap around, leading you to under-allocate and then overflow. Validate both factors (e.g. reject implausibly large n) before allocating with malloc(sz * n).malloc must be freed exactly once after use.fopen failed and you pass its NULL result to fread, behavior is undefined. Always check fopen's return first.Concrete use case. Media and file tools rely on fread constantly. A WAV or PNG loader reads a fixed-size header struct, validates a magic number, then reads the payload in chunks. A file-copy utility (cp-style) loops fread/fwrite over a 4–64 KB buffer. A checksum tool (md5sum, sha256sum) streams a file through fread chunk by chunk so it never needs to hold the whole file in memory. Game engines load save files and asset packs as sequences of fread record reads.
Professional best-practice habits.
Beginner rules:
ferror after reading, and treat a short single-record read as failure."rb"; close with fclose and check the result.sizeof the actual type for sz (e.g. sizeof(struct Rec)), never a hard-coded number.Advanced habits:
sz * n overflow.fread-ing raw structs, since padding/endianness differ per platform.1. (Beginner) Byte counter. Write a program that takes a filename argument, opens it with "rb", and prints the total number of bytes using an fread loop with a 1024-byte buffer. Print read N bytes. Requirements: check fopen, drive the loop with the return value, check ferror after the loop, fclose at the end. Example: on a 3000-byte file → read 3000 bytes. Concepts: loop idiom, byte-as-item reads, ferror.
2. (Beginner) First N bytes. Implement int read_first_bytes(const char *path, char *buf, int n) that reads up to n bytes from the start of the file into buf and returns how many were actually read (or -1 on open/read error). Hint: a single fread(buf, 1, n, f) plus a ferror check is enough. Concepts: short reads, return value as count. (Mirrors the related exercise Read the first N bytes.)
3. (Intermediate) Read one binary int. Implement int read_int_binary(const char *path, int *out) that reads one int (native layout) from the start of the file into *out. Return 0 on success, -1 on failure. Requirement: treat fread(out, sizeof(int), 1, f) != 1 as failure (file too short or error). Edge case: a 3-byte file must fail, not return garbage. Concepts: record reads, single-item failure check. (Mirrors Read one binary int.)
4. (Intermediate) Read an array of records. Define struct Point { int x, y; };. Read up to 100 such points from a binary file into an array and print how many were read and their values. Requirements: size sz with sizeof(struct Point), cap n at the array capacity, process exactly the returned count, distinguish EOF from error. Concepts: record-oriented fread, buffer capacity, item count.
5. (Challenge) Header-then-payload parser. A file begins with a header struct { unsigned char magic[4]; unsigned int count; } followed by count int values. Write a program that: validates the file is long enough for the header, checks the magic equals {'D','A','T','1'}, rejects an implausibly large count (e.g. > 1,000,000) to avoid overflow/over-allocation, allocates space for count ints with malloc, reads exactly that many, and prints their sum. Requirements: every fread count checked, all allocations freed, all errors reported via stderr. Hint: read the header with fread(&hdr, sizeof hdr, 1, f) != 1. Concepts: mixed reads, untrusted-size validation, overflow safety, dynamic allocation + cleanup.
size_t fread(void *buf, size_t sz, size_t n, FILE *f) reads up to n items of sz bytes into buf.sz=1 it happens to equal the byte count, which is why the streaming idiom uses sz=1.feof(f) (clean end) vs ferror(f) (I/O error) to find out why, and check after the loop or after any single-record read.fread does no bounds checking or allocation. Provide a buffer of at least sz * n bytes, never process more items than it returned, and validate untrusted sz/n against overflow.while (!feof(f)). Open binary files with "rb" and fclose (checking the result) when done.fread for binary data and fixed-size records; use fgets for text lines and fscanf for formatted text.