Educational Blog

How to Read a File in C

Learn the core patterns for safe, reliable file input in C.

Use a file by opening it with fopen, checking the return value, then reading with fgets, fread, or fscanf depending on the shape of the data. The core pattern in C is always the same: open, verify, read, process, close. If you keep that sequence disciplined, file I/O stays predictable instead of turning into a source of crashes and partial reads.

The basic file-reading flow

Reading a file in C starts with FILE *fp = fopen(path, mode);. If fp is NULL, stop immediately and handle the error. That one check prevents most downstream bugs, because every standard library call that expects a valid file handle will misbehave or fail if you ignore the open step.

The typical loop looks like this:

  1. Open the file.
  2. Confirm the pointer is not NULL.
  3. Read data in a loop.
  4. Process each chunk or line.
  5. Close the file with fclose.

That pattern is simple, but it scales from tiny text files to larger parsing jobs.

Choosing the right reading method

Different files need different tools. A text configuration file is not the same as a binary blob, and a line-oriented log is not the same as a fixed-width record file.

File typeBest fitWhy it works
Short text linesfgetsEasy line-by-line handling
Formatted valuesfscanfConvenient for structured text
Whole binary blocksfreadFast bulk reads
Character-by-characterfgetcGood for custom parsers

The method matters because it affects buffering, error handling, and how much control you have over the input.

Reading a text file line by line

For most beginners, fgets is the cleanest starting point. It reads a line into a buffer and stops when it hits a newline or runs out of space. You must still check for truncation if a line might exceed your buffer size.

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

    char line[256];
    while (fgets(line, sizeof(line), fp) != NULL) {
        printf("%s", line);
    }

    fclose(fp);
    return 0;
}

A few details matter here:

  • fgets keeps the newline if it fits in the buffer.
  • The buffer must be large enough for the longest expected line, or you need logic to detect partial lines.
  • The loop ends when fgets returns NULL, which can mean end-of-file or an error.

If you want to distinguish the two, call ferror(fp) and feof(fp) after the loop.

Reading formatted data with fscanf

fscanf is useful when each line follows a reliable pattern, such as name age score. It is convenient, but it is also easier to misuse than fgets, because formatting mismatches can silently break your parsing logic.

#include <stdio.h>

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

    char name[64];
    int age;

    while (fscanf(fp, "%63s %d", name, &age) == 2) {
        printf("%s is %d\n", name, age);
    }

    fclose(fp);
    return 0;
}

Use width limits with strings, like %63s, to avoid buffer overflow. Also, be aware that fscanf stops at whitespace for %s, so it is not a good fit for names or fields that may contain spaces unless you build a more careful parser.

Reading binary files with fread

When the file contains raw bytes or structured binary records, use fread. It reads a block of memory at a time and is usually the right tool for images, serialized structs, or custom binary formats.

#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.bin", "rb");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }

    unsigned char buffer[1024];
    size_t bytes_read;

    while ((bytes_read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
        /* process bytes_read bytes */
    }

    if (ferror(fp)) {
        perror("fread");
    }

    fclose(fp);
    return 0;
}

A few points are easy to miss:

  • Open binary files with "rb" on platforms that distinguish text and binary modes.
  • fread returns the number of items read, not always the exact byte count you expect unless you read one byte at a time.
  • A short read at the end of the file is normal.

Checking errors the right way

Good file-reading code does not just assume success. It verifies each step.

What to check

  • fopen returns NULL if the file cannot be opened.
  • fgets returns NULL on end-of-file or error.
  • fread may return fewer items than requested.
  • ferror tells you whether an actual I/O error occurred.
  • feof tells you whether end-of-file was reached.

A practical rule is to check the file pointer immediately after fopen, then check the read result inside the loop, and then inspect error state if the loop ends unexpectedly.

Common mistakes to avoid

Many file-reading bugs come from small oversights rather than complicated logic.

  1. Forgetting to check the result of fopen.
  2. Using a buffer that is too small for the input.
  3. Assuming fscanf will always match the file format.
  4. Confusing text mode and binary mode.
  5. Forgetting to call fclose.
  6. Reading past the end by ignoring return values.

The safest habit is to treat every input as incomplete until the library confirms otherwise.

A practical example: reading a simple text file

Suppose you have a file called tasks.txt with one task per line:

Write report
Review pull request
Send invoice

A straightforward reader can print each line and count them.

#include <stdio.h>

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

    char line[128];
    int count = 0;

    while (fgets(line, sizeof(line), fp) != NULL) {
        count++;
        printf("Task %d: %s", count, line);
    }

    fclose(fp);
    printf("Read %d tasks\n", count);
    return 0;
}

This example shows the most common pattern in a compact form. The file is opened once, read in a loop, and closed once. There is no extra ceremony.

When to prefer a line-based approach

A line-based approach is usually better when the file is human-authored or editable in a text editor. That includes:

  • Logs
  • Notes
  • Lists
  • CSV-like files
  • Simple configuration files

Line-oriented parsing gives you flexibility. You can trim whitespace, ignore comments, split fields, and reject malformed input without relying on a rigid scanf pattern.

When to prefer block reads

Block reads are better when you care about speed or the file is already structured as bytes. That includes:

  • Image and media data
  • Binary save files
  • Large archives
  • Custom protocol captures

In those cases, fread is usually more efficient than reading one character or line at a time, especially for large files.

A small checklist before you ship

Before you treat your file-reading code as finished, verify these items:

  • The file path is correct.
  • The fopen mode matches the file type.
  • Every return value is checked.
  • Buffers are large enough for the input.
  • The file is always closed.
  • Errors are reported in a way you can debug later.

That checklist catches most real-world file I/O issues early.

Final takeaway

The question is not just how to read a file in C, but how to do it safely. The answer is to pick the right API for the file type, check every return value, and keep the control flow simple. Start with fopen, read with fgets, fscanf, or fread depending on the format, and always close the file when you are done.

Once that habit is automatic, file input becomes one of the most dependable parts of your C programs.

Written by

c-double.com Editorial Team

Editorial team

c-double.com publishes practical how-to guides and educational articles with clear steps and useful context.