Regla 0x4004h: Mantené la simetría de recursos al abrir y cerrar archivos en el mismo nivel de abstracción
Archivos y E/S (0x40XX)
0x4004h: Mantené la simetría de recursos al abrir y cerrar archivos en el mismo nivel de abstracción¶
Enunciado normativo¶
DEBE cerrarse cada archivo en el mismo nivel de abstracción en que se lo abrió. Si se delega la propiedad del recurso, DEBE existir un módulo o estructura administradora simétrica que documente quién lo cierra.
La regla no dice “dónde” cerrar, sino “quién”: el dueño del recurso debe ser identificable y único, y la apertura y el cierre deben ser operaciones pareadas.
¿Por qué existe esta regla?¶
El problema¶
Un archivo abierto es un recurso con dueño. Si la función que lo abre lo
devuelve o lo pasa a otra, la responsabilidad de cerrarlo queda indefinida. En
esos casos suele pasar que cada parte asume que la otra lo cierra, y el
descriptor termina huérfano. El síntoma aparece recién mucho después, cuando el
proceso agota su tabla de descriptores y fopen empieza a fallar con EMFILE
aunque los archivos existan.
La simetría de niveles es una heurística de propiedad: si abrir y cerrar
viven en la misma función, el análisis es local y trivial. Si el recurso cruza
fronteras, hace falta una convención explícita de propiedad. El patrón más
robusto es el TAD con puntero opaco (0x301Dh: Diseñá los Tipos de Datos Abstractos utilizando punteros opacos), donde un módulo expone
crear y destruir y el cliente nunca manipula el descriptor.
Consecuencias de violarla¶
| Tipo de consecuencia | Efecto concreto |
|---|---|
| Fuga de recursos | Descriptores que nunca se cierran agotan la tabla del proceso (EMFILE). |
| Ambigüedad de propiedad | Nadie sabe si debe cerrar; se cierra dos veces o ninguna. |
| Datos no volcados | Un flujo de escritura huérfano deja el búfer sin descargar. |
| Mantenibilidad | Agregar una rama nueva agrava la fuga sin que se note en la revisión. |
Fundamento en el estándar y en la cátedra¶
ISO/IEC 9899:2011 §7.21.5.1 vincula fclose al flujo creado por fopen; todo
flujo debe terminar en fclose (o en la finalización del programa). La cátedra
adopta la simetría como política de propiedad porque el curso exige TDAs con
punteros opacos (0x301Dh: Diseñá los Tipos de Datos Abstractos utilizando punteros opacos) y liberación con NULL posterior
(0x301Eh: Asigná NULL al puntero tras liberar un recurso opaco en el ámbito del cliente), y ese modelo necesita dueños claros.
Alcance y excepciones¶
Aplica a la propiedad de todo FILE * que cruce fronteras de función, módulo o
TAD. No prohíbe pasar un FILE * como parámetro: permite que una función
escriba sobre un flujo ajeno, siempre que la propiedad quede documentada
(0x3006h: Documentá la propiedad de los recursos al utilizar punteros) y el dueño lo cierre.
Excepciones razonables: los flujos de larga vida (un log abierto al inicio y cerrado al final del programa) y los flujos estándar abiertos por el runtime.
Ejemplos exhaustivos¶
❌ Contraejemplo 1 — Función que abre y retorna sin cerrar¶
int verificar(const char *ruta)
{
FILE *f = fopen(ruta, "r");
if (f == NULL) {
return 0;
}
char linea[128];
fgets(linea, sizeof(linea), f);
return 1;
}Por qué falla: la función abrió el recurso y lo abandonó al retornar; el descriptor queda abierto. Aunque el programa arranque bien, cada llamada filtra uno y el fallo aparece recién tras muchas iteraciones.
❌ Contraejemplo 2 — Apertura y cierre en niveles distintos¶
int main(void)
{
FILE *f = fopen("log.txt", "a");
if (f == NULL) {
return -1;
}
registrar(f, "inicio");
return 0;
}Por qué falla: main es dueño de f, pero registrar no lo cierra ni lo
puede cerrar (el dueño sigue usándolo). Nadie cierra al final: el descriptor
queda vivo hasta que termina el proceso y, en un lazo, la fuga se acumula.
✅ Ejemplo conforme 1 — Misma función abre y cierra¶
int verificar(const char *ruta)
{
FILE *f = fopen(ruta, "r");
if (f == NULL) {
return 0;
}
char linea[128];
int leyo = fgets(linea, sizeof(linea), f) != NULL;
fclose(f);
return leyo;
}La apertura y el cierre viven en el mismo nivel: el análisis del recurso es
local y no hay camino que retorne con f abierto.
✅ Ejemplo conforme 2 — TAD administrador con operaciones simétricas¶
archivo_t *a = archivo_abrir("datos.txt");
if (a == NULL) {
return -1;
}
archivo_procesar(a);
archivo_cerrar(a);
a = NULL;El módulo archivo expone abrir y cerrar como pareja y oculta el FILE *
detrás de un puntero opaco (0x301Dh: Diseñá los Tipos de Datos Abstractos utilizando punteros opacos). El cliente delega la propiedad y la
asimetría es imposible por construcción; tras cerrar, anula el puntero
(0x301Eh: Asigná NULL al puntero tras liberar un recurso opaco en el ámbito del cliente).
⚠️ Casos límite¶
Flujos de larga vida: un log se abre una vez y se cierra al final; hay que documentar ese dueño único, típicamente
main.Paso de
FILE *como parámetro: válido si la función no adquiere la propiedad; documentalo conconstcuando corresponda.stdin/stdout/stderr: abiertos por el runtime; no se consideran fugas mientras el programa viva.Cierre en el camino de error: debe estar en el mismo nivel que la apertura, no en el llamador (ver 0x4004h: Mantené la simetría de recursos al abrir y cerrar archivos en el mismo nivel de abstracción).
Cómo detectarla¶
| Herramienta | Comando | Señal |
|---|---|---|
giger | análisis de call graph | Funciones que reciben un FILE * y otras que lo abren sin devolverlo. |
cppcheck | cppcheck --enable=all archivo.c | resourceLeak en caminos que no cierran. |
| Revisión manual | — | Todo fopen debe tener un fclose en el mismo nivel o un módulo dueño documentado. |
Checklist de autocontrol¶
¿Quién es el dueño del
FILE *y está documentado?¿La pareja
fopen/fclosevive en el mismo nivel de abstracción?Si delego la propiedad, ¿existe una función destructora simétrica?
¿Anulé el puntero después de que el administrador cerró el recurso?
Reglas relacionadas¶
0x4001h: Manejá correctamente la apertura y cierre de archivos — toda apertura debe tener su cierre en algún punto.
0x4009h: Prohibición de anidar llamadas a fopen() directamente dentro de funciones de E/S — no anidar
fopenen otra llamada rompe la simetría.0x400Ah: Prohibición de operar sobre flujos de archivo tras haber invocado fclose() (use-after-close) — el puntero deja de ser válido tras el cierre.
0x3006h: Documentá la propiedad de los recursos al utilizar punteros — documentar la propiedad de los recursos con punteros.
0x2001h: Las funciones deben usar cláusulas de guarda y retornos anticipados para reducir la anidación profunda — centralizar la limpieza evita fugas por salidas prematuras.
Antipatrón: Retorno prematuro con fuga de recursos de archivo¶
Síntoma en el código del estudiante¶
Una función abre un archivo y tiene varias ramas de error que retornan
anticipadamente, pero solo el camino feliz ejecuta fclose. Cada return
intermedio abandona la función dejando el descriptor abierto. El defecto se
agrava cuando la apertura se anida en otra llamada, porque entonces ni siquiera
existe un puntero sobre el cual cerrar (ver regla asociada).
Diagnóstico¶
Mecanismo del defecto¶
En C no hay destructores ni liberación automática: cada camino de salida de la
función debe encargarse de los recursos que la función adquirió. Un return
temprano es, a los ojos del sistema operativo, igual que cualquier otra salida:
el marco de pila se destruye, las variables locales desaparecen, pero el objeto
FILE y su descriptor siguen existiendo en el kernel.
El resultado es una fuga acumulativa. El proceso tiene una cantidad máxima de
descriptores abiertos (ulimit -n, típicamente 1024). Si la función se llama en
un lazo —por cada línea de un archivo, por cada registro de una lista— la fuga
crece hasta agotar la tabla, momento en el que toda apertura comienza a fallar
con EMFILE aunque los archivos existan y sean accesibles.
Consecuencia observable¶
Al principio nada se nota. Tras suficientes llamadas, la función que abre el archivo empieza a fallar de forma sistemática y el mensaje del sistema es “Too many open files”. El error aparece lejos de la causa, lo que hace la depuración costosa.
Fundamento en el estándar C11¶
ISO/IEC 9899:2011 §7.21.5.1 asigna a fclose la tarea de desasociar el flujo y
liberar sus recursos; el estándar no libera flujos automáticamente al salir de
una función. La cátedra exige centralizar la limpieza al final de funciones que
adquieren recursos (0x2001h: Las funciones deben usar cláusulas de guarda y retornos anticipados para reducir la anidación profunda) y mantener un único punto de retorno
(0x200Ch: Cada función debe tener a lo sumo un return) precisamente para no dejar ramas como esta.
Corrección idiomática¶
❌ Código con el antipatrón¶
int analizar(const char *ruta)
{
FILE *f = fopen(ruta, "r");
if (f == NULL) {
return -1;
}
if (archivo_vacio(f)) {
return -2; /* fuga: no se cierra f */
}
if (leer_datos(f) < 0) {
return -3; /* fuga: no se cierra f */
}
fclose(f);
return 0;
}Por qué es incorrecto: los dos return intermedios saltan el fclose. Si la
función se llama muchas veces con archivos vacíos, cada llamada filtra un
descriptor aunque el programa nunca abra más de un archivo por vez.
✅ Código refactorizado¶
int analizar(const char *ruta)
{
FILE *f = fopen(ruta, "r");
if (f == NULL) {
return -1;
}
int estado = 0;
if (archivo_vacio(f)) {
estado = -2;
} else if (leer_datos(f) < 0) {
estado = -3;
}
fclose(f);
return estado;
}El resultado se registra en estado y la limpieza queda en un único punto al
final, común a todos los caminos. Así el fclose se ejecuta siempre, sin
importar cómo terminó la lógica, y la función conserva un solo return.
Errores típicos al compilar o ejecutar¶
No hay error de compilación: la fuga es un defecto de runtime. Herramientas como
valgrind o el propio límite del sistema la hacen visible.
$ valgrind --track-fds=yes ./procesar lote.txt
...
==12345== FILE DESCRIPTORS: 87 open at exit.Tras muchas iteraciones, las aperturas fallan.
$ ./procesar lote_grande.txt
fallo al abrir registro_900.txt: Too many open filesChecklist de verificación¶
¿Todo camino de salida de la función pasa por el
fclose?¿Usé una variable de estado y un único
returnen lugar de múltiples?¿El recurso se cierra en el mismo nivel en que se abrió?
¿Corrí
valgrind --track-fds=yespara detectar descriptores huérfanos?
Reglas relacionadas¶
0x4009h: Prohibición de anidar llamadas a fopen() directamente dentro de funciones de E/S — regla asociada: la apertura anidada impide cerrar.
0x4004h: Mantené la simetría de recursos al abrir y cerrar archivos en el mismo nivel de abstracción — la simetría exige cerrar donde se abrió.
0x4001h: Manejá correctamente la apertura y cierre de archivos — el ciclo de vida del flujo termina en
fclose.0x2001h: Las funciones deben usar cláusulas de guarda y retornos anticipados para reducir la anidación profunda — centralizar la limpieza en funciones con recursos.
0x400Ah: Prohibición de operar sobre flujos de archivo tras haber invocado fclose() (use-after-close) — anular el puntero después de cerrar evita el uso colgante.