Chapter 17 of 24

17. File input and output

A safe first workflow for files

A file lets a program keep or read information outside the program's immediate interaction. C represents an open file with a FILE * value, which the standard input/output functions use to track that file.

  1. Open the file in the mode you need, such as reading existing text or writing a new file.
  2. Check that opening succeeded before attempting to read or write.
  3. Check the result of each operation and close the file when finished, including on error paths.

Start with a short text file and one line at a time. Binary data, file positions, and buffering build on the same open-check-use-close pattern.

The FILE * interface from <stdio.h> supports text and binary files.

#include <stdio.h>

int main(void)
{
    FILE *file = fopen("notes.txt", "r");
    if (file == NULL) {
        perror("notes.txt");
        return 1;
    }

    char line[256];
    while (fgets(line, sizeof line, file) != NULL) {
        fputs(line, stdout);
    }

    if (ferror(file)) {
        perror("reading notes.txt");
        fclose(file);
        return 1;
    }

    if (fclose(file) == EOF) {
        perror("closing notes.txt");
        return 1;
    }
    return 0;
}

Common modes include "r" (read), "w" (write and truncate/create), "a"
(append), and binary variants such as "rb" and "wb". Check whether
fopen succeeded, handle read/write errors, and close files you opened.

For binary data, fread and fwrite operate on bytes/objects, but raw
structure dumps are not a portable interchange format: padding, byte order,
and type representation can differ. Define a format explicitly for data that
must survive across platforms.

File modes and error handling

Opening a file is an operation that can fail: the path may not exist,
permissions may be insufficient, or the device may be unavailable. Check
fopen before using the returned stream. Opening in "w" mode truncates an
existing file, so use it only when replacing that file is intended. Use "a"
when adding data at the end.

Input routines signal end-of-file and errors through their return values.
After a loop that reads until failure, use ferror to distinguish an I/O
error from ordinary end-of-file when that distinction matters. Output can
fail too, including at fclose when buffered data is finally written. Check
the return values for operations whose failure matters to your program.

Text files should have a defined format. If a record contains a title and
number, decide how separators and special characters are represented, how
invalid records are rejected, and whether a newline ends each record. Do not
assume one fgets call always reads a complete logical record.

Always close a successfully opened stream. If multiple resources are held at
once, arrange cleanup so every error path releases resources already
acquired. In larger functions, a single cleanup section can make this easier
to audit.

#include <stdio.h>

int copy_file(const char *source_name, const char *destination_name)
{
    FILE *source = fopen(source_name, "rb");
    if (source == NULL) {
        return 0;
    }

    FILE *destination = fopen(destination_name, "wb");
    if (destination == NULL) {
        fclose(source);
        return 0;
    }

    int ok = 1;
    int byte;
    while ((byte = fgetc(source)) != EOF) {
        if (fputc(byte, destination) == EOF) {
            ok = 0;
            break;
        }
    }
    if (ferror(source)) {
        ok = 0;
    }
    if (fclose(destination) == EOF) {
        ok = 0;
    }
    if (fclose(source) == EOF) {
        ok = 0;
    }
    return ok;
}

This checks open, read, write, and close failures. It does not remove a
partially written destination on failure; a real application should define
that policy.

Checkpoint: Explain why "w" can be dangerous for an existing file and
how to tell an input error from ordinary end-of-file.

File positions and records

For a simple text file, process it sequentially with functions such as
fgets, fgetc, and fprintf. For formatted records, validate every field
and reject partial records. fscanf has the same conversion-count and input
recovery concerns as scanf; it is not automatically a robust parser.

The standard library also has fseek, ftell, and rewind, but text-stream
positioning has portability constraints. Binary streams permit byte-oriented
positioning where supported, yet their contents still need a defined format.
Do not assume ftell is a universally portable byte count for every text
stream.

If a file must be replaced safely, write a new complete file and check every
write and close result before replacing the original using the platform's
documented file operations. Standard C alone does not provide a fully
portable transactional file-replacement guarantee.

#include <limits.h>
#include <stddef.h>
#include <stdio.h>

int read_record_at_start(FILE *file, char *buffer, size_t capacity)
{
    if (file == NULL || buffer == NULL || capacity == 0 ||
        capacity > (size_t)INT_MAX) {
        return 0;
    }
    if (fseek(file, 0L, SEEK_SET) != 0) {
        return 0;
    }
    return fgets(buffer, (int)capacity, file) != NULL;
}

For general code, also ensure capacity can be represented as int before
passing it to fgets, whose size parameter has type int. fseek behavior
depends on the stream and mode; check its result.

Standard streams and buffering

Hosted programs begin with stdin, stdout, and stderr. Output may be
buffered, so text may not appear immediately until a newline, flush, or close.
Use fflush(stdout) when an interactive prompt must be visible before waiting
for input, and check its result when failure matters. Avoid mixing buffered
stdio operations with low-level file descriptor operations on the same
underlying file unless the platform's synchronization rules are followed.

#include <stdio.h>

int main(void)
{
    fputs("Continue? ", stdout);
    if (fflush(stdout) == EOF) {
        return 1;
    }

    int answer = getchar();
    if (answer == EOF) {
        return 1;
    }
    return answer == 'y' || answer == 'Y' ? 0 : 1;
}

The explicit flush ensures the prompt is sent before input is read. This
small example does not consume any extra characters on the input line.

Exercises: count lines in a file; copy a text file while checking errors;
save and reload a simple list using a documented text format.


Suggested topics

0% read