Fonction free() et comportement inprévisible...

C

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

Bonjour,

Au cours d'un de mes TP de C, il nous a été demandé de créer un programme simulant un tirage de l'Euromillion. Ce que j'ai fait. Le soucis c'est que j'utilise des pointeurs, et que lorsque j'utilise free à la fin pour liberer la mémoire, au bout d'en moyenne 5 executions de mon code, il râle en me mettant une erreur (qui peut changer) :

*** glibc detected *** ./01: double free or corruption (out): 0x00000000015bc010 ***

ou

*** glibc detected *** ./01: free(): invalid next size (fast): 0x00000000016a1010 ***

Voici le code en question :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/*
*   Structure contenant toutes les boules et les etoiles tirees
*/
typedef struct Euromillion {
    int boules[5];
    int etoiles[2];
} Euromillion;

/*
*   Pour générer un nombre aleatoire
*/
int r(int mod)
{
    int r = rand() % mod;
    return r;
}

/*
*   Fonction permetant de generer les boules
*/
int *les_boules(int *pt_tabB) 
{
    int i;
    pt_tabB = (int *) malloc(sizeof(int) * 50);
    for(i=1;i<=50;i++)
        *(pt_tabB+i-1) = i;

    return pt_tabB;
}
/*
*   Fonction permetant de generer les etoiles
*/
int *les_etoiles(int *pt_tabE)
{
    int i;
    pt_tabE = (int *) malloc(sizeof(int) * 9);
    for(i=1;i<=9;i++)
        *(pt_tabE+i-1) = i;

    return pt_tabE;
}

/*
*   Fonction permetant de tirer aleatoirement les boules et les etoiles
*/
Euromillion Tirage(int *pt_b,int *pt_e)
{
    Euromillion var;
    int i,a=0;

    for(i=0;i<5;i++)
    {
        do 
            a = r(50); // on tire un nombre aleatoire
        while(*(pt_b+a-1)==255); 
        // et on recommence le tirage tant que la valeur tiree l'a deja été (donc =255)


        var.boules[i] = a; // on la met dans notre structure
        *(pt_b+a-1) = 255; // on enleve la boule de notre liste
    }

    for(i=0;i<2;i++)
    {
        do 
            a = r(9); // on tire un nombre aleatoire
        while(*(pt_e+a-1)==255); 
        // et on recommence le tirage tant que la valeur tiree l'a deja été (donc =255)


        var.etoiles[i] = a; // on la met dans notre structure
        *(pt_e+a-1) = 255; // on enleve la boule de notre liste
    }

    return var;
}


int main(void)
{   
    // initialisation du generateur de nombres aleatoires
    srand(time(NULL));
    int i;
    int *et;
    int *bo;
    // generation des nos boules & etoiles
    et = les_etoiles(et);
    bo = les_boules(bo);

    // ouverture du fichier
    FILE *f;
    f = fopen("Euromillion.txt", "a");
    // affichage des boules
    printf("===== AFFICHAGE DES BOULES/ETOILES ===== \n");
    for(i=0;i<50;i++)
        printf("%d,", *(bo+i));
    printf("\n");
    for(i=0;i<9;i++)
        printf("%d,", *(et+i));
    printf("\n");

    // realisation du tirage et affichage
    printf("===== RESULTATS DU TIRAGE ===== \n");
    Euromillion resultats = Tirage(bo,et);

    // on complete la structure
    for(i=0;i<5;i++)
    {
        // sur stdout
        printf("%d,", resultats.boules[i]);
        // dans le fichier
        fprintf(f, "%d ", resultats.boules[i]);

    }
    printf("\n");
    // un petit tiret pour separer avec les etoiles
    fprintf(f, "- ");

    for(i=0;i<2;i++)
    {
        // sur stdout
        printf("%d,", resultats.etoiles[i]);
        // dans le fichier
        fprintf(f, "%d ", resultats.etoiles[i]);
    }
    printf("\n");
    // et on revient la ligne
    fprintf(f, "\n");
    // on ferme le fichier
    fclose(f);

    // et on libere nos petits pointeurs
    free(et);
    free(bo);

    // and... voilà
    return 0;
}

Les free() en question sont tout à la fin du main. Et je ne comprends pas, pourtant entre chaque execution, les malloc sont sensé se faire correctement, non ? Logiquement 1 malloc au debut = 1 free à la fin, et c'est pourtant ce que je fais :-/

Si qqun ici est capable de m'éclairer sur le pourquoi du comment de cette bizzarerie, je serait bien curieux de savoir ce qu'il se passe à l'intérieur…

+0 -0

Puisqu'on en est à améliorer la lisibilité du code (ce qui peut permettre de rendre l'erreur évidente), personne n'écrit:

1
2
    for(i=1;i<=50;i++)
        *(pt_tabB+i-1) = i;

Tous les codeurs C écrivent :

1
2
3
4
#define NB_BOULE 50

    for(i=0;i<NB_BOULE;i++)
        *(pt_tabB+i) = i;

C'est pas le define qui m'intéresse, mais le fait de parcourir le tableau de n cases avec un indice qui va de 0 à n-1 et pas de 1 à n.

+2 -0

C'est pas le define qui m'intéresse, mais le fait de parcourir le tableau de n cases avec un indice qui va de 0 à n-1 et pas de 1 à n.

Natalya

Oui, c'est aussi ce que je fais d'habitude, mais là, j'avais besoin d'écrire les valeurs en partant de 1 et en arrivant à 50 (par tant pour parcourir le tableau, mais pour le completer), et je voulais utiliser le i pour les 2

Ceci dit merci pour vos conseils, je prends note. Je ferait une version avec un peu plus de if pour bien verifier que les malloc passent bien.

+0 -0

Bonsoir,

Dans ton code tu ne fais aucun test sur les codes de retour. Sur des mallocs de cette taille, il y a peu de chances que tu tombes sur un os, mais tu dois toujours vérifier ce que te retournent tes fonctions. Quand tu commences à dialoguer avec le système d'exploitation (en lui demandant de la mémoire avec malloc, ou à ouvrir un fichier avec fopen) tu dois prendre en compte le fait qu'il puisse te dire non. Si une fonction a échoué soit tu essaies de rattraper l'erreur, soit tu fermes le programme. Le fait d'afficher un message d'erreur et de fermer le programme rend le debugage plus simple. Si tu es rigoureux, tu devrais avoir l'impression de passer ta vie à tester des pointeurs.

Ensuite, utiliser l'allocation dynamique c'est bien, mais l'utiliser quand c'est utile c'est encore mieux :) Ici, tu veux faire un tirage d'euro-million. Tu vas piocher un nombre fixe de boules parmi un nombre fixe de boules et faire la même chose pour les étoiles. Par définition, ce que tu fais est statique pas dynamique.

A quoi bon faire :

1
int *etoiles = malloc (50 * sizeof(*etoiles));

Alors que

1
int etoiles [50];

fais la même chose ? La meilleure façon de gérer la mémoire c'est encore d'éviter de gérer la mémoire. Ici, utiliser des tableaux t'aurait évité des soucis.

Ah enfin évite d'utiliser la notation pointeur quand tu parcours un tableau, la notation avec des crochets est plus agréable à lire et évite des erreurs bêtes. Je pense que Stranger a raison en disant que ton erreur vient de là. Tu as du corrompre la mémoire quelque part ce qui a fait crier free. Par contre, j'ai testé le programme chez moi et il n'y a pas d'erreurs.

Salut,

  • Si tu manipule un tableau, utilise-le comme tableau. La syntaxe *(x + n) est particulièrement dangereuse et je suis prêt à parier que le bug vient de là, parce qu'elle est différente de x[n].

On peut avoir plus de détails ? Pour moi *((x+n)+m) est potentiellement différent de *(x+(n+m)) mais *(x+n) et x[n] sont bien équivalents ?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* ... */
      do 
          a = r(50); // on tire un nombre aleatoire
      while(*(pt_b+a-1)==255); 
      // et on recommence le tirage tant que la valeur tiree l'a deja été (donc =255)


      var.boules[i] = a; // on la met dans notre structure
      *(pt_b+a-1) = 255; // on enleve la boule de notre liste
              /* ... */
      do 
          a = r(9); // on tire un nombre aleatoire
      while(*(pt_e+a-1)==255); 
      // et on recommence le tirage tant que la valeur tiree l'a deja été (donc =255)


      var.etoiles[i] = a; // on la met dans notre structure
      *(pt_e+a-1) = 255; // on enleve la boule de notre liste

Comme indiqué par Natalya, le problème arrive lorsque a prend la valeur nulle dans ce tirage. Tu te retrouves alors à utiliser pt_X-1. C'est un dépassement de tampon, et les dépassements de tampon sont un comportement indéfini d'après la norme C, qui veut dire que dans ce cas, on n'a aucune garantie sur le comportement du programme et tout peut arriver. Ici le comportement du programme sur ta machine amène la libc à croire qu'elle a déjà libéré le bloc de mémoire concerné.

Oui, c'est strictement équivalent, mon message était mal rédigé. Cette syntaxe est identique et dangereuse parce que :

  • une fois que tu manipule de simples adresses et pas un tableau, c'est très facile de perdre l'information de type au détour d'un calcul, en particulier avec des types similaires mais de tailles différentes (int16 vs int32 ?) et donc d'accéder à la mauvaise case ;

? Je ne vois pas ce que tu veux dire. Si t est de type T* (et n de type int), t + n sera aussi de type T* et aucune information de type n'est perdue.

  • on se rend moins facilement compte que l'index du tableau a une valeur absurde (tiens donc, c'est apparemment le problème ici) ;
  • c'est une syntaxe inhabituelle et peu lisible qui complique la relecture du code, et donc son déboguage.

Stranger

Je suis d'accord que t[n] est plus idiomatique.

Je n'ai pas dit que ça perdait le type, j'ai dit qu'il était facile de perdre le type, par exemple en passant au niveau supérieur avec des void* et des indexations à grands coups de sizeof. Indexer un tableau de cette façon fonctionne parfaitement, je dis juste que c'est une très mauvaise habitude.

Stranger

L'arithmétique avec un pointeur sur void est un autre comportement indéfini (voir ici) donc je ne suis pas convaincu que ça fonctionne parfaitement. Et quand bien même, je ne vois pas en quoi ce que tu décris serait uniquement possible avec la notation *(t+n) et pas avec t[n], ça n'a rien avoir avec ce dont on parlait… Bref, personnellement je vais m'arrêter là car que crois qu'on a assez fait dériver le sujet comme ça.

Merci pour vos réponses, problème corrigé donc. Je tâcherai de mettre en application vos conseils à l'avenir. Malgré ce que peux bien me demander le prof ; parce-que c'est vrai que la notation *(p+i) est finalement bien moins pratique à manipuler que le classique p[i].

+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