Learn C With Me #09 — File I/O, Part One | Live

In Lesson 9 of Learn C With Me, we begin one of the most practical topics we’ve encountered so far: File Input/Output in C.

Using Beej’s Guide to C as our study material, we learn how C represents open files using FILE *, how to open and close files, and how to read and write text data using several different standard library functions.

We cover:

  • What file input/output means in C
  • The FILE * data type
  • The concept of streams
  • The standard streams stdin, stdout, and stderr
  • Why stdout and stderr are separate streams
  • A quick look at shell redirection
  • The difference between text and binary streams
  • Opening files with fopen()
  • Understanding file modes such as "r", "w", "a", and their variants
  • Closing files with fclose()
  • Reading individual characters with fgetc()
  • Understanding EOF
  • Why fgetc() returns an int instead of a char
  • Reading an entire file one character at a time
  • Reading text one line at a time with fgets()
  • Using a character array as an input buffer
  • Understanding the size argument passed to fgets()
  • What happens when a line is larger than the buffer
  • Working with newline characters read from a file
  • Formatted file input with fscanf()
  • Reading multiple values from structured text files
  • Why scanf-style functions need to be treated carefully with untrusted input
  • Limiting string field widths to help avoid buffer overflows
  • Opening a file for writing
  • How "w" mode truncates an existing file
  • Writing individual characters with fputc()
  • Writing strings with fputs()
  • Writing formatted output with fprintf()

One of the important ideas in this lesson is that C treats files as streams. The same general model also applies to stdin, stdout, and stderr, which helps explain why functions such as fprintf() can write either to a file or directly to standard output.

We also spend plenty of time experimenting with fgets() and its buffer size. Rather than just accepting the example, we change the size, inspect the results, and work through what actually happens when the buffer isn’t large enough to contain an entire line.

Later, we move on to formatted input with fscanf(), where things get more interesting — and potentially more dangerous. We look at how formatted input can overflow a buffer if we’re careless and why field widths matter when reading strings.

Finally, we turn things around and start writing text files, using fputc(), fputs(), and fprintf() to generate output ourselves.

As usual, we experiment, make predictions, get confused, consult the documentation, make mistakes, get help from the live chat, and try to understand what C is actually doing rather than simply memorizing function calls.

This is Part One of File Input/Output. We stop just before Binary File I/O, which we’ll continue with in the next lesson.

Homework #09 — File Input/Output, Part One

For this week’s homework, we’re going to build a small text file analyzer.

The goal is to practice what we learned in Chapter 9 — File Input/Output, up to and including Writing Text Files.

We are not using Binary File I/O yet.

The task

Create a text file called:

input.txt

Put several lines of text inside it. Use whatever text you like.

For example:

Learning C is fun.
Pointers were confusing.
Structs were pretty cool.
Now we can read files!

Your C program should open this file, read its contents, calculate some basic statistics, print the results to the terminal, and finally write those results to another file.


1. Open the input file

Open:

input.txt

for reading using:

fopen()

Store the returned FILE *.

If opening the file fails, print an error message to:

stderr

and stop the program.

Remember to close the file when you’re finished with it.


2. Count the characters

Read the file one character at a time using:

fgetc()

Count how many characters the file contains.

Stop when you reach:

EOF

Print the result.

For example:

Characters: 92

The exact number will obviously depend on your input.txt.


3. Count the lines

While reading the file, also count the number of lines.

Think about what character marks the end of a line in a text file.

Your output should now contain something like:

Characters: 92
Lines: 4

Important

Try to solve this yourself using what we’ve already learned about characters, loops and strings.


4. Read the file using fgets()

Now reopen the file and read it again, but this time use:

fgets()

Create a character array to use as a buffer.

For example:

char buffer[100];

Read the file one line at a time and print each line to the terminal.

Experiment with the size of the buffer.

What happens if you change:

char buffer[100];

to something much smaller, such as:

char buffer[10];

Does fgets() still work?

What happens to a line that doesn’t fit into the buffer?

Try it before looking up the answer.


5. Count words

Add a word counter.

For this homework, we’ll use a deliberately simple definition:

A word is a sequence of characters separated by spaces or newlines.

You don’t need to handle every possible kind of whitespace perfectly.

For example:

Hello world
This is C

contains:

5 words

Use what we’ve already learned about:

  • loops
  • characters
  • conditions
  • arrays
  • strings

to work out a solution.


6. Write the results to a file

Create:

stats.txt

using fopen() in write mode.

Write your results into the file using:

fprintf()

The resulting file could look something like:

File statistics

Characters: 92
Words: 17
Lines: 4

Close the output file when you’re finished.


7. Use the other output functions

We learned three functions for writing text:

fputc()
fputs()
fprintf()

Your program must use all three at least once.

How you use them is up to you.

For example:

  • fputs() could write the heading
  • fprintf() could write the statistics
  • fputc() could write a newline

Don’t use them mechanically just to satisfy the requirement. Try to understand what each function is useful for.


Bonus challenge — Number the lines

When displaying the contents of input.txt, add line numbers.

Instead of:

Learning C is fun.
Pointers were confusing.
Structs were pretty cool.

print:

1: Learning C is fun.
2: Pointers were confusing.
3: Structs were pretty cool.

Use fgets() to read each line and printf() to display the line number.


Bonus challenge — Break fscanf()

During the stream we saw why formatted input can be dangerous when the input doesn’t match what the program expects.

Create a separate small experiment using:

fscanf()

Make a text file containing some structured data and successfully read it.

Then deliberately change the input so it no longer matches what your fscanf() call expects.

Try:

  • unexpected text instead of a number
  • a string longer than your destination array
  • adding a field width to %s

Observe what happens.

Don’t just make it fail — try to understand why the behavior changes.


Rules

Use only concepts we’ve covered so far in Learn C With Me, up to the part of Chapter 9 covered in this stream.

You may use:

FILE *
fopen()
fclose()
fgetc()
fgets()
fscanf()
fputc()
fputs()
fprintf()
EOF
stdin
stdout
stderr

along with everything we’ve already learned in previous lessons.

Do not use:

  • Binary File I/O
  • fread()
  • fwrite()
  • dynamic memory allocation
  • command-line arguments
  • techniques from later chapters

The goal is not to make the shortest possible program.

The goal is to become comfortable with the idea that a file is a stream of data, and that we can open that stream and process it using the tools C gives us.

As always: experiment.

Change the input. Make the buffer too small. Remove the file. Use the wrong file mode. Predict what will happen before running the program.

And if something breaks, read the compiler output, inspect what your program actually did, and post your code and questions in the thread.

this time i think i did follow the steps to its fullest but did not bother making it condensed so no DRY code here.

secret code
#include <stdio.h>

int main(void) {
    FILE *in;
    in = fopen("input.txt", "r");
    if (!in) {
        fprintf(stderr, "Error: file could not open\n");
        return 1;
    }
    
    int charcount = 0;
    int linecount = 0;
    int wordcount = 0;
    int c;
    char buffer[100];
    char word[256];
    
    while ((c = fgetc(in)) != EOF) {
        charcount++;
        if (c == '\n') {
            linecount++;
        }
    }
    rewind(in); // not in lesson if i remember but opens file again at the start

    while (fscanf(in, "%s", word) == 1) {
        wordcount++;
        //bonus point fscanf(in, "%5s", word) would break do to 5 limit if word was bigger 
    }
    rewind(in);

    int current_line = 1;
    while (fgets(buffer, sizeof buffer, in) != NULL) {
        printf("%d: %s", current_line, buffer);
        current_line++; // bonus point again XD
    }
    printf("\n"); //not sure why but new lines are included exept the one next to NULL in the end
    rewind(in);

    printf("Characters: %d\n", charcount);
    printf("Lines: %d\n", linecount);
    printf("Words: %d\n", wordcount);

    FILE *out = fopen("stats.txt", "w");
    if (!out) {
        fprintf(stderr, "Error opening stats.txt\n");
        fclose(in);
        return 1;
    }
    fprintf(out, "File statistics\n\n");
    fprintf(out, "Characters: %d\n", charcount);
    fprintf(out, "Lines: %d\n", linecount);
    fprintf(out, "Words: %d\n", wordcount);
    fputc('\n', out);
    fputs("made you look XD", out);

    fclose(in);
    fclose(out);
    return 0;
}
input file

random text that has no value exept for learning! and it is extra long to force buffer to make a new one at some point
Learning C is fun.
Pointers were confusing.
Structs were pretty cool.
Now we can read files!

stats.txt file

File statistics

Characters: 211
Lines: 4
Words: 41

made you look XD

terminal out put with counted lines including on the one that is too long for the buffer