C gives you four main ways to read a file. They are not interchangeable — each is designed for a different type of data and use case. Using the wrong one leads to truncated reads, buffer overflows, or silent data corruption.

Opening and Closing a File

Every file operation starts with fopen and ends with fclose:

#include <stdio.h>

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

    /* read operations here */

    fclose(fp);
    return 0;
}

Always check the return value of fopen. It returns NULL if the file does not exist, the path is wrong, or permissions are denied. perror prints the system error message — far more useful than a generic “file not found” string.

Common mode strings: "r" (read text), "rb" (read binary), "w" (write, truncates), "a" (append).

fgets — Read Line by Line

Use fgets when your file is line-oriented (CSV, config files, logs):

#include <stdio.h>

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

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

    fclose(fp);
    return 0;
}

fgets(buffer, size, stream) reads at most size - 1 characters, stopping at a newline or EOF. The newline is included in the buffer. The buffer is always null-terminated.

Stripping the trailing newline

#include <string.h>

char line[256];
fgets(line, sizeof(line), fp);

/* Remove n if present */
size_t len = strlen(line);
if (len > 0 && line[len - 1] == 'n') {
    line[len - 1] = '';
}

When lines are longer than the buffer

If a line is longer than size - 1 characters, fgets reads a partial line and leaves the rest in the stream. The next call reads the continuation. Always size your buffer generously or handle the partial-read case explicitly.

fscanf — Parse Structured Data

Use fscanf when your file has a consistent, predictable format:

#include <stdio.h>

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

    char name[64];
    int score;

    while (fscanf(fp, "%63s %d", name, &score) == 2) {
        printf("%-20s %dn", name, score);
    }

    fclose(fp);
    return 0;
}

fscanf returns the number of items successfully read. Check this return value — if it is less than expected, you hit a format mismatch, EOF, or an error.

fscanf pitfall: whitespace handling

fscanf with %s stops at any whitespace. It cannot read a name like “John Smith” as a single token. Use fgets followed by sscanf for lines with embedded spaces.

fread — Read Raw Binary Data

Use fread for binary files: images, compiled programs, serialised structs:

#include <stdio.h>
#include <stdlib.h>

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

    /* find file size */
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    rewind(fp);

    /* allocate buffer and read entire file */
    unsigned char *buf = malloc(size);
    if (buf == NULL) { fclose(fp); return 1; }

    size_t read = fread(buf, 1, size, fp);
    if ((long)read != size) {
        fprintf(stderr, "Short read: got %zu of %ld bytesn", read, size);
    }

    printf("Read %zu bytesn", read);

    free(buf);
    fclose(fp);
    return 0;
}

fread(ptr, size, count, stream) reads count items of size bytes each. It returns the number of items read — which may be less than count at EOF or on error. Always check the return value. Understanding when to use malloc here is important — see our guide on malloc vs calloc vs realloc for choosing the right allocation function.

getc / fgetc — Read One Character at a Time

#include <stdio.h>

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

    int c;
    while ((c = fgetc(fp)) != EOF) {
        putchar(c);
    }

    fclose(fp);
    return 0;
}

Note that c is declared as int, not char. EOF is typically -1, which does not fit in a char on systems where char is unsigned. Using char c causes an infinite loop because c != EOF is always true.

Error Handling — ferror and feof

while (fgets(line, sizeof(line), fp) != NULL) {
    /* process line */
}

if (ferror(fp)) {
    perror("Read error");
} else if (feof(fp)) {
    /* clean end of file — normal */
}

fgets returns NULL for both EOF and errors. Use feof and ferror after the loop to distinguish between a normal end-of-file and an actual I/O error.

Quick Comparison

Function Best for Handles lines? Binary safe?
fgets Text files, line by line Yes No
fscanf Structured text (numbers, tokens) Partial No
fread Binary files, bulk reads No Yes
fgetc Character-by-character processing No Yes

You can test all of these with sample input files using our c code compiler — paste code with hardcoded strings and use sscanf or fmemopen to simulate file reads without needing actual files.

TL;DR

  • Always check fopen return value — it returns NULL on failure
  • Use fgets for line-by-line text reading
  • Use fscanf for structured, predictable formats
  • Use fread for binary files or large bulk reads
  • Use int not char for the variable that holds fgetc‘s return value
  • Call fclose every time — always, no exceptions