Raw Data Persistence with fread and fwrite in C
In the C programming language, raw data persistence without text
formatting is achieved through the standard library functions
fread and fwrite. This article explores how
these functions manage block-based binary input and output, examining
their operational mechanisms, parameters, internal buffering, and
critical considerations like memory alignment and cross-platform
compatibility.
Function Signatures and Block Architecture
The functions fread and fwrite are declared
in <stdio.h> with the following signatures:
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);Both functions operate on block-based units defined by two
parameters: size (the size of a single element in bytes)
and count (the number of elements to process). The total
volume of data transferred during a call is calculated as
size * count bytes.
Unlike formatted I/O functions such as fprintf or
fscanf, which parse data into human-readable ASCII or UTF-8
strings, fread and fwrite perform direct
memory-to-stream byte copying. Memory addresses referenced by
ptr are treated as opaque sequences of bytes.
How fwrite Persists
Data
When persisting data to disk with fwrite, the function
copies a contiguous sequence of bytes from the memory location starting
at ptr to the specified file stream:
- Direct Memory Copy: The memory layout of the object (such as an array, struct, or primitive variable) is read exactly as it exists in RAM.
- Stream Buffering: The bytes are transferred to the
internal buffer associated with
FILE *stream. Depending on the stream's buffering configuration (full, line, or unbuffered), the data is flushed to the operating system's file descriptor immediately or when the buffer fills. - Return Value:
fwritereturns the total number of full items (not bytes) successfully written. If this number is less thancount, an error occurred or the disk became full.
To ensure byte integrity on platforms that distinguish between text
and binary files (such as Windows), the stream must be opened using a
binary mode flag like "wb".
How fread Restores
Data
fread mirrors the persistence process by transferring
raw bytes from the stream directly into the memory block pointed to by
ptr:
- Raw Ingestion: The specified number of bytes
(
size * count) is pulled from the file stream's buffer or directly read from disk via operating system read calls. - Memory Overwrite: The ingested bytes are placed sequentially into the target buffer, reconstructing the in-memory state of the stored structure or array.
- Return Value and EOF:
freadreturns the number of fully read elements. A return value lower thancountsignals either the end-of-file (EOF) or an I/O error, which can be distinguished usingfeof(stream)andferror(stream).
Memory Layout and Portability Considerations
Because fread and fwrite dump and restore
raw memory blocks, their use introduces several architectural
dependencies:
- Structure Padding: Compilers insert padding bytes into structures to align data members to word boundaries. Two compilers, or the same compiler with different optimization flags, may arrange structure padding differently, causing data corruption if reading across different builds.
- Endianness: Raw data is stored according to the host system’s byte ordering. A 32-bit integer written on a little-endian machine (e.g., x86/ARM) will read with reversed byte values on a big-endian system.
- Pointer Invalidation: Serializing structures
containing pointer variables with
fwriteonly persists the memory address, not the underlying data. When reloaded withfread, those addresses are dangling pointers and will lead to undefined behavior if dereferenced.