Leer un archivo línea a línea en C: fgets, getline y fread

Leer un archivo de texto en C línea a línea, de cuatro formas: con fgets, que es estándar; detectando las líneas que no caben en el búfer; con getline, que admite líneas de cualquier longitud; y leyendo el archivo entero en memoria con fread.

Partimos de este archivo, notas.txt, con líneas vacías y una que solo tiene espacios:

cat notas.txt
salidaComprar café

Llamar al dentista
   
Revisar el informe de octubre
Pagar la luz

Línea a línea con fgets

fgets es la forma estándar y portable: lee hasta el salto de línea, o hasta llenar el búfer, y devuelve nullptr al llegar al final del archivo.

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

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

    char linea[256];
    int numero = 0;
    while (fgets(linea, sizeof linea, f) != nullptr) {
        numero++;
        linea[strcspn(linea, "\n")] = '\0';
        printf("%d: [%s]\n", numero, linea);
    }
    if (ferror(f)) {
        perror("al leer notas.txt");
    }
    fclose(f);
    return 0;
}
salida1: [Comprar café]
2: []
3: [Llamar al dentista]
4: [   ]
5: [Revisar el informe de octubre]
6: [Pagar la luz]

fgets deja el '\n' al final de la línea. La forma más limpia de quitarlo es linea[strcspn(linea, "\n")] = '\0': strcspn devuelve la posición del primer salto de línea, o la longitud de la cadena si no hay ninguno (la última línea de un archivo puede no tenerlo), y en los dos casos escribir ahí un nulo es seguro.

Cuando una línea no cabe en el búfer

Si una línea es más larga que el búfer, fgets la devuelve a trozos, y el programa cuenta una línea de más por cada trozo. Para detectarlo, basta mirar si el trozo termina en '\n':

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

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

    char linea[128];
    int trozos = 0, lineas = 0;
    while (fgets(linea, sizeof linea, f) != nullptr) {
        trozos++;
        size_t n = strlen(linea);
        if (n > 0 && linea[n - 1] == '\n') {
            lineas++;
        }
    }
    fclose(f);
    printf("%d trozos leídos, %d líneas completas\n", trozos, lineas);
    return 0;
}
salida5 trozos leídos, 3 líneas completas

Una línea de 300 caracteres con un búfer de 128 sale en tres trozos. Si necesitas líneas de cualquier longitud, hay una función mejor.

Líneas de cualquier longitud con getline

getline no es del estándar de C sino de POSIX, así que está en Linux y macOS (en Windows, no con MSVC). Reserva y amplía ella misma el búfer, devuelve la longitud de la línea leída, y al final hay que liberar el búfer una sola vez:

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

static bool en_blanco(const char *s)
{
    for (; *s != '\0'; s++) {
        if (!isspace((unsigned char) *s)) return false;
    }
    return true;
}

int main(void)
{
    const char *archivos[] = {"notas.txt", "larga.txt"};
    char *linea = nullptr;
    size_t capacidad = 0;

    for (int i = 0; i < 2; i++) {
        FILE *f = fopen(archivos[i], "r");
        if (f == nullptr) {
            perror(archivos[i]);
            continue;
        }
        int total = 0, utiles = 0;
        ssize_t largo, maximo = 0;
        while ((largo = getline(&linea, &capacidad, f)) != -1) {
            total++;
            if (!en_blanco(linea)) utiles++;
            if (largo > maximo) maximo = largo;
        }
        fclose(f);
        printf("%s: %d líneas, %d con texto, la más larga de %zd bytes\n", archivos[i], total, utiles, maximo);
    }
    free(linea);
    return 0;
}
salidanotas.txt: 6 líneas, 4 con texto, la más larga de 30 bytes
larga.txt: 3 líneas, 3 con texto, la más larga de 301 bytes

El mismo búfer sirve para todos los archivos: getline lo agranda cuando hace falta y lo reutiliza. La longitud que devuelve incluye el salto de línea. El #define _POSIX_C_SOURCE de la primera línea pide a la biblioteca que declare las funciones de POSIX; sin él, con -std=c23 estricto, getline no aparecería.

Leer el archivo entero de una vez

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

char *leer_todo(const char *ruta, size_t *tam)
{
    FILE *f = fopen(ruta, "rb");
    if (f == nullptr) return nullptr;

    char *datos = nullptr;
    size_t usado = 0, capacidad = 0;
    char bloque[4096];
    size_t n;
    while ((n = fread(bloque, 1, sizeof bloque, f)) > 0) {
        if (usado + n + 1 > capacidad) {
            size_t nueva = capacidad ? capacidad * 2 : 8192;
            while (nueva < usado + n + 1) nueva *= 2;
            char *mas = realloc(datos, nueva);
            if (mas == nullptr) {
                free(datos);
                fclose(f);
                return nullptr;
            }
            datos = mas;
            capacidad = nueva;
        }
        for (size_t i = 0; i < n; i++) datos[usado + i] = bloque[i];
        usado += n;
    }
    bool error = ferror(f);
    fclose(f);
    if (error || datos == nullptr) {
        free(datos);
        return nullptr;
    }
    datos[usado] = '\0';
    *tam = usado;
    return datos;
}

int main(void)
{
    size_t tam;
    char *texto = leer_todo("notas.txt", &tam);
    if (texto == nullptr) {
        perror("notas.txt");
        return EXIT_FAILURE;
    }
    int saltos = 0;
    for (size_t i = 0; i < tam; i++) saltos += texto[i] == '\n';
    printf("%zu bytes, %d saltos de línea\n", tam, saltos);
    free(texto);
    return 0;
}
salida81 bytes, 6 saltos de línea

Se lee por bloques y se va ampliando la memoria, en vez de preguntar el tamaño del archivo con fseek y ftell: así funciona también con la entrada estándar, con tuberías y con archivos que crecen mientras se leen. El nulo final permite tratar el contenido como una cadena si es texto. Este programa se compiló con AddressSanitizer, que no encontró ningún error ni fuga.

  • ParaUsaOjo
  • Líneas de texto, portablefgetslas líneas largas llegan a trozos
  • Líneas de cualquier longitudgetline (POSIX)un free al final
  • Todo el archivofread por bloquescomprueba ferror
  • Carácter a carácterfgetc en un intEOF no cabe en un char

Todo el código de esta página se compiló con GCC 15.3 (C23, -Wall -Wextra, sin avisos) y se ejecutó en Linux, en un contenedor limpio sin acceso a la red, antes de publicarla; las salidas están copiadas de esa ejecución.