File Handling

C · Lesson 23 of 32 7 min read Updated

Everything a program stores in variables disappears when it ends. To keep data between runs, such as marks, settings or a day's sales, you save it in a file. C's file functions come from the same stdio.h header you already use for printf, and they work in a very similar way.

In this lesson you will open, write, read and close text files, handle errors properly, read comma-separated data, append to files, and store structures in binary files.

Opening and closing a file

fopen opens a file and returns a FILE *, a pointer to a structure that the library uses to track the open file. If the file cannot be opened, fopen returns a null pointer, so you must always check. When you are finished, fclose writes out any remaining data and releases the file.

The second argument of fopen is the mode:

ModeMeaningIf the file existsIf it does not exist
"r"ReadReads from the startFails
"w"WriteEmpties it firstCreates it
"a"AppendAdds to the endCreates it
"r+"Read and writeKeeps the contentsFails
"w+"Read and writeEmpties it firstCreates it
"wx"Write, exclusive (C11)Fails, protecting the fileCreates it

Add b for binary files, as in "rb" or "wb". On Linux and macOS it makes no difference, but on Windows text mode converts line endings, so binary data must use b.

Writing a text file

fprintf works exactly like printf, but its first argument is the file to write to.

C
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    const char *names[] = {"Aarav", "Priya", "Kabir"};
    int marks[] = {78, 91, 64};

    FILE *fp = fopen("marks.txt", "w");
    if (fp == nullptr) {
        perror("marks.txt");
        return EXIT_FAILURE;
    }

    for (int i = 0; i < 3; i++) {
        fprintf(fp, "%s,%d\n", names[i], marks[i]);
    }
    fclose(fp);

    printf("Saved 3 records to marks.txt\n");
    return 0;
}
Output
Saved 3 records to marks.txt

The program prints only the confirmation; the data goes into the file. Open marks.txt in VS Code and you will see:

Text
Aarav,78
Priya,91
Kabir,64

The file is created in the program's working directory, the folder you ran it from, which is not always the folder holding the source code. fputs("text", fp) and fputc('x', fp) are also available for writing plain strings and single characters.

Reading a file line by line

fgets reads one line from a file into a buffer, just as it reads from the keyboard with stdin. It returns a null pointer at the end of the file, which makes a natural loop condition.

C
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *fp = fopen("marks.txt", "r");
    if (fp == nullptr) {
        perror("marks.txt");
        return EXIT_FAILURE;
    }

    char line[100];
    int number = 0;
    while (fgets(line, sizeof line, fp) != nullptr) {
        number++;
        printf("%d: %s", number, line);   // line already ends with '\n'
    }
    fclose(fp);
    return 0;
}
Output
1: Aarav,78
2: Priya,91
3: Kabir,64

Reporting errors with perror

When fopen fails, the C library stores an error code in the variable errno. perror prints your message followed by a description of that error:

C
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *fp = fopen("attendance.txt", "r");
    if (fp == nullptr) {
        perror("Could not open attendance.txt");
        return EXIT_FAILURE;
    }
    fclose(fp);
    return 0;
}

If the file does not exist, the output is:

Output
Could not open attendance.txt: No such file or directory

Other common reasons are "Permission denied" and a wrong folder path. perror writes to stderr, the error stream, so the message still appears even when normal output is redirected to a file.

Reading structured data

Each line of marks.txt holds a name and a number separated by a comma, a simple CSV (comma-separated values) format. Read each line with fgets, then split it with sscanf:

C
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *fp = fopen("marks.txt", "r");
    if (fp == nullptr) {
        perror("marks.txt");
        return EXIT_FAILURE;
    }

    char line[100];
    char name[50];
    int marks;
    int count = 0, total = 0;

    while (fgets(line, sizeof line, fp)) {
        if (sscanf(line, "%49[^,],%d", name, &marks) == 2) {
            printf("%-6s %3d\n", name, marks);
            total += marks;
            count++;
        } else {
            printf("Skipping bad line: %s", line);
        }
    }
    fclose(fp);

    if (count > 0) {
        printf("Average of %d students: %.2f\n", count, (double) total / count);
    }
    return 0;
}
Output
Aarav   78
Priya   91
Kabir   64
Average of 3 students: 77.67

%49[^,] is a scanset: it reads up to 49 characters that are not a comma, which leaves room for the '\0' in the 50-character name. Checking that sscanf returned 2 means a damaged line is skipped instead of crashing the program or producing nonsense.

To add records without destroying the old ones, open the file in append mode: fopen("marks.txt", "a"). Every write then goes to the end of the file, which is how log files work.

Binary files: fwrite and fread

A text file stores numbers as characters. A binary file stores the raw bytes of your variables, which is compact and fast to load. fwrite(ptr, size, count, fp) writes count items of size bytes, and fread reads them back. Both return how many items they actually handled.

C
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int roll;
    int marks;
} Record;

int main(void)
{
    Record out[] = {{101, 78}, {102, 91}, {103, 64}};

    FILE *fp = fopen("records.bin", "wb");
    if (fp == nullptr) {
        perror("records.bin");
        return EXIT_FAILURE;
    }
    size_t written = fwrite(out, sizeof out[0], 3, fp);
    fclose(fp);
    printf("Wrote %zu records\n", written);

    Record in[3];
    fp = fopen("records.bin", "rb");
    if (fp == nullptr) {
        perror("records.bin");
        return EXIT_FAILURE;
    }
    size_t got = fread(in, sizeof in[0], 3, fp);
    fseek(fp, 0, SEEK_END);
    printf("File size: %ld bytes\n", ftell(fp));
    fclose(fp);

    for (size_t i = 0; i < got; i++) {
        printf("Roll %d: %d\n", in[i].roll, in[i].marks);
    }
    return 0;
}
Output
Wrote 3 records
File size: 24 bytes
Roll 101: 78
Roll 102: 91
Roll 103: 64

Each Record is 8 bytes on common systems (two 4-byte int values), so three records take 24 bytes. fseek(fp, 0, SEEK_END) moves the file position to the end, and ftell reports that position, which is the file size. rewind(fp) moves back to the start. Binary files are not portable between systems with different type sizes or byte orders, and you must never write a structure that contains pointers, because the addresses mean nothing in the next run. For data you want to read or share, text formats are safer.

Common mistakes

  • Not checking fopen. Using a null FILE * crashes the program. Check and call perror.
  • Opening with "w" by accident. It empties an existing file immediately. Use "a" to add, or "wx" to refuse to overwrite.
  • Forgetting fclose. Data may stay in memory buffers and never reach the disk, and open files are a limited resource.
  • Looping on feof. Loop on the result of fgets, fscanf or fread instead.
  • Looking in the wrong folder. Relative file names are resolved from the working directory, not the source file's folder.

Key points

  • fopen returns a FILE * or a null pointer; always check it and fclose the file when done.
  • "r" reads, "w" creates or empties, "a" appends; add b for binary data.
  • Write text with fprintf and read it line by line with fgets, then parse with sscanf.
  • perror prints a clear reason, such as "No such file or directory", when an operation fails.
  • fwrite and fread move raw bytes; fseek, ftell and rewind move the file position.
  • Never loop on feof; test the return value of the reading function.

Exercise

Create a text file expenses.csv (by hand or with a small program) containing the lines Rent,12000, Groceries,4350, Electricity,1150 and Internet,799. Write a program that reads it, prints each item with its amount aligned in columns, and prints the total. Report an error with perror if the file cannot be opened.

Show solution
C
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *fp = fopen("expenses.csv", "r");
    if (fp == nullptr) {
        perror("expenses.csv");
        return EXIT_FAILURE;
    }

    char line[100];
    char item[40];
    int amount;
    int total = 0;

    while (fgets(line, sizeof line, fp)) {
        if (sscanf(line, "%39[^,],%d", item, &amount) == 2) {
            printf("%-12s %6d\n", item, amount);
            total += amount;
        }
    }
    fclose(fp);
    printf("%-12s %6d\n", "Total", total);
    return 0;
}
Output
Rent          12000
Groceries      4350
Electricity    1150
Internet        799
Total         18299

The scanset %39[^,] reads the item name up to the comma, and the width stops it from overflowing item. Lines that do not match are ignored.

Quick Quiz

1. What does fopen("data.txt", "w") do if data.txt already contains text?

2. What does fgets return when it reaches the end of a file?

3. Why is while (!feof(fp)) a bad reading loop?

4. Which pair of functions reads and writes raw binary records?

Learn C with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call