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
fopenreturn value — it returns NULL on failure - Use
fgetsfor line-by-line text reading - Use
fscanffor structured, predictable formats - Use
freadfor binary files or large bulk reads - Use
intnotcharfor the variable that holdsfgetc‘s return value - Call
fcloseevery time — always, no exceptions