Comment libérer toutes les ressources d'un programme ?

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

Bonjour,

Afin de finaliser mon projet en C, j’ai besoin de libérer toutes les ressources utilisées par mon programme. Pour cela, l’idée est de stocker la liste des ressources dans une variable globale void* ressources[] et de parcourir cette liste en libérant toutes les ressources une à une.

Bien qu’il compile, n’exécutez pas le programme ci-dessous au risque d’une erreur à l’exécution ligne 21 !

#include <stdio.h>
#include <stdlib.h>
#define MAX_RESRC 5

// Liste des ressources utilisées par le programme
void* ressources[MAX_RESRC] = {NULL};

int main(void)
{
    // Création de plusieurs ressources
    int*   p  = (int*)  malloc(4 * sizeof(int));
    FILE*  f  = fopen("test.txt", "r");

    // Ajoutons ces ressources à la liste
    ressources[0] = (void*) p;
    ressources[1] = (void*) f;

    // Libérons enfin les ressources
    for (int i = 0; i < MAX_RESRC; ++i)
        if (ressources[i] != NULL)
            free(ressources[i]);

    return 0;
}

Comment faire, à la ligne 21, pour distinguer les fichiers des ressources créées avec malloc ?
Les premiers se ferment avec fclose, les seconds avec free.

Ce n’est pas une bonne approche de raisonner ainsi. Ton programme sera assez lourd à maintenir et sera peu évolutif en fonction de l’évolution de tes ressources et de leur diversité. Dans ton cas il n’y a aucun intérêt à mélanger des pointeurs de types différents dans un tableau fourre tout.

Le mieux est d’allouer et de désallouer les ressources via des fonctions dédiées et d’effectuer ces opérations au moment opportun. C’est-à-dire quand tu n’en as plus besoin ce qui n’est pas forcément la fin du programme mais peut être avant.

+3 -0

Tu associes à chacune des ressources la fonction qui permet de la libérer (free et fclose dans ton exemple) ?

J’avais utilisé un truc comme ça pour un de mes anciens projets :

typedef void (*func_dtor_t)(void *); /* Destructor callback */

typedef struct resource_t {
    void *ptr;
    func_dtor_t dtor_func;
    struct resource_t *next;
#ifdef DEBUG
    int lineno;
    const char *filename;
#endif /* DEBUG */
} resource_t;

static resource_t *resources = NULL; /* LIFO */

#ifdef DEBUG
# define resource_register(ptr, dtor) \
    _resource_register((ptr), (dtor), __FILE__, __LINE__)
#else
# define resource_register(ptr, dtor) \
    _resource_register((ptr), (dtor))
#endif /* DEBUG */

#ifdef DEBUG
void _resource_register(void *ptr, func_dtor_t dtor_func, const char *filename, int lineno)
#else
void _resource_register(void *ptr, func_dtor_t dtor_func)
#endif /* DEBUG */
{
    resource_t *res;

    assert(NULL != ptr);
    assert(NULL != dtor_func);

    res = malloc(sizeof(*res)); // TODO: gérer un NULL
    res->next = resources;
    res->ptr = ptr;
    res->dtor_func = dtor_func;
#ifdef DEBUG
    res->filename = filename; // no dup
    res->lineno = lineno;
#endif /* DEBUG */
    resources = res;
}

void resources_free(void)
{
    if (NULL != resources) {
        resource_t *current, *next;

        current = resources;
        while (NULL != current) {
            next = current->next;
#if defined(DEBUG) && 0
            fprintf(stderr, "freeing %p, registered in %s at line %d\n", current->ptr, current->filename, current->lineno);
#endif /* DEBUG */
            current->dtor_func(current->ptr);
            free(current);
            current = next;
        }
        resources = NULL;
    }
}

Utilisé comme ceci par exemple :

int main(void)
{
    int*   p;
    FILE*  f;

    atexit(resources_free);
    if (NULL == (p = malloc(4 * sizeof(*p))) {
        // ...
        exit(EXIT_FAILURE);
    }
    ressource_register(p, free);
    if (NULL == (f = fopen("test.txt", "r"))) {
        // ...
        exit(EXIT_FAILURE);
    }
    ressource_register(f, fclose);

    return EXIT_SUCCESS;
}

Il "existe" Resource acquisition is initialization mais bon, en pratique, c’est guère utile ou plutôt applicable.

+0 -0

Comment faire, à la ligne 21, pour distinguer les fichiers des ressources créées avec malloc ?
Les premiers se ferment avec fclose, les seconds avec free.

info-matique

Tu ne peux pas. C’est le principe d’utiliser void*.

Déjà, il faut que tu saches que lorsque ton programme se ferme, toutes les ressources utilisées sont fermées. Les pointeurs sont libérés (free) et les fichiers sont fermés (fclose ou close).

Bref, il vaut mieux les gérer séparément. Ne regroupe que ce qui va ensemble !

+0 -0

Le mieux est d’allouer et de désallouer les ressources via des fonctions dédiées et d’effectuer ces opérations au moment opportun. C’est-à-dire quand tu n’en as plus besoin ce qui n’est pas forcément la fin du programme mais peut être avant.

Je vais montrer où je coince avec cette solution dans mon projet.

Pour simplifier, voici à quoi ressemble le code de mon projet en résumé :

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

void my_realloc(int** p, int size)
{
    *p = (int*) realloc(*p, size+1);

    if (*p == NULL)
        // Erreur : fuite de mémoire, 'file' n'a pas été fermé avec fclose
        exit(1);
}

int main(void)
{
    FILE* file = fopen("test.txt", "r");
    if (!file) exit(1);

    // ....
    int* p = (int*) malloc(3 * sizeof(int));
    
    // ....
    // Le programme se rend compte qu'il a besoin d'augmenter
    // la taille de p d'une unité
    my_realloc(&p, 3*sizeof(int));

    // ...
    free(p);
    fclose(file);
    return 0;
}

Mon prof demande à fermer toutes les ressources avant un exit.
Comment fermer le fichier file lorsque je quitte le programme ligne 10 ?

J’ai pensé à envoyer le fichier file en paramètre, sauf que dans mon vrai projet, ça reviendrait à envoyer 3 fichiers en paramètres à toutes les fonctions qui peuvent potentiellement appeler exit.

Je suis un peu perdu… ?

Salut,

Ce n’est pas le rôle de ta fonction realloc de libérer le fichier, elle doit gérer sa partie de l’erreur puis signaler à son appelant qu’il y a eu une erreur.
Tu peux faire ça par un code de retour, c’est ce que fait realloc standard, il ne va pas faire un exit ni fermer tes fichiers, il te renvoie un NULL pour que tu sache que ton pointeur n’est pas valide car la réallocation a échouée, ainsi lorsque tu récupère NULL, tu sais qu’il faut fermer tes ressources et quitter le programme.

+1 -0

Merci, il me reste alors un petit souci.
J’ai implémenté cette solution (bien qu’il soit possible d’éviter l’appel à my_realloc mais c’est pour l’exemple) :

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

int my_realloc(int** p, int size)
{
    *p = (int*) realloc(*p, size+1);
    return (*p != NULL); // renvoie 0 si échec de realloc
}

int main(void)
{
    FILE* file = fopen("test.txt", "r");
    if (!file) exit(1);

    // ....
    int* p = (int*) malloc(3 * sizeof(int));
    
    // ....
    // Le programme se rend compte qu'il a besoin d'augmenter
    // la taille de p d'une unité
    if (!my_realloc(&p, 3*sizeof(int)))
    {
        fclose(file);
        exit(1);
    }

    // ...
    free(p);
    fclose(file);
    return 0;
}

Le souci, c’est que c’est maintenant la fonction main qui prend la responsabilité de gérer l’erreur. ça alourdit le main et je ne suis pas sûr que ce soit son rôle de gérer les erreurs et de quitter. J’ai l’impression, en tout cas, j’y connais pas grand chose. :/

Au contraire, c’est l’un des rôles du main dans un programme bien structuré. Dans un programme pas trop gros (mais pas trop petit non plus), main sert à récupérer les arguments, à initialiser ton programme, à lancer les fonctions de base de ton programme et quitter proprement. Il ne fait rien d’autres.

En fait main est une fonction glu, c’est lui qui va faire le pont entre tes différentes fonctions et structures de données. Quand le programme atteint une certaine taille, il n’est pas rare que le rôle de glu soit réalisé par différentes fonctions en plus de main.

+1 -0

Voilà, j’ai adapté le code aux recommandations et il ressemble maintenant à ça :

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

int main(int argc, char** argv)
{
    if (argc < 3)
    {
        printf("Merci de spécifier tous les paramètres requis.\n");
        exit(0);
    }

    int* p = (int*) malloc(3*sizeof(int));

    if (!p)
        exit(1);

    FILE* clients = fopen(argv[1], "r");
    
    if (!clients)
    {
        free(p);
        exit(1);
    }
    
    FILE* data = fopen(argv[2], "r");
    
    if (!data)
    {
        fclose(clients);
        free(p);
        exit(1);
    }
    
    //...
    
    fclose(data);
    fclose(clients);
    free(p);
    return 0;
}

Est-ce bien de cette manière-là que vous m’avez conseillé de gérer les ressources ?

Je sais pas si mes guidelines sont valables en C, mais exit est un peu un ultime recours pour s’assurer que les erreurs de gestion de ressources n’affecte pas le système, si tu gères correctement tu dois pouvoir mettre des return

Aussi dans la mesure où ces affectations ne sont pas dépendantes entre elles et n’ont pas de traitement spécifique, elles sont plus une précondition à ton programme, je pense que quelque chose comme ça est plus lisible

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

int main(int argc, char** argv)
{
    int status = EXIT_SUCCESS;
    if (argc < 3)
    {
        printf("Merci de spécifier tous les paramètres requis.\n");
        status = EXIT_FAILURE;
    }
    else
    {
        int* p = (int*) malloc(3*sizeof(int));
        FILE* clients = fopen(argv[1], "r");
        FILE* data = fopen(argv[2], "r");
    
        if (!p || !clients || !data)
            status = EXIT_FAILURE;
        else
        {
            //...
        }
        if(data) fclose(data);
        if(clients) fclose(clients);
        free(p);
    }
    return status;
}
+0 -0

Si on t’impose d’utiliser exit, c’est peut-être que l’on souhaite te faire utiliser atexit ? Je te laisse tapper man 3 atexit.

Attention, si realloc echoue, il ne libère pas la mémoire déjà alouée.

Donc :

int my_realloc(int** p, int size)
{
    *p = (int*) realloc(*p, size+1);
    return (*p != NULL); // renvoie 0 si échec de realloc
}

Ici, tu as une fuite de mémoire dès que realloc échoue car tu ne peux plus libérer le pointeur *p.
Étonnant que personne ne t’ai déjà fait la remarque.

int my_realloc(int** p, int size)
{
    int* t = NULL;
    t = realloc(*p, size+1);

    if( t ) *p = t;
    else free(*p), *p = NULL;

    return (*p != NULL); // renvoie 0 si échec de realloc
}

Sinon, j’aime bien la méthode de @romantik, je la trouve adapté à la situation. Après il faut être sûr que ce soit bien ta situation.

+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