1. FILE* y descriptor: dos capas distintas
Los temas anteriores utilizaron enteros como 0, 1 y 3 para referenciar aperturas del núcleo. La biblioteca estándar de C añade otra abstracción: un flujo, representado mediante un puntero a FILE. Se declara en stdio.h y se usa con funciones como fopen, fgets, fprintf, fread y fclose.
Un FILE* referencia un objeto de la biblioteca que conserva información de un flujo: acceso, búferes, posición lógica e indicadores de fin y error, entre otros estados. En los archivos ordinarios de Linux se apoya en un descriptor, pero el puntero y el entero no son intercambiables.
programa
│ fprintf / fwrite / fputs
▼
FILE* y buffer de stdio memoria del proceso
│ cuando la biblioteca entrega datos
▼
descriptor → apertura estado del nucleo
│ write y mecanismos del sistema de archivos
▼
cache del nucleo → dispositivo almacenamiento y persistencia
Una llamada printf puede añadir bytes a un búfer sin llamar todavía a write. A la inversa, fgetc puede entregar un byte que ya estaba en memoria sin pedir uno nuevo al núcleo. No hay una correspondencia obligatoria de una llamada de biblioteca con una llamada al sistema.
Los tres flujos estándar
Stdin, stdout y stderr son FILE* preparados por el entorno de ejecución. En el caso habitual se apoyan en los descriptores 0, 1 y 2. Usa printf(...) o fprintf(stdout, ...) para la salida formateada; usa write(STDOUT_FILENO, ...) para la interfaz de descriptor. Pasar stdout a write no es una conversión válida.
El objeto FILE pertenece a la implementación. No inspecciones sus campos internos para obtener el fd o cambiar sus indicadores: utiliza las interfaces públicas. También existen flujos de memoria y otras extensiones que no se reducen a un archivo persistente con descriptor utilizable.
Los búferes no son el archivo
El búfer de salida puede contener bytes que aún no se entregaron al núcleo. El de entrada puede contener bytes obtenidos por anticipado que el programa todavía no consumió. Por eso la posición lógica del flujo puede diferir de la posición actual de la apertura subyacente sin que haya un error.
La caché del núcleo es otra capa. Desactivar el búfer de stdio no desactiva esa caché ni fuerza escrituras físicas. Esta separación será importante al interpretar el rendimiento y la durabilidad.
2. Abrir y cerrar flujos con fopen y fclose
#include <stdio.h>
FILE *fopen(const char *ruta, const char *modo);
int fclose(FILE *flujo);
FILE *f = fopen("datos.bin", "rb");
if (f == NULL) {
perror("fopen");
/* No usar f como un flujo valido. */
}
Fopen recibe una cadena de modo, no las banderas O_RDONLY o O_CREAT de open. Devuelve un FILE* o NULL si falla. En Linux/POSIX errno ayuda a describir errores de apertura, pero la comprobación de éxito sigue siendo el puntero, no que errno valga cero.
| Modo | Acceso | Si falta | Si existe |
|---|---|---|---|
r | Lectura. | Falla. | Conserva contenido; comienza al principio. |
w | Escritura. | Crea. | Trunca a cero. |
a | Escritura anexada. | Crea. | Cada escritura se coloca al final. |
r+ | Lectura y escritura. | Falla. | Conserva contenido. |
w+ | Lectura y escritura. | Crea. | Trunca a cero. |
a+ | Lectura y escritura anexada. | Crea. | Conserva contenido; las escrituras van al final. |
Añadir b, como en rb, wb o r+b, expresa modo binario. En POSIX no cambia la interpretación de los bytes, pero otras plataformas pueden distinguir texto y binario. Para manipular contenido arbitrario utilizaremos los modos con b. Las escrituras en anexado no dejan de ir al final por haber reposicionado el flujo para leer.
No supongas una posición inicial de lectura idéntica para a+ en todas las bibliotecas: si necesitas leer desde el comienzo, posiciónalo explícitamente. Las reglas completas se describen en fopen(3). El modo + habilita ambas direcciones, pero exige reglas de transición que veremos en la sección 10.
Creación y permisos
En la apertura ordinaria de un archivo nuevo de POSIX se solicitan permisos de lectura y escritura sujetos a umask y a las reglas del directorio. Fopen no recibe un tercer argumento mode como open. Si necesitas expresar una política de apertura más precisa, puedes obtener un fd con open y asociarlo después con fdopen.
El modo w puede truncar antes de que tu programa haga la primera escritura. La misma precaución del tema 5 sigue vigente: validar argumentos después de abrir no deshace sus efectos. Para creación exclusiva existe el modificador x de C11 en modos de creación como wx; no lo confundas con un permiso de ejecución.
Fclose consume el flujo incluso si informa un error
Fclose intenta entregar salida pendiente y liberar los recursos del flujo. Devuelve 0 si tiene éxito y EOF si falla. Después de la llamada, el objeto no debe volver a utilizarse ni cerrarse otra vez, independientemente del resultado. Asignar NULL a tu variable ayuda a expresar ese cambio de propiedad.
No sustituyas fclose(f) por close(fileno(f)): cerrar solo el descriptor deja a stdio con un recurso que cree válido y puede perder salida pendiente o cerrar más tarde un número reutilizado. El propietario del flujo debe cerrar el nivel adecuado.
3. Caracteres, líneas, formatos y bloques
Stdio agrupa varias formas de trabajar con un flujo. La elección depende del formato que necesita tu programa, no de que el archivo tenga una extensión concreta.
| Forma | Funciones | Contrato que importa |
|---|---|---|
| Caracteres. | fgetc, fputc. | Fgetc devuelve int para representar cada unsigned char y EOF. |
| Líneas o fragmentos de texto. | fgets, fputs. | Fgets termina en NUL y puede entregar solo parte de una línea larga. |
| Formato. | fprintf, fscanf. | Interpretan formatos; sus retornos no equivalen siempre a bytes transferidos. |
| Bloques. | fread, fwrite. | Cuentan elementos de un tamaño declarado. |
Fgetc necesita int, no char
int c;
while ((c = fgetc(f)) != EOF) {
unsigned char byte = (unsigned char)c;
/* Procesar byte; puede valer 0 o 255. */
}
if (ferror(f)) {
/* El final del bucle fue un error de lectura. */
}
Guardar primero el retorno en char puede perder la distinción entre un byte válido y EOF. El valor 0 de un byte tampoco es EOF. El retorno especial señala que no se obtuvo un carácter y los indicadores distinguen fin de entrada y error.
Fgets no garantiza que cupo una línea entera
Fgets recibe la capacidad del arreglo y reserva espacio para el terminador. Conserva el salto de línea cuando lo encuentra y cabe. Si la línea es más larga, otra llamada continuará leyendo sus fragmentos. Comprobar que hay una cadena válida no demuestra que se haya recibido un registro lógico completo.
Fputs no añade un salto de línea automáticamente. Printf y fprintf interpretan conversiones y devuelven una cantidad de caracteres producidos o un valor negativo ante error. Para leer números con fscanf, el retorno suele contar conversiones correctas, no bytes; un retorno cero por texto incompatible tampoco equivale a EOF. Leer una línea y analizarla después suele facilitar un manejo explícito de entradas inválidas.
Datos binarios y estructuras
Fread y fwrite son apropiadas para regiones de bytes sin terminación. Escribir una struct completa no diseña por sí solo un formato portable: puede incluir relleno, representaciones y orden de bytes dependientes de la plataforma. La biblioteca mueve elementos; tu aplicación decide su serialización.
4. fread y fwrite cuentan elementos
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
unsigned char buf[4096];
size_t n = fread(buf, 1, sizeof buf, f);
/* Al usar elementos de un byte, n es la cantidad de bytes completos. */
Si llamas fread(buf, 4, 10, f), pides diez elementos de cuatro bytes. El retorno 3 significa tres elementos completos, no tres bytes. Para transportar bytes arbitrarios suele ser más sencillo usar size = 1: así el retorno representa exactamente la cantidad de bytes completos obtenidos.
El retorno de fread es size_t: no comunica -1. Si devuelve menos elementos que los pedidos, hay que consultar feof y ferror. Un elemento final incompleto no se cuenta como completo; usar elementos grandes complica interpretar el fragmento. No supongas que puedes tratar su parte incompleta como un objeto ya inicializado.
Fwrite correcto no equivale a write ya completado
Fwrite puede aceptar todos los elementos dentro del búfer de la biblioteca y devolver el total antes de entregar esos datos al núcleo. El error puede aparecer en una llamada posterior, en fflush o en fclose. Comprobar el retorno sigue siendo necesario, pero no cierra toda la operación de salida.
unsigned char datos[] = { 'A', 0, 'B', '\n' };
size_t n = fwrite(datos, 1, sizeof datos, f);
if (n != sizeof datos) {
/* Error o escritura incompleta en el flujo: no anunciar exito. */
}
/* Todavia hay que comprobar fflush o el fclose que corresponda. */
No copies mecánicamente el bucle de write para reintentar un fwrite que encontró un error. Hay estado en el flujo y no basta con repetir hasta que aparezca un total. Primero identifica el indicador, conserva el diagnóstico y define una política de recuperación. Los programas de este tema comunican el fallo en lugar de prometer un reintento universal.
Si size o nmemb son cero, no se pide transferir elementos y el retorno cero no demuestra EOF ni un error de salida. El manual de fread(3) y fwrite(3) explica los retornos y sus unidades.
5. EOF, ferror y el orden de las comprobaciones
Un flujo conserva indicadores. Feof consulta si está marcado el fin de entrada; ferror consulta el de error. No son operaciones que intenten adivinar el futuro ni llamadas que transfieran datos.
for (;;) {
size_t n = fread(buf, 1, capacidad, f);
/* Procesar los n bytes validos, si los hay. */
if (ferror(f)) {
/* Informar fallo y terminar como incompleto. */
break;
}
if (feof(f))
break;
}
El patrón while (!feof(f)) usado como si garantizara otra lectura válida suele procesar una vuelta de más o reutilizar datos anteriores. El indicador se activa al encontrar el final durante una operación. No sustituye examinar cuánto se recibió en esa llamada.
Una lectura corta puede activar EOF en la misma llamada
Fread puede realizar varias lecturas internas para satisfacer su petición. En un archivo estable de diez bytes, pedir cuatro puede devolver 4, 4 y 2; en la tercera llamada ya puede quedar activo EOF, porque la biblioteca intentó completar el pedido y encontró el final. No es obligatorio esperar una cuarta llamada de aplicación para reconocerlo.
Si pides cinco, las dos primeras llamadas pueden obtener cinco cada una sin haber intentado leer después del final. Entonces feof sigue desactivado hasta otra lectura. Llegar exactamente a una longitud y haber encontrado EOF durante una operación son situaciones distintas.
Los indicadores permanecen hasta una operación apropiada
Clearerr borra los indicadores de error y EOF. No modifica permisos, no reabre un archivo y no repara un dispositivo: solo permite que el programa gestione el estado del flujo. Una operación de posicionamiento correcta como fseek o fseeko limpia EOF; no debe suponerse que borra también cualquier indicador de error anterior.
Rewind vuelve al comienzo y limpia los indicadores, pero no devuelve un estado para comprobar el éxito como fseek. En los ejemplos usaremos fseeko y verificaremos su retorno. En Linux, para describir un fallo de lectura con errno lo conservaremos antes de imprimir otras observaciones.
6. Experimento: observar las lecturas de stdio
Leer-stdio.c recibe una ruta y un tamaño de bloque opcional, de 1 a 4096. Imprime los bytes válidos y los indicadores después de cada fread. No mezcla la longitud del arreglo con el resultado de la operación ni utiliza strlen sobre los datos.
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <inttypes.h>
#include <string.h>
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3) {
fprintf(stderr, "uso: %s ARCHIVO [BLOQUE:1..4096]\n", argv[0]);
return 2;
}
size_t pedido = 4;
if (argc == 3) {
char *fin;
errno = 0;
unsigned long valor = strtoul(argv[2], &fin, 10);
if (errno != 0 || fin == argv[2] || *fin != '\0' ||
valor < 1 || valor > 4096 || argv[2][0] == '-') {
fprintf(stderr, "bloque invalido\n");
return 2;
}
pedido = (size_t)valor;
}
FILE *f = fopen(argv[1], "rb");
if (f == NULL) {
perror("fopen entrada");
return 1;
}
unsigned char buf[4096];
uintmax_t total = 0;
unsigned long llamada = 0;
int estado = 0;
for (;;) {
size_t n = fread(buf, 1, pedido, f);
int hay_error = ferror(f) != 0;
int hay_fin = feof(f) != 0;
int codigo = hay_error ? errno : 0;
llamada++;
printf("fread %lu: pedido=%zu retorno=%zu eof=%d error=%d",
llamada, pedido, n, hay_fin, hay_error);
for (size_t i = 0; i < n; i++)
printf(" %02x", (unsigned int)buf[i]);
putchar('\n');
total += (uintmax_t)n;
if (hay_error) {
fprintf(stderr, "lectura incompleta: %s\n",
codigo ? strerror(codigo) : "indicador de error activo");
estado = 1;
break;
}
if (hay_fin)
break;
}
printf("total=%ju\n", total);
if (fclose(f) == EOF) {
perror("fclose entrada");
estado = 1;
}
f = NULL;
if (fflush(stdout) == EOF || ferror(stdout)) {
fprintf(stderr, "fallo al producir el informe\n");
estado = 1;
}
return estado;
}
Los indicadores se normalizan a 0 o 1 para que el informe no dependa del valor no nulo concreto que devuelva la biblioteca. Si se reciben bytes y también se detecta un error, se muestran los bytes válidos y se conserva el resultado de fallo.
$ LAB10=$(mktemp -d "$HOME/tema10.XXXXXX")
$ cd "$LAB10"
$ printf 'ABCDEFGHIJ' > letras.txt
$ printf 'A\0B\n' > binario.dat
$ touch vacio.dat
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 leer-stdio.c -o leer-stdio
$ ./leer-stdio letras.txt 4
fread 1: pedido=4 retorno=4 eof=0 error=0 41 42 43 44
fread 2: pedido=4 retorno=4 eof=0 error=0 45 46 47 48
fread 3: pedido=4 retorno=2 eof=1 error=0 49 4a
total=10
$ ./leer-stdio letras.txt 5
fread 1: pedido=5 retorno=5 eof=0 error=0 41 42 43 44 45
fread 2: pedido=5 retorno=5 eof=0 error=0 46 47 48 49 4a
fread 3: pedido=5 retorno=0 eof=1 error=0
total=10
Guarda el código dentro de LAB10 antes de compilar. Las salidas representan archivos regulares estables en el entorno Linux utilizado. La diferencia importante es que fread cuenta bytes del pedido de la aplicación y conserva indicadores; sus llamadas internas a read no se deducen contando las líneas del informe.
$ ./leer-stdio binario.dat 8
fread 1: pedido=8 retorno=4 eof=1 error=0 41 00 42 0a
total=4
$ ./leer-stdio vacio.dat
fread 1: pedido=4 retorno=0 eof=1 error=0
total=0
7. Tres modos de búfer y setvbuf
Un búfer agrupa o anticipa transferencias. La biblioteca puede evitar una llamada al núcleo por cada pequeño fragmento de la aplicación, y el programa puede elegir una política antes de comenzar las operaciones del flujo.
| Modo | Constante | Idea principal |
|---|---|---|
| Por bloques. | _IOFBF | Se acumula salida y se entrega en grupos; también puede entregarse por petición explícita o cierre. |
| Por líneas. | _IOLBF | Un salto de línea puede completar la entrega de la salida pendiente, además de las otras condiciones de vaciado. |
| Sin búfer de stdio. | _IONBF | No se usa esa acumulación de usuario; siguen existiendo otras capas. |
En el entorno habitual, stdout conectado a una terminal suele ser por líneas; redirigido a un archivo suele ser por bloques. Stderr no es completamente bufferizado por defecto. No bases un programa en adivinar un número fijo de bytes de búfer para cualquier biblioteca y destino.
FILE *f = fopen("salida.bin", "wb");
if (f == NULL) {
/* Manejar fallo. */
}
char almacenamiento[BUFSIZ];
if (setvbuf(f, almacenamiento, _IOFBF, sizeof almacenamiento) != 0) {
/* La configuracion fallo; no suponer que quedo aplicada. */
}
/* Usar el flujo y cerrarlo antes de que almacenamiento deje de existir. */
Setvbuf debe llamarse después de asociar el flujo al archivo y antes de las demás operaciones sobre él. Si proporcionas memoria propia, debe seguir válida hasta que el flujo ya no la use, normalmente hasta fclose. Un arreglo local que desaparece al volver de una función no puede servir al flujo que esa función devuelve.
Un modo no fija exactamente las llamadas al sistema
La implementación puede entregar una escritura grande directamente, agrupar varias pequeñas o realizar transferencias que no coincidan exactamente con tu capacidad. No prometas que BUFSIZ determina el tamaño de cada write. El contrato de visibilidad de la salida y la elección de estrategias internas son niveles distintos.
La lectura anticipada también es importante: al pedir un carácter, la biblioteca puede obtener varios kilobytes y conservarlos. El siguiente fgetc puede servirse sin volver a leer del núcleo. La posición lógica avanza por lo consumido por el programa, no necesariamente por toda la anticipación interna.
8. Experimento: cuándo llegan los bytes al núcleo
Buffer-salida.c abre un archivo, configura un modo y escribe dos fragmentos: uno y dos seguido de salto de línea. Consulta el tamaño del objeto con fstat mediante fileno, antes y después de fflush. Esa consulta de metadatos no lee ni reposiciona el descriptor.
#define _POSIX_C_SOURCE 200809L
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
static int mostrar(FILE *f, const char *etapa)
{
int fd = fileno(f);
if (fd == -1)
return -1;
struct stat s;
if (fstat(fd, &s) == -1)
return -1;
printf("%s: longitud observada=%jd\n", etapa, (intmax_t)s.st_size);
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 3) {
fprintf(stderr, "uso: %s ARCHIVO bloque|linea|sin\n", argv[0]);
return 2;
}
int modo;
if (strcmp(argv[2], "bloque") == 0)
modo = _IOFBF;
else if (strcmp(argv[2], "linea") == 0)
modo = _IOLBF;
else if (strcmp(argv[2], "sin") == 0)
modo = _IONBF;
else {
fprintf(stderr, "modo desconocido\n");
return 2;
}
FILE *f = fopen(argv[1], "wb");
if (f == NULL) {
perror("fopen salida");
return 1;
}
char buffer[BUFSIZ];
int estado = 1;
if (setvbuf(f, modo == _IONBF ? NULL : buffer, modo, sizeof buffer) != 0) {
fprintf(stderr, "setvbuf no pudo configurar el flujo\n");
goto cerrar;
}
if (mostrar(f, "inicial") == -1) {
perror("fstat inicial");
goto cerrar;
}
if (fputs("uno", f) == EOF) {
perror("fputs uno");
goto cerrar;
}
if (mostrar(f, "tras uno") == -1) {
perror("fstat uno");
goto cerrar;
}
if (fputs("dos\n", f) == EOF) {
perror("fputs dos");
goto cerrar;
}
if (mostrar(f, "tras dos con salto") == -1) {
perror("fstat dos");
goto cerrar;
}
if (fflush(f) == EOF) {
perror("fflush salida");
goto cerrar;
}
if (mostrar(f, "tras fflush") == -1) {
perror("fstat final");
goto cerrar;
}
estado = 0;
cerrar:
if (fclose(f) == EOF) {
perror("fclose salida");
estado = 1;
}
f = NULL;
if (fflush(stdout) == EOF || ferror(stdout)) {
fprintf(stderr, "fallo del informe\n");
estado = 1;
}
return estado;
}
$ cd "$LAB10"
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 buffer-salida.c -o buffer-salida
$ ./buffer-salida bloque.txt bloque
inicial: longitud observada=0
tras uno: longitud observada=0
tras dos con salto: longitud observada=0
tras fflush: longitud observada=7
$ ./buffer-salida linea.txt linea
inicial: longitud observada=0
tras uno: longitud observada=0
tras dos con salto: longitud observada=7
tras fflush: longitud observada=7
$ ./buffer-salida sin.txt sin
inicial: longitud observada=0
tras uno: longitud observada=3
tras dos con salto: longitud observada=7
tras fflush: longitud observada=7
Los tres resultados finales contienen los mismos siete bytes: unodos y salto de línea. Los tamaños intermedios reflejan la implementación y el modo usado; no son un calendario universal de llamadas al sistema. En particular, cualquier biblioteca puede tener estrategias diferentes de entrega dentro de su contrato.
$ cmp bloque.txt linea.txt
$ cmp linea.txt sin.txt
$ od -An -tx1 bloque.txt
75 6e 6f 64 6f 73 0a
No guardes el informe de stdout en el mismo archivo que el programa abre para estudiar sus datos: introducirías otro escritor sobre el objeto observado. Usa rutas distintas para que las mediciones describan el flujo que interesa.
9. Qué garantizan fflush y fclose
Fflush sobre un flujo de salida intenta entregar a su archivo asociado la salida pendiente de stdio. Devuelve cero si tiene éxito y EOF si informa un error. Fflush(NULL) intenta vaciar los flujos de salida abiertos; no selecciona un documento por nombre.
Fflush vacía una capa de biblioteca, no fuerza por sí solo persistencia en el dispositivo. Tras un vaciado correcto los bytes pueden estar en cachés del núcleo. Fsync y las reglas del sistema de archivos resuelven otra parte del contrato de durabilidad.
Fclose realiza la finalización del flujo y comprueba también la entrega que necesite. Si llamas antes a fflush y funciona, fclose todavía puede informar otro fallo. Si fclose devuelve EOF, el flujo de todas formas ya no debe reutilizarse; no vuelvas a intentarlo con el mismo FILE*.
Un error puede aparecer tarde
En Linux, /dev/full permite observar una salida que se abre pero rechaza las escrituras. Según cuándo se entregue el búfer, fputs o fwrite pueden parecer correctos y el fallo aparecer al hacer fflush o fclose. Usa buffer-salida sobre ese dispositivo si está disponible:
$ LC_ALL=C ./buffer-salida /dev/full bloque
$ echo $?
1
El programa mostrará el error en la operación que lo detecte; las líneas exactas de observación y diagnóstico dependen de cuándo se intentó la entrega. No concluyas que el disco real se llenó: /dev/full es un dispositivo de prueba. La regla es comprobar todas las fronteras relevantes, no esperar un punto de fallo idéntico en cualquier biblioteca.
Fflush(stdin) no es un borrador portable del teclado
ISO C define fflush para salida o para un flujo de actualización cuya operación más reciente no sea de entrada. No debe utilizarse fflush(stdin) como una receta general para limpiar caracteres pendientes de una terminal. POSIX añade reglas para flujos de entrada posicionables, relacionadas con descartar anticipación y sincronizar el offset, pero eso no convierte una terminal en un archivo reposicionable.
Si el programa debe descartar el resto de una línea de entrada, debe leerlo y reconocer su final según su formato. Si debe alternar accesos a un archivo de actualización, utiliza una operación de posicionamiento apropiada. El comportamiento de vaciado depende de qué capa y qué contrato se está usando.
10. Alternar lectura y escritura con +
Los modos r+, w+ y a+ permiten ambas direcciones, pero no autorizan cualquier secuencia sin transición. La biblioteca puede tener datos anticipados o salida aún pendiente que no coinciden con el cursor del descriptor.
| Transición | Regla de coordinación |
|---|---|
| Escritura → lectura. | Interponer fflush o una operación de posicionamiento apropiada. |
| Lectura → escritura. | Interponer posicionamiento, salvo la excepción de una lectura que encontró EOF. |
| Cambiar la ubicación. | Comprobar fseek/fseeko/fsetpos, según el tipo de posición usado. |
Fseeko(f, 0, SEEK_CUR) es útil sobre un archivo regular para sincronizar y conservar la posición lógica actual antes de escribir. No sustituye esas reglas en una tubería donde posicionar no es posible. Para moverse al comienzo utilizaremos fseeko(f, 0, SEEK_SET).
Fseek y fseeko
Fseek utiliza long y ftell devuelve long. POSIX ofrece fseeko y ftello con off_t, adecuados para el modelo de archivos grandes del tema 7. En Linux emplearemos las macros de características antes de las cabeceras. Para un archivo binario regular las posiciones son offsets en bytes; no extrapoles todos los cálculos a las posiciones de flujos de texto de cualquier plataforma.
Fgetpos y fsetpos trabajan con fpos_t, que puede contener información adicional del estado de conversión. No lo trates como un entero sobre el que sumar tamaños arbitrarios. Elige la interfaz conforme al tipo de flujo y al formato que necesitas.
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <stdio.h>
#include <inttypes.h>
static int mostrar_posicion(FILE *f, const char *etapa)
{
off_t p = ftello(f);
if (p == (off_t)-1)
return -1;
printf("%s: posicion logica=%jd\n", etapa, (intmax_t)p);
return 0;
}
static int leer(FILE *f, size_t pedido)
{
unsigned char buf[16]; /* este ejemplo solo pide 2 o 16 */
size_t n = fread(buf, 1, pedido, f);
int fallo = ferror(f) != 0;
printf("fread: %zu bytes; eof=%d", n, feof(f) != 0);
for (size_t i = 0; i < n; i++)
printf(" %02x", (unsigned int)buf[i]);
putchar('\n');
if (fallo) {
fprintf(stderr, "error al leer el flujo\n");
return -1;
}
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "uso: %s ARCHIVO_EXISTENTE\n", argv[0]);
return 2;
}
FILE *f = fopen(argv[1], "r+b");
if (f == NULL) {
perror("fopen actualizacion");
return 1;
}
int estado = 1;
if (leer(f, 2) == -1)
goto cerrar;
if (mostrar_posicion(f, "tras lectura") == -1) {
perror("ftello");
goto cerrar;
}
if (fseeko(f, 0, SEEK_CUR) == -1) {
perror("sincronizar lectura antes de escribir");
goto cerrar;
}
if (fwrite("xy", 1, 2, f) != 2) {
fprintf(stderr, "fwrite incompleto\n");
goto cerrar;
}
if (fseeko(f, 0, SEEK_SET) == -1) {
perror("posicionar antes de leer");
goto cerrar;
}
if (leer(f, 16) == -1)
goto cerrar;
if (mostrar_posicion(f, "tras segunda lectura") == -1) {
perror("ftello final");
goto cerrar;
}
if (fseeko(f, 0, SEEK_SET) == -1) {
perror("volver al comienzo");
goto cerrar;
}
printf("eof tras fseeko=%d\n", feof(f) != 0);
estado = 0;
cerrar:
if (fclose(f) == EOF) {
perror("fclose actualizacion");
estado = 1;
}
f = NULL;
if (fflush(stdout) == EOF || ferror(stdout)) {
fprintf(stderr, "fallo del informe\n");
estado = 1;
}
return estado;
}
$ cd "$LAB10"
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 actualizar-stdio.c -o actualizar-stdio
$ printf 'ABCDEFGHIJ' >actualizacion.bin
$ ./actualizar-stdio actualizacion.bin
fread: 2 bytes; eof=0 41 42
tras lectura: posicion logica=2
fread: 10 bytes; eof=1 41 42 78 79 45 46 47 48 49 4a
tras segunda lectura: posicion logica=10
eof tras fseeko=0
$ cat actualizacion.bin; printf '\n'
ABxyEFGHIJ
La primera operación de posicionamiento coordina lectura y escritura sin volver al comienzo. La segunda coordina escritura y lectura y además elige posición 0. La lectura de 16 encuentra los diez bytes disponibles y activa EOF. El último fseeko vuelve a cero y limpia ese indicador.
El programa modifica el archivo existente, no publica un reemplazo atómico. Si falla después de progreso puede dejar cambios aplicados. Su objetivo es observar el contrato de actualización, no ofrecer rollback ni evitar modificaciones de otros procesos.
11. fdopen, fileno y referencias compartidas
POSIX ofrece un puente entre las dos capas. Fileno obtiene el descriptor asociado a un flujo cuando existe; no lo duplica ni transfiere su propiedad. Fdopen asocia un descriptor existente con un FILE* nuevo cuyo modo debe ser compatible con el acceso ya concedido.
int fd = open("datos.bin", O_RDWR | O_CLOEXEC);
if (fd == -1) {
/* Manejar error de open. */
}
FILE *f = fdopen(fd, "r+b");
if (f == NULL) {
int codigo = errno;
close(fd); /* fdopen no tuvo exito: el descriptor sigue siendo nuestro */
errno = codigo;
/* Informar el error principal. */
} else {
fd = -1; /* la propiedad ahora se gestiona mediante f */
/* Usar f y comprobar fclose(f) al terminar. */
}
Es un esquema que requiere las cabeceras y las ramas de salida de error de un programa completo. Un fdopen correcto no hace dup: fclose cerrará el descriptor asociado. Si haces también close por fuera, introduces el mismo problema de doble cierre y reutilización de números que vimos antes.
Fdopen no repite las reglas de creación de fopen
El descriptor ya existe. En particular, el modo w de fdopen no vuelve a truncar el archivo como haría fopen con w, y no crea una ruta nueva. El modo debe ser compatible con la apertura, y el anexado tiene sus condiciones sobre el estado de esa referencia. No deduzcas todos sus efectos solo porque las cadenas se parezcan.
No mezclar accesos sin coordinar los manejadores
Después de fgetc, stdio puede tener bytes anticipados mientras el descriptor ya avanzó más lejos. Ejecutar read sobre fileno(f) sin coordinar puede saltarse datos que aún están en el búfer. En salida, write directo puede adelantarse a bytes pendientes de fprintf y cambiar el orden aparente.
POSIX establece reglas de coordinación de los manejadores activos. No basta con decir «ambos llegan al mismo archivo». Para tareas sencillas, elige una interfaz de transferencia por apertura y no mezcles las dos. Consultar metadatos con fstat, como en buffer-salida, no es lo mismo que leer, escribir o reposicionar por fuera de stdio.
Hacer fdopen sobre dup(fd) evita compartir la misma entrada de descriptor, pero las dos referencias siguen llegando a la misma apertura y su offset. Además habría búferes de biblioteca independientes. Duplicar no elimina automáticamente el problema de coordinación.
Fork duplica memoria de biblioteca, no solo referencias
Si un proceso tiene salida pendiente y hace fork, ambos procesos pueden heredar una copia del búfer. Si los dos terminan vaciándolo mediante las rutas normales de stdio, los bytes pueden aparecer dos veces. Vaciar y comprobar los flujos antes de fork, o evitar que el hijo vacíe búferes heredados cuando corresponda, forma parte del diseño.
El uso de _exit en hijos y las restricciones después de fork en procesos multihilo deben entenderse dentro del programa concreto. La lección aquí es distinguir el búfer copiado en memoria de la descripción de archivo abierta que los procesos pueden compartir. Exec correcto no realiza un fclose normal de todos los flujos anteriores para salvar su salida pendiente.
12. Cuándo elegir stdio o llamadas directas
| Necesidad | Interfaz que suele encajar | Motivo |
|---|---|---|
| Leer texto por líneas y producir informes. | Stdio. | Ofrece fgets, fprintf y gestión de búferes. |
| Transferir bloques binarios de archivos. | Ambas, con contrato explícito. | Fread/fwrite o read/write pueden expresar la tarea. |
| Preparar redirecciones y ejecutar programas. | Descriptores. | Dup2 y las tablas del núcleo trabajan con enteros. |
| Operaciones con offset explícito. | Pread/pwrite. | Evitan mover el cursor compartido como estudiamos en el tema 7. |
| E/S no bloqueante con control fino de pendientes. | Descriptores y estado de la aplicación. | EAGAIN y los mecanismos de espera deben integrarse conscientemente. |
| Opciones de apertura o dispositivos específicos. | Open y otras interfaces del sistema, quizá fdopen después. | Permiten expresar condiciones fuera de fopen portable. |
Stdio no elimina los errores de E/S; cambia la forma de observarlos y el nivel donde se conserva parte del estado. Las llamadas directas no garantizan ser más rápidas por definición: un programa que hace un write por carácter puede rendir mucho peor que otro que agrupa mediante stdio.
Una comparación justa necesita igualar el trabajo, comprobar errores y considerar la política de búfer. En el tema 11 mediremos ese coste. Antes de medir, decide si necesitas latencia interactiva, capacidad de transferencia o control de las operaciones; optimizar una puede cambiar otra.
La comodidad no elimina el contrato
Un programa con fopen puede ser correcto y breve si comprueba sus retornos, las transiciones de actualización y el cierre. Otro con read y write puede ser incorrecto si ignora parciales. La calidad no depende de elegir siempre la capa más baja, sino de conocer qué garantiza la elegida.
13. Simulador de búferes y posiciones
Elige un escenario y reinicia. En salida, el flujo comienza con un archivo vacío y un búfer didáctico de ocho bytes. En entrada, el archivo contiene ABCDE seguido de salto de línea y FGHIJ; una recarga anticipa hasta ocho bytes. La vista distingue bytes consumidos por la aplicación, contenido pendiente y posición de la apertura.
El flujo puede adelantarse o retrasarse respecto al descriptor
| Paso | Operación | Resultado y efecto |
|---|
La estrategia de recargas y vaciados del modelo es deliberadamente fija para verla paso a paso. No reproduce el número exacto de read o write de una biblioteca real. Los contadores de transferencias al núcleo son simulados; escribir un fragmento largo puede provocar varias entregas en este modelo.
La operación de error simulada activa ferror sin transferir bytes; los errores reales pueden aparecer después de progreso y requieren su contrato concreto. Clearerr limpia indicadores y no cambia posiciones ni datos. Fseeko al comienzo limpia EOF y coordina el búfer del modelo, pero conserva el indicador de error. Fclose invalida el flujo para nuevas operaciones.
Secuencias para predecir
- En salida por bloques, escribe ABC. El archivo sigue vacío y el búfer conserva tres bytes. Fflush los entrega sin cerrar el flujo.
- Repite con salida por líneas y añade D más salto. Se entrega ABCD y el salto al completar la línea. En salida sin búfer, ABC ya se entregó antes.
- En entrada, ejecuta un fgetc. La aplicación consumió un byte, pero la apertura del modelo avanzó ocho y quedan siete anticipados.
- Consume los once bytes del archivo sin pedir uno adicional. Puede quedar EOF desactivado hasta otra operación que encuentre el final.
- Provoca un error, vuelve al comienzo y observa ferror. Después usa clearerr para distinguir las dos clases de indicadores.
- Cierra un flujo de salida con datos pendientes. El modelo los entrega al archivo y después invalida el flujo. No se interpreta esa entrega como escritura física durable.
14. Resumen
- FILE* es una referencia a estado de biblioteca; el descriptor es una entrada interpretada por el núcleo.
- Fopen devuelve un flujo o NULL. Los modos r, w, a y sus variantes + expresan acceso y efectos de apertura.
- Fgetc usa int; fgets puede devolver fragmentos de línea; fread y fwrite cuentan elementos.
- Con size igual a uno, los retornos de fread/fwrite son cantidades de bytes completos.
- Feof y ferror consultan indicadores de operaciones anteriores, no predicen si la próxima tendrá éxito.
- Los búferes de usuario pueden acumular salida o anticipar entrada, separando posición lógica y offset del núcleo.
- Setvbuf se configura antes de operar y la memoria proporcionada debe conservar su vida útil.
- Fflush entrega salida pendiente de stdio; fclose finaliza el flujo y puede comunicar errores tardíos.
- Alternar lectura y escritura en un flujo de actualización exige coordinación mediante las operaciones apropiadas.
- Fdopen no duplica el descriptor; fileno no transfiere su propiedad. Mezclar manejadores requiere coordinación.
- Vaciar biblioteca, cerrar referencias y garantizar persistencia son compromisos diferentes.
15. Preguntas de repaso y práctica
Separa en cada respuesta el estado del flujo, el descriptor y el objeto del sistema de archivos.
1. ¿FILE* y fd son dos formas intercambiables de representar lo mismo?
No. El primero referencia un objeto de biblioteca con búferes e indicadores; el segundo es un entero que identifica una entrada de la tabla del núcleo. Las funciones esperan el tipo de referencia de su propia interfaz.
2. ¿Una llamada a printf implica exactamente un write?
No. Puede acumular datos, provocar una o varias transferencias o aprovechar una política de búfer. No hay una equivalencia obligatoria de una llamada por cada capa.
3. ¿Qué hace fopen con w si el archivo ya existe?
Solicita truncarlo a cero. El efecto puede ocurrir antes de la primera escritura, igual que una apertura de creación y truncado del nivel inferior.
4. ¿Qué retorno de fread(buf, 4, 10, f) representa doce bytes completos?
El retorno 3: tres elementos completos de cuatro bytes. Para contar directamente bytes es más sencillo usar size igual a uno.
5. ¿Por qué el retorno de fread no se compara con -1?
Porque usa size_t y cuenta elementos completos. Un retorno corto se interpreta consultando feof y ferror; el error no se comunica mediante un ssize_t de valor -1.
6. ¿Fwrite completo garantiza que el núcleo ya aceptó todos los bytes?
No necesariamente. Pueden estar aceptados en el búfer de stdio. Un fallo puede aparecer al entregar más datos, en fflush o en fclose.
7. ¿Por qué fgetc devuelve int?
Para distinguir todos los valores de unsigned char del valor especial EOF. Guardarlo primero en char puede perder esa distinción.
8. ¿Fgets siempre devuelve una línea entera?
No. El arreglo puede ser demasiado pequeño y recibir solo un fragmento. Se debe reconocer si apareció el salto de línea o si hace falta continuar según el formato.
9. ¿Feof permite saber por adelantado que otra lectura tendrá datos?
No. Consulta un indicador activado al encontrar fin de entrada. La lectura y su retorno se comprueban primero; no se reutiliza contenido anterior por confiar en while(!feof).
10. ¿Qué diferencia hay entre los pedidos de cuatro y cinco sobre diez bytes?
Con cuatro, la última porción de dos puede activar EOF al intentar completar el pedido. Con cinco, las dos primeras pueden completarse exactamente y hará falta otra lectura para encontrar el final.
11. ¿Clearerr repara el archivo o el dispositivo?
No. Borra los indicadores de EOF y error del flujo. La causa que impidió la operación puede seguir presente y debe gestionarse por separado.
12. ¿Cuándo debe configurarse setvbuf y cuánto vive un búfer propio?
Antes de las demás operaciones sobre el flujo, después de asociarlo al archivo. La memoria propia debe seguir válida mientras stdio pueda usarla, normalmente hasta finalizar el flujo.
13. ¿_IONBF desactiva la caché del núcleo?
No. Elimina esa acumulación de la biblioteca en usuario; las cachés y la política de almacenamiento del sistema siguen siendo otras capas.
14. ¿Fflush de salida correcto demuestra persistencia ante un corte de luz?
No. Entrega datos pendientes de la biblioteca al archivo asociado, pero pueden quedar en caché del núcleo. La durabilidad exige otras operaciones y garantías.
15. ¿Se puede reutilizar FILE* si fclose devuelve EOF?
No. La finalización consume el flujo también al informar un error. No se vuelve a leer, escribir ni cerrar usando ese puntero.
16. ¿Basta con fflush para cualquier transición de lectura a escritura en r+?
No como regla portable. Se interpone una operación de posicionamiento, salvo la excepción de una lectura que encontró EOF. La transición inversa permite fflush o posicionamiento.
17. ¿Fdopen duplica el descriptor y fileno permite cerrarlo por separado sin más?
No. Fdopen exitoso asocia el descriptor existente y fclose lo cerrará; fileno solo lo obtiene. Cerrar por fuera puede dejar a stdio con una referencia incoherente y producir doble cierre o pérdida de datos.
18. ¿Por qué puede duplicarse una salida pendiente después de fork?
Porque ambos procesos pueden conservar copias del búfer de usuario y vaciarlas después. Compartir la apertura del núcleo no fusiona esas copias de memoria de la biblioteca.
Para practicar
- Ejecuta leer-stdio con bloques 1, 4, 5 y 16 sobre letras.txt. Anota los retornos y el momento en que aparece EOF. Compara con leer-bloques del tema 6 sin deducir el número de llamadas internas.
- Lee un archivo que contenga todos los valores de byte y otro con NUL intercalados. Comprueba la longitud y los valores hexadecimales sin usar strlen como medida de la entrada.
- Cambia una prueba de fread con size uno por elementos de cuatro bytes sobre una longitud que no sea múltiplo de cuatro. Explica qué cuenta el retorno y por qué un elemento parcial necesita cuidado.
- Ejecuta buffer-salida en sus tres modos y compara el contenido final con cmp. Distingue los tamaños intermedios de la igualdad de bytes.
- Si existe /dev/full, observa qué operación detecta el fallo de salida en cada modo. Verifica que el estado final es de error aunque una operación anterior pareciera correcta.
- Modifica el tamaño del búfer propio del experimento y escribe un fragmento mayor que él. Observa los puntos de entrega sin convertir una medición concreta en una garantía universal de stdio.
- Repite actualizar-stdio con un archivo corto y con uno vacío. Predice dónde se aplica xy y qué indicadores cambian al volver al comienzo.
- Adapta un archivo abierto con open para asociarlo con fdopen. Dibuja quién debe cerrarlo si fdopen falla y quién lo cierra si tiene éxito.
- En el simulador de entrada consume los once bytes mediante fgetc. Observa la diferencia entre consumir el último y pedir otro después. Repite con fread de tres bytes.
- En el simulador escribe ABC y D con salto en los tres modos. Compara salida pendiente, posición lógica y entregas simuladas al núcleo.
- Provoca un error simulado, ejecuta fseeko y después clearerr. Explica qué indicador limpia cada operación y por qué no se modifican los datos al limpiar las marcas.
En el tema 11 compararemos tamaños de búfer y rendimiento. Con las capas de este tema podrás interpretar qué significa reducir llamadas al sistema, qué se está midiendo al vaciar una salida y por qué la comprobación de errores debe mantenerse durante las mediciones.