Text files

A file in C is a FILE * from stdio.h, opened with a mode and closed before the program ends. Text mode writes and reads formatted lines, and the file functions reuse the printing and scanning of lesson 10 with a stream in place of the terminal. Every open can fail, and every result is checked.

Writing a text file

The mode "w" creates the file or empties an existing one. fprintf writes formatted text to the stream, and fclose flushes the buffer and returns the final status. The result is a normal text file with two lines.

#include <stdio.h> int main(void) { FILE *file = fopen("notes.txt", "w"); if (file == NULL) { perror("notes.txt"); return 1; } fprintf(file, "first line\n"); fprintf(file, "second line\n"); if (fclose(file) != 0) { perror("fclose"); return 1; } printf("wrote two lines\n"); return 0; }

gcc -Wall -Wextra write.c -o write
./write
cat notes.txt

wrote two lines
first line
second line

The program prints its own line and the shell prints the file. The file is created in the current directory and its size depends on the two strings; the newline is what makes cat and the next section's line reader see two lines.

Reading line by line

fgets reads at most one line into a buffer and returns NULL at the end of the file. The loop from lesson 10 works unchanged; the file is just another stream. The newline stays in the buffer, so the lines print exactly as they were written.

#include <stdio.h> int main(void) { FILE *file = fopen("notes.txt", "r"); char line[64]; int number = 0; if (file == NULL) { perror("notes.txt"); return 1; } while (fgets(line, sizeof line, file) != NULL) { number++; printf("%d: %s", number, line); } fclose(file); return 0; }

gcc -Wall -Wextra read.c -o read
./read

1: first line
2: second line

The number comes from the loop, not from the file, and the format has no newline of its own because the line already ends with one. A line longer than 63 characters arrives in pieces, which is the limit of the buffer rather than of the file.

Appending

The mode "a" opens the file for writing at the end, creating it when it does not exist. Opening with "w" would have emptied the two lines from the first section. The program appends one line and then opens the same file for reading to prove the result.

#include <stdio.h> int main(void) { FILE *file = fopen("notes.txt", "a"); if (file == NULL) { perror("notes.txt"); return 1; } fprintf(file, "third line\n"); fclose(file); file = fopen("notes.txt", "r"); if (file == NULL) { perror("notes.txt"); return 1; } char line[64]; while (fgets(line, sizeof line, file) != NULL) { printf("%s", line); } fclose(file); return 0; }

gcc -Wall -Wextra append.c -o append
./append

first line
second line
third line

The three lines exist because the first section created the file. Running only this program on a fresh directory would print one line, and running it twice would print the third line twice; append mode never truncates. The read after the write proves that the buffer was flushed by fclose.

When the open fails

A missing file is the normal failure of fopen, and it returns NULL with errno set. perror prints the program's label and the system message in one call; strerror gives the same text for a manual line.

#include <errno.h> #include <stdio.h> #include <string.h> int main(void) { FILE *file = fopen("missing.txt", "r"); if (file == NULL) { fprintf(stderr, "error %d: %s\n", errno, strerror(errno)); perror("missing.txt"); return 1; } fclose(file); return 0; }

gcc -Wall -Wextra errors.c -o errors
./errors; echo "status: $?"

error 2: No such file or directory
missing.txt: No such file or directory
status: 1

Both lines describe the same error: the number 2 is ENOENT and the text comes from the C library. The error output goes to standard error, which is why it appears before the status line when the two are captured together.

Truncation and a wrong format

The mode is a promise about the existing contents. Opening with "w" to read something and write it back empties the file before the first read, and the program below shows the result:

#include <stdio.h> int main(void) { FILE *file = fopen("notes.txt", "w"); if (file == NULL) { return 1; } fclose(file); printf("file is now empty\n"); return 0; }

gcc -Wall -Wextra truncate.c -o truncate
./truncate
wc -l notes.txt

file is now empty
0 notes.txt

The file still exists, with zero lines. A program that edits a file should read it completely, write a new temporary file, and rename it, rather than opening the original with "w". The second mistake is a format that does not match the argument, which fprintf checks exactly like printf:

#include <stdio.h> int main(void) { FILE *file = fopen("out.txt", "w"); if (file == NULL) { return 1; } fprintf(file, "value: %s\n", 42); fclose(file); return 0; }

gcc -Wall -Wextra badformat.c -o badformat

badformat.c: In function 'main':
badformat.c:10:28: warning: format '%s' expects argument of type 'char *', but argument 3 has type 'int' [-Wformat=]
10 | fprintf(file, "value: %s\n", 42);
| ~^ ~~
| | |
| | int
| char *
| %d

The program is not run here: with %s the library reads the integer 42 as an address and tries to print the string there, which is a crash at best. The warning names the conversion that should have been used, %d, before the file is touched.

Exercises

  1. Write a program that counts the lines, words and bytes of a file opened from the command line, with the count passed in argc/argv.
  2. Copy a file line by line into a second file, and stop with an error when either open fails.
  3. Read a file of numbers, one per line, with fgets and strtol, and print the sum and the number of invalid lines.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does the mode "w" do to an existing file?

    Answer

    It empties the file before writing

    Write mode truncates the file, which is why editing in place needs a temporary file and a rename.

  2. What does fgets keep in the buffer?

    Answer

    The newline, when there is room for it

    The newline stays in the buffer, so a plain printf of the line reproduces the original break.

  3. What does fclose do besides releasing the stream?

    Answer

    It flushes buffered output and reports errors

    The buffered bytes are written out, and the return value reports whether the final write succeeded.

  4. What does perror print?

    Answer

    A label and the system message for errno

    perror combines the caller's label with the text for the current error number, in one call.

  5. What does this program print? #include <stdio.h> int main(void){FILE*f=fopen("/tmp/notes.txt","w");if(!f)return 1;fprintf(f,"first line\n");fprintf(f,"second line\n");fclose(f);printf("wrote two lines\n");return 0;}

    Answer

    wrote two lines

    The program prints its own message after closing the stream.

  6. What does this program print for the two-line file? #include <stdio.h> int main(void){FILE*f=fopen("/tmp/notes.txt","w");fprintf(f,"first line\nsecond line\n");fclose(f);f=fopen("/tmp/notes.txt","r");char l[64];int n=0;while(fgets(l,sizeof l,f)){n++;printf("%d: %s",n,l);}fclose(f);return 0;}

    Answer

    1: first line / 2: second line

    The loop numbers each line from one and prints the stored newline, so each entry ends cleanly.

  7. What does this program print? #include <stdio.h> int main(void){FILE*f=fopen("/tmp/notes.txt","w");fprintf(f,"first line\nsecond line\n");fclose(f);f=fopen("/tmp/notes.txt","a");fprintf(f,"third line\n");fclose(f);f=fopen("/tmp/notes.txt","r");char l[64];while(fgets(l,sizeof l,f))printf("%s",l);fclose(f);return 0;}

    Answer

    first line / second line / third line

    Append mode keeps the existing text, and the new line goes after the second one.

  8. What does this program print? #include <errno.h> #include <stdio.h> #include <string.h> int main(void){FILE*f=fopen("missing-file.txt","r");if(f==NULL){printf("error %d: %s\n",errno,strerror(errno));return 1;}fclose(f);return 0;}

    Answer

    error 2: No such file or directory

    ENOENT is 2, and strerror prints the system message for it.

  9. Why is opening a file with "w" before reading it a mistake?

    Answer

    The file is truncated before the read

    Write mode empties the file immediately, so the read finds nothing; the temporary-file pattern avoids this.

  10. What does GCC warn about in fprintf(file, "value: %s\n", 42)?

    Answer

    That %s expects a char *, not an int

    The format is checked like printf, and the warning names %d as the conversion for the integer argument.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev