Topic 87 of 100 File Handling Interactive Tutorial

File Handling in C: Opening, Reading, Writing, and Closing Files

13 mins read Last updated: 22 Aug 2026 30,400 views
C Compiler 0.00%

What is File Handling in C? Core Concept & Direct Summary

Core Objective & Problem Statement

How does the `FILE*` stream pointer work with `fopen()`, `fclose()`, `fgetc()`, `fputc()`, and file access modes?

Direct Answer Summary

Everything your programs have done so far lives and dies with the process. An array of student records, a computed report, a running total — the moment main returns, it's gone. That's fine for a calculator. It's useless for anything that needs to remember.

File handling is how C programs persist data. And the good news is that the mechanism is built on something you already understand deeply: pointers.

main.c
Run Online
FILE *fp = fopen("data.txt", "r");

That's the whole idea in one line. A file is a stream of bytes, and C gives you a FILE * — a handle that refers to that stream. You then use fprintf/fgets/fread exactly as you've been using printf/scanf, except the target is a file instead of the screen and keyboard.

This page covers the complete lifecycle: opening with the right mode, writing, reading, detecting the end and errors, binary vs text, repositioning, and the error handling that separates code that works from code that works reliably.

(Suggested image: the hero graphic — a program block on the left connected by a labeled FILE *fp arrow to a file on disk, with "stream of bytes" marked along the arrow)

How Does File Handling in C Work? In-Depth Mechanics & Architecture

The Stream Model and the FILE Pointer

C doesn't hand you disk sectors or file tables. It gives you an abstraction: an ordered stream of bytes that you read from or write to sequentially.

Three streams exist before your program does anything — no fopen required:

StreamFILE * nameTypically connects to
Standard inputstdinKeyboard / piped input
Standard outputstdoutConsole (line-buffered)
Standard errorstderrConsole (unbuffered)

That's why printf("hi") works without opening anything — printf is convenient shorthand for fprintf(stdout, ...). And it's why fprintf(stderr, "error\n") is the right way to report problems: it reaches the console even when stdout has been redirected to a file.

To reach a file, you ask for a new stream:

main.c
Run Online
FILE *fp = fopen("products.txt", "r");

fp is your handle. It's not the file — it's a reference to a runtime structure that tracks the file, the current position, buffering state, and error flags. Same mental shape as any other pointer: give it to functions, check it for NULL, don't use it after it's released.

Buffering matters and explains several "why didn't my data appear?" mysteries: C accumulates writes in a memory buffer and flushes them to disk in blocks, because a million one-byte disk writes would be catastrophically slow. The consequence: data you "wrote" may still be in memory until the buffer fills, you call fflush, or you call fclose.

(Suggested image: the program → FILE → buffer → disk chain, with the buffer highlighted to show data sitting in memory before reaching disk)*


fopen: The Mode Table You Must Know

main.c
Run Online
FILE *fopen(const char *filename, const char *mode);

The mode string decides everything about what's allowed and what happens to existing data. This is the most consequential table on the page — picking the wrong mode silently destroys data.

ModePurposeFile must exist?Existing contentCreates file?
"r"Read onlyYesPreservedNo — returns NULL if missing
"w"Write onlyNo⚠ TRUNCATED — erased immediatelyYes
"a"Append onlyNoPreserved — writes go to the endYes
"r+"Read and writeYesPreservedNo
"w+"Read and writeNo⚠ TRUNCATEDYes
"a+"Read and appendNoPreserved (reads anywhere, writes at end)Yes
"x" variants ("wx", "rbx")Exclusive create (C11)Must not exist—Yes, fails if it does

The life-saving distinction:"w"erases the file the instant you open it — before you write a single byte. Open the wrong file with "w" and its contents are already gone; no amount of careful code after that recovers it. If you mean "add to the end," that is "a", always.

Add "b" for binary:"rb", "wb", "ab", "r+b". This disables newline translation and end-of-file-character interpretation — essential for images, executables, structs, and anything where every byte must survive unchanged.

And always check the return value.fopen returns NULL for a missing file, a bad path, no permission, a read-only volume, too many open files, and more:

main.c
Run Online
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
    perror("data.txt");
    /* prints: data.txt: No such file or directory */ return 1;
    /* never continue with a NULL FILE* */
}

perror is the standard companion here — it appends the operating system's explanation of what went wrong, which is far more useful than a generic message. Using fp without that check means passing NULL into fgets, which crashes.

(Suggested image: the mode table rendered as a decision graphic — "want to keep existing data? → a; want to start fresh? → w; want to fail if missing? → r" — with the "w truncates immediately" warning called out)


fclose: More Than Cleanup

Every successful fopen needs a matching fclose. Three reasons, all of which will bite you eventually if ignored:

  1. It flushes the output buffer. This is the big one. Data you fprintf'd may still be sitting in memory. fclose writes it out. Skip it and your file can be missing data or empty entirely.
  2. It releases the OS file handle. Operating systems limit how many files a process can hold open (often a few hundred to a few thousand). Leak handles in a loop and fopen starts returning NULL.
  3. It reports deferred errors. Disk full, quota exceeded, device disconnected — these often surface only when the buffer is pushed to disk. So fclose's return value is worth checking:
main.c
Run Online
if (fclose(fp) != 0) {
    perror("fclose");
    /* the write may not have completed! */ return 1;
}

Pattern to internalize:everyfopen on every code path — including early error returns — must reach an fclose. That's the same ownership discipline as malloc/free from the dynamic memory tutorial, applied to streams.

Need to flush without closing?fflush(fp) pushes buffered output to disk and leaves the stream open — useful before reading back a file you just wrote, or for logging that must appear immediately.

(Suggested image: the write path showing data in a buffer, then the fclose arrow pushing it to disk, with an "early return without fclose = lost data" branch marked)


Writing to Files

Four functions, and the choice is about formatting level:

main.c
Run Online
FILE *fp = fopen("out.txt", "w");

fprintf — formatted, like printf

main.c
Run Online
fprintf(fp, "%d|%s|%.2f|%d\n", 101, "Wireless Mouse", 24.99, 15);

Same format specifiers as printf, same rules — the only difference is the destination. This is the go-to for text files, because you control the exact layout.

fputs — a string, verbatim

main.c
Run Online
fputs("Header line\n", fp);
/* ⚠ does NOT add a newline — you supply it */

Compare with puts, which does append a newline. Use fputs when you want precise control over line endings.

fputc — one character

Same relationship to putchar as fputs has to puts.

fwrite — raw bytes

main.c
Run Online
size_t written = fwrite(buffer, sizeof(char), length, fp);

Binary, unformatted: fwrite(pointer, size_of_one_item, count_of_items, fp). Returns how many items were written (not bytes) — compare it against your count to detect partial writes. This is the one for images, structs, and arrays.

A complete write cycle:

main.c
Run Online
FILE *fp = fopen("products.txt", "w");
if (fp == NULL) {
    perror("products.txt");
    return 1;
}
fprintf(fp, "%d|%s|%.2f|%d\n", 101, "Wireless Mouse", 24.99, 15);
fprintf(fp, "%d|%s|%.2f|%d\n", 102, "Mechanical Keyboard", 89.50, 8);
if (fclose(fp) != 0) {
    perror("fclose");
    return 1;
}

(Suggested image: the four write functions arranged by level — fputc (one char), fputs (a string), fprintf (formatted), fwrite (raw bytes))


Reading from Files

Reading has more moving parts, because you must handle "there's nothing left" correctly — and getting that wrong is the single most common file-handling bug in C.

fgets — the line reader, and the workhorse

main.c
Run Online
char *fgets(char *buffer, int size, FILE *fp);
  • Reads at most size - 1 characters, then appends '\0'
  • Stops at a newline — and keeps it (so your buffer usually ends in "\n")
  • Returns buffer on success, NULL at end-of-file or on error

That size limit is a safety feature: fgets can never overflow your buffer. This is why fgets beats gets (which was removed from the language for exactly this reason).

main.c
Run Online
char line[128];
while (fgets(line, sizeof line, fp) != NULL) {
    line[strcspn(line, "\r\n")] = '\0';
    /* strip the trailing newline */ printf("[%s]\n", line);
}

The read-loop rule: never test feof()

This is worth stating flatly, because the wrong version looks so reasonable:

main.c
Run Online
while (!feof(fp)) {
    /* ✗ WRONG */ fgets(line, sizeof line, fp);
    printf("%s\n", line);
    /* prints the last line TWICE */
}

feof only becomes true after a read has already failed. So this loop reads past the end once, gets NULL, ignores it, and processes the stale buffer contents again — duplicating the final line. feof is a diagnostic ("did the last read hit EOF?"), not a predictive flag.

The correct rule:test the return value of the read function itself.

main.c
Run Online
while (fgets(line, sizeof line, fp) != NULL) {
    /* ✓ CORRECT */ printf("%s", line);
}

Every read function returns a detectable failure: fgets → NULL, fgetc → EOF, fscanf → the number of items matched, fread → items read. Trust those, not feof.

fgetc — one character, and the int trap

main.c
Run Online
int c;
/* ← must be int, NOT char */
while ((c = fgetc(fp)) != EOF) {
    putchar(c);
}

Why int and not char?fgetc returns either a byte value (0–255, or 0–255 reinterpreted for char) or the sentinel EOF, which is a negative int. If you store the result in a char, a legitimate byte of value 0xFF becomes indistinguishable from EOF — and your loop stops early on binary data. int is mandatory. (This is a genuine, classic bug.)

fscanf — formatted reading

main.c
Run Online
int matched = fscanf(fp, "%d %s %lf", &id, name, &price);
if (matched == 3) {
    /* all three fields read */
}

fscanf returns how many items it successfully converted — so 3 means success, fewer means the input didn't match. It returns EOF at end of file.

Two hazards: a bare %s in fscanf has no width limit (buffer overflow), and if input doesn't match the format, the offending characters stay in the stream so the same failure repeats forever in a loop. Name a width — %49s — and prefer fgets + parsing for anything nontrivial.

The robust pattern: read the line, then parse it

For structured text, the most reliable approach combines both — fgets for safe, bounded reading, then sscanf to pull fields out of the line you already have:

main.c
Run Online
while (fgets(line, sizeof line, fp) != NULL) {
    line[strcspn(line, "\r\n")] = '\0';
    if (sscanf(line, "%d|%47[^|]|%lf|%d", &p.id, p.name, &p.price, &p.quantity) != 4) {
        fprintf(stderr, "Skipping malformed line: %s\n", line);
        continue;
        /* bad row never corrupts the totals */
    }
    /* p is fully populated and safe */
}

Why this wins:

  • Bounded reads — no overflow, ever, because the line length is capped
  • Recoverable parse errors — a malformed line is skipped with a message, and the loop continues cleanly. A failing fscanf loop can't do that
  • %47[^|] reads up to 47 characters stopping at the | delimiter, so names with spaces work naturally

That three-line pattern handles CSV, pipe-delimited, log files, and config files — it's the single most reusable snippet on this page.

(Suggested image: the wrong-vs-right read loop side by side, with the duplicated final line highlighted in the wrong one)


EOF vs Error: Distinguishing the Two

Both fgets returning NULL and fgetc returning EOF can mean two different things: clean end of file, or a genuine read error (bad disk, unplugged drive, I/O fault). They look identical from the return value — so after the loop, ask which it was:

main.c
Run Online
while (fgets(line, sizeof line, fp) != NULL) {
    /* ... */
}
if (ferror(fp)) {
    perror("read error");
    /* an actual failure occurred */
} else if (feof(fp)) {
    puts("Reached end of file normally");
    /* expected, fine */
}
  • feof(fp) → the last read stopped because the file ended
  • ferror(fp) → the last read stopped because of an I/O error

Check ferrorfirst. A program that silently treats a disk error as "end of file" will report success on truncated data — the worst possible outcome.

(Suggested image: the loop exit split into two diagnosis branches — one labeled "clean EOF", one labeled "I/O error, report it")


Text Mode vs Binary Mode

Text mode ("r", "w")Binary mode ("rb", "wb")
NewlinesTranslated: \n ↔ \r\n on WindowsNo translation — bytes pass through
EOF characterCtrl+Z (0x1A) treated as end of file on WindowsNot special
Byte countsMay differ from what you wroteExactly what you wrote
Use forSource, CSV, config, logsImages, executables, structs, compressed data
Size on diskPlatform-dependent (Windows adds \r per line)Platform-independent

The \r\n translation is real and affects sizes. Write three lines with "\n" endings on Windows in text mode and the file on disk contains \r\n per line — three extra bytes. Read it back in text mode and the \r is translated away, so your program sees \n again. Consistent and harmless if both sides use text mode.

The trap arrives when modes are mixed:

main.c
Run Online
FILE *fp = fopen("image.png", "r");
/* ✗ text mode on binary data */

On Windows, every 0x0A byte in that image may be expanded to 0x0D 0x0A on write, and a 0x1A byte may end the read early — silently corrupting the data. Binary data requires "b". Always.

(Suggested image: the same logical content shown written to disk in text mode (with \r\n and Ctrl+Z marked) vs binary mode (bytes unchanged))


Binary I/O: fwrite and fread

For raw data — no formatting, no delimiters, just bytes:

main.c
Run Online
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *fp);
size_t fread(void *ptr, size_t size, size_t count, FILE *fp);
main.c
Run Online
typedef struct {
    int id;
    double value;
    char tag[8];
}
Record;
Record out[3] = {
{
        1, 1.5, "a"
    }
    , {
        2, 2.5, "b"
    }
    , {
        3, 3.5, "c"
    }
};
/* --- write --- */FILE *fp = fopen("records.bin", "wb");
/* note the "b" */if (fp == NULL) {
    perror("records.bin");
    return 1;
}
size_t written = fwrite(out, sizeof(Record), 3, fp);
if (written != 3) {
    fprintf(stderr, "Only %zu of 3 records written\n", written);
}
fclose(fp);
/* --- read back --- */Record in[3];
fp = fopen("records.bin", "rb");
if (fp == NULL) {
    perror("records.bin");
    return 1;
}
size_t readCount = fread(in, sizeof(Record), 3, fp);
printf("Read %zu records\n", readCount);
/* 3 */fclose(fp);

Both take (data, size_of_item, count_of_items, fp) and return the item count. Arranging them this way is what makes reading an unknown-length array of records easy:

main.c
Run Online
Record buffer[100];
size_t n = fread(buffer, sizeof(Record), 100, fp);
/* however many were there */

Two honest warnings about binary struct I/O:

  1. Padding travels with it.fwrite writes the struct's padding bytes too, which contain whatever was in memory — so files are bigger than the data, and two logically identical records can produce different bytes. memcmp on structs is unreliable for the same reason.
  2. The format isn't portable. Member offsets, padding, and sizes differ across compilers and platforms, and endianness flips multi-byte values between architectures. A .bin file of structs written on one machine may be garbage on another.

So: binary struct I/O is excellent for transient, same-machine, same-build data (caches, save files, session state) and inappropriate for interchange formats. For anything crossing a boundary, serialize field by field in a defined order and width.

(Suggested image: the fread/fwrite call shape annotated — data, item size, item count, stream — with the returned item count highlighted as the thing to check)


Repositioning: fseek, ftell, rewind

Streams are sequential by default, but you can jump:

FunctionPurpose
fseek(fp, offset, origin)Move to a position — origin is SEEK_SET, SEEK_CUR, or SEEK_END
ftell(fp)Report the current position
rewind(fp)Jump back to the start and clear the error indicator
fgetpos / fsetposLarger-file-safe position handles
main.c
Run Online
/* Random access into a binary file of fixed-size records */fseek(fp, 2 * sizeof(Record), SEEK_SET);
/* jump straight to record #2 */fread(&rec, sizeof(Record), 1, fp);
/* read just that one */
long pos = ftell(fp);
printf("Now at byte %ld\n", pos);

Fixed-size records + fseek is the foundation of random access — a direct analogue of array indexing. With sizeof(Record) uniform, record N starts at byte N × sizeof(Record), so you can jump to any record without reading the ones before it. That's the same offset arithmetic as in the arrays-of-structures tutorial, applied to a file.

Two cautions:

  • ftell on a text-mode file is not a reliable byte offset on Windows, because of \r\n translation. Use binary mode, or treat positions as opaque values.
  • After writing, you must flush or reposition before reading from the same stream — buffered output and input can otherwise interleave incorrectly. fflush(fp) or fseek(fp, 0, SEEK_SET) first.

(Suggested image: a binary file of fixed-size records with fseek jumping directly to record 2, bypassing 0 and 1 — labeled "random access")


📋 Complete Program: Write, Read, Append, Read Again

A full lifecycle in one program — and note how every fopen is paired with a checked fclose, and every read tests the return value.

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FILE_NAME "products.txt"
#define LINE_MAX 128
#define NAME_MAX 48
typedef struct {
    int id;
    char name[NAME_MAX];
    double price;
    int quantity;
}
Product;
/* ---- Write a fresh file (mode "w": creates, and TRUNCATES if it exists) ---- */int writeInitialFile(void) {
    FILE *fp = fopen(FILE_NAME, "w");
    if (fp == NULL) {
        perror("fopen for writing");
        return 0;
    }
    fprintf(fp, "%d|%s|%.2f|%d\n", 101, "Wireless Mouse", 24.99, 15);
    fprintf(fp, "%d|%s|%.2f|%d\n", 102, "Mechanical Keyboard", 89.50, 8);
    fprintf(fp, "%d|%s|%.2f|%d\n", 103, "USB-C Cable", 12.75, 40);
    if (fclose(fp) != 0) {
        /* flush buffers + release the handle */ perror("fclose");
        return 0;
    }
    return 1;
}
/* ---- Append one record (mode "a": never truncates, writes at end) ---- */int appendProduct(int id, const char *name, double price, int qty) {
    FILE *fp = fopen(FILE_NAME, "a");
    if (fp == NULL) {
        perror("fopen for appending");
        return 0;
    }
    fprintf(fp, "%d|%s|%.2f|%d\n", id, name, price, qty);
    return fclose(fp) == 0;
}
/* ---- Read every record and report ---- */void readAndReport(const char *heading) {
    FILE *fp = fopen(FILE_NAME, "r");
    if (fp == NULL) {
        perror("fopen for reading");
        return;
    }
    char line[LINE_MAX];
    int records = 0;
    int totalQty = 0;
    double totalValue = 0.0;
    printf("\n%s\n", heading);
    printf("%-5s %-22s %9s %6s %11s\n", "ID", "Name", "Price", "Qty", "Value");
    puts("-----------------------------------------------------------");
    /* The CORRECT loop: test the read, never test feof() */ while (fgets(line, sizeof line, fp) != NULL) {
        Product p;
        line[strcspn(line, "\r\n")] = '\0';
        /* strip the newline */ if (line[0] == '\0') continue;
        /* skip blank lines */
        /* Parse id|name|price|qty — %[^|] reads up to the next '|' */ if (sscanf(line, "%d|%47[^|]|%lf|%d", &p.id, p.name, &p.price, &p.quantity) != 4) {
            fprintf(stderr, "Skipping malformed line: %s\n", line);
            continue;
            /* never let bad data skew the math */
        }
        double value = p.price * p.quantity;
        printf("%-5d %-22s %9.2f %6d %11.2f\n", p.id, p.name, p.price, p.quantity, value);
        records++;
        totalQty += p.quantity;
        totalValue += value;
    }
    if (ferror(fp)) {
        /* error, not clean EOF */ perror("read error");
    }
    printf("-----------------------------------------------------------\n");
    printf("Records: %d   Total quantity: %d   Total value: %.2f\n", records, totalQty, totalValue);
    fclose(fp);
}
int main(void) {
    puts("=== FILE HANDLING DEMO ===");
    if (!writeInitialFile()) return 1;
    puts("\nWrote 3 records to products.txt (mode \"w\")");
    readAndReport("--- Reading back (mode \"r\") ---");
    if (!appendProduct(104, "Laptop Stand", 35.00, 6)) return 1;
    puts("\nAppended 1 record (mode \"a\") - existing data preserved");
    readAndReport("--- Reading again ---");
    return 0;
}

Sample Run (verified against a real file)

main.c
Run Online
=== FILE HANDLING DEMO ===
Wrote 3 records to products.txt(mode "w")
--- Reading back(mode "r") ---ID Name Price Qty Value-----------------------------------------------------------101 Wireless Mouse 24.99 15 374.85102 Mechanical Keyboard 89.50 8 716.00103 USB-C Cable 12.75 40 510.00-----------------------------------------------------------Records: 3 Total quantity: 63 Total value: 1600.85
Appended 1 record(mode "a") - existing data preserved
--- Reading again ---ID Name Price Qty Value-----------------------------------------------------------101 Wireless Mouse 24.99 15 374.85102 Mechanical Keyboard 89.50 8 716.00103 USB-C Cable 12.75 40 510.00104 Laptop Stand 35.00 6 210.00-----------------------------------------------------------Records: 4 Total quantity: 69 Total value: 1810.85

What's actually on disk

After step one — three records, 88 bytes:

main.c
Run Online
101|Wireless Mouse|24.99|15102|Mechanical Keyboard|89.50|8103|USB-C Cable|12.75|40

After the append — four records, 114 bytes:

main.c
Run Online
101|Wireless Mouse|24.99|15102|Mechanical Keyboard|89.50|8103|USB-C Cable|12.75|40104|Laptop Stand|35.00|6

Read the run carefully — four things are being taught at once:

  1. "w" created the file, "r" read it, "a" appended without destroying anything. The second report shows records 101–103 intact, then 104 added at the end. (Had the append used "w", all three original records would be gone and the total would read 210.00.)
  2. Totals are computed from parsed fields, not trusted input.Records: 3 / quantity 63 / value 1600.85, then 4 / 69 / 1810.85 — every number traceable to price × quantity of a successfully parsed line.
  3. The malformed-line guard never fired, but it's what makes the report trustworthy: a corrupt row is reported and skipped rather than silently adding nonsense to the totals.
  4. Byte counts are text-mode byte counts. On Windows the \n written by fprintf becomes \r\n on disk, so each record costs one extra byte (88 = 85+3, 114 = 110+4). On Linux the same program produces 85 and 110 bytes — the mode, not your code, determines that.

(Suggested image: the file on disk shown twice — 3 records/88 bytes, then 4 records/114 bytes — beside the two report tables, with the append arrow marked "existing data preserved")


A Second Pattern: Counting Lines, Words, and Characters

Reading character by character with fgetc — the classic way to process a whole file, and the correct place for the int return type:

main.c
Run Online
#include <stdio.h>
#include <ctype.h>
int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "usage: %s <filename>\n", argv[0]);
        return 1;
    }
    FILE *fp = fopen(argv[1], "r");
    if (fp == NULL) {
        perror(argv[1]);
        return 1;
    }
    int c;
    /* MUST be int — fgetc returns EOF or a byte */ long lines = 0, words = 0, chars = 0;
    int inWord = 0;
    while ((c = fgetc(fp)) != EOF) {
        chars++;
        if (c == '\n') lines++;
        if (isspace(c)) {
            inWord = 0;
        } else if (!inWord) {
            inWord = 1;
            words++;
        }
    }
    if (ferror(fp)) {
        perror("read error");
        fclose(fp);
        return 1;
    }
    fclose(fp);
    printf("lines=%ld  words=%ld  chars=%ld\n", lines, words, chars);
    return 0;
}

Three points worth absorbing:

  • int c is not optional. With char c, a byte of 0xFF looks like EOF and the loop ends early — a bug that only appears on certain files.
  • Read a character at a time and the file's structure becomes your problem to detect. Words are counted by tracking a transition from non-space to space (inWord), which is how every wc-style tool works.
  • argc/argv let the filename come from the command line — wc.exe myfile.txt — which is what makes a file utility reusable instead of hardcoded. argv[0] is the program name, argv[1] the first argument.

(Suggested image: the fgetc loop with the inWord state machine illustrated — characters flowing in, word boundaries marked where a non-space follows a space)


File Handling Cheat Sheet

TaskCode
Open for read (must exist)FILE *fp = fopen(path, "r");
Open for write (truncates)fopen(path, "w")
Open to add to the endfopen(path, "a")
Open binaryfopen(path, "rb") / "wb"
Check the openif (fp == NULL) { perror(path); ... }
Close (and flush)if (fclose(fp) != 0) perror("fclose");
Flush without closingfflush(fp)
Write formatted textfprintf(fp, "%d,%s\n", n, s)
Write a string (no newline added)fputs(s, fp)
Write one charfputc(c, fp)
Write raw bytesfwrite(buf, size, count, fp)
Read a line (bounded, safe)while (fgets(line, sizeof line, fp) != NULL)
Read one char (as int)while ((c = fgetc(fp)) != EOF)
Read formatted fieldsfscanf(fp, "%d %49s", &n, name) == 2
Parse a line you already readsscanf(line, fmt, ...)
Read raw bytessize_t n = fread(buf, size, count, fp)
Detect clean end of filefeof(fp) — after a read failed
Detect an I/O errorferror(fp) — check this first
Move to the startrewind(fp)
Jump to a positionfseek(fp, offset, SEEK_SET)
Current positionlong p = ftell(fp)
Delete a fileremove(path)
Rename a filerename(old, new)
Report an error with reasonperror("context")

(Suggested image: the cheat sheet as a two-column reference card — good candidate for a downloadable/shareable graphic)


Common Mistakes

  1. Not checking fopen for NULL — passing NULL to fgets/fprintf crashes. Check every open.
  2. while (!feof(fp)) — reads once past the end and processes stale data, duplicating the last line. Test the read's return value instead.
  3. Using char for fgetc's return — a 0xFF byte becomes indistinguishable from EOF; the loop stops early.
  4. Forgetting fclose on some path — the output buffer is never flushed (data loss) and handles leak until fopen fails.
  5. Not checking fclose's return — deferred write errors (disk full) go unreported; the program claims success.
  6. Using "w" when you meant "a" — the file is erased at open time, before any of your careful code runs. This is the most destructive single mistake in C file handling.
  7. Using "a+" and expecting to write in the middle — writes always append, regardless of the current read position.
  8. Opening binary data in text mode — newline expansion and the Ctrl+Z EOF character corrupt the bytes silently on Windows.
  9. fscanf in a loop without a width limit — %s has no bound (buffer overflow), and unmatched input isn't consumed, so the loop can spin forever.
  10. Forgetting that fputs adds no newline — all output runs together on one line.
  11. Not stripping the newline fgets keeps — comparisons like if (strcmp(line, "yes") == 0) fail because the line is "yes\n".
  12. Assuming data is on disk before fclose — it may still be buffered. fflush or close before another process (or another fopen) reads it.
  13. Using ftell on a text-mode file as a byte offset — \r\n translation makes positions unreliable. Use binary mode.
  14. fwrite-ing structs and expecting portability — padding, member offsets, and endianness differ across platforms. Serialize for interchange.
  15. Using fread's return as a byte count — it returns items, not bytes. Compare against the item count you asked for.
  16. Mixing fscanf and fgets on the same stream — a leftover newline or partial token is left unread and the next read sees it.
  17. Two fopens on the same file with separate buffers — interleaved writes and stale reads. Open once, use one stream.

(Suggested image: "17 File Handling Mistakes" checklist — with #2, #6, and #10 highlighted as the ones that silently corrupt or destroy data)


Final Thoughts

File handling in C is fopen / fclose plus four read functions and four write functions — the same formatted-I/O skills you already have, redirected from the console to a stream. The FILE * handle is a pointer, stdin/stdout/stderr are just pre-opened streams, and everything else follows from those two facts.

What makes the difference between working and reliable code is the discipline around it:

  • Check every fopen for NULL and report with perror
  • Choose the mode deliberately — "r" to read, "a" to add, "w" only when destroying the old contents is intended
  • Test the read function's return value, never feof, to end a loop
  • Use int for fgetc, and bound every %s and fgets with a size
  • Read the line with fgets, parse it with sscanf — that pair survives malformed input; a bare fscanf loop often doesn't
  • fclose on every path, and check its return — that's where buffered data actually reaches disk
  • Match the mode to the data — "b" for anything that isn't text
  • Never fwrite a struct to a file you'll share — serialize field by field

Get those habits in place and persistence stops being the hard part of your programs. And once data can be read from and written to files, whole categories of programs open up: configuration loaders, log processors, report generators, save systems, and the CSV-and-text pipelines that connect C to everything else.


Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
int main() {
    FILE *fp = fopen("sample_output.txt", "w");
    if (fp == NULL) {
        perror("Failed to open file");
        return 1;
    }
    fprintf(fp, "Hello from C File Handling!\
");
    fprintf(fp, "Operating Systems rely on C file streams.\
");
    fclose(fp);
    printf("File written and closed successfully.\
");
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Always check if (fp == NULL) immediately after fopen(), and always close files with fclose(fp).

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: What happens if a C program exits without calling `fclose()` on an open file?

A: While modern OS kernels reclaim the file descriptor upon process termination, buffered data currently in userspace memory may fail to flush to disk, causing data loss or corruption.
Q

Q: How do text mode and binary mode differ in file handling on Windows?

A: Text mode converts `\ ` to `\\r\ ` automatically on write and vice versa on read; binary mode (`"rb"`/`"wb"`) reads and writes exact unadulterated raw bytes.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations