4 min read
On this page

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 NULL to fprintf, fread, or fclose is undefined behavior.
  • Using feof as a loop condition. while (!feof(f)) processes the last item twice because feof only 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 fgets with a size parameter.
  • Mixing text and binary modes. Opening a binary file in text mode on Windows corrupts the data because \r\n is translated.
  • Buffer overflows with fscanf. Always use width specifiers: %63s instead of %s.

Key Takeaways

  • All standard I/O uses FILE* handles obtained from fopen. Always check for NULL.
  • Use fprintf/fscanf for text, fread/fwrite for binary data.
  • Read lines with fgets, never gets.
  • Use feof and ferror after a short read to determine the cause, not as loop conditions.
  • Control buffering with setvbuf. Use fflush to force output.
  • tmpfile creates secure temporary files that auto-delete on close.
  • Always close files with fclose to flush data and release resources.