Chapter 9: File I/O: Interacting with Files

Chapter 9: File I/O: Interacting with Files

Most programs need to interact with the outside world, and often this means reading data from or writing data to files on a storage device (like an SSD or hard drive). This allows your programs to store persistent data, process large datasets, or communicate with other applications.

In C, file input/output (I/O) is handled through a set of standard library functions declared in <stdio.h>. This chapter will cover:

  • Opening and closing files.
  • Reading and writing data in text mode.
  • Reading and writing data in binary mode.
  • Handling file errors.
  • Positioning within a file.

9.1 The FILE Pointer and File Operations

All file operations in C revolve around a special type: the FILE pointer. When you open a file, the fopen() function returns a pointer to a FILE structure, which contains all the necessary information about the file (its name, mode, buffer, current position, etc.). You use this FILE * to refer to the file in subsequent operations.

The basic workflow for file I/O is:

  1. Open the file using fopen().
  2. Perform read/write operations.
  3. Close the file using fclose().

9.1.1 fopen(): Opening a File

fopen() is used to open a file in a specified mode.

Syntax:

FILE *fopen(const char *filename, const char *mode);
  • filename: A string (path) specifying the name of the file to open.
  • mode: A string specifying how the file should be opened.

Common File Modes:

ModeDescription
"r"Read mode. Opens an existing file for reading. Returns NULL if the file doesn’t exist.
"w"Write mode. Opens a file for writing. If the file exists, its content is truncated (deleted). If it doesn’t exist, a new one is created.
"a"Append mode. Opens a file for writing at the end of the file. If the file doesn’t exist, a new one is created.
"r+"Read and Write. Opens an existing file for both reading and writing.
"w+"Read and Write. Creates a new file for reading and writing. If the file exists, its content is truncated.
"a+"Read and Write. Opens for reading and appending. If the file doesn’t exist, a new one is created.

Binary Modes: To open a file in binary mode, append b to the mode string (e.g., "rb", "wb", "ab"). Binary mode handles data as raw bytes, without any special translation of characters like newline characters (which can be \n or \r\n depending on the OS in text mode).

Return Value: fopen() returns a FILE * on success, or NULL if the file could not be opened (e.g., file not found, permission denied). Always check for NULL!

9.1.2 fclose(): Closing a File

fclose() closes the file associated with the FILE * pointer, flushing any buffered data to disk and releasing system resources.

Syntax:

int fclose(FILE *stream);
  • stream: The FILE * pointer returned by fopen().
  • Returns 0 on success, EOF (End Of File, usually -1) on error.

9.2 Text File I/O

For human-readable data, text file I/O uses functions that interpret data as characters and perform conversions (e.g., between integer values and their string representations).

9.2.1 fprintf(): Formatted Output to File

Similar to printf(), but writes formatted data to a specified FILE *.

Syntax:

int fprintf(FILE *stream, const char *format, ...);

Example:

#include <stdio.h>

int main() {
    FILE *file_ptr;
    char name[] = "Alice";
    int age = 30;
    float height = 1.65f;

    // Open file "person.txt" in write mode ("w")
    file_ptr = fopen("person.txt", "w");

    // Check for errors
    if (file_ptr == NULL) {
        perror("Error opening file for writing"); // Prints "Error opening file for writing: <system error message>"
        return 1;
    }

    // Write formatted data to the file
    fprintf(file_ptr, "Name: %s\n", name);
    fprintf(file_ptr, "Age: %d\n", age);
    fprintf(file_ptr, "Height: %.2f meters\n", height);

    printf("Data written to person.txt successfully.\n");

    fclose(file_ptr); // Close the file

    return 0;
}

9.2.2 fscanf(): Formatted Input from File

Similar to scanf(), but reads formatted data from a specified FILE *.

Syntax:

int fscanf(FILE *stream, const char *format, ...);

Example (reading person.txt created above):

#include <stdio.h>

int main() {
    FILE *file_ptr;
    char name[50];
    int age;
    float height;
    char buffer[100]; // To read and discard labels like "Name:", "Age:"

    // Open file "person.txt" in read mode ("r")
    file_ptr = fopen("person.txt", "r");

    if (file_ptr == NULL) {
        perror("Error opening file for reading");
        return 1;
    }

    // Read formatted data from the file
    // Note: We read the label ("Name:") into buffer, then the actual name.
    // The space in format string allows skipping whitespace.
    fscanf(file_ptr, "%s %s\n", buffer, name); // Reads "Name:" then "Alice"
    fscanf(file_ptr, "%s %d\n", buffer, &age);  // Reads "Age:" then 30
    fscanf(file_ptr, "%s %f %s\n", buffer, &height, buffer); // Reads "Height:", 1.65, "meters"

    printf("Data read from person.txt:\n");
    printf("Name: %s\n", name);
    printf("Age: %d\n", age);
    printf("Height: %.2f meters\n", height);

    fclose(file_ptr);

    return 0;
}

Caution with fscanf(): It can be tricky to use fscanf() reliably for arbitrary text files, especially if the format isn’t strictly controlled. It’s often safer to read entire lines and then parse them.

9.2.3 fgets(): Reading a Line from File

Reads a whole line (up to n-1 characters or until a newline \n or EOF) into a buffer.

Syntax:

char *fgets(char *buffer, int n, FILE *stream);
  • buffer: Pointer to the character array where the line will be stored.
  • n: The maximum number of characters to read (including the null terminator \0).
  • stream: The FILE * pointer.
  • Returns buffer on success, or NULL on error or EOF.

Example:

#include <stdio.h>
#include <stdlib.h> // For EXIT_SUCCESS, EXIT_FAILURE

int main() {
    FILE *file_ptr;
    char line[100]; // Buffer to store each line

    file_ptr = fopen("sample.txt", "w"); // Create a sample file first
    if (file_ptr == NULL) { perror("Error creating sample.txt"); return EXIT_FAILURE; }
    fprintf(file_ptr, "First line of text.\n");
    fprintf(file_ptr, "Second line here.\n");
    fprintf(file_ptr, "Third and final line.\n");
    fclose(file_ptr);

    file_ptr = fopen("sample.txt", "r");
    if (file_ptr == NULL) {
        perror("Error opening sample.txt for reading");
        return EXIT_FAILURE;
    }

    printf("Content of sample.txt:\n");
    while (fgets(line, sizeof(line), file_ptr) != NULL) {
        printf("%s", line); // Print the line (it already contains \n)
    }

    // Check if loop terminated due to error or EOF
    if (ferror(file_ptr)) { // Check if an error occurred
        perror("Error reading file");
    }

    fclose(file_ptr);

    return EXIT_SUCCESS;
}

9.2.4 fputs(): Writing a String to File

Writes a string to a file. It does not append a newline character automatically.

Syntax:

int fputs(const char *str, FILE *stream);
  • str: The string to write.
  • stream: The FILE * pointer.
  • Returns a non-negative value on success, EOF on error.

Example:

#include <stdio.h>

int main() {
    FILE *file_ptr;
    char text1[] = "Hello C File I/O!\n";
    char text2[] = "This is another line.\n";

    file_ptr = fopen("output.txt", "w");
    if (file_ptr == NULL) { perror("Error opening output.txt"); return 1; }

    fputs(text1, file_ptr); // Write text1 (includes newline)
    fputs(text2, file_ptr); // Write text2 (includes newline)
    fputs("A final message.", file_ptr); // This line won't have a newline unless added

    fclose(file_ptr);
    printf("Data written to output.txt.\n");
    return 0;
}

9.3 Binary File I/O

Binary files store data exactly as it’s represented in memory, byte by byte. This is efficient for numerical data, custom data structures, images, audio, etc., and avoids text conversions.

9.3.1 fread(): Reading Binary Data

Reads count items, each of size bytes, from stream into ptr.

Syntax:

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
  • ptr: Pointer to the memory block where the data will be stored.
  • size: Size of each item to read (in bytes). Use sizeof().
  • count: Number of items to read.
  • stream: The FILE * pointer (opened in binary read mode, e.g., "rb").
  • Returns the number of items successfully read. Can be less than count if EOF or error.

9.3.2 fwrite(): Writing Binary Data

Writes count items, each of size bytes, from ptr to stream.

Syntax:

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
  • ptr: Pointer to the memory block from where the data will be read.
  • size: Size of each item to write (in bytes). Use sizeof().
  • count: Number of items to write.
  • stream: The FILE * pointer (opened in binary write mode, e.g., "wb").
  • Returns the number of items successfully written. Can be less than count if error.

Example: Writing and Reading a struct in Binary Mode

#include <stdio.h>
#include <stdlib.h> // For malloc, free
#include <string.h> // For strcpy

// Define a struct to store data
typedef struct {
    int id;
    char name[50];
    float value;
} Item;

int main() {
    FILE *file_ptr;
    Item item1 = {101, "Laptop", 1200.50f};
    Item item2 = {102, "Mouse", 25.99f};
    Item read_item; // To store read data

    // --- Write to binary file ---
    file_ptr = fopen("items.bin", "wb"); // Open in binary write mode
    if (file_ptr == NULL) { perror("Error opening items.bin for writing"); return 1; }

    fwrite(&item1, sizeof(Item), 1, file_ptr); // Write item1
    fwrite(&item2, sizeof(Item), 1, file_ptr); // Write item2
    printf("Wrote 2 items to items.bin.\n");

    fclose(file_ptr);

    // --- Read from binary file ---
    file_ptr = fopen("items.bin", "rb"); // Open in binary read mode
    if (file_ptr == NULL) { perror("Error opening items.bin for reading"); return 1; }

    printf("Reading items from items.bin:\n");
    while (fread(&read_item, sizeof(Item), 1, file_ptr) == 1) {
        printf("ID: %d, Name: %s, Value: %.2f\n",
               read_item.id, read_item.name, read_item.value);
    }

    // Check for read errors other than EOF
    if (ferror(file_ptr)) {
        perror("Error reading from items.bin");
    }

    fclose(file_ptr);

    return 0;
}

Portability Note: Writing structures directly to binary files can sometimes lead to portability issues across different systems or compilers due to variations in structure padding and byte order (endianness). For maximum portability, consider writing individual members one by one or using standardized serialization formats.

9.4 File Positioning: fseek(), ftell(), rewind()

Sometimes you need to move to a specific location within a file to read or write.

  • fseek(): Sets the file position indicator for the stream. Syntax: int fseek(FILE *stream, long offset, int origin);

    • stream: The FILE * pointer.
    • offset: Number of bytes to move from the origin. Can be positive (forward) or negative (backward).
    • origin: Position from which offset is measured:
      • SEEK_SET (or 0): Beginning of the file.
      • SEEK_CUR (or 1): Current file position.
      • SEEK_END (or 2): End of the file.
    • Returns 0 on success, non-zero on error.
  • ftell(): Returns the current file position indicator (offset from the beginning of the file). Syntax: long ftell(FILE *stream);

    • Returns the current position, or -1L on error.
  • rewind(): Sets the file position indicator to the beginning of the file. Syntax: void rewind(FILE *stream);

    • Equivalent to fseek(stream, 0L, SEEK_SET); but also clears the error indicator.

Example:

#include <stdio.h>

int main() {
    FILE *file_ptr;
    char buffer[100];

    file_ptr = fopen("sample_text.txt", "w+"); // Open for read/write, create if not exist
    if (file_ptr == NULL) { perror("Error opening sample_text.txt"); return 1; }

    fprintf(file_ptr, "Hello, World!\nThis is line two.\n");

    // After writing, the file position is at the end.
    long current_pos = ftell(file_ptr);
    printf("Current position after writing: %ld bytes\n", current_pos); // Approx 30

    rewind(file_ptr); // Go back to the beginning of the file

    // Read the first line
    fgets(buffer, sizeof(buffer), file_ptr);
    printf("First line after rewind: %s", buffer);

    // Seek to a specific position (e.g., 7 bytes from the beginning)
    fseek(file_ptr, 7, SEEK_SET); // Go to 'W' in "Hello, World!"
    fgets(buffer, sizeof(buffer), file_ptr); // Read from there
    printf("After seeking 7 bytes from start: %s", buffer); // Output: World!\nThis is line two.\n

    // Seek backwards from current position
    fseek(file_ptr, -10, SEEK_CUR); // Go back 10 bytes from current position
    current_pos = ftell(file_ptr);
    printf("Position after seeking backwards: %ld\n", current_pos);
    fgets(buffer, sizeof(buffer), file_ptr);
    printf("Read after seeking backwards: %s", buffer); // Prints part of the remaining line

    fclose(file_ptr);
    return 0;
}

9.5 Error Handling with Files

  • ferror(FILE *stream): Returns non-zero if an error occurred on the stream.
  • feof(FILE *stream): Returns non-zero if the end-of-file indicator is set for the stream.
  • clearerr(FILE *stream): Clears the error and EOF indicators for the given stream.
  • perror(const char *s): Prints a system-dependent error message to stderr, prefixed by s. Use this after fopen() fails or ferror() indicates an error.

Always include robust error checking, especially when dealing with external resources like files.

Exercise 9.1: Log File Generator

Write a C program that simulates writing log messages to a file.

  1. Open a file named application.log in append mode ("a").
  2. If the file cannot be opened, print an error and exit.
  3. Write 5 log entries to the file. Each entry should include:
    • A timestamp (you can use time() from <time.h> and ctime() to get a simple string, or just a counter for simplicity).
    • A log level (e.g., INFO, WARNING, ERROR).
    • A message (e.g., “Application started.”, “User login failed.”, “Data processed successfully.”).
  4. After writing, close the file.
  5. Then, open the same application.log file in read mode ("r"), read all its content line by line using fgets(), and print it to the console.
  6. Close the file.

Example Log Entry Format: [2025-11-03 10:30:00] [INFO] Application started.

Hint for simple timestamp (or use a counter):

#include <time.h>
// ...
time_t current_time;
time(&current_time);
fprintf(file_ptr, "[%s] [INFO] Application started.\n", ctime(&current_time));

Note: ctime() adds a newline itself, so adjust fprintf format string.

Exercise 9.2: Simple Inventory Persistence (Mini-Challenge with Structs and Binary I/O)

Building on Exercise 8.2 (Product struct):

  1. Define the Product struct and ProductCategory enum as in Exercise 8.2 (or copy them here).
  2. Create a function void save_products(const Product products[], int count, const char *filename) that takes an array of Product structs, their count, and a filename. This function should:
    • Open the file in binary write mode ("wb").
    • Write the count of products as an int at the beginning of the file.
    • Then, use fwrite() to write the entire array of Product structs to the file.
    • Handle file opening errors.
  3. Create a function Product* load_products(int *count, const char *filename) that takes a pointer to an int (to store the loaded count) and a filename. This function should:
    • Open the file in binary read mode ("rb").
    • Read the count of products from the beginning of the file.
    • Dynamically allocate memory for an array of Product structs using malloc based on the loaded count.
    • Read the entire array of Product structs from the file using fread().
    • Handle file opening errors and memory allocation errors.
    • Return a pointer to the dynamically allocated array of products.
  4. In your main function:
    • Create an array of a few Product structs and initialize them.
    • Call save_products() to save them to inventory.bin.
    • Call load_products() to load them back into a new dynamically allocated array.
    • Print the details of the loaded products to verify.
    • Remember to free() the dynamically allocated memory after you’re done.

You now have the knowledge to enable your C programs to interact with external files, a crucial capability for almost any practical application. Whether storing configuration, logging events, or managing data, file I/O is a fundamental skill. In the next chapter, we’ll explore the C preprocessor, which allows you to modify your source code before it even gets to the compiler.