File I/O in C
Learn fopen/fclose, fread/fwrite, fprintf/fscanf, binary file handling, and proper error checking for file operations.
Opening and Closing Files
File I/O in C uses the FILE * handle returned by fopen. Every open file consumes an OS file descriptor, so always close files when you’re done. Write operations are buffered — data may not reach disk until fclose is called, which is why closing is not optional.
#include <stdio.h>
int main(void) {
/* fopen returns NULL on failure — always check before using */
FILE *fp = fopen("data.txt", "w");
if (fp == NULL) {
perror("fopen"); /* prints: fopen: No such file or directory */
return 1;
}
fprintf(fp, "Line 1\n");
fprintf(fp, "Line 2\n");
fprintf(fp, "Value: %d\n", 42);
fclose(fp); /* flushes buffered data to disk and releases file descriptor */
return 0;
}
File open modes:
| Mode | Meaning |
|---|---|
"r" | Read (file must exist) |
"w" | Write (creates or truncates) |
"a" | Append (creates or appends) |
"r+" | Read and write (file must exist) |
"w+" | Read and write (creates or truncates) |
"rb" | Read binary |
"wb" | Write binary |
"ab" | Append binary |
Reading Text Files
fgets is the safest way to read text line by line. It always null-terminates and limits how many bytes it reads, preventing buffer overflows. fscanf is convenient for structured data but fragile with unexpected input.
#include <stdio.h>
#include <string.h>
/* Read line by line — the standard safe pattern for text files */
void read_lines(const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) { perror("fopen"); return; }
char line[256];
int lineno = 0;
while (fgets(line, sizeof(line), fp) != NULL) {
lineno++;
/* Remove the trailing newline that fgets preserves */
size_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0';
printf("%3d: %s\n", lineno, line);
}
if (ferror(fp)) perror("fgets");
fclose(fp);
}
/* Read the entire file into a heap buffer — useful for parsing */
char *read_entire_file(const char *filename, long *out_size) {
FILE *fp = fopen(filename, "rb");
if (!fp) return NULL;
fseek(fp, 0, SEEK_END); /* seek to end to find size */
long size = ftell(fp);
fseek(fp, 0, SEEK_SET); /* seek back to beginning */
char *buf = malloc(size + 1);
if (!buf) { fclose(fp); return NULL; }
size_t read = fread(buf, 1, size, fp);
buf[read] = '\0'; /* null-terminate so it can be used as a C string */
fclose(fp);
if (out_size) *out_size = (long)read;
return buf; /* caller must free() */
}
int main(void) {
/* Write a test file */
FILE *fp = fopen("test.txt", "w");
fprintf(fp, "Hello\nWorld\nC programming\n");
fclose(fp);
read_lines("test.txt");
long size;
char *contents = read_entire_file("test.txt", &size);
if (contents) {
printf("\nFile contents (%ld bytes):\n%s", size, contents);
free(contents);
}
return 0;
}
Writing Text Files
Writing structured data to text files requires formatting it in a way that can be read back. A common pattern is CSV: write with fprintf, read back with fgets + sscanf. Checking fclose’s return value catches write errors that only surface at flush time.
#include <stdio.h>
#include <time.h>
typedef struct {
char name[64];
int score;
int level;
} PlayerRecord;
void save_records(const char *filename, const PlayerRecord *records, int n) {
FILE *fp = fopen(filename, "w");
if (!fp) { perror("fopen"); return; }
/* Write a header with timestamp and column names */
time_t now = time(NULL);
fprintf(fp, "# Saved at: %s", ctime(&now));
fprintf(fp, "# name,score,level\n");
for (int i = 0; i < n; i++) {
fprintf(fp, "%s,%d,%d\n",
records[i].name, records[i].score, records[i].level);
}
fclose(fp);
printf("Saved %d records to %s\n", n, filename);
}
int load_records(const char *filename, PlayerRecord *records, int max) {
FILE *fp = fopen(filename, "r");
if (!fp) { perror("fopen"); return -1; }
char line[256];
int count = 0;
while (count < max && fgets(line, sizeof(line), fp)) {
if (line[0] == '#') continue; /* skip comment lines */
/* sscanf parses the line — safer than fscanf directly */
if (sscanf(line, "%63[^,],%d,%d",
records[count].name,
&records[count].score,
&records[count].level) == 3) {
count++;
}
}
fclose(fp);
return count;
}
int main(void) {
PlayerRecord out[] = {
{"Alice", 9800, 15},
{"Bob", 7200, 12},
{"Carol", 11500, 18},
};
save_records("scores.csv", out, 3);
PlayerRecord in[10];
int n = load_records("scores.csv", in, 10);
for (int i = 0; i < n; i++) {
printf("%-10s score=%5d level=%d\n",
in[i].name, in[i].score, in[i].level);
}
return 0;
}
Binary File I/O
Binary I/O with fread/fwrite stores data as raw bytes — exactly as it exists in memory. This is more efficient than text for large datasets and preserves the exact numeric values without floating-point formatting loss. Always use "rb"/"wb" modes for binary files to prevent newline translation on Windows.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
/* Simple binary file format:
Header: magic(4) + version(1) + count(4)
Data: array of int32_t values */
#define MAGIC "MYDB"
typedef struct {
char magic[4];
uint8_t version;
int32_t count;
} FileHeader;
int write_binary(const char *filename, const int32_t *data, int32_t count) {
FILE *fp = fopen(filename, "wb");
if (!fp) { perror("fopen"); return -1; }
FileHeader hdr;
memcpy(hdr.magic, MAGIC, 4);
hdr.version = 1;
hdr.count = count;
/* fwrite returns the number of items written — check for errors */
if (fwrite(&hdr, sizeof(hdr), 1, fp) != 1) goto error;
if (fwrite(data, sizeof(int32_t), count, fp) != (size_t)count) goto error;
fclose(fp);
return 0;
error:
perror("fwrite");
fclose(fp);
return -1;
}
int32_t *read_binary(const char *filename, int32_t *out_count) {
FILE *fp = fopen(filename, "rb");
if (!fp) { perror("fopen"); return NULL; }
FileHeader hdr;
if (fread(&hdr, sizeof(hdr), 1, fp) != 1) goto error;
/* Validate magic bytes to detect wrong file type */
if (memcmp(hdr.magic, MAGIC, 4) != 0) {
fprintf(stderr, "Invalid file format\n");
goto error;
}
int32_t *data = malloc(hdr.count * sizeof(int32_t));
if (!data) goto error;
if ((int32_t)fread(data, sizeof(int32_t), hdr.count, fp) != hdr.count) {
free(data);
goto error;
}
fclose(fp);
*out_count = hdr.count;
return data;
error:
fclose(fp);
return NULL;
}
int main(void) {
int32_t values[] = {10, 20, 30, 40, 50};
write_binary("data.bin", values, 5);
int32_t count;
int32_t *loaded = read_binary("data.bin", &count);
if (loaded) {
for (int32_t i = 0; i < count; i++) printf("%d ", loaded[i]);
printf("\n");
free(loaded);
}
return 0;
}
File Positioning
fseek and ftell let you jump to any position in a file without reading sequentially. This enables random access patterns — reading record N directly, updating a specific field in place, or finding the file size without reading the whole file.
#include <stdio.h>
int main(void) {
FILE *fp = fopen("test.bin", "wb+"); /* write+read binary */
if (!fp) return 1;
/* Write 10 integers sequentially */
for (int i = 0; i < 10; i++) fwrite(&i, sizeof(int), 1, fp);
/* Seek to element at index 5 and overwrite it */
fseek(fp, 5 * sizeof(int), SEEK_SET);
int val = 99;
fwrite(&val, sizeof(int), 1, fp);
/* Seek back and read element 5 to verify */
fseek(fp, 5 * sizeof(int), SEEK_SET);
fread(&val, sizeof(int), 1, fp);
printf("Element 5: %d\n", val); /* 99 */
/* Report current position */
long pos = ftell(fp);
printf("File position: %ld bytes\n", pos);
/* Seek to end to find total file size */
fseek(fp, 0, SEEK_END);
printf("File size: %ld bytes\n", ftell(fp));
fclose(fp);
return 0;
}
fseek origins: SEEK_SET (from beginning), SEEK_CUR (from current position), SEEK_END (from end).