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

FunctionBest forHandles lines?Binary safe?
fgetsText files, line by lineYesNo
fscanfStructured text (numbers, tokens)PartialNo
freadBinary files, bulk readsNoYes
fgetcCharacter-by-character processingNoYes

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