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:
| Modo | Descripción |
|---|---|
| r | Abre un archivo de texto existente y permite leer el archivo. |
| w | Abre 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). |
| a | Abre 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
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
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
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:
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 <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
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 <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
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
// 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 <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
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
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) |