Skip to main content
C intermediate Lesson 14 of 23

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:

ModeMeaning
"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).

Frequently Asked Questions

What is the difference between text mode and binary mode?
In text mode, the C library may translate newlines (\n becomes \r\n on Windows). In binary mode, bytes are read and written exactly as stored. Always use binary mode for non-text files to avoid corruption.
Do I always need to close files?
Yes. fclose flushes the write buffer and releases OS file handles. If you don't close files, data may not be written (it sits in the buffer) and you'll eventually run out of file descriptors.
What is the difference between fgets and fscanf for reading text?
fgets reads a whole line (safer, handles spaces). fscanf reads formatted data and stops at whitespace by default. Prefer fgets + sscanf over fscanf for robustness.