Skip to article frontmatterSkip to article content

Archivos

Trabajando con archivos (de texto) en C

El manejo de archivos es una capacidad fundamental en la mayoría de las aplicaciones. En C, la librería estándar de E/S (stdio.h) provee un conjunto robusto y de bajo nivel para interactuar con el sistema de archivos. Este apunte amplía el manejo de archivos de texto, cubriendo no solo las operaciones básicas sino también el posicionamiento dentro del archivo y, de manera crucial, una gestión de errores detallada y profesional.

Flujo completo de operaciones con archivos

Figure 1:Diagrama de flujo que muestra la secuencia completa de operaciones al trabajar con archivos: abrir, verificar NULL, realizar operaciones, verificar errores y cerrar.

El FILE, la conexión con el archivo

Toda operación sobre archivos en C se realiza a través de un puntero a una estructura especial y opaca llamada FILE. Esta estructura, definida en la biblioteca estándar <stdio.h>, actúa como un intermediario que contiene toda la información de estado necesaria para gestionar el flujo de datos ( stream ) hacia y desde el archivo.

Concepto del puntero FILE* como intermediario

Figure 2:El puntero FILE* actúa como “manija” o “handle” que conecta tu programa con el archivo físico en disco. La estructura FILE contiene toda la información necesaria para gestionar las operaciones.

Dentro de esta estructura, el sistema operativo y la biblioteca estándar de C manejan los detalles como:

Al ser una estructura opaca, vos no necesitás conocer ni manipular sus miembros internos directamente. En su lugar, interactuás con el archivo a través de funciones que reciben un puntero a esta estructura.

Para declarar un puntero a FILE, la sintaxis es simple:

FILE *puntero_archivo;

Este puntero, una vez que la función fopen() lo inicializa exitosamente, se convierte en tu identificador único para interactuar con ese archivo específico hasta que lo cierres con fclose().

Una Analogía con Arreglos y Punteros

La idea de usar un puntero para manejar una entidad compleja les debe resultar familiar, ya que hemos trabajado con arreglos. En sí, el concepto es bastante similar y se relaciona directamente con el manejo de punteros, un tema fundamental que exploraremos en detalle más adelante.

Por ahora, es suficiente que entiendas que puntero_archivo es tu “manija” o “handle” para leer, escribir y manipular el archivo que abriste.

Otro detalle importante, los argumentos de tipo cadena, se expresan utilizando esta notación, por lo que donde vean algo como char *modo, interprétenlo que es equivalente a char mode[]

Apertura de Archivos: fopen()

La función fopen() es el punto de entrada crucial para cualquier operación de archivo. Actúa como un puente entre tu programa y el sistema de archivos del sistema operativo. Su tarea es solicitar el acceso a un archivo específico en un modo determinado.

Si el sistema operativo concede el acceso, fopen() reserva los recursos necesarios, inicializa la estructura FILE con la información pertinente y te devuelve un puntero a dicha estructura. Si por alguna razón la operación falla (el archivo no existe, no tenés permisos, etc.), la función te devolverá NULL.

La sintaxis, definida en <stdio.h>, es la siguiente:

FILE *fopen(const char *pathname, const char *mode);

Program 1:Sintaxis de fopen()

Modos de apertura

Elegir el modo correcto es fundamental, ya que determina el comportamiento del puntero del archivo y lo que sucede con el contenido que ya estaba en el archivo.

Modos de apertura de archivos con fopen()

Figure 3:Guía visual de los diferentes modos de apertura y un diagrama de decisión para elegir el modo correcto según tus necesidades.

Modo

Descripción

Si el archivo no existe

Si el archivo existe

Caso de Uso Típico

"r"

Lectura (Read): Abre un archivo de texto para leer.

Falla (devuelve NULL).

El puntero se posiciona al inicio.

Leer un archivo de configuración, procesar datos de entrada.

"w"

Escritura (Write): Abre un archivo de texto para escribir.

Se crea un archivo nuevo.

El contenido se borra (trunca a cero).

Guardar un nuevo documento, generar un archivo de log desde cero.

"a"

Añadir (Append): Abre un archivo de texto para escribir al final.

Se crea un archivo nuevo.

El puntero se posiciona al final. Los datos existentes se conservan.

Añadir eventos a un archivo de log existente.

"r+"

Lectura y Escritura: Abre para actualizar.

Falla (devuelve NULL).

El puntero se posiciona al inicio. Permite leer y sobreescribir.

Modificar un registro específico en un archivo de datos.

"w+"

Escritura y Lectura: Abre para actualizar, borrando el contenido.

Se crea un archivo nuevo.

El contenido se borra. Permite escribir y luego leer desde el inicio.

Archivos temporales que necesitás escribir y luego releer.

"a+"

Añadir y Lectura: Abre para actualizar, posicionando la escritura al final.

Se crea un archivo nuevo.

El puntero se posiciona al final para escribir, pero podés moverlo para leer.

Leer datos de un log y luego añadir nuevos eventos al final.

Manejo de Errores en la Apertura

Cuando fopen() devuelve NULL, la variable global errno (definida en <errno.h>) se establece con un código de error específico del sistema. Para mostrar un mensaje de error legible por humanos, podés usar la función perror().

#include <stdio.h>
#include <errno.h> // Necesario para perror()

int main() {
    FILE *p_archivo;
    p_archivo = fopen("archivo_inexistente.txt", "r");

    if (p_archivo == NULL) {
        // Imprime un mensaje descriptivo del último error ocurrido
        perror("Error al intentar abrir el archivo");
        return 1; // Termina el programa con un código de error
    }

    printf("Archivo abierto con éxito.\n");
    // ... operaciones con el archivo ...
    fclose(p_archivo);

    return 0;
}

Program 2:Verificación de errores al abrir un archivo

Al ejecutar este código, perror() probablemente imprimiría algo como:

Error al intentar abrir el archivo: No such file or directory`

Binario vs. Texto

Por defecto, los modos listados arriba operan en modo texto. Esto implica que el sistema puede realizar conversiones automáticas de los finales de línea para adaptarse a la convención de la plataforma (por ejemplo, convertir \n a \r\n en Windows).

Para trabajar con archivos binarios —como imágenes, audio, ejecutables o cualquier archivo donde cada byte importa—, es crucial evitar estas traducciones. Para ello, simplemente agregá una b al final del modo (ej. "rb", "wb+", "ab").

Trabajar con archivos binarios es importante, pero complejo y requiere de un par de cosas más que no hemos visto del lenguaje. Para quienes deseen chusmear como se hace, en la sección extra, hay un apunte referido a como trabajar de esta forma los archivos.

Escribiendo

Existen tres funciones para escribir en archivos, que van desde caracteres individuales, cadenas, y terminando en cadenas con formato.

fputc

La función fputc se utiliza para escribir un único carácter en un flujo de archivo (file stream). Es una herramienta fundamental para la manipulación de archivos a bajo nivel en C.

/**
 * Escribe un carácter en un flujo de archivo.
 *
 * @param character a escribir. Se pasa como un `int` pero
 *               se convierte internamente a `unsigned char`.
 * @param stream Puntero al objeto `FILE` que identifica el
 *               flujo donde se escribirá el carácter.
 *
 * @return Si la operación es exitosa, devuelve el mismo
 *               carácter que se escribió (promocionado a `int`).
 *         Si ocurre un error, devuelve la constante `EOF`
 *               y activa el indicador de error del flujo.
 */
int fputc(int character, FILE *stream);

fputs

Escribe una cadena. No añade el carácter de nueva línea (\n) automáticamente. Devuelve un valor no negativo si tiene éxito, o EOF en caso de error.

/**
 * Escribe una cadena de caracteres en un flujo de archivo.
 *
 * @param cadena de caracteres terminada en nulo que se va a escribir.
 * @param stream Puntero al objeto `FILE` que identifica el flujo de salida.
 *
 * @return Devuelve un valor no negativo si la operación es exitosa.
 *         Devuelve la constante `EOF` para indicar un error.
 */
int fputs(const char *cadena, FILE *stream);

fprintf

La opción más versátil. Escribe datos con formato, análogamente a printf(). Devuelve el número de caracteres escritos, o un valor negativo si ocurre un error.

/**
 * @brief Escribe datos con formato en un flujo de archivo.
 *
 * @param stream Puntero al objeto `FILE` que identifica el
 *                   flujo de salida.
 * @param formato Cadena de caracteres que contiene el texto
 *                   a escribir. Puede contener especificadores
 *                   de formato (ej. %d, %f, %s) que serán reemplazados
 *                   por los argumentos subsiguientes.
 * @param ... Lista variable de argumentos. Debe haber un argumento
 *               por cada especificador de formato en la cadena `format`.
 *
 * @return Si la operación es exitosa, devuelve el número total de caracteres escritos.
 *         Si ocurre un error de escritura, devuelve un número negativo.
 */
int fprintf(FILE *stream, const char *formato, ...);

Ejemplo de escritura completo

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

int main(void) {
    // 1. Abrir el archivo en modo escritura ("w").
    FILE *salida = fopen("factura_completa.txt", "w");
    if (salida == NULL) {
        perror("Error al abrir el archivo 'factura_completa.txt'");
        return EXIT_FAILURE;
    }

    // 2. Escribir un encabezado usando fputs()
    // fputs() escribe una cadena de caracteres en el archivo.
    const char *encabezado = "--- Documento de Factura ---\n\n";
    if (fputs(encabezado, salida) == EOF) {
        perror("Error escribiendo el encabezado con fputs()")
        fclose(salida);
        return EXIT_FAILURE;
    }

    // 3. Escribir datos formateados usando fprintf()
    // fprintf() permite escribir datos con formato (como printf, pero a un archivo).
    const char *item_1 = "Placa de Video RTX 4080";
    int cantidad_1 = 1;
    double precio_1 = 1200000.75;
    int chars_escritos_1 = fprintf(salida, "Item: %s\nCantidad: %d\nPrecio: %.2f ARS\n\n", item_1, cantidad_1, precio_1);
    if (chars_escritos_1 < 0) {
        perror("Error al formatear y escribir el item 1 con fprintf()")
        fclose(salida);
        return EXIT_FAILURE;
    }

    // 4. Escribir un separador de línea usando fputc()
    // fputc() escribe un solo carácter en el archivo.
    int i;
    for (i = 0; i < 30; i++) {
        if (fputc('-', salida) == EOF) {
            perror("Error escribiendo separador con fputc()")
            fclose(salida);
            return EXIT_FAILURE;
        }
    }
    if (fputc('\n', salida) == EOF) {
        perror("Error escribiendo nueva linea con fputc()")
        fclose(salida);
        return EXIT_FAILURE;
    }

    // 5. Escribir otro item usando una combinación de las tres funciones.
    const char *item_2 = "Memoria RAM DDR5 32GB";
    int cantidad_2 = 2;
    double precio_2 = 180000.00;

    if (fputs("Detalle del Item 2:\n", salida) == EOF) {
        perror("Error escribiendo detalle del item 2 con fputs()")
        fclose(salida);
        return EXIT_FAILURE;
    }
    if (fprintf(salida, "  Nombre: %s\n", item_2) < 0) {
        perror("Error escribiendo nombre del item 2 con fprintf()")
        fclose(salida);
        return EXIT_FAILURE;
    }
    if (fprintf(salida, "  Unidades: %d\n", cantidad_2) < 0) {
        perror("Error escribiendo unidades del item 2 con fprintf()")
        fclose(salida);
        return EXIT_FAILURE;
    }
    if (fprintf(salida, "  Valor Unitario: %.2f ARS\n", precio_2) < 0) {
        perror("Error escribiendo valor unitario del item 2 con fprintf()")
        fclose(salida);
        return EXIT_FAILURE;
    }

    // 6. Cerrar el archivo. Es crucial para asegurar que todos los datos en el búfer se guarden en el disco.
    if (fclose(salida) != 0) {
        perror("Error al cerrar el archivo");
        return EXIT_FAILURE;
    }

    printf("Archivo 'factura_completa.txt' creado y cerrado exitosamente.\n");
    return EXIT_SUCCESS;
}

Program 3:Ejemplo de escritura y gestión de errores

Leyendo

fgetc

La función fgetc se utiliza para leer un único carácter desde un flujo de archivo. Es la contraparte directa de fputc.

/**
 * @brief Lee un carácter desde un flujo de archivo.
 *
 * @param stream Puntero al objeto `FILE` que identifica el flujo de entrada.
 *
 * @return Si la operación es exitosa, devuelve el carácter leído (promocionado a `int`).
 * @return Si se alcanza el final del archivo o si ocurre un error, devuelve `EOF`.
 */
int fgetc(FILE *stream);

fgets

La función fgets se utiliza para leer una línea o una cadena de caracteres desde un flujo de archivo. Es más segura que la antigua función gets porque permite especificar un tamaño máximo para el búfer, evitando desbordamientos, una práctica recomendada por la regla Regla 0x001Ch: Preferí fgets sobre gets y scanf para leer cadenas.

/**
 * @brief Lee una cadena de caracteres desde un flujo de archivo.
 *
 * La lectura se detiene cuando se encuentra un carácter de nueva línea (`\n`),
 * cuando se alcanza el final del archivo (EOF), o después de que se hayan
 * leído (num - 1) caracteres. El carácter de nueva línea, si es leído,
 * se incluye en la cadena. Se añade un carácter nulo (`\0`) al final.
 *
 * @param cadena de caracteres donde se almacenará la cadena leída.
 * @param numero máximo de caracteres a ser leídos (incluyendo el carácter nulo final).
 * @param stream Puntero al objeto `FILE` que identifica el flujo de entrada.
 *
 * @return En caso de éxito, devuelve el puntero `str`.
 *         Si se alcanza el final del archivo antes de leer algún carácter, 
                o si ocurre un error, devuelve `NULL`.
 */
char *fgets(char *cadena, int numero, FILE *stream);

fscanf

La función fscanf se utiliza para leer datos con formato desde un flujo de archivo. Funciona de manera análoga a scanf, pero operando sobre un archivo en lugar de la entrada estándar.

/**
 * @brief Lee datos con formato desde un flujo de archivo.
 *
 * @param[in] stream Puntero al objeto `FILE` que identifica el flujo de entrada.
 * @param[in] format Cadena de caracteres que especifica cómo interpretar los datos leídos.
 * @param[out] ... Lista variable de punteros a las variables donde se almacenarán los datos leídos.
 *
 * @return Devuelve el número de elementos de entrada asignados exitosamente.
 * @return Puede devolver `EOF` si se encuentra el final del archivo o ocurre un error antes de la primera asignación.
 */
int fscanf(FILE *stream, const char *format, ...);

Leyendo un archivo, paso a paso

El código de ejemplo es una demostración de cómo leer un archivo de texto de manera segura y eficiente en C. La estrategia principal es separar la entrada/salida (E/S) del procesamiento de datos. En lugar de intentar interpretar los datos directamente desde el archivo con fscanf(), lee el archivo línea por línea en un espacio de memoria temporal (un búfer) y luego analiza esa línea. Este enfoque es más resiliente a errores de formato.

A continuación, se descompone el código sección por sección.

1. Inclusiones y definiciones (#include y #define)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_LINEA 512
#define NOMBRE_ARCHIVO "factura.txt"
2. Apertura del archivo y manejo de errores
FILE *entrada = fopen(NOMBRE_ARCHIVO, "r");
if (!entrada) {
    perror("No se pudo abrir 'factura.txt' para lectura");
    return EXIT_FAILURE;
}
3. Bucle principal de lectura
char buffer[MAX_LINEA];
int numero_linea = 0;

while (fgets(buffer, sizeof(buffer), entrada) != NULL) {
    numero_linea++;
    // ... procesamiento de la línea ...
}
4. Procesamiento y análisis de cada línea (Parsing)
if (strncmp(buffer, "Item:", 5) == 0) {
    char item_nombre[100];
    int cantidad;
    double precio;

    int campos_leidos = sscanf(buffer, "Item: %99[^,], Cantidad: %d, Precio: %lf ARS",
                               item_nombre, &cantidad, &precio);

    if (campos_leidos == 3) {
        // ... éxito ...
    } else {
        // ... fallo ...
    }
}
5. Verificación post-lazo
if (ferror(entrada)) {
    perror("Ocurrió un error de lectura en el archivo");
} else if (feof(entrada)) {
    printf("\nProcesamiento completado. Se llegó al final del archivo.\n");
}
6. Limpieza y cierre
clearerr(entrada);
fclose(entrada);
return EXIT_SUCCESS;

Posicionamiento en Archivos: Acceso Aleatorio

No siempre querés leer un archivo secuencialmente. Las funciones de posicionamiento te permiten moverte a cualquier punto del archivo.

ftell

La función ftell se utiliza para obtener la posición actual del indicador de posición del fichero (el “cursor”) dentro de un flujo. Devuelve esta posición como un número de bytes desde el inicio del archivo.

/**
 * @brief Obtiene la posición actual del indicador de posición del fichero.
 *
 * @param[in] stream Puntero al objeto `FILE` que identifica el flujo.
 *
 * @return Si es exitoso, devuelve el valor actual del indicador de posición.
 * @return En caso de error, devuelve -1L y la variable global `errno` se establece a un valor positivo.
 */
long int ftell(FILE *stream);

fseek

La función fseek es la herramienta principal para mover el indicador de posición del fichero a una ubicación específica dentro del flujo. Permite un control preciso, moviendo el cursor un número determinado de bytes (offset) desde un punto de origen (origin).

/**
 * @brief Establece el indicador de posición del fichero a una nueva posición.
 *
 * @param stream Puntero al objeto `FILE` que identifica el flujo.
 * @param offset Desplazamiento en bytes relativo al parámetro `origin`.
 * @param origin Posición desde donde se calcula el desplazamiento. Los valores pueden ser:
 * - `SEEK_SET`: Inicio del archivo.
 * - `SEEK_CUR`: Posición actual.
 * - `SEEK_END`: Final del archivo.
 *
 * @return Devuelve 0 si la operación es exitosa.
 *         Devuelve un valor distinto de cero en caso de error.
 */
int fseek(FILE *stream, long int offset, int origin);

rewind

La función rewind es un caso especial y simplificado de fseek. Su única función es mover el indicador de posición del fichero de vuelta al inicio del archivo. Además, limpia cualquier indicador de error que pudiera tener el flujo.

/**
 * Reposiciona el indicador de posición del fichero al inicio del flujo.
 *
 * Esta función es funcionalmente equivalente a fseek(stream, 0L, SEEK_SET),
 * pero además borra el indicador de error del flujo.
 *
 * @param stream Puntero al objeto `FILE` que identifica el flujo.
 */
void rewind(FILE *stream);

Ejemplo de uso

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

int main(void) {
    FILE *archivo = fopen("factura.txt", "r");
    if (!archivo) {
        perror("No se pudo abrir el archivo");
        return EXIT_FAILURE;
    }

    // Moverse al final del archivo
    if (fseek(archivo, 0, SEEK_END) != 0) {
        perror("Error en fseek a SEEK_END");
        fclose(archivo);
        return EXIT_FAILURE;
    }

    // Obtener la posición actual, que es el tamaño del archivo
    long tamano = ftell(archivo);
    if (tamano == -1L) {
        perror("Error en ftell");
        fclose(archivo);
        return EXIT_FAILURE;
    }
    printf("El archivo tiene %ld bytes.\n", tamano);

    // Moverse a la posición ANTERIOR al último byte para leerlo.
    // Si el archivo termina con \n, esto leerá el carácter previo.
    if (tamano > 1 && fseek(archivo, -2L, SEEK_END) != 0) {
        perror("Error en fseek para leer el último carácter");
        fclose(archivo);
        return EXIT_FAILURE;
    }

    int ultimo_caracter = fgetc(archivo);
    if (ultimo_caracter != EOF) {
        printf("El último carácter imprimible del archivo es: '%c'\n", (char)ultimo_caracter);
    }

    // Volver al principio
    rewind(archivo);
    printf("Después de 'rewind', la posición es: %ld\n", ftell(archivo));

    fclose(archivo);
    return EXIT_SUCCESS;
}

Program 4:Uso de fseek() y ftell() para leer el último carácter

Cierre de Archivos: fclose(), el Paso Final

fclose(FILE *stream) disocia el archivo del puntero FILE. Es una operación crítica que:

  1. Vacía el búfer de salida: Asegura que todos los datos escritos con funciones como fprintf o fputs se escriban físicamente en el disco.

  2. Libera recursos del sistema: El sistema operativo tiene un límite en la cantidad de archivos que un proceso puede tener abiertos simultáneamente.

Devuelve 0 si tiene éxito y EOF si ocurre un error.

Aunque parezca una simple formalidad, la llamada a fclose() también puede fallar. Esto es particularmente cierto al escribir archivos: si el disco se llena o el medio de almacenamiento se desconecta, el vaciado final del búfer (el flush) fallará. Ignorar el valor de retorno de fclose() podría hacerte creer que la operación fue exitosa cuando en realidad los últimos datos se perdieron. La única forma de estar 100% seguro de que toda la información se guardó correctamente es verificar el resultado del cierre.

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

int main(void) {
    FILE *log_file = fopen("app.log", "a");
    if (log_file == NULL) {
        perror("No se pudo abrir el log");
        return EXIT_FAILURE;
    }

    fprintf(log_file, "El programa inició una operación crítica.\n");

    // ... el resto del programa ...

    fprintf(log_file, "La operación crítica finalizó.\n");

    // Cerramos el archivo y VERIFICAMOS el resultado.
    if (fclose(log_file) != 0) {
        // Si fclose falla, el error queda registrado en errno.
        perror("FALLO CRÍTICO al cerrar el archivo de log");
        // En un programa real, esto podría requerir una acción de emergencia,
        // ya que los últimos datos podrían no haberse guardado.
        return EXIT_FAILURE;
    }

    printf("Log escrito y cerrado correctamente.\n");
    return EXIT_SUCCESS;
}

Program 5:Verificación del cierre de un archivo

Funciones y variables para la gestión de errores

stderr: El flujo de error estándar

En C, tenés tres flujos de comunicación estándar:

¿Por qué separar stdout de stderr? Para poder redirigir la salida. Imaginate que ejecutás tu programa y guardás el resultado en un archivo: ./mi_programa > salida.txt

Si usaras printf (que escribe en stdout) para los errores, estos quedarían mezclados con los datos correctos en salida.txt, haciendo difícil su detección. Al usar fprintf(stderr, ...) o perror(), los mensajes de error se imprimen en la consola por separado, permitiéndote ver los problemas incluso cuando la salida “buena” está siendo redirigida.

Uso general: Se utilizá stderr para mensajes de error, diagnósticos o advertencias.

errno: El código del último error

errno es una variable global (técnicamente, una macro que se expande a una expresión modificable) definida en <errno.h>. Las funciones de sistema y de la biblioteca estándar de C la utilizan para comunicar qué salió mal cuando fallan.

Uso general: Consultá errno solo después de haber detectado que una función ha fallado (por ejemplo, verificando un retorno NULL o -1).

perror(const char *s): El informador directo

perror es la forma más sencilla de reportar un error. Hace dos cosas:

  1. Imprime la cadena que le pasaste como argumento.

  2. Inmediatamente después, imprime dos puntos (:) y la descripción textual correspondiente al valor actual de errno.

Situación de uso: Ideal para herramientas de línea de comandos o scripts donde necesitás un mensaje de error rápido, estándar y sin formato complejo. Es menos flexible pero muy conveniente.

// Si errno es 2 ("No such file or directory")
perror("Error al leer el archivo de configuración");
// Salida en stderr:
// Error al leer el archivo de configuración: No such file or directory

strerror(int errnum): El traductor flexible

strerror te da más control. Toma un número de error (casi siempre le pasarás errno) y devuelve un puntero a una cadena de caracteres (char *) con la descripción del error. Vos sos responsable de cómo y dónde imprimir esa cadena.

Situación de uso: Imprescindible cuando necesitás:

// Si errno es 13 ("Permission denied")
fprintf(stderr, "[FATAL] Imposible acceder al recurso. Razón: %s\n", strerror(errno));
// Salida en stderr:
// [FATAL] Imposible acceder al recurso. Razón: Permission denied

Ejercicios Propuestos

Solution to Exercise 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <stdio.h>
#include <stdlib.h>

#define EXITO 0
#define ERROR -1

/**
 * Agrega una entrada de texto a un archivo de diario.
 *
 * @param nombre_archivo La ruta del archivo de diario.
 *      #PRE: No puede ser NULL.
 * @param entrada El texto a agregar en el diario.
 *      #PRE: No puede ser NULL.
 *
 * @returns EXITO si la entrada se escribió correctamente, ERROR en caso contrario.
 *
 * @post El archivo especificado por nombre_archivo contendrá la nueva entrada
 *       al final del mismo, seguida de un salto de línea.
 */
int agregar_entrada_diario(const char *nombre_archivo, const char *entrada)
{
    int resultado_operacion = EXITO;
    FILE *p_archivo = NULL;

    // 1. Abrir el archivo en modo "append" (añadir)
    p_archivo = fopen(nombre_archivo, "a");
    if (p_archivo == NULL)
    {
        perror("Error al abrir el diario");
        resultado_operacion = ERROR;
    }

    // 2. Escribir la entrada y un salto de línea si la apertura fue exitosa
    if (resultado_operacion == EXITO)
    {
        if (fputs(entrada, p_archivo) == EOF)
        {
            perror("Error al escribir la entrada en el diario");
            resultado_operacion = ERROR;
        }
    }

    if (resultado_operacion == EXITO)
    {
        if (fputc('\n', p_archivo) == EOF)
        {
            perror("Error al escribir el salto de línea");
            resultado_operacion = ERROR;
        }
    }

    // 3. Cerrar el archivo, incluso si la escritura falló
    if (p_archivo != NULL)
    {
        if (fclose(p_archivo) != 0)
        {
            perror("Error al cerrar el diario");
            // Si ya había un error, se mantiene. Si no, se establece ahora.
            if (resultado_operacion == EXITO)
            {
                resultado_operacion = ERROR;
            }
        }
    }

    return resultado_operacion;
}

int main(void)
{
    const char *MI_DIARIO = "diario.txt";
    int resultado = 0;

    printf("Escribiendo primera entrada...\n");
    resultado = agregar_entrada_diario(MI_DIARIO, "Hoy fue un día soleado.");
    if (resultado == ERROR)
    {
        fprintf(stderr, "No se pudo escribir la primera entrada.\n");
        return EXIT_FAILURE;
    }

    printf("Escribiendo segunda entrada...\n");
    resultado = agregar_entrada_diario(MI_DIARIO, "Aprendí a manejar archivos en C.");
    if (resultado == ERROR)
    {
        fprintf(stderr, "No se pudo escribir la segunda entrada.\n");
        return EXIT_FAILURE;
    }

    printf("Entradas agregadas al diario '%s' con éxito.\n", MI_DIARIO);

    return EXIT_SUCCESS;
}
Solution to Exercise 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <stdio.h>
#include <stdlib.h>

#define MAX_LONGITUD_LINEA 1024
#define ERROR_APERTURA -1
#define ERROR_LECTURA -2

/**
 * Cuenta el número de líneas en un archivo de texto.
 *
 * @param nombre_archivo La ruta del archivo a leer.
 *      #PRE: No puede ser NULL.
 *
 * @returns El número de líneas contadas si la operación es exitosa.
 *          Retorna ERROR_APERTURA si el archivo no puede abrirse.
 *          Retorna ERROR_LECTURA si ocurre un error durante la lectura.
 *
 * @post El archivo no es modificado.
 */
int contar_lineas(const char *nombre_archivo)
{
    int cantidad_lineas = 0;
    FILE *p_archivo = NULL;
    char buffer[MAX_LONGITUD_LINEA];

    p_archivo = fopen(nombre_archivo, "r");
    if (p_archivo == NULL)
    {
        perror("Error al abrir el archivo para contar líneas");
        return ERROR_APERTURA;
    }

    while (fgets(buffer, sizeof(buffer), p_archivo) != NULL)
    {
        cantidad_lineas++;
    }

    // Después del bucle, verificar si salimos por error o por fin de archivo
    if (ferror(p_archivo))
    {
        perror("Error de lectura mientras se contaban las líneas");
        cantidad_lineas = ERROR_LECTURA; // Sobrescribimos el conteo con un código de error
    }

    if (fclose(p_archivo) != 0)
    {
        perror("Error al cerrar el archivo después de contar");
        if (cantidad_lineas >= 0) // No sobrescribir un error de lectura previo
        {
            cantidad_lineas = ERROR_APERTURA; // Reutilizamos código de error
        }
    }

    return cantidad_lineas;
}

int main(void)
{
    const char *NOMBRE_ARCHIVO = "diario.txt";
    // Crear un archivo de prueba primero
    FILE *p_archivo_prueba = fopen(NOMBRE_ARCHIVO, "w");
    if (p_archivo_prueba != NULL)
    {
        fputs("Primera línea.\n", p_archivo_prueba);
        fputs("Segunda línea.\n", p_archivo_prueba);
        fputs("Tercera línea.\n", p_archivo_prueba);
        fclose(p_archivo_prueba);
    }

    printf("Contando líneas en el archivo '%s'...\n", NOMBRE_ARCHIVO);
    int lineas = contar_lineas(NOMBRE_ARCHIVO);

    if (lineas >= 0)
    {
        printf("El archivo contiene %d líneas.\n", lineas);
    }
    else
    {
        fprintf(stderr, "Ocurrió un error al procesar el archivo (código: %d).\n", lineas);
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}
Solution to Exercise 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define EXITO 0
#define ERROR -1
#define MAX_BUFFER 4096

/**
 * Copia el contenido de un archivo de texto a otro.
 *
 * @param ruta_origen La ruta del archivo a leer.
 *      #PRE: No puede ser NULL.
 * @param ruta_destino La ruta del archivo donde se escribirá el contenido.
 *      #PRE: No puede ser NULL.
 *
 * @returns EXITO si la copia fue completamente exitosa, ERROR si ocurrió algún fallo.
 *
 * @post Si la operación es exitosa, el archivo en ruta_destino tendrá el mismo
 *       contenido que el de ruta_origen.
 */
int copiar_archivo(const char *ruta_origen, const char *ruta_destino)
{
    int estado_operacion = EXITO;
    FILE *p_origen = NULL;
    FILE *p_destino = NULL;
    char buffer[MAX_BUFFER];
    bool continuar_bucle = true;

    p_origen = fopen(ruta_origen, "r");
    if (p_origen == NULL)
    {
        perror("Error al abrir el archivo de origen");
        estado_operacion = ERROR;
    }

    if (estado_operacion == EXITO)
    {
        p_destino = fopen(ruta_destino, "w");
        if (p_destino == NULL)
        {
            perror("Error al abrir el archivo de destino");
            estado_operacion = ERROR;
        }
    }

    while (estado_operacion == EXITO && continuar_bucle)
    {
        if (fgets(buffer, sizeof(buffer), p_origen) != NULL)
        {
            if (fputs(buffer, p_destino) == EOF)
            {
                perror("Error al escribir en el archivo de destino");
                estado_operacion = ERROR;
            }
        }
        else
        {
            continuar_bucle = false; // Se terminó de leer o hubo un error
        }
    }

    // Verificar si el bucle terminó por un error de lectura
    if (p_origen != NULL && ferror(p_origen))
    {
        perror("Error de lectura en el archivo de origen");
        estado_operacion = ERROR;
    }

    // Cerrar ambos archivos, verificando errores en cada cierre
    if (p_origen != NULL && fclose(p_origen) != 0)
    {
        perror("Error al cerrar el archivo de origen");
        estado_operacion = ERROR;
    }
    if (p_destino != NULL && fclose(p_destino) != 0)
    {
        perror("Error al cerrar el archivo de destino");
        estado_operacion = ERROR;
    }

    return estado_operacion;
}

int main(void)
{
    const char *ARCHIVO_ORIGEN = "original.txt";
    const char *ARCHIVO_COPIA = "copia.txt";

    // Crear archivo original de prueba
    FILE *p_temp = fopen(ARCHIVO_ORIGEN, "w");
    if (p_temp != NULL)
    {
        fprintf(p_temp, "Línea 1 del original.\n");
        fprintf(p_temp, "Línea 2 con algunos caracteres especiales: áéíóú.\n");
        fprintf(p_temp, "Fin del archivo original.\n");
        fclose(p_temp);
    }

    printf("Copiando '%s' a '%s'...\n", ARCHIVO_ORIGEN, ARCHIVO_COPIA);
    if (copiar_archivo(ARCHIVO_ORIGEN, ARCHIVO_COPIA) == EXITO)
    {
        printf("Archivo copiado con éxito.\n");
    }
    else
    {
        fprintf(stderr, "La copia del archivo falló.\n");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}
Solution to Exercise 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>
#include <stdlib.h>

#define EXITO 0
#define ERROR -1
#define ARCHIVO_LOG "eventos.log"

/**
 * Registra un mensaje de evento en un archivo de log.
 *
 * @param mensaje El mensaje a registrar.
 *      #PRE: No puede ser NULL.
 *
 * @returns EXITO si el evento se registró correctamente, ERROR en caso contrario.
 *
 * @post El archivo de log contendrá el nuevo mensaje al final.
 */
int registrar_evento(const char *mensaje)
{
    int resultado = EXITO;
    FILE *p_log = fopen(ARCHIVO_LOG, "a");

    if (p_log == NULL)
    {
        perror("Error al abrir el archivo de log");
        resultado = ERROR;
    }
    else
    {
        // Escribir el mensaje y un salto de línea
        if (fprintf(p_log, "%s\n", mensaje) < 0)
        {
            perror("Error al escribir en el archivo de log");
            resultado = ERROR;
        }

        // Cerrar el archivo
        if (fclose(p_log) != 0)
        {
            perror("Error al cerrar el archivo de log");
            resultado = ERROR;
        }
    }

    return resultado;
}

int main(void)
{
    printf("Registrando eventos...\n");

    if (registrar_evento("[INFO] El sistema ha iniciado.") != EXITO)
    {
        fprintf(stderr, "Fallo al registrar el primer evento.\n");
        return EXIT_FAILURE;
    }

    if (registrar_evento("[WARN] El disco está casi lleno.") != EXITO)
    {
        fprintf(stderr, "Fallo al registrar el segundo evento.\n");
        return EXIT_FAILURE;
    }

    if (registrar_evento("[FATAL] No se pudo conectar a la base de datos.") != EXITO)
    {
        fprintf(stderr, "Fallo al registrar el tercer evento.\n");
        return EXIT_FAILURE;
    }

    printf("Eventos registrados en '%s'.\n", ARCHIVO_LOG);

    return EXIT_SUCCESS;
}
Solution to Exercise 5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define EXITO 0
#define ERROR -1
#define MAX_LINEA 256
#define MAX_PRODUCTO 100

/**
 * Procesa un archivo CSV de ventas, calculando e imprimiendo el total por línea.
 *
 * @param nombre_archivo La ruta del archivo CSV a procesar.
 *      #PRE: No puede ser NULL.
 *
 * @returns EXITO si el archivo se procesó (incluso si algunas líneas fallaron),
 *          ERROR si no se pudo abrir el archivo o hubo un error de lectura irrecuperable.
 *
 * @post Se imprimirán en la salida estándar los totales de las líneas bien formadas.
 */
int procesar_ventas(const char *nombre_archivo)
{
    int estado_general = EXITO;
    FILE *p_archivo = fopen(nombre_archivo, "r");
    char buffer[MAX_LINEA];
    size_t numero_linea = 0;

    if (p_archivo == NULL)
    {
        perror("No se pudo abrir el archivo de ventas");
        return ERROR;
    }

    while (fgets(buffer, sizeof(buffer), p_archivo) != NULL)
    {
        numero_linea++;

        // Ignorar líneas vacías o que son comentarios
        if (buffer[0] == '\n' || buffer[0] == '#')
        {
            continue; // Esta es una excepción permitida a la regla 0x0006h
        }

        char nombre_producto[MAX_PRODUCTO];
        double precio = 0.0;
        int cantidad = 0;

        // Usar sscanf para parsear la línea. Formato: string-hasta-coma,double,int
        int campos_leidos = sscanf(buffer, "%99[^,],%lf,%d", nombre_producto, &precio, &cantidad);

        if (campos_leidos == 3)
        {
            double total_linea = precio * (double)cantidad;
            printf("Línea %zu: Producto '%s', Total: %.2f\n", numero_linea, nombre_producto, total_linea);
        }
        else
        {
            fprintf(stderr, "[Advertencia] Línea %zu mal formada: %s", numero_linea, buffer);
        }
    }

    if (ferror(p_archivo))
    {
        perror("Ocurrió un error de lectura");
        estado_general = ERROR;
    }

    if (fclose(p_archivo) != 0)
    {
        perror("Error al cerrar el archivo de ventas");
        estado_general = ERROR;
    }

    return estado_general;
}

int main(void)
{
    const char *ARCHIVO_VENTAS = "ventas.csv";

    // Crear archivo de ventas de prueba
    FILE *p_temp = fopen(ARCHIVO_VENTAS, "w");
    if (p_temp != NULL)
    {
        fprintf(p_temp, "Teclado Mecanico,150.50,2\n");
        fprintf(p_temp, "Mouse Gamer,75.00,5\n");
        fprintf(p_temp, "\n"); // Línea vacía
        fprintf(p_temp, "Monitor 24 pulgadas,300.25,1\n");
        fprintf(p_temp, "# Esto es un comentario, debe ser ignorado\n");
        fprintf(p_temp, "Webcam,no_es_un_precio,3\n"); // Línea mal formada
        fclose(p_temp);
    }

    printf("Procesando archivo '%s'...\n", ARCHIVO_VENTAS);
    if (procesar_ventas(ARCHIVO_VENTAS) == ERROR)
    {
        fprintf(stderr, "No se pudo completar el procesamiento del archivo.\n");
        return EXIT_FAILURE;
    }

    printf("\nProcesamiento finalizado.\n");
    return EXIT_SUCCESS;
}

Glosario

Búfer

En el contexto de la programación y los sistemas operativos, un búfer (del inglés buffer) es una región de memoria física (generalmente en la RAM) que se utiliza para almacenar datos de forma temporal mientras se transfieren de un lugar a otro.

El objetivo principal de un búfer es optimizar el rendimiento y gestionar las diferencias de velocidad entre dos procesos o dispositivos. Por ejemplo, en las operaciones de entrada/salida (I/O), los datos se acumulan en un búfer antes de ser procesados o escritos en un dispositivo físico como un disco duro. Esto permite que el sistema realice menos operaciones de escritura/lectura, pero de mayor tamaño, lo cual es significativamente más eficiente.

Pensá en el proceso de escribir en un archivo como si fuera enviar una carta. Escribir carácter por carácter directamente al disco (sin búfer) sería como llevar cada letra individualmente hasta el correo. Es ineficiente y lento.

Usar un búfer es como escribir la carta completa en una hoja de papel (el búfer en la memoria). Una vez que terminaste la carta (el búfer se llenó o cerraste el archivo), la llevás al correo en un solo viaje. Este método es mucho más rápido y organizado.

Concepto de búfer en operaciones de archivos

Figure 4:Comparación entre operaciones sin búfer (ineficientes) y con búfer (eficientes), mostrando cómo el búfer optimiza las operaciones de E/S.