File Handling in C — 'w' Mode Truncated Our Logs
A daemon restart with fopen("daemon.log", "w") wiped 2 hours of logs.
20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.
- C file I/O uses FILE* pointers for buffered access to disk files
- fopen() opens a file; always check for NULL before using the pointer
- fread() and fwrite() handle raw binary data with fixed-size records
- fseek() provides O(1) random access when combined with sizeof arithmetic
- fclose() flushes buffers and releases the file descriptor — skipping it loses data
- Text mode ('r','w') and binary mode ('rb','wb') differ in newline translation on Windows
Imagine your program's memory is a whiteboard — fast, but erased the moment you walk out of the room. A file is a notebook you leave on the desk: it survives after you leave, and anyone (or any program) can open it later and pick up exactly where you left off. File handling in C is just the set of tools that lets your program open that notebook, read what's written, add new pages, and close it properly when it's done. Forget to close it and it's like leaving the notebook soaking in the rain — you risk losing everything.
Every meaningful program eventually needs to outlive itself. A configuration it reads on startup, a log it writes during a crash, a CSV it exports for a client — none of that works if data only lives in RAM. File handling is the bridge between your program's fleeting in-memory world and the persistent world on disk. That's not a nice-to-have feature; it's table stakes for anything production-grade.
The C standard library solves this with a thin but powerful abstraction: the FILE pointer. Instead of dealing with raw OS system calls (which differ between Linux, Windows, and macOS), you get a portable, consistent API — fopen, fread, fwrite, fprintf, fscanf, fclose — that works identically everywhere C runs. The OS complexity is hidden behind that single FILE* handle, which is exactly why C file I/O has survived for fifty years without being replaced.
By the end of this article you'll be able to open files safely, read and write both text and binary data, handle errors the way real production code does, and avoid the three mistakes that trip up even experienced developers. You'll also walk away with the conceptual model that makes every fread/fwrite call feel obvious rather than magical.
What File Handling in C Actually Does — and Why 'w' Mode Destroys Your Data
File handling in C is the set of standard library functions (fopen, fclose, fprintf, fscanf, etc.) that allow a program to read from and write to persistent storage via a FILE* stream. The core mechanic is that fopen returns a pointer to a FILE structure that holds the stream state, buffer, and file descriptor; you then use that pointer for all subsequent I/O operations until fclose flushes the buffer and releases the resource.
In practice, the mode string you pass to fopen determines the initial state of the file pointer and whether the file is truncated or appended. 'w' opens the file for writing, creates it if it doesn't exist, and — critically — truncates the file to zero length immediately upon success. This happens before your first fprintf call. There is no undo. The file's previous content is gone the moment fopen returns. 'a' (append) does not truncate; 'r+' opens for read/write without truncation but requires the file to exist.
Use 'w' only when you intend to replace the entire file content from scratch — for example, writing a fresh configuration file or a new log file per run. Never use 'w' for appending logs, updating records in place, or any scenario where existing data must survive. In production systems, a single 'w' where 'a' was intended can silently erase hours of critical log data, making this one of the most common and costly C I/O mistakes.
Opening and Closing Files — The FILE* Lifecycle You Must Respect
Every file operation in C starts with fopen() and must end with fclose(). The FILE* pointer you get back isn't a file — it's a control structure the C runtime uses to buffer data, track your position, and manage the connection to the OS file descriptor. Think of it as a library card: you need it to check out books, and you must return it when you're done.
fopen() takes two arguments: the file path and a mode string. The mode tells the OS what you intend to do — 'r' for reading an existing file, 'w' for writing (creating or truncating), 'a' for appending, and their binary variants 'rb', 'wb', 'ab'. Choosing the wrong mode is one of the most common sources of data loss in C programs.
fopen() returns NULL on failure — network drive unavailable, wrong permissions, file doesn't exist in 'r' mode. You must check for NULL before doing anything else. Skipping that check is not a style issue; it's a segfault waiting to happen. Always pair your NULL check with perror() so the error message tells you exactly what went wrong, not just that something did.
perror() and exit — not printf("Error").Reading and Writing Text Files — fprintf, fscanf, fgets in Practice
Text file I/O is the most common form of file handling you'll write. Configuration files, CSV exports, log files, INI parsers — they're all text. C gives you three main tools: fprintf/fscanf (formatted, mirrors printf/scanf), fgets (line-by-line reading, the safe choice), and fputc/fgetc (character by character, useful for parsers).
fscanf looks tempting but has a reputation for being fragile — it stops at whitespace, which means names with spaces silently break it. fgets is almost always the better choice for reading: it reads an entire line up to a newline or the buffer limit, whichever comes first, so it never overflows your buffer (unlike gets, which was removed from C11 for that exact reason).
The pattern for reading a text file line by line is a while loop with fgets as the condition. When fgets hits end-of-file it returns NULL, ending the loop cleanly. Inside the loop you can parse, tokenise, or process each line however you need. This pattern powers everything from simple config readers to lightweight CSV parsers in embedded systems.
strlen()-1, which breaks on empty lines.Binary File I/O — fread and fwrite for Structs and Raw Data
Text files are human-readable but inefficient for structured data. If you're storing a thousand employee records, writing each integer as a string of ASCII digits wastes space and requires parsing on the way back in. Binary mode writes the raw bytes from memory directly to disk — what's in the struct goes in, what's on disk comes back out. Same layout, no conversion.
fwrite(pointer, size_of_one_element, count, file) is the workhorse. It returns the number of elements successfully written — if that's less than count, something went wrong and you need to check ferror(). fread mirrors it exactly: same four arguments, returns how many elements it actually read.
The critical thing to understand is that binary files are not portable across different CPU architectures. A file written on a little-endian x86 machine will have its integer bytes in reverse order compared to what a big-endian SPARC machine expects. For embedded systems or single-platform tools this doesn't matter. For cross-platform data exchange, use text or a defined binary format like protobuf. Know this tradeoff before you choose binary.
Random Access and File Positioning — fseek, ftell and rewind
Sequential reading is fine for logs and config files, but a database that re-reads the whole file to update one record is a performance disaster. C gives you fseek() for jumping to any byte position, ftell() for finding out where you currently are, and rewind() as a shortcut for jumping back to the start.
fseek(file, offset, origin) takes three arguments. The origin can be SEEK_SET (from the start of the file), SEEK_CUR (relative to current position), or SEEK_END (relative to the end — useful for finding the file size). The offset is a signed long, so you can move forward or backward.
The real power of fseek emerges with fixed-size structs. Because every Employee record is exactly sizeof(Employee) bytes, you can jump to the nth record with fseek(file, n * sizeof(Employee), SEEK_SET). This is how simple flat-file databases work — and understanding this pattern demystifies a lot of file-based data storage you'll encounter in embedded systems and legacy codebases.
ftell() + rewind() trio is the standard C idiom for getting a file's size, since C has no dedicated file-size function in the standard library. Interviewers love asking this — and the follow-up is whether this works on binary vs text files (it's only guaranteed accurate for binary mode).Error Handling and Safe File I/O Patterns for Production Code
Production file I/O isn't just about reading and writing — it's about handling failures gracefully. fread and fwrite return the number of items actually transferred, which may be less than the count you asked for. You must check these returns and then call ferror() to distinguish between an end-of-file condition and a genuine I/O error. feof() tests for end-of-file but is only valid after a failed read — don't use it to predict future reads.
Buffering is another hidden concern. The C runtime buffers output internally and only flushes when the buffer fills or fclose is called. setvbuf() lets you control the buffer size or disable buffering entirely, which is critical for writing logs in crash-prone environments. For production wrappers, use a helper function that combines fopen, error checking, and setvbuf into a single call — this reduces the surface area for mistakes.
Finally, remember that the file system is a shared resource. File locks (via flock or fcntl) become necessary when multiple processes write to the same file. C's standard I/O library doesn't provide locking, so you must use OS-specific calls or external coordination.
- fopen can fail: file doesn't exist, permissions, full disk, network mount down.
- fwrite can fail partway through: disk full, quota exceeded, interrupted.
- fclose is the only way to guarantee data has been flushed to disk.
- Multiple processes writing to one file need external locking — C's stdio doesn't provide it.
- Error recovery should be part of the design, not an afterthought.
File Opening Modes — The Silent Data Killer Is ios::trunc
Every file open call carries a mode. No mode means defaults. And defaults will destroy your data without warning.
When you open an ofstream without specifying a mode, C++ applies ios::out | ios::trunc by default. That trunc flag erases the file's existing content the moment the stream constructs. One misconfigured constructor, and your customer's configuration file is gone.
ios::app is your friend when you need to append. ios::ate positions the cursor at the end but still lets you seek backward. ios::binary turns off newline translations that corrupt binary data on Windows.
Always be explicit. Write std::ofstream logfile("transactions.log", std::ios::app) and never rely on implicit behavior. Mode flags compose with bitwise OR — that's not a coincidence, it's a design choice that gives you fine-grained control. Use it.
File Open Failure — Checking Is Cheap, Silently Corrupting Is Not
A file open failure is not an exception. It's a silent state change in the stream object. If you don't check, you'll read garbage, write to nowhere, and wonder why your report is empty.
The method or the boolean conversion operator on the stream will tell you the truth. Use one of them. Every time. Before reading or writing a single byte.is_open()
Common failure causes: file doesn't exist for ios::in, permissions insufficient for write, path is a directory, or the disk is full. These aren't edge cases — they happen in production daily.
Pattern: open, check, bail or handle. Never assume. If opening a config file fails, fall back to defaults, log the error, and continue. If opening a database file fails, stop and scream. The response depends on criticality, but the check is mandatory.
if (file)) works on all stream states, not just open. But is_open() is more precise — it only fails if the open call itself failed. Use it for open checks.is_open() or the stream's boolean operator before any read or write.Production Log Truncation Caused by w Mode in File Rotation
- Always verify the fopen mode matches the intended behaviour — 'w' is destructive.
- Implement log rotation or appending as a deployment step to avoid data loss.
- Add a startup check that logs a warning if the file already exists when opening in 'w' mode.
perror() to print the system error.ferror() to distinguish between EOF and error. If ferror is 0, it's EOF – partial read. If ferror is non-zero, I/O error occurred. Use perror() to see the error.fflush() after each write if immediate persistence is required, or ensure fclose() is called. Check if buffer size is large (setvbuf can help).ls -la app_log.txt (check file existence and permissions)perror("fopen") — prints the system error messageKey takeaways
fopen()'s return value for NULL and call perror() immediatelyCommon mistakes to avoid
3 patternsNot checking if fopen() returned NULL
fopen() in a NULL check followed by perror() and a clean exit; treating file I/O as infallible is the most common C file-handling bug.Forgetting fclose() or only calling it on the happy path
fclose() is called in every exit path (including error exits), or structure code so a single cleanup block always runs before returning; in complex functions, consider goto cleanup: at the end.Using 'w' mode when 'a' or 'r+' was intended
Interview Questions on This Topic
What is the difference between fread/fwrite and read/write system calls, and when would you choose one over the other?
Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.
That's C Basics. Mark it forged?
7 min read · try the examples if you haven't