CodeMIRAI
0 XP

Working with files

6 min read 8 runnable examples Quiz included

Everything a program keeps in variables disappears when it ends. To keep data (scores, settings, a log, a saved game) a program writes it to a file, and reads it back next time. C's file functions are in <stdio.h>, the same header as printf, and they work much like printf and scanf, but with a file at the other end.

Opening a file: FILE and fopen

C
FILE *f = fopen("scores.txt", "w");

fopen opens a file and returns a FILE *, a pointer to the information C keeps about the open file (where it is, how far you've read, its buffer). Every other file function takes that pointer. The second argument is the mode:

ModeOpens forIf the file existsIf it doesn't
"r"readingreads from the startfopen fails, returns NULL
"w"writingempties it firstcreates it
"a"appendingwrites at the endcreates it
"r+"reading and writingkeeps itfails
"w+"reading and writingempties itcreates it
"a+"reading and appendingkeeps itcreates it

Add b (as in "rb" or "wb") for binary files. On Linux and macOS it changes nothing; on Windows, text mode translates line endings, so binary data needs b.

Writing a file

C
#include <stdio.h>

int main() {
    FILE *f = fopen("scores.txt", "w");
    if (f == NULL) {
        printf("Couldn't open scores.txt for writing\n");
        return 1;
    }
    fprintf(f, "Asha %d\n", 92);         // like printf, into the file
    fprintf(f, "Ravi %d\n", 78);
    fputs("Meera 85\n", f);              // a plain string, no formatting
    fclose(f);                           // finish writing and release the file
    printf("Saved 3 scores\n");
    return 0;
}
Output
Saved 3 scores
  • fprintf(f, ...) is printf with a file as its first argument.
  • fputs(text, f) writes a string as it is. Unlike puts, it adds no new line.
  • fputc(ch, f) writes one character.
  • fclose(f) finishes the job. Writes go through a buffer in memory, and fclose makes sure everything reaches the disk. Forgetting it can lose the end of your data, and every open file uses up one of a limited number of slots.

Reading line by line: fgets

fgets(line, size, f) reads one line (up to size - 1 characters, including the new line) and returns NULL at the end of the file, which makes the standard reading loop:

C
#include <stdio.h>

int main() {
    FILE *out = fopen("notes.txt", "w");
    if (out == NULL) return 1;
    fputs("Buy milk\nCall Ravi\nFinish the C lesson\n", out);
    fclose(out);

    FILE *f = fopen("notes.txt", "r");
    if (f == NULL) {
        printf("Couldn't open notes.txt\n");
        return 1;
    }
    char line[100];
    int n = 0;
    while (fgets(line, sizeof line, f) != NULL) {
        n++;
        printf("%d: %s", n, line);       // line already ends with \n
    }
    fclose(f);
    return 0;
}
Output
1: Buy milk
2: Call Ravi
3: Finish the C lesson

Reading numbers: fscanf

fscanf is scanf reading from a file, and it returns how many values it read, so it can drive a loop too:

C
#include <stdio.h>

int main() {
    FILE *out = fopen("marks.txt", "w");
    if (out == NULL) return 1;
    fprintf(out, "Asha 92\nRavi 78\nMeera 85\nKabir 64\n");
    fclose(out);

    FILE *f = fopen("marks.txt", "r");
    if (f == NULL) return 1;
    char name[30];
    int mark, count = 0, total = 0, best = -1;
    char top[30] = "";
    while (fscanf(f, "%29s %d", name, &mark) == 2) {   // stop when a line doesn't give 2 values
        count++;
        total += mark;
        if (mark > best) {
            best = mark;
            snprintf(top, sizeof top, "%s", name);
        }
    }
    fclose(f);
    printf("%d students, average %.1f, top: %s with %d\n", count, (double) total / count, top, best);
    return 0;
}
Output
4 students, average 79.8, top: Asha with 92

Looping while (fscanf(...) == 2) stops cleanly at the end of the file, and also at a badly formatted line, instead of looping forever on input it can't read.

Character by character: fgetc and EOF

fgetc(f) returns the next character, or the special value EOF (end of file) when there are none left. It returns an int, not a char, precisely so that EOF (usually -1) can't be confused with a real character:

C
#include <stdio.h>

int main() {
    FILE *out = fopen("poem.txt", "w");
    if (out == NULL) return 1;
    fputs("Twinkle twinkle little star\nHow I wonder what you are\n", out);
    fclose(out);

    FILE *f = fopen("poem.txt", "r");
    if (f == NULL) return 1;
    int ch;                                 // int, so it can hold EOF
    int chars = 0, words = 0, lines = 0, inWord = 0;
    while ((ch = fgetc(f)) != EOF) {
        chars++;
        if (ch == '\n') lines++;
        if (ch == ' ' || ch == '\n' || ch == '\t') {
            inWord = 0;
        } else if (!inWord) {
            inWord = 1;                     // the first letter of a new word
            words++;
        }
    }
    fclose(f);
    printf("%d lines, %d words, %d characters\n", lines, words, chars);
    return 0;
}
Output
2 lines, 10 words, 54 characters

That's the core of the Unix wc (word count) program. Note the pattern (ch = fgetc(f)) != EOF: read, store and test in one condition.

Appending

Mode "a" adds to the end and never erases what's there, which is what a log needs:

C
#include <stdio.h>

void logLine(const char *message) {
    FILE *f = fopen("app.log", "a");
    if (f == NULL) return;
    fprintf(f, "%s\n", message);
    fclose(f);
}

int main() {
    remove("app.log");                      // start this demo with no log
    logLine("started");
    logLine("user signed in");
    logLine("lesson 3 completed");

    FILE *f = fopen("app.log", "r");
    if (f == NULL) return 1;
    char line[100];
    while (fgets(line, sizeof line, f) != NULL) printf("log: %s", line);
    fclose(f);
    return 0;
}
Output
log: started
log: user signed in
log: lesson 3 completed

remove(name) deletes a file, and rename(old, new) renames one; both return 0 when they succeed.

Copying a file

Read from one, write to the other, until the end:

C
#include <stdio.h>

int main() {
    FILE *out = fopen("original.txt", "w");
    if (out == NULL) return 1;
    fputs("Line one\nLine two\n", out);
    fclose(out);

    FILE *src = fopen("original.txt", "r");
    FILE *dst = fopen("copy.txt", "w");
    if (src == NULL || dst == NULL) {
        printf("Couldn't open the files\n");
        return 1;
    }
    int ch, copied = 0;
    while ((ch = fgetc(src)) != EOF) {
        fputc(ch, dst);
        copied++;
    }
    fclose(src);
    fclose(dst);
    printf("Copied %d characters\n", copied);
    return 0;
}
Output
Copied 18 characters

Binary files: fwrite and fread

Text files store numbers as digits you can read. Binary files store the bytes exactly as they are in memory: smaller, faster, and no conversion, but not readable in a text editor. fwrite and fread move whole blocks at once:

C
#include <stdio.h>

int main() {
    int scores[5] = {92, 78, 85, 64, 99};

    FILE *f = fopen("scores.bin", "wb");
    if (f == NULL) return 1;
    size_t written = fwrite(scores, sizeof scores[0], 5, f);   // 5 ints from the array
    fclose(f);

    int back[5] = {0};
    f = fopen("scores.bin", "rb");
    if (f == NULL) return 1;
    size_t got = fread(back, sizeof back[0], 5, f);
    fclose(f);

    printf("wrote %d, read %d:", (int) written, (int) got);
    for (int i = 0; i < 5; i++) printf(" %d", back[i]);
    printf("\n");
    return 0;
}
Output
wrote 5, read 5: 92 78 85 64 99

Both take the address of the data, the size of one item, how many items, and the file, and return how many items they actually moved, which is how you detect a short or damaged file. Structures can be saved the same way, which is how simple programs save a whole record in one call.

Reporting errors: perror

When fopen fails, the C library records why in errno (from <errno.h>). strerror(errno) turns that into words, and perror prints your message followed by them:

C
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main() {
    FILE *f = fopen("no-such-file.txt", "r");
    if (f == NULL) {
        printf("Couldn't open no-such-file.txt: %s\n", strerror(errno));
        perror("no-such-file.txt");     // the same reason, on the error channel
        return 1;
    }
    fclose(f);
    return 0;
}
Output
Couldn't open no-such-file.txt: No such file or directory

perror writes to standard error (stderr), the error channel, which stays separate from normal output (so its line, no-such-file.txt: No such file or directory, isn't in the output above); fprintf(stderr, ...) writes there too. That's why programs can send results to a file while errors still reach the screen.

The file functions at a glance

TaskFunction
Open / closefopen(name, mode), fclose(f)
Write formatted / a string / a characterfprintf, fputs, fputc
Read formatted / a line / a characterfscanf, fgets, fgetc
Binary blocksfwrite, fread
End of filethe reading function's result: NULL, EOF, or fewer items than asked
Delete / renameremove, rename
Why it failedperror, errno
Finished this lesson?Mark it complete to earn +20 XP and move your progress on.