Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

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)

Universidad Nacional de Río Negro

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 consecuenciaEfecto concreto
Fuga de recursosDescriptores que nunca se cierran agotan la tabla del proceso (EMFILE).
Ambigüedad de propiedadNadie sabe si debe cerrar; se cierra dos veces o ninguna.
Datos no volcadosUn flujo de escritura huérfano deja el búfer sin descargar.
MantenibilidadAgregar 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

Cómo detectarla

HerramientaComandoSeñal
gigeranálisis de call graphFunciones que reciben un FILE * y otras que lo abren sin devolverlo.
cppcheckcppcheck --enable=all archivo.cresourceLeak en caminos que no cierran.
Revisión manualTodo fopen debe tener un fclose en el mismo nivel o un módulo dueño documentado.

Checklist de autocontrol

Reglas relacionadas

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 files

Checklist de verificación

Reglas relacionadas