web-dev-qa-db-fra.com

Comment gérer correctement une panne malloc en C, surtout quand il y a plus d'un malloc?

Supposons que cela fasse partie de mon code:

 int foo()
 {  
    char *p, *q ;
    if((p = malloc(BUFSIZ)) == NULL) {
        return ERROR_CODE;
    }
    if((q = malloc(BUFSIZ)) == NULL) {
        free(p)
        return ERROR_CODE;
    }
    /* Do some other work... */

    free(p);
    free(q);  
 }

Puisqu'il est possible que le premier malloc réussisse mais que le second échoue, j'utilise free(p) dans le deuxième "gestionnaire d'erreurs". Mais que faire s'il y a plus de malloc et si je veux modifier le code (ajuster leurs commandes, ajouter ou supprimer des malloc)?

Je sais qu'en C++, il y a des choses comme RAII et sauf exception, etc. Mais en général, quelle est la bonne façon de gérer l'échec de malloc en C? (peut-être en utilisant goto?)

32
Roun

Votre code est correct, mais pour beaucoup de variables, je préfère:

int
foo()
{
    char *p = NULL;
    char *q = NULL;
    int ret = 0;

    if (NULL == (p = malloc(BUFSIZ)))
    {
        ret = ERROR_CODE;
        goto error;
    }

    // possibly do something here

    if (NULL == (q = malloc(BUFSIZ)))
    {
        ret = ERROR_CODE;
        goto error;
    }

    // insert similar repetitions

    // hopefully do something here

  error:
    free (p);
    free (q);
    return ret;
}

Notez que libérer NULL est défini comme un no-op.

Cela évite n niveaux de retrait pour les variables n. Vous pouvez nettoyer les descripteurs de fichiers, etc. de la même manière (bien que vous deviez mettre une condition autour de la close()).

Maintenant, si vous savez que vous pouvez les allouer tous en même temps, dasblinkenlight a une bonne réponse, mais voici une autre façon:

int
foo()
{
    int ret = 0;
    char *p = malloc(BUFSIZ);
    char *q = malloc(BUFSIZ);
    char *r = malloc(BUFSIZ);
    if (!p || !q || !r)
    {
        ret = ERROR_CODE;
        goto exit;
    }

    // do something

  exit:
    free(p);
    free(q);
    free(r);
    return ret;
}

Dernière possibilité: si vous voulez réellement quitter le programme en cas d'échec de malloc, envisagez d'utiliser l'option M_CHECK_ACTION De mallopt. Ceci fait vérifier les défauts de malloc() et appelle abort(), imprimant éventuellement un message utile.

Depuis la page de manuel:

NOM

mallopt - définir les paramètres d'allocation de mémoire

SYNOPSIS

  #include <malloc.h>

  int mallopt(int param, int value);

DESCRIPTION

La fonction mallopt() ajuste les paramètres qui contrôlent le comportement des fonctions d'allocation de mémoire (voir malloc(3)). L'argument param spécifie le paramètre à modifier et value spécifie la nouvelle valeur de ce paramètre.

Les valeurs suivantes peuvent être spécifiées pour param:

M_CHECK_ACTION

La définition de ce paramètre contrôle la réponse de la glibc lorsque divers types d'erreurs de programmation sont détectés (par exemple, libérer le même pointeur deux fois). Les 3 bits les moins significatifs (2, 1 et 0) de la valeur affectée à ce paramètre déterminent le comportement de la glibc, comme suit:

Bit: Si ce bit est défini, imprimez un message sur une ligne sur stderr qui fournit des détails sur l'erreur. Le message commence par la chaîne "*** glibc detected ***", Suivie du nom du programme, du nom de la fonction d'allocation de mémoire dans laquelle l'erreur a été détectée, d'une brève description de l'erreur et de l'adresse mémoire où l'erreur a été détectée .

Bit 1: Si ce bit est défini, après avoir imprimé tout message d'erreur spécifié par le bit 0, le programme se termine en appelant abort(3). Dans les versions glibc depuis 2.4, si le bit 0 est également défini, entre l'impression du message d'erreur et l'abandon, le programme imprime également une trace de pile à la manière de backtrace(3), et imprime le mappage de mémoire du processus dans le style de /proc/[pid]/maps (voir proc(5)).

Bit 2: (depuis la glibc 2.4) Ce bit n'a d'effet que si le bit 0 est également activé. Si ce bit est défini, le message sur une ligne décrivant l'erreur est simplifié pour ne contenir que le nom de la fonction où l'erreur a été détectée et la brève description de l'erreur.

34
abligh

Puisqu'il est parfaitement OK de passer NULL à free(), vous pouvez allouer tout ce dont vous avez besoin en "ligne droite", tout vérifier en une seule fois, puis tout libérer une fois que vous êtes que vous ayez effectué ou non un travail:

char *p = malloc(BUFSIZ);
char *q = malloc(BUFSIZ);
char *r = malloc(BUFSIZ);
if (p && q && r) {
    /* Do some other work... */
}
free(p);
free(q);
free(r);

Cela fonctionne tant qu'il n'y a pas de dépendances intermédiaires, c'est-à-dire que vous n'avez pas de structures avec des dépendances à plusieurs niveaux. Lorsque vous le faites, c'est une bonne idée de définir une fonction pour libérer une telle structure, sans supposer que tous les blocs de mémoire sont non -NULL.

25
dasblinkenlight

Pour un grand nombre d'allocations, j'investirais du temps dans la création d'un gestionnaire de mémoire qui garde la trace des allocations. De cette façon, vous n'avez jamais à vous soucier des fuites, que la fonction réussisse ou non.

L'idée générale est de créer un wrapper pour malloc qui enregistre les allocations réussies, puis les libère sur demande. Pour libérer de la mémoire, vous passez simplement une taille spéciale à la fonction wrapper. En utilisant une taille de 0 pour libérer de la mémoire est approprié si vous savez qu'aucune de vos allocations réelles ne sera destinée à 0 blocs de taille. Sinon, vous pouvez utiliser ~0ULL comme taille de demande de libération.

Voici un exemple simple qui permet jusqu'à 100 allocations entre les libres.

#define FREE_ALL_MEM 0

void *getmem( size_t size )
{
    static void *blocks[100];
    static int count = 0;

    // special size is a request to free all memory blocks
    if ( size == FREE_ALL_MEM )
    {
        for ( int i = 0; i < count; i++ )
            free( blocks[i] );
        count = 0;
        return NULL;
    }

    // using a linked list of blocks would allow an unlimited number of blocks
    // or we could use an array that can be expanded with 'realloc'
    // but for this example, we have a fixed size array
    if ( count == 100 )
        return NULL;

    // allocate some memory, and save the pointer in the array
    void *result = malloc( size );
    if ( result )
        blocks[count++] = result;

    return result;
}

int foo( void )
{
    char *p, *q;

    if ( (p = getmem(BUFSIZ)) == NULL ) {
        return ERROR_CODE;
    }
    if ( (q = getmem(BUFSIZ)) == NULL ) {
        getmem( FREE_ALL_MEM );
        return ERROR_CODE;
    }

    /* Do some other work... */

    getmem( FREE_ALL_MEM );
    return SUCCESS_CODE;
}
3
user3386109

SI vous vous attendez à allouer un grand nombre d'articles, cela Can devient désordonné. Essayez d'éviter l'approche "goto". Pas à cause de l'ancienne éthique du "goto is bad", mais parce que de cette façon, il peut y avoir de la folie et des fuites de mémoire.

C'est un peu exagéré pour un petit nombre de malloc, mais vous pouvez envisager quelque chose comme cette approche:

void free_mem(void **ptrs, size_t len)
{
    for (size_t i = 0; i < len; ++i)
    {
        free(ptrs[i]);
        ptrs[i] = NULL;
    }
}

int foo(...)
{
    void *to_be_freed[N];
    int next_ptr = 0;
    for (size_t i = 0; i < N; ++i) to_be_freed[i] = NULL;

    p = malloc(..);
    if (!p)
    {
        free_mem(to_be_freed,N);
        return ERROR_CODE;
    }
    to_be_freed[next_ptr++] = p;

    // Wash, rinse, repeat, with other mallocs
    free_mem(to_be_freed,N)
    return SUCCESS;
}

En réalité, vous pouvez probablement envelopper malloc avec quelque chose qui suit cela. Mettez le tableau et la taille du tableau dans une structure et passez-le avec la taille d'allocation souhaitée.

2
kdopen

c'est une question d'habitude, mais je préfère:

int returnFlag = FAILURE;

if ((p = malloc...) != NULL)
{
    if ((q = malloc..) != NULL)
    {
        // do some work
        returnFlag = SUCCESS; // success only if it is actually success

        free(q);
    }
    free(p);
}

return returnFlag; // all other variants are failure
2
Iłya Bursov

Je pense que la première réponse est le but le plus général car elle peut être utilisée pour des erreurs autres que celles causées par malloc. Cependant, je supprimerais les gotos et utiliserais une boucle de passage unique comme ça.

int foo()
{
  char *p = NULL;
  char *q = NULL;
  int ret = 0;
  do {
    if (NULL == (p = malloc(BUFSIZ)))
    {
      ret = ERROR_CODE;
      break;
    }

    // possibly do something here

    if (NULL == (q = malloc(BUFSIZ)))
    {
      ret = ERROR_CODE;
      break;
    }

    // insert similar repetitions

    // hopefully do something here
  } while(0);
  free (p);
  free (q);
  return ret;
}
2
Gray