Everything a program computes disappears when it exits, unless it writes it down. C's file interface lives in <stdio.h> - the same header as printf - and it is deliberately similar: a file is a stream of bytes, and the functions that work on the screen have twins that take a stream argument.
The handle type is FILE *. You never look inside a FILE; it is an opaque type (see typedef) and you only ever hold a pointer to one.
The Three Steps
Every file operation follows the same shape: open, use, close.
The NULL check is not optional paranoia. fopen fails whenever the file does not exist (in read mode), the directory is not writable, the path is wrong, or the process is out of file handles - and every one of those returns NULL. Using a NULL handle is a segmentation fault.
Sending the error to stderr rather than stdout is the convention: it is unbuffered, and it can be redirected separately from real output.
fopen Modes
The second argument is a short mode string. Getting it wrong is the most destructive mistake in this whole area, because "w" silently empties an existing file.
| Mode | Read | Write | If the file exists | If it does not |
|---|---|---|---|---|
"r" | yes | no | opens at the start | fails, returns NULL |
"w" | no | yes | truncated to empty | created |
"a" | no | yes | writes at the end only | created |
"r+" | yes | yes | opens at the start | fails, returns NULL |
"w+" | yes | yes | truncated to empty | created |
"a+" | yes | yes | reads anywhere, writes at the end | created |
Append a b to any of these ("rb", "wb", "ab+") for binary mode - more on that at the end.
Two rules that prevent real data loss:
- Use
"r"when you mean to read."r+"on a typo'd filename fails safely;"w+"creates an empty file and you have lost nothing but time."w"on the right filename when you meant"r"destroys the data. - Use
"a"for logs. Everyfprintflands at the end regardless of where the stream was positioned, which is exactly what a log wants.
Writing and Reading in One Program
The editor below runs a complete round trip - it creates a file, writes records into it, closes it, reopens it for reading, and prints what it finds.
fprintf and fscanf are printf and scanf with a stream as the first argument; everything about their format specifiers is identical, including the %31s width that keeps name from overflowing.
The loop condition is == 2, the number of items the format asks for. Testing against EOF instead is a classic bug: a malformed line makes fscanf return 0, not EOF, and the loop spins forever on input it cannot consume.
Reading Line by Line with fgets
fscanf is convenient for rigidly formatted data. For text files - configuration, logs, CSV, anything a human wrote - read whole lines. This loop is the canonical one:
What makes fgets the right default:
- It takes the buffer size, so it cannot overflow. Pass
sizeof lineand the call stays correct if you resize the array. - It returns
NULLat end of file or on error, which is a clean loop condition. - It keeps the newline when the line fit in the buffer. That is useful - no
'\n'in what you got means the line was longer than your buffer and the rest is still waiting.strcspn(line, "\n")finds the newline's index (or the string length if there is none), so assigning'\0'there trims it either way.
To distinguish a real end of file from an error, ask after the loop:
if (ferror(in)) {
fprintf(stderr, "read error\n");
} else if (feof(in)) {
/* normal end */
}
Do not write while (!feof(fp)) as a loop condition. feof only becomes true after a read has already failed, so that loop processes the final buffer contents one extra time. Test the return value of the read function instead - as both loops above do.
Character at a Time: fgetc and fputc
For byte-level work - counting characters, transforming a file, copying - fgetc and fputc handle one character per call.
One detail that matters: c is declared int, not char. fgetc returns an int so that it can return every possible byte value and the distinct sentinel EOF (which is -1). Storing it in a char makes the comparison against EOF either always false or falsely true for the byte 0xFF, depending on whether char is signed on your platform. This is one of C's oldest gotchas.
Checking Errors Properly
A production read looks like this:
errno holds a code describing the last failure and strerror turns it into a sentence; perror prints your message plus that sentence in one call. Both need <errno.h> and <string.h> respectively.
fclose can also fail - it flushes buffered data, and the write may not fit on the disk - so for anything important, check it:
if (fclose(fp) != 0) {
fprintf(stderr, "failed to flush and close\n");
}
Binary Mode in One Paragraph
Text mode may translate line endings (on Windows, \n becomes \r\n when written and back when read) and may treat certain bytes specially. For data that is not text - an image, a struct dumped verbatim, a compressed blob - open with b and use fread/fwrite, which move raw bytes:
fwrite(ptr, size, count, fp) writes count items of size bytes and returns how many it managed; fread mirrors it. Be aware that a file written this way is tied to the machine that wrote it - struct padding, integer size, and byte order all leak into the bytes - so it is fine for a cache or scratch file and wrong for a format other programs must read.
Common Mistakes
- Not checking
fopenforNULL. The crash that follows is blamed on the read, not the open. - Opening with
"w"when you meant"r". The file is emptied before you notice. - Forgetting
fclose. Buffered output is lost, and each unclosed file leaks a handle. while (!feof(fp)). Processes the last line twice. Test the read call instead.char c = fgetc(fp). Breaks theEOFcomparison. Useint.fscanf("%s", buf)with no width. The same buffer overflow as scanf on the keyboard.- Relative paths.
fopen("data.txt", "r")looks in the working directory, not next to the executable. If a file "disappears", that is usually why.
Frequently Asked Questions
How do you open a file in C?
FILE *fp = fopen("data.txt", "r"); opens a file for reading and returns a FILE * handle, or NULL if it failed. Always check for NULL before using the handle, and call fclose(fp) when you are done.
What are the fopen modes in C?
"r" read (file must exist), "w" write (creates, or truncates an existing file to empty), "a" append (creates, writes at the end). Adding + makes each one read-and-write: "r+", "w+", "a+". Adding b ("rb", "wb") opens in binary mode.
How do you read a file line by line in C?
Use fgets in a while loop: while (fgets(line, sizeof line, fp) != NULL) { ... }. It stops at each newline or when the buffer is full, returns NULL at end of file, and cannot overflow because you pass the buffer size.
Why is my file empty after writing to it in C?
Most often you forgot fclose. Output is buffered, so data may still be sitting in memory when the program ends abnormally. fclose flushes and closes; fflush(fp) flushes without closing. The other cause is opening with "w" a second time, which truncates the file you just wrote.