C Programming Read File Line By Line

5 min read

Reading a text file line by line is one of the most fundamental operations in C programming. Day to day, whether you are parsing configuration files, processing logs, or handling CSV data, the ability to consume input sequentially—rather than loading an entire file into memory—is critical for building dependable, scalable applications. Unlike higher-level languages that offer built-in readline() methods or foreach loops over file handles, C requires a deeper understanding of file pointers, buffers, and memory management to achieve the same result safely.

Understanding the Core Mechanisms

Before diving into specific functions, Make sure you grasp how C views files. Which means in the C standard library, a file is represented by a FILE* pointer returned by fopen(). This structure maintains the current position within the file stream, error flags, and end-of-file status. It matters. When we talk about reading "line by line," we are essentially asking the program to read characters from the current stream position until a newline character (\n) is encountered, the end of the file (EOF) is reached, or a specified buffer limit is hit.

Worth pausing on this one.

There are three primary approaches to this task in standard C:

  1. But fgets(): The standard, buffered approach for fixed-size buffers. 2. getline(): A POSIX extension (standardized in C11 via getdelim) that handles dynamic memory allocation automatically.
  2. Character-by-character parsing: Using fgetc() for custom parsing logic when standard line delimiters are insufficient.

Each method has distinct trade-offs regarding buffer safety, memory overhead, and portability.

Method 1: The Standard Approach with fgets()

The fgets() function is the workhorse of line-oriented input in ISO C. It is defined in <stdio.h> with the following signature:

char *fgets(char *str, int n, FILE *stream);

How fgets() Works

  • str: Pointer to a character array (buffer) where the line will be stored.
  • n: Maximum number of characters to read (including the null terminator). fgets() reads at most n-1 characters.
  • stream: The FILE* pointer returned by fopen().

Crucially, fgets() retains the newline character (\n) in the buffer if space permits. It appends a null terminator (\0) automatically. It returns the buffer pointer on success, or NULL on error or EOF Practical, not theoretical..

A Complete fgets() Example

#include 
#include 
#include 

#define BUFFER_SIZE 1024

int main(int argc, char *argv[]) {
    // 1. Validate command line arguments
    if (argc != 2) {
        fprintf(stderr, "Usage: %s \n", argv[0]);
        return EXIT_FAILURE;
    }

    // 2. Open the file in read mode
    FILE *fp = fopen(argv[1], "r");
    if (fp == NULL) {
        perror("Error opening file");
        return EXIT_FAILURE;
    }

    // 3. Allocate buffer on the stack
    char buffer[BUFFER_SIZE];
    int line_number = 0;

    // 4. Plus, handle newline retention
        // fgets keeps the '\n'. = NULL) {
        line_number++;
        
        // 5. Also, read loop
    while (fgets(buffer, BUFFER_SIZE, fp) ! We often want to strip it for processing.
        

        // 6. Process the line (here we just print with line numbers)
        printf("%4d: %s\n", line_number, buffer);
    }

    // 7. Check for read errors vs clean EOF
    if (ferror(fp)) {
        perror("Error reading file");
        fclose(fp);
        return EXIT_FAILURE;
    }

    // 8. Cleanup
    fclose(fp);
    return EXIT_SUCCESS;
}

Handling the "Long Line" Problem with fgets()

A significant limitation of fgets() is the fixed buffer size. If a line in the file exceeds BUFFER_SIZE - 1 characters, fgets() reads a partial line (ending without a newline). The next call to fgets() continues reading the rest of that same logical line.

To handle arbitrarily long lines with fgets(), you must implement a dynamic buffer resizing strategy:

  1. Check if the last character in the buffer is \n.
  2. In practice, if not (and not EOF), the line was truncated. Day to day, 3. realloc a larger dynamic buffer, copy existing data, and call fgets() again into the remaining space.

This manual memory management is precisely why many developers prefer the next method on POSIX systems It's one of those things that adds up..

Method 2: The Modern POSIX Approach with getline()

For developers targeting Linux, macOS, or other POSIX-compliant systems (and C11 compliant compilers), getline() is vastly superior for general-purpose line reading. It is defined in <stdio.h> (feature test macro _POSIX_C_SOURCE 200809L or _GNU_SOURCE often required) That's the part that actually makes a difference..

Signature and Behavior

ssize_t getline(char **lineptr, size_t *n, FILE *stream);
  • lineptr: Address of a char* pointer. If *lineptr is NULL, getline() allocates memory via malloc(). If not NULL, it assumes the buffer is malloc'd and resizes it via realloc() as needed.
  • n: Address of a size_t variable holding the current allocation size. Updated by the function.
  • Return: Number of characters read (including newline), or -1 on failure/EOF.

getline() Example: Zero Buffer Management

#define _POSIX_C_SOURCE 200809L // Required for getline() on many systems
#include 
#include 

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s \n", argv[0]);
        return EXIT_FAILURE;
    }

    FILE *fp = fopen(argv[1], "r");
    if (!fp) {
        perror("fopen");
        return EXIT_FAILURE;
    }

    char *line = NULL; // Must be initialized to NULL
    size_t len = 0;    // Size of allocated buffer
    ssize_t read;      // Return value of getline
    int line_count = 0;

    while ((read = getline(&line, &len, fp)) != -1) {
        line_count++;
        
        // getline includes the newline. Remove it for clean processing.
        

        printf("[%d] (%zu chars): %s\n", line_count, (size_t)read, line);
    }

    // CRITICAL: Free the buffer allocated by getline
    free(line);
    
    if (ferror(fp)) {
        perror("getline");
        fclose(fp);
        return EXIT_FAILURE;
    }

    fclose(fp);
    return EXIT_SUCCESS;
}

Why getline() wins:

  • No arbitrary limits: Handles lines of any length (limited only by available RAM).
  • Automatic memory management: Handles malloc/realloc internally.
  • Performance: Uses internal buffering efficiently; fewer system calls than manual `
Latest Drops

Fresh Off the Press

Along the Same Lines

You Might Want to Read

Thank you for reading about C Programming Read File Line By Line. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home