int fileExists(const char *path) {
FILE *fp = fopen(path, "r");
if (fp == NULL) return 0;
fclose(fp);
return 1;
}C File Input and Output Operations: Complete Practical Guide
What is C File Input and Output Operations? Core Concept & Direct Summary
How do binary file functions (`fread()`, `fwrite()`, `fseek()`, `ftell()`, `rewind()`) read and write binary structures on disk?
Most C tutorials teach file I/O function by function: here's fopen, here's fgets, here's fwrite. That's the right way to learn the topic — but it's the wrong way to use it. When you actually sit down to write code, you don't think "I need fgets." You think:
- "How do I copy this file?"
- "How do I count the lines?"
- "How do I find which line contains this word?"
- "How do I change one record inside a file?"
- "How do I read the whole thing into memory?"
This page is organized that way — by task. Each recipe is complete, self-contained, and correct, with the error checking that real programs need and toy examples omit.
But first, one organizing framework, because it explains which function to reach for.
(Suggested image: the hero graphic — a segmented bar of the four I/O levels (character / line / formatted / block) with the corresponding function pairs labeled under each segment)
How Does C File Input and Output Operations Work? In-Depth Mechanics & Architecture
The Four Levels of File I/O
Every file operation in C sits at one of four granularities. Identifying the level tells you the function almost automatically.
How to choose:
- Processing every byte or counting things? → Character level.
fgetcin a loop. - Reading text records of unknown length? → Line level.
fgets, then parse. - The file has a strict, regular structure? → Formatted. But prefer line level +
sscanf, which is more robust. - Performance matters, or the data is binary? → Block level.
fread/fwritein large chunks.
Two notes on the near-duplicate functions:
getcvsfgetcandputcvsfputcare functionally identical. The difference:getc/putcmay be implemented as macros (potentially faster, but you can't take their address or pass them as arguments without care), whilefgetc/fputcare guaranteed real functions. Preferfgetc/fputcfor predictability.ungetc(c, fp)pushes one character back onto the stream — useful when a parser reads one character too many and needs to "unread" it. Only one character of pushback is guaranteed.
And the "formatted vs unformatted" vocabulary you'll see everywhere: character, line, and block I/O are unformatted (they handle raw characters/bytes/lines); fprintf/fscanf are formatted (they interpret text according to a format string).
(Suggested image: a decision tree — "need every byte? no → records of text? yes → lines are unbounded? → fgets + sscanf" — mapping to the four levels)
Operation Reference
Opening and closing
Input (reading)
Output (writing)
Positioning (binary streams)
Status and errors
File-level operations
(Suggested image: the reference tables as a printable one-page cheat sheet — high shareability)
Recipe 1 — Does This File Exist?
The straightforward approach: if you can open it for reading, it exists.
Caveats worth knowing:
It doesn't tell you why it failed — missing, no permission, and bad path all return "no." If you need the reason, open and keep the
FILE *:Some systems let you "open" a directory for reading, so this check can report a directory as an existing file.
stat()(POSIX) distinguishes them, but it's not standard C.There's a race: the file could vanish between your check and your use. So this is for reporting ("file not found, sorry"), not for guarding logic. Open once and handle
NULL— that's always the safer structure.
Recipe 2 — Get the File Size
How it works: jump to the end, ask where you are.
Three details that make it correct:
- Binary mode is mandatory. In text mode on Windows,
\r\ntranslation means the reported position may not equal the byte count — you'd get 106 for a file that occupies 111 bytes on disk. - Both
fseekandftellcan fail — pipes, terminals, and some devices aren't seekable. Check both. - Use an
okflag, not-1alone, because-1is a legitimate "error" but so is a zero-byte file plus an error path you didn't distinguish. Separating "value" from "success" avoids ambiguity — the same pattern as returning a count from a lookup plus a-1not-found signal.
On non-seekable streams (a pipe, stdin from a pipe), you can't measure size this way. You have to read until EOF and count.
Recipe 3 — Copy a File (Binary-Safe)
Why this version is right, in five points:
- Both modes are binary. A text-mode copy on Windows can expand bytes, corrupting images, archives, and executables.
fread's return value is the loop condition. It returns items read — withsize= 1, that's bytes.0means EOF or error; theferrorcheck afterwards tells you which.- Blocks, not bytes. Copying 4 KB at a time means one
freadper 4096 bytes instead of per byte — orders of magnitude fewer function calls and syscalls. - The partial-read handling.
freadmay return fewer items than requested, andfwritemay write fewer. Passingn(what you actually got) tofwriteand comparing its return is the correct pairing. - The leaked-handle guard. If opening
outfails, the input file must be closed on that path too. In a multi-file operation, every failure branch needs to release what's already open — the same ownership discipline asmalloc/free.
(Suggested image: the copy loop as a pipeline — 4 KB block read from source, block written to destination, repeated until fread returns 0 — with the equal-return check marked)
Recipe 4 — Count Lines, Words, and Characters
The word-counting logic is a two-state machine: a word begins at a transition from whitespace to non-whitespace. That correctly handles multiple spaces, tabs, and leading/trailing whitespace — and it's how wc works.
Two requirements worth restating: int c (a char can't distinguish a 0xFF byte from EOF), and ferror after the loop (a disk error must not be reported as a clean end-of-file).
Recipe 5 — Read a Whole File Into Memory
Four things to get right:
size + 1— you need room for the NUL terminator if the buffer is to be used as a string.- Trust
got, notsize. The file could have changed between measuring and reading.gotis what actually arrived. - The caller owns the buffer — document it. The
malloc/freepairing is the caller's responsibility, exactly as the dynamic memory tutorial describes. - It loads everything at once — fine for a config file or a few megabytes, a bad idea for a multi-gigabyte log. For large files, stream with
fgetsorfreadblocks instead.
Recipe 6 — Search a File, Report Line Numbers
Why the loop counter is correct here:lineno increments on every fgets success, before the match test — so line numbers are true positions. The one subtlety: when a line exceeds LINE_MAX, fgets returns it in multiple pieces, and each piece counts as a "line." That's the fgets long-line behavior covered in the mistakes section — for a line-number tool, either size the buffer generously or detect the split (see mistake #12).
strstr gives substring matching. Swap in strncmp for a "starts with" test, or strcmp for an exact-match test.
(Suggested image: the search loop with lineno incrementing before the test, and the three matching lines highlighted with their numbers)
Recipe 7 — Filter Lines Into a New File
fputs writes the line including the newline it still holds from fgets — so the output file keeps proper line structure. Using fprintf(out, "%s\n", line) here would double the newline.
This is the shape of grep-style tools, log extractors, and config submitters.
Recipe 8 — Read a Delimited File Into Structs
The details that make this production-grade:
n < maxItemsis checked first — never write past the caller's buffer, no matter how many lines the file has.%47[^|]— reads up to 47 characters and stops at the|, so names with spaces work, and the width prevents overflow intoname[48].== 4— the parse succeeded only if all four fields converted. Anything else means a corrupt row.- Skip-and-continue on bad lines, with a message to
stderr. One malformed row doesn't abort the load or poison the data. - Returns the count loaded, and
-1only for a genuine open/read failure — distinguishing "couldn't open" from "opened but the file was empty."
Recipe 9 — Write Structs Back Out
Two checks that matter more than they look:
fprintf's return is negative on failure. Disks fill up and quotas get hit mid-loop. Checking each write turns a silent truncation into a reported error.fclose's return is where buffered writes finally land. This is the last chance to detect that the data never made it — and skipping the check means your program reports success on a partial file.
And use %s with a delimited format for human-readable, diff-friendly, portable files. For binary output you'd use fwrite, with the portability caveats from the file handling pillar page.
Recipe 10 — Update a Record In Place
There's no "seek to line 3 and rewrite it" when lines have variable length. The reliable technique: write a new file, then replace the old one atomically.
Why this pattern is the correct one:
- Variable-length lines can't be rewritten in place. Changing
3to3000shifts every following byte, so you'd have to rewrite the remainder anyway. Writing a fresh file is simpler and safer. - The original survives until the end. If anything fails — read error, disk full, write error — the old file is untouched. With in-place rewriting, a crash mid-way leaves you with neither version.
renameis the atomic swap. On POSIX it replaces the destination in one operation. On Windows you mustremovefirst, and the window between remove and rename is non-atomic — worth knowing if power loss is a concern.- Unparseable lines are passed through, not dropped. A data file with a comment line shouldn't lose it during an update.
- Nothing changed? Don't touch the file. Skipping the replace avoids needless writes and keeps modification times meaningful.
This is how editors, databases, and config tools do it. It's also why the pattern shows up in interviews.
(Suggested image: the temp-file-and-rename flow — original file read, new file written, original removed, temp renamed into place, with "original intact until success" labeled)
Recipe 11 — Append a Timestamped Log Line
Mode "a" is the whole point — it cannot destroy existing content, and every write goes to the end regardless of where the read position is.
On buffering: a log that must survive a crash should be flushed promptly:
Real logging often wants the file opened once and kept open, rather than reopened per message.
(Suggested image: the append path — existing log lines preserved above, a new timestamped line added at the end, with mode "a" labeled "never truncates")
Recipe 12 — Clear or Truncate a File
Note the subtlety: you don't need to write anything. Opening a file with "w" truncates it immediately, in the fopen call itself. That's why "w" is dangerous by accident — and convenient by intent.
To avoid creating the file if it doesn't exist, check first:
Recipe 13 — Merge Several Files Into One
Binary mode and block copying make this work for text and binary sources. The loop opens, drains, and closes one source at a time — so you never hold more than two handles open, no matter how many files you merge.
Recipe 14 — Redirect stdout to a File
Instead of passing a FILE * to every printing function, redirect the stream itself:
The uses: making a program's normal printf output land in a file with zero code changes, which is exactly what shell redirection does (myprog > report.txt) — except freopen does it inside the program, so it works even when the program is launched by double-clicking.
Two cautions:
- Flush
stdoutbefore redirecting away (fflush(stdout)), or buffered console output may end up in the file. - Keep errors on
stderr. Redirectingstdoutto a file means any diagnostics you send there are invisible;fprintf(stderr, ...)still reaches the console.
Recipe 15 — Temporary Files
tmpfile() creates a file and arranges for it to be removed automatically on close or normal program termination — no name to manage, no cleanup to forget, no chance of colliding with a real file. Ideal for scratch space you need to read back during a single run.
Note the rewind: after writing, the position is at the end. You must reposition before reading (fflush + rewind, or fseek).
And a caveat:tmpfile() may fail if the temp directory is unwritable or full — always check for NULL.
Buffering and Performance
Buffering is why fgetc in a loop isn't as slow as it looks: C reads a large block from the OS, then serves your single-character requests from memory. Understanding it explains performance surprises:
When to intervene:
- Bulk copying or processing megabytes → use
fread/fwritewith a 4 KB–64 KB buffer (Recipe 3). - A log or progress indicator that must appear instantly →
fflushat the right moments, or_IONBFif correctness demands it. - Thousands of small writes → batch them into one buffer and do one
fwrite. The single biggest file-I/O performance mistake is onefprintfper byte. - Reading the same file repeatedly → read it once into memory (Recipe 5) if it fits.
(Suggested image: the buffering diagram — many small program calls served from one memory buffer, with large periodic reads from the OS crossing the boundary)
📋 Complete Program: A File Operations Tool
All the core operations in one runnable tool — create, measure, count, search, and copy.
Sample Run (every value verified by executing the operations)
The number that teaches the most: 111 vs 106
Same file. fileSize reports 111 bytes; the text-mode count reports 106 characters. That 5-byte gap is not a bug — it's one \r per line.
The program wrote five "\n" characters in text mode. On Windows, text mode translated each to \r\n on disk:
Those two functions reached different byte counts because they used different modes — "rb" for the exact on-disk size, "r" to get content back with newlines normalized to \n. On Linux, both would report 106.
That's the practical value of the four-level framework: the level you choose decides whether you're counting storage bytes or content characters, and those are legitimately different numbers.
(Suggested image: the same file shown twice — as disk bytes with visible \r\n markers (111) and as logical content lines (106) — with the 5-byte difference attributed to newline translation)
Common Mistakes
These are the operational pitfalls — complementary to the open/close lifecycle mistakes covered in the file handling guide.
Sizing a "read whole file" buffer as
sizeinstead ofsize + 1— no room for the NUL terminator; string functions run off the end.Trusting the
ftellsize for how many bytesfreadwill return — the file can change in between. Usefread's return value.Assuming
freadreturns bytes — it returns items. Withsize> 1, the number is smaller. Compare against the item count you requested.Copying in text mode — expansion corrupts binary files. Use
"rb"/"wb".fgetswith too small a buffer on long lines — a 1000-character line into achar[64]returns 16 "lines." Line counts inflate, records split, and each piece looks like valid data. Not reported as an error. Size buffers generously or detect the missing newline:Leaking an open handle when a later open fails — in copy/merge operations, every failure branch must close what's already open.
Using
renameto overwrite on Windows withoutremovefirst — the call fails, leaving a stray temp file and an unmodified original.Replacing the original file before verifying the new one was written completely — check your write and
fclosereturns, then swap.fseekon a non-seekable stream — pipes, terminals, and some devices can't be positioned;fseekfails. Check its return and fall back to sequential reading.Ignoring that
ftellcan return-1— a bad position treated as a size silently produces nonsense.Not flushing
stdoutbeforefreopenorexit— buffered output can be lost or land in the wrong place.Redirecting
stdoutand losing error messages — keep diagnostics onstderr.Reading a file you're still writing to — unless you
fflushorfclosefirst, the data may not be on disk yet, so the reader sees a short or empty file.Opening a temp file by a fixed name in a shared directory — another process can clobber it. Prefer
tmpfile(), or a name including a PID/timestamp.One
fprintfper byte in a loop — the dominant performance mistake. Batch into a buffer and write once.ferrornever checked — a disk error reported as a clean end-of-file means you silently accept truncated data as complete.Assuming file operations are atomic — a crash between "remove original" and "rename temp" on Windows leaves no file. Keep backups where that matters.
(Suggested image: "17 File I/O Operations Mistakes" checklist — with #5, #8, and #13 highlighted as the ones that produce plausible-but-wrong results)
Final Thoughts
File I/O operations in C reduce to a small, learnable set of moves once you organize them by level — character, line, formatted, block — and by task.
The recipes on this page cover the overwhelming majority of what real programs need: measure a file, count its contents, search it, filter it, load it into structs, write it back, update one record safely, append to a log, merge several files, copy it in blocks, and clean up after yourself. Each one is short. What makes them reliable rather than merely working is a consistent set of habits applied every time:
- Check every
fopenforNULL, and close what you opened on every exit path - Trust the read function's return value, never
feof— and checkferrorto tell an error from an ending - Match the mode to the data —
"b"for anything that isn't text - Use
intforfgetc, and bound every buffer and every%s - Read a line, then parse it —
fgets+sscanfsurvives malformed input; a barefscanfloop often doesn't - Check
fprintf/fwritereturns andfclose's return — that's where buffered failures surface - Never rewrite a file in place — write a temp file and swap it, so a failure leaves the original intact
Get those right and the difference between code that works on your machine and code that works everywhere, on any input, becomes small and mechanical — which is exactly what you want from the most mundane and most depended-upon part of a program.
Runnable C Code Example & Practical Implementation
#include <stdio.h>
typedef struct {
int id;
char name[20];
double balance;
}
Account;
int main() {
Account accWrite = {
1001, "John Doe", 4500.75
};
// Write binary record
FILE *fp = fopen("account.bin", "wb");
if (fp) {
fwrite(&accWrite, sizeof(Account), 1, fp);
fclose(fp);
}
// Read binary record back
Account accRead;
fp = fopen("account.bin", "rb");
if (fp) {
fread(&accRead, sizeof(Account), 1, fp);
fclose(fp);
printf("Binary Record: ID: %d | Name: %s | Balance: $%.2f\
",
accRead.id, accRead.name, accRead.balance);
}
return 0;
}Key Takeaways, Best Practices & Memory Safety Rules
"rb" or "wb" modes to prevent Windows newline translation from corrupting binary image/data files.Frequently Asked Questions & Technical Interview Insights
Technical Interview & Viva Questions
Frequently asked C programming interview questionsQ: How do you determine the total size of a file in bytes using standard C library functions?
Q: What do `fread` and `fwrite` return?
Related Tools, Tutorials & Authoritative References
Recommended Interactive Tools
- Online C Compiler & Runner — Interactive online GCC sandbox to execute C code in real time.
- Online C Code Editor & IDE — Multi-file C programming environment with code formatting.
- C++ Programming Tutorial — Transition from procedural C to Object-Oriented C++ with STL.
- JavaScript Programming Tutorial — Master modern JavaScript fundamentals, DOM APIs, and async patterns.
Authoritative Standards & Citations
- ISO/IEC 9899 (C Language Standard) — Official international C specification by ISO/IEC JTC 1/SC 22/WG 14.
- GNU GCC Documentation — Official reference manual for the GNU Compiler Collection and C libraries.
- The C Programming Language (K&R) — Definitive classic work by Brian Kernighan & Dennis Ritchie at Bell Labs.
- C Reference Documentation (cppreference.com) — Verified reference covering C89 through C23 language features.