C / FILES AND ERRORS
fopen modes and text versus binary streams
Choose the right fopen mode string for any file task, and know when text-mode translation silently changes the bytes that reach disk.
What you will learn
- Pick r, w, or a by asking: must the file exist, may I destroy it, where do writes go?
- Remember that "w" empties the file inside fopen, before your first write
- Add "b" for any non-text data; it is free on POSIX and mandatory on Windows
- Use "r+" to edit bytes in place and "wx" to create a file only if it is new
Understanding fopen modes and text versus binary streams
fopen's second argument looks like a tiny word, but it is really three independent answers packed together. The first character says whether the file must already exist: "r" fails if it does not, while "w" and "a" create it. It also says what happens to bytes that are already there: "w" truncates the file to zero length during the call to fopen, before your program writes anything, while "a" leaves them alone and forces every later write to the current end of the file. Adding "+" grants the missing direction of I/O, so "r+" reads and writes an existing file in place and "w+" still truncates first, but the "+" never changes the existence or truncation rule the first letter already set.
The optional "b" picks between two kinds of stream. A text stream is allowed to translate between your program's '\n' and whatever the host uses to end a line: on Windows every '\n' you write becomes the two bytes 0x0D 0x0A, and every 0x0D 0x0A you read collapses back into one '\n'. A binary stream is a plain byte pipe: what you hand fwrite is what lands on disk, byte for byte. On Linux and macOS a line already ends with a single 0x0A, so the two stream types are the same thing and "b" does nothing, which is precisely why a missing "b" in code that writes a struct or a PNG is a bug you cannot see until someone builds it on Windows.
Because the mode decides what the operating system is asked to do, it also decides how fopen can fail, and fopen reports every failure the same way: it returns NULL. "r" on a path that does not exist, "w" in a directory you cannot write to, "wx" (added in C11) on a file that already exists, all give NULL, and the stream must not be touched afterwards. Update modes add one rule that has nothing to do with permissions: with "r+", "w+" or "a+" you may not follow a write with a read unless fflush or a positioning call comes between them, and you may not follow a read with a write without a positioning call or hitting end-of-file. Ignore that and you read whatever the buffer happened to hold, which is the standard's way of saying the behaviour is undefined.
<stdio.h>
<stdlib.h>
static void dump(const char *path)
{
FILE *f = fopen(path, "rb");
long n = 0;
int c;
if (f == NULL) {
printf("%-10s missing\n", path);
return;
}
printf("%-10s \"", path);
while ((c = fgetc(f)) != EOF) {
if (c == '\n')
printf("\\n");
else
putchar(c);
n++;
}
printf("\" (%ld bytes)\n", n);
fclose(f);
}
static FILE *open_or_die(const char *path, const char *mode)
{
FILE *f = fopen(path, mode);
if (f == NULL) {
fprintf(stderr, "cannot open %s in mode \"%s\"\n", path, mode);
exit(EXIT_FAILURE);
}
return f;
}
int main(void)
{
FILE *f;
remove("notes.txt"); /* start from a known state */
remove("absent.txt");
dump("notes.txt");
f = open_or_die("notes.txt", "w"); /* create, or truncate to 0 bytes */
fputs("alpha\n", f);
fclose(f);
dump("notes.txt");
f = open_or_die("notes.txt", "a"); /* keep old bytes, write at the end */
fputs("beta\n", f);
fclose(f);
dump("notes.txt");
f = open_or_die("notes.txt", "w"); /* truncation happens inside fopen */
fclose(f);
dump("notes.txt");
f = fopen("absent.txt", "r"); /* "r" never creates a file */
printf("fopen(\"absent.txt\", \"r\") -> %s\n", f == NULL ? "NULL" : "stream");
if (f != NULL)
fclose(f);
return 0;
}The mode string decides three separate things at once, whether the file must exist, whether its old contents survive, and whether newline bytes get translated, and fopen commits to all three before you write a single byte.
Worked examples
What the b actually changes
Writes CR LF pairs as raw bytes, then counts them through a binary stream and a text stream.
<stdio.h>
static long count_bytes(const char *path, const char *mode)
{
FILE *f = fopen(path, mode);
long n = 0;
if (f == NULL)
return -1;
while (fgetc(f) != EOF)
n++;
fclose(f);
return n;
}
int main(void)
{
FILE *f = fopen("crlf.bin", "wb");
if (f == NULL)
return 1;
fputs("a\r\nb\r\n", f); /* six bytes, stored exactly as given */
fclose(f);
printf("binary mode read: %ld\n", count_bytes("crlf.bin", "rb"));
printf("text mode read: %ld\n", count_bytes("crlf.bin", "r"));
return 0;
}Example explained
Line 1fopen("crlf.bin", "wb") gives a binary stream, so both 0x0D bytes reach the file untouched and the file is 6 bytes long.
Line 2Reading with "rb" counts 6 because a binary stream hands back exactly the bytes on disk.
Line 3Reading with "r" also counts 6 here, since on Linux and macOS text and binary streams are identical.
Line 4The same program on Windows prints 4 for the text read, because each 0x0D 0x0A is collapsed into one '\n' with no error reported.
Append mode versus r+ with the same fseek
Shows that fseek cannot make a write in append mode land anywhere but the end, while "r+" overwrites in place.
<stdio.h>
int main(void)
{
FILE *f;
int c;
f = fopen("log.txt", "w");
if (f == NULL)
return 1;
fputs("AAAA", f);
fclose(f);
f = fopen("log.txt", "a"); /* append mode */
if (f == NULL)
return 1;
fseek(f, 0, SEEK_SET); /* ask for the start of the file */
fputs("Z", f); /* the write goes to the end anyway */
fclose(f);
f = fopen("log.txt", "r+"); /* update in place, no truncation */
if (f == NULL)
return 1;
fseek(f, 0, SEEK_SET);
fputs("B", f); /* this one really lands on byte 0 */
fclose(f);
f = fopen("log.txt", "r");
if (f == NULL)
return 1;
while ((c = fgetc(f)) != EOF)
putchar(c);
putchar('\n');
fclose(f);
return 0;
}Example explained
Line 1fopen("log.txt", "a") does not truncate, so "AAAA" survives and the file becomes "AAAAZ".
Line 2The fseek in append mode moves the position, but the standard forces every write to the then current end of file, so "Z" cannot land at byte 0.
Line 3fopen("log.txt", "r+") requires the file to exist and keeps its bytes, so the identical fseek now really does aim the next write at byte 0.
Line 4fputs("B", f) overwrites rather than inserts: the file stays 5 bytes long and the first 'A' is gone.
Important notes
On Linux and macOS the "b" is accepted and ignored, so omitting it never fails locally; that is exactly what makes it a portability bug rather than a compile error.
The first character must be r, w or a. A mode like "rw" is not a set of flags: glibc ignores the stray w, gives you a read-only stream, and the first fputs fails instead of fopen.
Common mistakes
Using "w" when "r+" was meant: fopen truncates before it returns, so the read that follows sees an empty file and the original contents are already gone.
Leaving off the "b" when writing structs or images: on Windows every 0x0A inside the data expands to 0x0D 0x0A, so the file grows and reads back corrupted, while the Linux tests still pass.
Opening with "a" and fseek-ing back to update an earlier record: append mode forces the write to end-of-file, so you silently add a duplicate at the bottom instead of changing anything.
Try it yourself
Change, predict, then run
In one program, open the same file four times in a row: "w" and write "x", then "a" and write "y", then "r+" with fseek(f, 0, SEEK_SET) and write "z", then "r" to print the whole file. Write down your prediction of the final contents before running it.
Open the C workspaceCheck your understanding
A program writes a 100-byte struct with a single fwrite after fopen(path, "w"). On Linux the file is 100 bytes; the same source compiled on Windows produces a larger file. What explains the difference?
- Mode "w" is a text stream, and a Windows text stream expands every 0x0A byte in the data into 0x0D 0x0A
- fwrite on Windows prefixes the file with a byte-order mark identifying the encoding
- The Windows C library pads every text stream out to a multiple of 512 bytes
- The Windows compiler inserts more padding into the struct, so sizeof is larger
Show answer
Without the "b" the stream is a text stream, and a Windows text stream turns each 0x0A it is handed into the two-byte line terminator; it has no way to know that this 0x0A came from the middle of an int rather than the end of a line. Option 3 is tempting because struct layout really does differ between ABIs, but that would change sizeof at compile time and fwrite would still put exactly that many bytes on disk, whereas the extra bytes here appear between fwrite and the file, in the translation layer. The fix is fopen(path, "wb").