stdio File Operations
The FILE* Handle
All standard I/O in C revolves around the FILE type, defined in <stdio.h>. You never create a FILE directly; you obtain a pointer from fopen and pass it to other functions.
Three FILE* handles are available automatically:
stdin-- standard input (keyboard by default)stdout-- standard output (terminal by default)stderr-- standard error (terminal by default, unbuffered)
fopen & fclose
fopen opens a file and returns a FILE*. fclose closes it and flushes any buffered data.
#include <stdio.h>
int main(void) {
FILE *f = fopen("data.txt", "w");
if (f == NULL) {
perror("fopen");
return 1;
}
fprintf(f, "Hello, file!\n");
fclose(f);
return 0;
}
Common mode strings:
| Mode | Meaning |
|---|---|
"r" |
Read (file must exist) |
"w" |
Write (creates or truncates) |
"a" |
Append (creates or appends) |
"r+" |
Read and write (file must exist) |
"w+" |
Read and write (creates or truncates) |
"a+" |
Read and append (creates or appends) |
"rb" |
Read in binary mode |
"wb" |
Write in binary mode |
Always check the return value of fopen. A NULL return means the file could not be opened.
Text Mode vs Binary Mode
In text mode (the default on some platforms), the C library translates line endings. On Windows, \n is written as \r\n and read back as \n. On Unix, no translation occurs.
In binary mode (add b to the mode string), no translation occurs on any platform. Use binary mode for non-text data: images, serialized structs, network protocols.
FILE *text = fopen("notes.txt", "r"); /* text mode */
FILE *bin = fopen("image.png", "rb"); /* binary mode */
fprintf & fscanf
fprintf writes formatted text to a file. fscanf reads formatted text from a file.
#include <stdio.h>
int main(void) {
FILE *f = fopen("scores.txt", "w");
if (!f) { perror("fopen"); return 1; }
fprintf(f, "Alice %d\n", 95);
fprintf(f, "Bob %d\n", 87);
fprintf(f, "Carol %d\n", 92);
fclose(f);
f = fopen("scores.txt", "r");
if (!f) { perror("fopen"); return 1; }
char name[64];
int score;
while (fscanf(f, "%63s %d", name, &score) == 2) {
printf("%-10s %d\n", name, score);
}
fclose(f);
return 0;
}
Alice 95
Bob 87
Carol 92
fscanf returns the number of items successfully matched. Always check it.
fread & fwrite
fread and fwrite handle binary data in blocks:
#include <stdio.h>
struct Record {
int id;
double value;
};
int main(void) {
struct Record records[] = {
{1, 3.14},
{2, 2.72},
{3, 1.41},
};
int count = sizeof(records) / sizeof(records[0]);
/* Write records to binary file */
FILE *f = fopen("records.bin", "wb");
if (!f) { perror("fopen"); return 1; }
size_t written = fwrite(records, sizeof(struct Record), count, f);
printf("Wrote %zu records\n", written);
fclose(f);
/* Read records back */
struct Record loaded[3];
f = fopen("records.bin", "rb");
if (!f) { perror("fopen"); return 1; }
size_t read = fread(loaded, sizeof(struct Record), count, f);
printf("Read %zu records\n", read);
fclose(f);
for (size_t i = 0; i < read; i++) {
printf("id=%d value=%.2f\n", loaded[i].id, loaded[i].value);
}
return 0;
}
Wrote 3 records
Read 3 records
id=1 value=3.14
id=2 value=2.72
id=3 value=1.41
fread and fwrite return the number of elements (not bytes) successfully transferred.
Reading Line by Line: fgets
fgets reads up to n-1 characters or until a newline, whichever comes first. It always null-terminates the buffer.
#include <stdio.h>
#include <string.h>
int main(void) {
FILE *f = fopen("config.txt", "r");
if (!f) { perror("fopen"); return 1; }
char line[256];
int lineno = 0;
while (fgets(line, sizeof(line), f)) {
lineno++;
/* Remove trailing newline */
size_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n') {
line[len - 1] = '\0';
}
printf("%3d: %s\n", lineno, line);
}
fclose(f);
return 0;
}
Never use gets. It was removed from the C standard (C11) because it cannot limit input length and always causes buffer overflows. Use fgets instead.
feof & ferror
After a read function returns fewer items than expected, use feof and ferror to determine why:
#include <stdio.h>
int main(void) {
FILE *f = fopen("data.bin", "rb");
if (!f) { perror("fopen"); return 1; }
char buf[1024];
size_t total = 0;
while (1) {
size_t n = fread(buf, 1, sizeof(buf), f);
total += n;
if (n < sizeof(buf)) {
if (feof(f)) {
printf("End of file reached\n");
} else if (ferror(f)) {
perror("fread");
}
break;
}
}
printf("Total bytes read: %zu\n", total);
fclose(f);
return 0;
}
Do not use feof as a loop condition. The end-of-file flag is only set after a read fails, so while (!feof(f)) reads one extra iteration.
Buffering: setvbuf
The C library buffers I/O for performance. You can control the buffering mode with setvbuf:
#include <stdio.h>
int main(void) {
FILE *f = fopen("log.txt", "w");
if (!f) { perror("fopen"); return 1; }
/* No buffering: every write goes directly to the OS */
setvbuf(f, NULL, _IONBF, 0);
/* Line buffering: flush on every newline */
/* setvbuf(f, NULL, _IOLBF, 0); */
/* Full buffering with a 4KB buffer */
/* char buf[4096]; */
/* setvbuf(f, buf, _IOFBF, sizeof(buf)); */
fprintf(f, "This is written immediately\n");
fclose(f);
return 0;
}
| Mode | Constant | Behavior |
|---|---|---|
| Full | _IOFBF |
Flush when buffer is full |
| Line | _IOLBF |
Flush on newline or buffer full |
| None | _IONBF |
Write immediately (no buffer) |
By default, stdout is line-buffered when connected to a terminal and fully buffered when redirected to a file. stderr is always unbuffered.
fflush
fflush forces buffered data to be written to the OS:
printf("Processing...");
fflush(stdout); /* ensure the message appears before a long operation */
/* ... long operation ... */
printf(" done.\n");
Without fflush, the "Processing..." message might not appear until the newline in "done.\n" triggers a line-buffer flush.
fflush(NULL) flushes all open output streams.
Temporary Files
tmpfile creates a temporary file that is automatically deleted when closed:
#include <stdio.h>
int main(void) {
FILE *tmp = tmpfile();
if (!tmp) { perror("tmpfile"); return 1; }
fprintf(tmp, "temporary data: %d\n", 42);
rewind(tmp);
char buf[64];
if (fgets(buf, sizeof(buf), tmp)) {
printf("Read from temp: %s", buf);
}
fclose(tmp); /* file is automatically deleted */
return 0;
}
Read from temp: temporary data: 42
tmpfile is safer than creating a named temporary file because it avoids race conditions (another process grabbing the filename between creation and opening).
Common Pitfalls
- Not checking fopen's return value. Passing
NULLtofprintf,fread, orfcloseis undefined behavior. - Using feof as a loop condition.
while (!feof(f))processes the last item twice becausefeofonly returns true after a failed read. - Forgetting to fclose. On some systems, data is not flushed to disk until the file is closed. In long-running programs, unclosed files exhaust file descriptor limits.
- Using gets. It has no length limit and was removed from the standard. Use
fgetswith a size parameter. - Mixing text and binary modes. Opening a binary file in text mode on Windows corrupts the data because
\r\nis translated. - Buffer overflows with fscanf. Always use width specifiers:
%63sinstead of%s.
Key Takeaways
- All standard I/O uses
FILE*handles obtained fromfopen. Always check forNULL. - Use
fprintf/fscanffor text,fread/fwritefor binary data. - Read lines with
fgets, nevergets. - Use
feofandferrorafter a short read to determine the cause, not as loop conditions. - Control buffering with
setvbuf. Usefflushto force output. tmpfilecreates secure temporary files that auto-delete on close.- Always close files with
fcloseto flush data and release resources.