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:

  1. 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.
  2. 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.
  3. Return Value: fwrite returns the total number of full items (not bytes) successfully written. If this number is less than count, 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:

  1. 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.
  2. Memory Overwrite: The ingested bytes are placed sequentially into the target buffer, reconstructing the in-memory state of the stored structure or array.
  3. Return Value and EOF: fread returns the number of fully read elements. A return value lower than count signals either the end-of-file (EOF) or an I/O error, which can be distinguished using feof(stream) and ferror(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 fwrite only persists the memory address, not the underlying data. When reloaded with fread, those addresses are dangling pointers and will lead to undefined behavior if dereferenced.