C Lectura y escritura de archivos

En el capítulo anterior explicamos los dispositivos estándar de entrada y salida que maneja el lenguaje C. En este capítulo veremos cómo los programadores de C crean, abren y cierran archivos de texto o binarios.

Un archivo, ya sea de texto o binario, representa una serie de bytes. El lenguaje C no solo proporciona funciones de acceso de alto nivel, sino también llamadas de bajo nivel (OS) para manejar archivos en dispositivos de almacenamiento. Este capítulo explicará las llamadas importantes para la gestión de archivos.

Abrir archivos

Puede usarfopen( )la función para crear un archivo nuevo o abrir un archivo existente. Esta llamada inicializa un objeto de tipoFILEFILE; el tipoFILEcontiene toda la información necesaria para controlar el flujo. A continuación se muestra el prototipo de esta llamada a función:

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

Aquí,filenamees una cadena que se usa para nombrar el archivo; el modo de accesomodepuede ser uno de los siguientes valores:

ModoDescripción
rAbre un archivo de texto existente y permite leer el archivo.
wAbre un archivo de texto y permite escribir en el archivo. Si el archivo no existe, se creará un archivo nuevo. Aquí, su programa escribirá desde el principio del archivo. Si el archivo existe, su contenido se vaciará (es decir, la longitud se truncará a 0).
aAbre un archivo de texto para escribir en el archivo en modo de añadido. Si el archivo no existe, se creará un archivo nuevo. Aquí, su programa añadirá contenido al contenido existente del archivo.
r+Abre un archivo de texto y permite leer y escribir en el archivo.
w+Abre un archivo de texto y permite leer y escribir en el archivo. Si el archivo ya existe, se truncará a longitud cero; si no existe, se creará un archivo nuevo.
a+Abre un archivo de texto y permite leer y escribir en el archivo. Si el archivo no existe, se creará un archivo nuevo. La lectura comenzará desde el principio del archivo; la escritura solo puede ser en modo de añadido.

Si se trata de archivos binarios, se deben usar los siguientes modos de acceso en lugar de los modos anteriores:

"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"

Cerrar archivos

Para cerrar un archivo, use la función fclose( ). El prototipo de la función es el siguiente:

 int fclose( FILE *fp );

Si el archivo se cierra correctamente,fclose( )la función devuelve cero; si ocurre un error al cerrar el archivo, la función devuelveEOFEsta función, de hecho, vacía los datos del búfer, cierra el archivo y libera toda la memoria utilizada para ese archivo. EOF es una constante definida enstdio.hel archivo de cabecera.

La biblioteca estándar de C proporciona varias funciones para leer y escribir archivos por carácter o en forma de cadenas de longitud fija.

Escribir en archivos

La siguiente es la función más simple para escribir un carácter en un flujo:

int fputc( int c, FILE *fp );

La funciónfputc()escribe el valor de carácter del parámetro c en el flujo de salida al que apunta fp. Si la escritura tiene éxito, devuelve el carácter escrito; si ocurre un error, devuelveEOF. Puede usar la siguiente función para escribir una cadena terminada en null en el flujo:

int fputs( const char *s, FILE *fp );

La funciónfputs()escribe la cadenasen el flujo de salida al que apunta fp. Si la escritura tiene éxito, devuelve un valor no negativo; si ocurre un error, devuelveEOF. También puede usarint fprintf(FILE *fp,const char *format, ...)la función para escribir una cadena en un archivo. Pruebe el siguiente ejemplo:

Nota:Asegúrese de tener disponibletmpel directorio /tmp; si no existe, debe crearlo primero en su computadora.

/tmpGeneralmente es el directorio temporal en sistemas Linux. Si ejecuta en Windows, debe cambiarlo a un directorio que ya exista en su entorno local, por ejemplo: C:\tmp、D:\tmpetc.

Ejemplo

#include <stdio.h> int main() { FILE *fp = NULL; fp = fopen("/tmp/test.txt", "w+"); fprintf(fp, "This is testing for fprintf...\n"); fputs("This is testing for fputs...\n", fp); fclose(fp); }

Cuando el código anterior se compila y ejecuta, crea un archivo nuevo en el directorio /tmptest.txty escribe dos líneas usando dos funciones diferentes. A continuación, leamos este archivo.

Leer archivos

La siguiente es la función más simple para leer un solo carácter de un archivo:

int fgetc( FILE * fp );

fgetc()La función lee un carácter del archivo de entrada al que apunta fp. El valor de retorno es el carácter leído; si ocurre un error, devuelveEOF. La siguiente función le permite leer una cadena desde un flujo:

char *fgets( char *buf, int n, FILE *fp );

La funciónfgets()lee n - 1 caracteres del flujo de entrada al que apunta fp. Copia la cadena leída al búferbufy agrega al final unnullcarácter para terminar la cadena.

Si esta función encuentra un carácter de nueva línea '\n' o el final del archivo EOF antes de leer el último carácter, solo devuelve los caracteres leídos, incluido el carácter de nueva línea. También puede usarint fscanf(FILE *fp, const char *format, ...)la función para leer cadenas desde un archivo, pero se detiene al encontrar el primer espacio o carácter de nueva línea.

Ejemplo

#include <stdio.h> int main() { FILE *fp = NULL; char buff[255]; fp = fopen("/tmp/test.txt", "r"); fscanf(fp, "%s", buff); printf("1: %s\n", buff ); fgets(buff, 255, (FILE*)fp); printf("2: %s\n", buff ); fgets(buff, 255, (FILE*)fp); printf("3: %s\n", buff ); fclose(fp); }

Cuando el código anterior se compila y ejecuta, lee el archivo creado en la sección anterior y produce el siguiente resultado:

1: This
2: is testing for fprintf...

3: This is testing for fputs...

Primero,fscanf()el método solo leyóThisporque después encontró un espacio. Luego, la llamada afgets()lee el resto hasta el final de la línea. Finalmente, la llamada afgets()lee completamente la segunda línea.

Funciones de E/S binarias

Las dos funciones siguientes se utilizan para la entrada y salida binaria:

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);

freadyfwriteSon funciones centrales de la biblioteca estándar de C para la lectura y escritura por bloques de archivos binarios. Operan directamente sobre bloques de datos en memoria, evitando la sobrecarga de conversión de formato de texto, y son especialmente adecuadas para manejar archivos grandes y la lectura/escritura por lotes de datos estructurados.

Descripción de parámetros

Los cuatro parámetros tienen el mismo significado en ambas funciones, pero el flujo de datos es opuesto:

Parámetro Tipo Descripción Comportamiento de fread Comportamiento de fwrite
ptr void* / const void* Puntero al búfer de memoria Dirección de destino de lectura (escribe en memoria) Dirección de origen de datos (lee desde memoria)
size size_t Número de bytes por bloque de datos Tamaño de bloque de cada lectura Tamaño de bloque de cada escritura
count size_t Número de bloques de datos a leer/escribir Número esperado de bloques a leer Número esperado de bloques a escribir
stream FILE* Puntero a un archivo abierto Debe abrirse con "rb" Debe abrirse con "wb"

El número real de bytes operados essize × count; el valor de retorno es la cantidad de bloques leídos/escritos con éxito, no el número de bytes. Si el valor de retorno es menor que count, puede deberse al final del archivo (EOF) o a errores de lectura/escritura, y deben comprobarse por separado.

Modo binario y modo texto

Al usar fread/fwrite, el archivo debe abrirse en modo binario; de lo contrario, puede producirse corrupción de datos.

Por qué es obligatorio usar el modo binario

El modo texto en la plataforma Windows convierte automáticamente el carácter de nueva línea\nen\r\n, lo que destruye la integridad de los datos binarios.

En Linux/macOS, el modo texto y el modo binario se comportan de la misma manera, pero el código multiplataforma debe usar explícitamente"b"el indicador para garantizar la portabilidad.

Ejemplo

// Forma correcta: especificar explícitamente el modo binario
FILE *fp_read  = fopen("data.bin", "rb");  // Lectura binaria
FILE *fp_write = fopen("data.bin", "wb");  // Escritura binaria

// Forma incorrecta: modo texto (los datos pueden alterarse en Windows)
FILE *fp_bad = fopen("data.bin", "r");     // Falta el indicador "b"

Comparación con las funciones de texto

La elección de qué función usar depende del tipo de datos y del escenario de necesidad:

Característica fread / fwrite fprintf / fscanf
Método de procesamiento de datos Lee y escribe datos binarios de memoria tal cual Conversión según formato (número ↔ cadena)
Escenarios de aplicación Estructuras, arreglos, imágenes, audio, etc. Archivos de configuración, registros, informes y otros textos legibles por humanos
Rendimiento RápidoSin sobrecarga de conversión LentoRequiere analizar la cadena de formato
Precisión Sin pérdida de precisión Los números de punto flotante pueden perder precisión
Legible por humanos No (formato binario) Sí (formato de texto plano)

Diagrama de flujo de lectura y escritura de datos

La siguiente figura muestra el flujo de datos entre archivos y memoria para fread y fwrite:

fread/fwrite 数据读写流程

Escenarios de uso típicos

Leer y escribir datos de estructuras

Este es el escenario de uso más común de fread/fwrite: se puede leer o escribir toda la estructura como un bloque de datos de una sola vez.

Ejemplo

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

// Definir la estructura de estudiante
// Nota: se debe usar un array de longitud fija, no se puede usar un miembro puntero char *name
typedef struct {
    int id;             // Número de estudiante
    char name[50];      // Nombre (array fijo, no usar puntero)
    float score;        // Nota
} Student;

int main() {
    // ===== Escribir estructura en el archivo =====
    Student s = {101, "Alice", 95.5f};  // Inicializar datos
    FILE *fp = fopen("student.bin", "wb");
    if (fp == NULL) {
        perror("Error al abrir el archivo");
        return 1;
    }
    // Escribir toda la estructura como un bloque de datos
    size_t written = fwrite(&s, sizeof(Student), 1, fp);
    if (written != 1) {
        printf("Error de escritura"\n");
    }
    fclose(fp);

    // ===== Leer estructura del archivo =====
    fp = fopen("student.bin", "rb");
    if (fp == NULL) {
        perror("Error al abrir el archivo");
        return 1;
    }
    Student loaded;  // Sin inicializar, será rellenado por fread
    size_t read_count = fread(&loaded, sizeof(Student), 1, fp);
    if (read_count != 1) {
        printf("Error de lectura o fin de archivo"\n");
        fclose(fp);
        return 1;
    }
    printf("ID: %d, Nombre: %s, Nota: %.1f"\n", loaded.id, loaded.name, loaded.score);
    fclose(fp);
    return 0;
}
学号: 101, 姓名: Alice, 成绩: 95.5

La estructura no puede contener miembros puntero (comochar *name), porque fwrite solo guarda el valor de la dirección del puntero, no el contenido de la cadena a la que apunta. Al leer de vuelta, esa dirección ya no es válida, lo que produce un puntero colgante. La solución correcta es usar un array de longitud fija, comochar name[50]。

Lectura y escritura de datos de arrays

Los arrays se almacenan de forma contigua en memoria, por lo que se puede leer o escribir todo el array de una sola vez.

Ejemplo

#include <stdio.h>

int main() {
    // Array de datos original
    int data[] = {1, 2, 3, 4, 5};
    int count = 5;  // Número de elementos del array

    // ===== Escribir array en el archivo =====
    FILE *fp = fopen("array.bin", "wb");
    if (fp == NULL) {
        perror("Error al abrir el archivo");
        return 1;
    }
    // data es equivalente a &data
    fwrite(data, sizeof(int), count, fp);
    fclose(fp);
    printf("Se han escrito %d enteros en array.bin"\n", count);

    // ===== Leer array del archivo =====
    int buffer[5] = {0};  // Inicializar a 0 para verificar fácilmente el resultado de la lectura
    fp = fopen("array.bin", "rb");
    if (fp == NULL) {
        perror("Error al abrir el archivo");
        return 1;
    }
    size_t read_count = fread(buffer, sizeof(int), count, fp);
    if (read_count < count) {
        printf("Lectura incompleta: se esperaban %d, se leyeron %zu"\n", count, read_count);
    }
    fclose(fp);

    // Imprimir el resultado de la lectura
    printf("Datos leídos: ");
    for (int i = 0; i < count; i++) {
        printf("%d ", buffer[i]);
    }
    printf("\n");
    return 0;
}
已写入 5 个整数到 array.bin
读取到的数据: 1 2 3 4 5

Lectura y escritura por lotes de archivos grandes

Para archivos grandes, se debe usar un bucle para leer/escribir por bloques, evitando cargar todo el contenido en memoria de una sola vez.

Ejemplo

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

#define BUFFER_SIZE 1024 // Leer/escribir 1KB cada vez, se puede ajustar según sea necesario

// Función de copia de archivos eficiente
// Valor de retorno: 0 indica éxito, -1 indica error
int copy_file(const char *src, const char *dst) {
    char buffer[BUFFER_SIZE];  // Búfer intermedio
    FILE *in = fopen(src, "rb");
    FILE *out = fopen(dst, "wb");

    // Comprobar si ambos archivos se abrieron correctamente
    if (!in) {
        perror("Error al abrir el archivo de origen");
        return -1;
    }
    if (!out) {
        perror("Error al abrir el archivo de destino");
        fclose(in);   // Cerrar el archivo de origen ya abierto
        return -1;
    }

    // Leer en bucle, como máximo BUFFER_SIZE bytes cada vez
    size_t n;
    while ((n = fread(buffer, 1, BUFFER_SIZE, in)) > 0) {
        // fwrite con size=1, count=n, asegura escribir el número real de bytes leídos
        if (fwrite(buffer, 1, n, out) != n) {
            perror("Error de escritura");
            fclose(in);
            fclose(out);
            return -1;
        }
    }

    // Verificar si la lectura terminó por un error (no por EOF normal)
    if (ferror(in)) {
        perror("Error de lectura");
        fclose(in);
        fclose(out);
        return -1;
    }

    fclose(in);
    fclose(out);
    printf("Archivo copiado correctamente: %s -> %s"\n", src, dst);
    return 0;
}

int main() {
    // Ejemplo de uso: copiar el archivo de prueba ejemplo
    copy_file("ejemplo_input.bin", "ejemplo_output.bin");
    return 0;
}
文件复制成功: ejemplo_input.bin -> ejemplo_output.bin

La clave de este patrón es quefreaddevuelve el número real de bytes leídos, y luego se usafwritesolo se escribe el número real de bytes leídos, evitando escribir datos sobrantes al final.

Consideraciones clave

Verificación del valor de retorno

Después de llamar a fread/fwrite se debe comprobar el valor de retorno para distinguir entre una finalización normal y una situación anómala.

Ejemplo

#include <stdio.h>

int main() {
    int buffer[100];
    FILE *fp = fopen("data.bin", "rb");
    if (fp == NULL) {
        perror("Error al abrir el archivo");
        return 1;
    }

    // Intentar leer 100 enteros
    size_t count = fread(buffer, sizeof(int), 100, fp);

    if (count < 100) {
        // Distinguir entre EOF y error
        if (feof(fp)) {
            printf("Se ha llegado al final del archivo, se leyeron %zu elementos"\n", count);
            // En este punto se puede continuar procesando los datos leídos
        } else if (ferror(fp)) {
            perror("Ocurrió un error durante la lectura");
            fclose(fp);
            return 1;
        }
    } else {
        printf("Se leyeron correctamente todos los %zu elementos"\n", count);
    }

    fclose(fp);
    return 0;
}
已到达文件末尾,实际读取 47 个元素

Trampas en la lectura/escritura de estructuras

El diseño de la estructura en memoria se ve afectado por la estrategia de alineación del compilador, lo que puede causar problemas de compatibilidad entre plataformas.

Alineación de memoria inconsistente

Con diferentes compiladores u opciones de compilación, la misma estructura puede tener un diseño de memoria diferente.

Ejemplo

#include <stdio.h>

// Ejemplo problemático: con la alineación predeterminada, después de char hay 3 bytes de relleno
typedef struct {
    char c;      // Ocupa 1 byte, pero habrá relleno después
    int i;       // Alineado a 4 bytes, realmente comienza en el desplazamiento 4
} Mixed;

// Solución 1: usar pragma pack para forzar una alineación compacta
#pragma pack(push, 1) // Guardar la configuración de alineación actual y establecer alineación de 1 byte
typedef struct {
    char c;      // Desplazamiento 0, ocupa 1 byte
    int i;       // Desplazamiento 1, ocupa 4 bytes (sin relleno)
} PackedMixed;
#pragma pack(pop) // Restaurar la configuración de alineación anterior

// Solución 2: serializar manualmente los miembros uno a uno (la forma más segura)
// Este método no depende de la alineación del compilador y tiene la mejor compatibilidad entre plataformas

int main() {
    // Imprimir la diferencia de tamaño entre las dos estructuras
    printf("Tamaño de Mixed con alineación predeterminada: %zu bytes"\n", sizeof(Mixed));       // Normalmente es 8
    printf("Tamaño de PackedMixed con alineación compacta: %zu bytes"\n", sizeof(PackedMixed)); // Es 5
    return 0;
}
默认对齐 Mixed 大小: 8 字节
紧凑对齐 PackedMixed 大小: 5 字节

Práctica recomendada: al trabajar entre plataformas o para almacenamiento a largo plazo, se debe priorizar la serialización manual (leer/escribir miembro por miembro) en lugar de escribir la estructura directamente en el archivo. Esto evita por completo los problemas de alineación y orden de bytes.

Prohibido el miembro puntero

Este es uno de los errores más comunes entre los principiantes.

Ejemplo

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

// Ejemplo incorrecto: la estructura contiene un miembro puntero
typedef struct {
    int id;
    char *name;   // ¡Peligro! fwrite solo guarda el valor del puntero (dirección), no el contenido de la cadena
} BadStudent;

// Ejemplo correcto: usar un array fijo
typedef struct {
    int id;
    char name[100];  // Los datos reales se almacenan dentro de la estructura, se pueden leer/escribir de forma segura
} GoodStudent;

int main() {
    // Demostrar las consecuencias de hacerlo incorrectamente
    BadStudent bs;
    bs.id = 1;
    bs.name = malloc(50);
    strcpy(bs.name, "EJEMPLO");

    // Escribir BadStudent: en este momento se escribe la dirección del puntero name, pero no el contenido de la cadena
    FILE *fp = fopen("bad.bin", "wb");
    fwrite(&bs, sizeof(BadStudent), 1, fp);
    fclose(fp);

    // Leer de vuelta BadStudent: el valor del puntero name es la dirección que malloc devolvió en otro proceso, completamente inválida
    BadStudent loaded;
    fp = fopen("bad.bin", "rb");
    fread(&loaded, sizeof(BadStudent), 1, fp);
    fclose(fp);
    // loaded.name es un puntero colgante, ¡acceder a él provoca un comportamiento indefinido!

    free(bs.name);
    printf("Demostración completada, pero el puntero name cargado no es válido"\n");
    return 0;
}
演示完成,但 load 出来的 name 指针是无效的

Refresco del búfer

La E/S de la biblioteca estándar de C está almacenada en búfer; los datos críticos deben ser refrescados explícitamente para garantizar su persistencia.

Ejemplo

#include <stdio.h>

int main() {
    FILE *fp = fopen("critical.bin", "wb");
    if (fp == NULL) {
        perror("Error al abrir el archivo");
        return 1;
    }

    int data[] = {1, 2, 3, 4, 5};
    fwrite(data, sizeof(int), 5, fp);

    // Método 1: refrescar el búfer explícitamente, los datos se escriben inmediatamente en el disco
    fflush(fp);
    printf("Usar fflush para forzar el refresco al disco"\n");

    // Método 2: refresco automático al cerrar el archivo (recomendado)
    // fclose llama automáticamente a fflush internamente
    fclose(fp);
    printf("fclose también refresca el búfer automáticamente"\n");

    return 0;
}
使用 fflush 强制刷新到磁盘
fclose 也会自动刷新缓冲区

Posicionamiento del puntero de archivo

Combinando fseek y ftell se puede implementar acceso aleatorio al archivo, leyendo los datos de una posición específica según sea necesario.

Ejemplo

#include <stdio.h>

typedef struct {
    int id;
    char name[50];
    float score;
} Student;

int main() {
    FILE *fp = fopen("students.bin", "rb");
    if (fp == NULL) {
        perror("Error al abrir el archivo");
        return 1;
    }

    // Saltar al final del archivo para obtener su tamaño
    fseek(fp, 0, SEEK_END);
    long file_size = ftell(fp);
    printf("Tamaño del archivo: %ld bytes"\n", file_size);

    // Calcular cuántos registros de estructura hay en el archivo
    long record_count = file_size / sizeof(Student);
    printf("Contiene %ld registros de estudiantes"\n", record_count);

    // Saltar a la sexta estructura (el índice comienza en 0, desplazamiento = 5 * sizeof(Student))
    fseek(fp, sizeof(Student) * 5, SEEK_SET);
    Student s;
    fread(&s, sizeof(Student), 1, fp);
    printf("6.º registro: id=%d, name=%s"\n", s.id, s.name);

    fclose(fp);
    return 0;
}
文件大小: 6000 字节
包含 100 条学生记录
第 6 条记录: id=106, name=EJEMPLO

Resumen de errores comunes

La siguiente tabla resume los problemas más habituales al usar fread/fwrite y las prácticas correctas:

Tipo de error Manifestación del error Práctica correcta
No verificar el puntero de archivo Llamar a fread directamente después de que fopen falle, provocando un fallo Siempre comprobarfp != NULL
Ignorar el modo binario En Windows, los datos binarios se corrompen por la conversión de caracteres de nueva línea Usar"rb" / "wb"
Confundir el número de bloques y el número de bytes Tratar el valor de retorno como número de bytes, lo que causa errores lógicos Recuerde que el valor de retorno es el número de bloques; el número de bytes =valor de retorno × size
Desbordamiento del búfer Espacio asignado insuficiente, escritura fuera de los límites Asegurar que el espacio del búfer sea ≥size × count
Estructura con punteros Después de leer de vuelta, la dirección del puntero no es válida, lo que produce un puntero colgante Usar un array fijo o serializar manualmente cada miembro
Ignorar la verificación de errores No distinguir entre EOF y errores de lectura, datos incompletos Comprobar el valor de retorno; usefeof() / ferror()Determinar la causa
Datos no sincronizados La caída del programa provocó la pérdida de datos en el búfer Llamar después de escribir datos críticosfflush(fp)
Otras extensiones