Input, output and files · about 50 min

Opening, reading and writing files

Why this matters

Redirection lets a program read one input and write one output, but real programs juggle files: a configuration file, an input data set, an output report, a log. They also need to cope when a file is missing. This lesson covers the standard C way to work with files by name, the stdio functions you will use constantly, and the one loop pattern that reads a file correctly.

Opening a file: fopen

FILE *fopen(const char *path, const char *mode);

fopen asks the operating system to open the file at path and, if that works, allocates a FILE object (with its buffer) and returns a pointer to it. If it fails, it returns NULL. You must check, every time: a missing file is not exotic, it is the most common error a program meets.

   FILE *in = fopen("poem.txt", "r");

   in ---> +--------------------------+
           | FILE                     |
           |  fd: 3   (kernel handle) |
           |  buffer: [ 8192 bytes ]  |
           |  position, error flag,   |
           |  end-of-file flag ...    |
           +--------------------------+

The FILE lives on the heap inside the C library. You never look inside it; you pass the pointer to stdio functions. Descriptors 0, 1 and 2 are taken by the standard streams, so the first file a program opens usually gets descriptor 3.

The mode string says what you intend:

modereadwriteif file existsif missing
"r"yesnoread from the startfail (NULL)
"w"noyestruncate to emptycreate
"a"noyeswrite at the endcreate
"r+"yesyesstart at the beginning, keep contentsfail
"w+"yesyestruncatecreate
"a+"yesyesreads anywhere, writes at the endcreate

Adding b ("rb", "wb") means binary. On Linux it makes no difference; on Windows text mode translates line endings, so binary files must use b. The next lessons cover binary files.

Relative paths like "poem.txt" are relative to the process's current working directory, the directory you ran the program from, not the directory where the executable lives.

Closing: fclose

int fclose(FILE *stream);

fclose flushes any buffered output, releases the descriptor and frees the FILE. Forgetting it has real consequences: buffered output that was never flushed may be lost if the program ends abnormally, and a process can only have a limited number of files open at once (often 1024). After fclose, the pointer is dangling; do not use it again.

Reading lines: fgets

char *fgets(char *buf, int size, FILE *stream);

fgets reads characters into buf until it has stored a newline, or reached end of file, or filled size - 1 characters, whichever comes first. It always adds the terminating '\0'. It returns buf on success and NULL when nothing could be read (end of file or an error).

The newline is kept in the buffer. This program numbers the lines of a file:

#include <stdio.h>

int main(void)
{
    FILE *in = fopen("poem.txt", "r");
    if (in == NULL) {
        fprintf(stderr, "cannot open poem.txt\n");
        return 1;
    }

    char line[256];
    int n = 0;
    while (fgets(line, sizeof line, in) != NULL) {
        n++;
        printf("%3d  %s", n, line);
    }

    fclose(in);
    return 0;
}
$ ./numberlines
  1  Roses are red,
  2  violets are blue,
  3  C has no strings,
  4  only char arrays too.

The format is "%3d %s" with no \n because line already ends with one. sizeof line is 256, the size of the array, which keeps fgets inside it. If a line were longer than 255 characters, fgets would return the first 255, and the rest on the next call; the lesson on robust parsing shows how to detect that.

The other readers: fgetc and fscanf

fgetc(stream) reads one byte and returns it as an int, or EOF. It is getchar for any stream, and the loop is identical:

int c;
while ((c = fgetc(in)) != EOF) {
    /* use c */
}

fscanf(stream, format, ...) is scanf for any stream. It returns the number of items it successfully converted, which you compare against the number you asked for:

int a, b;
long total = 0;
while (fscanf(in, "%d %d", &a, &b) == 2)
    total += b;

This stops at end of file (fscanf returns EOF) and also at the first malformed record (it returns 0 or 1), which is usually what you want. The format %s reads one whitespace-separated word; always give it a width one less than the buffer size, as in %31s for char name[32], or a long word will overflow the array.

Writing: fprintf, fputs, fputc

These are the stream versions of printf, puts and putchar:

#include <stdio.h>

int main(void)
{
    FILE *out = fopen("squares.txt", "w");
    if (out == NULL) {
        fprintf(stderr, "cannot create squares.txt\n");
        return 1;
    }
    for (int i = 1; i <= 5; i++)
        fprintf(out, "%d %d\n", i, i * i);
    fclose(out);

    FILE *log = fopen("run.log", "a");
    if (log == NULL)
        return 1;
    fputs("wrote squares.txt\n", log);
    fclose(log);
    return 0;
}

Run it twice:

$ ./writelog
$ ./writelog
$ cat squares.txt
1 1
2 4
3 9
4 16
5 25
$ cat run.log
wrote squares.txt
wrote squares.txt

squares.txt has five lines, not ten: mode "w" emptied it on the second run. run.log grew, because "a" appends. Note that fputs, unlike puts, does not add a newline.

The correct reading loop, and the wrong one

Every stdio read function tells you whether it worked through its return value. The pattern is always "try to read; if it worked, use the data; otherwise stop":

while (fgets(line, sizeof line, in) != NULL)   /* good */
while ((c = fgetc(in)) != EOF)                 /* good */
while (fscanf(in, "%d", &x) == 1)              /* good */

There is also a function feof(stream), and beginners are drawn to writing:

while (!feof(in)) {              /* WRONG */
    fgets(line, sizeof line, in);
    printf("> %s", line);
}
$ ./feofbug
> Roses are red,
> violets are blue,
> C has no strings,
> only char arrays too.
> only char arrays too.

The last line is printed twice. feof only reports that a previous read already hit the end; it cannot know that the next one will. After the fourth line, no read has failed yet, so the loop runs a fifth time, fgets fails and leaves line untouched, and the stale contents are printed. feof and its partner ferror are for asking why a loop stopped, after it stopped:

if (ferror(in))
    fprintf(stderr, "read error\n");

Common mistakes

Recap

Going further: man 3 fopen lists every mode. Try opening the same file twice, once for reading and once for appending, and see what the reader sees after a write that has not been flushed yet.

Exercises

Exercise 1 · quiz

Choosing a mode

A program keeps a history file and must add one line to the end of it on every run, creating the file if it does not exist yet and never destroying earlier lines. Which fopen mode is right?

Answer
  • "a"

"a" (append) creates the file if needed and positions every write at the end. "w" would create it too, but truncates an existing file to zero length the moment it is opened, so the history would be wiped on every run. "r" and "r+" both fail if the file does not exist, and "r" cannot write at all.

Exercise 2 · write code

Count lines in a file

Write a program that takes a file name as its only command-line argument (argv[1]) and prints how many lines the file has, in exactly this form:

data.txt: 4 lines

Count lines by counting newline characters ('\n'), reading with fgetc in a loop.

Error handling:

File: main.c

Starting point

#include <stdio.h>

int main(int argc, char *argv[])
{
    /* check argc, open argv[1], count '\n' with fgetc, close, print */
    return 0;
}

Example with arguments data.txt

expected output

data.txt: 4 lines

Example with arguments empty.txt

expected output

empty.txt: 0 lines
Hint 1

Start with if (argc != 2), then FILE *f = fopen(argv[1], "r"); and check it against NULL.

Hint 2

The reading loop is the same shape as the getchar loop: int c; while ((c = fgetc(f)) != EOF) { ... }.

Hint 3

Remember fclose(f); before printing the result.

Reference solution
#include <stdio.h>

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "usage: prog FILE\n");
        return 2;
    }
    FILE *f = fopen(argv[1], "r");
    if (f == NULL) {
        fprintf(stderr, "cannot open %s\n", argv[1]);
        return 1;
    }
    int c;
    long lines = 0;
    while ((c = fgetc(f)) != EOF) {
        if (c == '\n')
            lines++;
    }
    fclose(f);
    printf("%s: %ld lines\n", argv[1], lines);
    return 0;
}

Exercise 3 · quiz

The feof loop

poem.txt has 4 lines, each ending in a newline. What does this print?

char line[256];
while (!feof(in)) {
    fgets(line, sizeof line, in);
    printf("> %s", line);
}
Answer
  • The 4 lines, then the last line a second time

feof does not predict the future; it reports whether a previous read already ran into the end. After the 4th fgets the end has not been hit yet (the newline was the last thing read), so the loop runs once more. That 5th fgets fails, returns NULL and leaves line unchanged, and the old contents are printed again. Always test the read function's return value instead: while (fgets(line, sizeof line, in) != NULL).

Exercise 4 · write code

Read one file, write another

The file scores.txt contains one record per line: a name (one word, at most 31 characters) and an integer score, separated by a space. Write a program that reads it with fscanf and writes a new file report.txt with one line per student, where the student passes with a score of 50 or more:

ada 91 pass
bob 42 fail

After writing the file, print to stdout exactly one line with the counts:

3 passed, 2 failed

If scores.txt cannot be opened, or report.txt cannot be created, print an error to stderr and exit with 1. The check inspects report.txt after your program runs.

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    FILE *in = fopen("scores.txt", "r");
    if (in == NULL) {
        fprintf(stderr, "cannot open scores.txt\n");
        return 1;
    }

    char name[32];
    int score;
    /* open report.txt for writing, then loop:
       while (fscanf(in, "%31s %d", name, &score) == 2) { ... } */

    fclose(in);
    return 0;
}

Example

expected output

3 passed, 2 failed
Hint 1

Open the output with FILE *out = fopen("report.txt", "w"); and check for NULL like the input.

Hint 2

Inside the loop use fprintf(out, ...), not printf, for the report lines; count passes and fails in two ints.

Hint 3

%31s reads one word into name but never more than 31 characters, leaving room for the terminating zero.

Reference solution
#include <stdio.h>

int main(void)
{
    FILE *in = fopen("scores.txt", "r");
    if (in == NULL) {
        fprintf(stderr, "cannot open scores.txt\n");
        return 1;
    }
    FILE *out = fopen("report.txt", "w");
    if (out == NULL) {
        fprintf(stderr, "cannot create report.txt\n");
        fclose(in);
        return 1;
    }

    char name[32];
    int score;
    int passed = 0, failed = 0;
    while (fscanf(in, "%31s %d", name, &score) == 2) {
        if (score >= 50) {
            fprintf(out, "%s %d pass\n", name, score);
            passed++;
        } else {
            fprintf(out, "%s %d fail\n", name, score);
            failed++;
        }
    }

    fclose(in);
    fclose(out);
    printf("%d passed, %d failed\n", passed, failed);
    return 0;
}