Gérer 3 fichiers mais.... il faut libérer les ressources !

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Désolé de poster autant de sujets aujourd’hui mais la remise de mon devoir en C approche à grands pas.

On me demande de gérer 3 fichiers et les potentielles erreurs associées.
J’ai pensé à cette solution pour le fichier main.c :

#include <stdio.h>

int main(int argc, char* argv[])
{
    if (argc < 4) return 1;

    /**
     *   Ouvrir les trois fichiers au début permet de s'assurer
     *   qu'ils sont tous disponibles en lecture ou écriture.
     **/

    FILE* clients = fopen(argv[1], "r");
    if (clients == NULL) return 1;
    
    FILE* data    = fopen(argv[2], "r");
    if (data == NULL) {
        fclose(clients); // Sinon fuite de mémoire !
        return 1;
    }
    
    FILE* output  = fopen(argv[3], "w");
    if (output == NULL) {
        fclose(clients);
        fclose(data);
        return 1;
    }
    
    /**
     * Ici se trouve le long code principal qui se charge de traiter les données
     * des fichiers clients et data, puis d'insérer le résultat dans output.
     **/

    fclose(clients);
    fclose(data);
    fclose(output);
    return 0;
}

Comme on le remarque, les lignes 23 et 24 doivent libérer les ressources si le dernier fichier n’a pas su être ouvert.
Pour des opérations aussi simple, le fichier de code est déjà très long !

J’aimerais résumer les lignes 12 à 26 en quelques lignes (genre 5 lignes de code) pour alléger le code de main(). D’ailleurs, je pense que cette fonction n’a pas à gérer les erreurs d’ouverture / fermeture de fichiers.

Bref, existe-t-il une bonne pratique pour gérer les fichiers qui me permettrait de simplifier mon code et le rendre plus propre (en évitant les fuites de mémoire) ?
Sinon, comment auriez-vous fait ?

Lu’!

Vu que tu fermes le programme littéralement juste après, sur aucun OS moderne ça ne poserait de problème puisque l’OS se chargera de récupérer les handlers de fichier, et basta.

En C, il n’est pas rare que dans une situation comme ça, on se contente de renvoyer vers une section cleanup: avec un goto et qu’on libère tout quoi qu’il arrive. Pour fclose, c’est un peu différent, parce que ça a été designé un peu n’importe comment, et donc un fclose(NULL) échoue au lieu de ne rien faire … Du coup, si on fait un tel renvoi, il faut contrôler que le pointeur n’est pas NULL avant de libérer.

FILE* f1 = fopen(...);
if(!f1) goto cleanup ;

FILE* f2 = fopen(...);
if(!f2) goto cleanup ;

...

cleanup:
if(f1) fclose(f1);
if(f2) fclose(f2);
...

if(!f1 || !f2 || ...) return 1;
else return 0;

Salut,

On me demande de gérer 3 fichiers et les potentielles erreurs associées.

[…]

Bref, existe-t-il une bonne pratique pour gérer les fichiers qui me permettrait de simplifier mon code et le rendre plus propre (en évitant les fuites de mémoire) ?
Sinon, comment auriez-vous fait ?

info-matique

Je te suggère d’utiliser une boucle, un tableau et une fonction de libération dédiée.

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

#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a)[0])


static void
close_all_files(FILE *fp, size_t size)
{
    for (size_t i = 0; i < size; ++i)
        if (fp[i] != NULL)
            if (fclose(fp[i]) == EOF)
                perror("fclose");
}


int
main(int argc, char **argv)
{
    FILE *fp[3] = { NULL };

    for (int i = 1; i < argc; ++i) {
        fp[i - 1] = fopen(argv[i], "r");

        if (fp[i - 1] == NULL) {
            perror("fopen");
            goto failure;
        }
    }

    close_all_files(fp, ARRAY_SIZE(fp));
    return EXIT_SUCCESS;
failure:
    close_all_files(fp, ARRAY_SIZE(fp));
    return EXIT_FAILURE;
}

Édit : ajout d’un appel à perror() en cas d’échec de fopen().

+0 -0

@Taurre solution très élégante, je m’y pencherais pour essayer de la comprendre. :)

En attendant, j’ai trouvé une solution à mon problème, qu’en pensez-vous de procéder ainsi ?

Je découpe en plusieurs fichiers pour alléger le main et je fais quelque chose comme ça :

main.c

#include <stdio.h>
#include <stdlib.h>
#include "files.c"

int main(int argc, char* argv[])
{
    if (argc < 4) return 1;

    FILE* files[3] = {NULL, NULL, NULL}; // Evitons les pointeurs fous
    open_files(argv, files);

    FILE* clients = files[0];
    FILE* data    = files[1];
    FILE* output  = files[2];
    
    /**
     * Ici se trouve le long code principal qui se charge de traiter les données
     * des fichiers clients et data, puis d'insérer le résultat dans output.
     **/

    fclose(clients);
    fclose(data);
    fclose(output);
    return 0;
}

files.c

FILE** open_files(char* argv[], FILE* result[])
{
    result[0] = fopen(argv[1], "r");
    if (result[0] == NULL) exit(1);

    result[1] = fopen(argv[2], "r");
    if (result[1] == NULL) {
        fclose(result[0]);
        exit(1);
    }

    result[2] = fopen(argv[3], "w");
    if (result[2] == NULL) {
        fclose(result[0]);
        fclose(result[1]);
        exit(1);
    }

    return result;
}

J’ai l’impression que ma solution tient la route, si je présente ça au devoir ?

Si tu utilises un tableau, de mon point de vue, il est beaucoup plus efficace de recourir à une boucle (c’est bien l’avantage des tableaux, d’ailleurs : on peut les parcourir aisément). Cela étant, si tu veux t’en passer, tu peux te simplifier un peu la vie dans open_files() comme suit.

FILE** open_files(char* argv[], FILE* result[])
{
    result[0] = fopen(argv[1], "r");
    if (result[0] == NULL)
        goto fopen_1;

    result[1] = fopen(argv[2], "r");
    if (result[1] == NULL)
        goto fopen_2;

    result[2] = fopen(argv[3], "w");
    if (result[2] == NULL)
        goto fopen_3;

    return result;
fopen_3:
    fclose(result[1]);
fopen_2:
    fclose(result[0]);
fopen_1:
    exit(1);
}
+0 -0

D’accord merci et dernières petites questions :

(1) Puis-je remplacer :

FILE* files[3] = {NULL, NULL, NULL}; // Evitons les pointeurs fous

…par :

FILE* files[3] = {NULL}; // Evitons les pointeurs fous

C’est bien deux lignes de codes strictement équivalentes ?

(2) Je n’ai pas très bien compris la ligne n°13, que fait-elle ?

if (fclose(fp[i] == EOF))

(3) Quelle différence entre int main(int argc, char* argv[]) et int main(int argc, char** argv) ?

(1) Puis-je remplacer :

FILE* files[3] = {NULL, NULL, NULL}; // Evitons les pointeurs fous

…par :

FILE* files[3] = {NULL}; // Evitons les pointeurs fous

C’est bien deux lignes de codes strictement équivalentes ?

info-matique

Le résultat est le même, en effet.

If an object that has automatic storage duration is not initialized explicitly, its value is indeterminate. If an object that has static or thread storage duration is not initialized explicitly, then:

[…]

  • if it has pointer type, it is initialized to a null pointer;

[…]

If there are fewer initializers in a brace-enclosed list than there are elements or members of an aggregate, or fewer characters in a string literal used to initialize an array of known size than there are elements in the array, the remainder of the aggregate shall be initialized implicitly the same as objects that have static storage duration.

ISO/IEC 9899:2017, doc. N2176, 6.7.9 Initialization, al. 10 et 21, p. 101 et 102.

(2) Je n’ai pas très bien compris la ligne n°13, que fait-elle ?

if (fclose(fp[i] == EOF))

Elle vérifie que la fonction fclose() ne rencontre pas une erreur (parce que oui, elle peut). Toutefois, dans les faits, cela est surtout utile si tu effectues des écritures (afin d’être certain que tout est bien écrit sur le disque).

(3) Quelle différence entre int main(int argc, char* argv[]) et int main(int argc, char** argv) ?

info-matique

La syntaxe. ;)

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte