Chapter 13

File handling in C

Streams, checked I/O, records and portability.

In this chapter

Learning outcomes

Distinguish file organisation from in-memory representation, use streams safely, and explain the portability limits of raw structure storage.

13.1 Data, information and persistence

Data are recorded values; information is the meaning obtained by interpreting them in context. A mark of 75 becomes useful when associated with a student, assessment and scale. A data structure organises values during execution. A file preserves encoded data beyond the lifetime of the process.

A file format defines how values are represented. File organisation defines how records are arranged and accessed. Sequential organisation reads records in order; indexed organisation adds a structure for locating records by a key; direct or hashed organisation maps keys to locations under a defined policy. These are design choices, not alternative names for C file-opening modes.

13.2 Streams and modes

C's standard I/O library uses a FILE * to manage a stream. Open with fopen, check for failure, perform checked operations and close with fclose. Closing may itself report a write failure, so do not assume that a previous successful write guarantees durable output.

Mode Meaning Important consequence
r Read existing file Fails if it cannot be opened
w Write Creates or truncates a file
a Append Writes go to the end
r+ Read and write existing file Does not truncate on open
wb / rb Binary write/read Binary mode where relevant

Use a temporary test file rather than an important existing file during experiments. The provided lab uses tmpfile, which creates a temporary stream for update and avoids choosing an external filename.

13.3 Text records

A text format might store roll mark on each line, for example 101 72.50. fprintf writes formatted text. Read bounded lines with fgets and parse them when records require robust validation. For a controlled numeric demonstration, fscanf is acceptable if the program checks how many conversions succeeded and handles malformed input.

open stream; if failure, report error
for each record:
    write fields; if failure, close and report error
flush and reposition if switching to reading
read records while the required conversions succeed
distinguish end of file from malformed data and I/O failure
close stream and check status

Do not use while (!feof(file)) as the read loop. End-of-file status becomes set after an attempted read encounters the end; it does not predict whether the next record can be read. Drive the loop with the read operation's result.

Update streams have sequencing rules when alternating reads and writes. A positioning operation such as fseek is a simple way to establish the intended position and direction after writing. Check its return value; not every stream is seekable.

13.4 Binary records and portability

fread and fwrite transfer objects as bytes and report the number of complete items transferred. Writing a whole structure with fwrite may be convenient for a local experiment, but it is not a portable interchange format: padding, integer sizes, byte order and floating representation can vary. A stored pointer is not a useful link when the file is loaded into another process.

A durable format should define field widths, encoding, byte order, record boundaries and versioning. Validate lengths and offsets before allocating or accessing memory. Fixed-size records can support calculated offsets, but the file format—not an assumed compiler layout—must define their size.

13.5 Connect files to data structures

A student-record system can load records into a list for editing, sort an array of records for reporting and build an index for repeated lookup. Saving the result is a distinct operation. If reliability matters, replacing a file safely may require a temporary output, checked flush/close operations and a platform-specific replacement policy; a simple fopen("records.txt", "w") truncates immediately.

The laboratory demonstrates the smaller, reproducible task: write two student records (roll number, one-word name and mark) to a temporary stream, reposition, read them back and assert equality. It does not implement a general database, recovery scheme or persistent index.

Practice

  1. Why can opening a file with w destroy information before the first explicit write?
  2. Explain the error in while (!feof(fp)) { fscanf(...); process(record); }.
  3. Why should a linked-list node's raw pointer not be stored as a persistent link?
  4. Propose a simple text format for student records and identify two malformed-record cases.
Check practice answers →

Programming laboratory

Download file_records.c · Contracts and run instructions

Compile this file on its own. Check the empty, boundary and ordinary cases before changing the implementation.

Read the complete C11 program
/* C11. Text-record round trip through an automatically removed temporary file. */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct { int roll; char name[24]; double mark; } Student;
int main(void)
{
    const Student original[] = {{101, "Asha", 84.5}, {102, "Ravi", 91.0}};
    FILE *file = tmpfile(); /* No named user file is overwritten. */
    if (file == NULL) { perror("tmpfile"); return EXIT_FAILURE; }
    for (size_t i = 0; i < 2; ++i) {
        if (fprintf(file, "%d\t%s\t%.1f\n", original[i].roll,
                    original[i].name, original[i].mark) < 0) {
            perror("write record"); fclose(file); return EXIT_FAILURE;
        }
    }
    /* Positioning is required when switching an update stream from write to read. */
    if (fflush(file) == EOF || fseek(file, 0, SEEK_SET) != 0) {
        perror("prepare read"); fclose(file); return EXIT_FAILURE;
    }
    for (size_t i = 0; i < 2; ++i) {
        Student recovered;
        if (fscanf(file, "%d %23s %lf", &recovered.roll,
                   recovered.name, &recovered.mark) != 3) {
            fputs("Could not read a complete record\n", stderr);
            fclose(file); return EXIT_FAILURE;
        }
        assert(recovered.roll == original[i].roll);
        assert(strcmp(recovered.name, original[i].name) == 0);
        assert(recovered.mark == original[i].mark);
        printf("%d %s %.1f\n", recovered.roll, recovered.name, recovered.mark);
    }
    int extra;
    if (fscanf(file, " %d", &extra) != EOF || ferror(file)) {
        fputs("Unexpected trailing data or read error\n", stderr);
        fclose(file); return EXIT_FAILURE;
    }
    if (fclose(file) == EOF) { perror("close temporary file"); return EXIT_FAILURE; }
    puts("file-record tests passed");
    return EXIT_SUCCESS;
}