Skip to content

EPST 2e année – TP5 Exercice 2

10 février 2013

Nous continuons avec les fonctions d’allocation et libération dynamiques de la mémoire.

Énoncé

Solution de l’exercice II

Comme d’habitude, essayez de résoudre l’exercice par vous-même, et ensuite comparez votre solution avec la mienne ci-dessous.

/*
 *  EPST 2e année - 2012-2013
 *  TP5 - Allocation dynamique de la mémoire
 *  Exercice 2 : Concaténation de tableaux
 *  Fichier source : concatab.c
 *  Compiler avec :
 *  gcc -Wall -o concatab concatab.c -std=c99
 *  Auteur : Amine "nh2" Brikci-Nigassa
 */
 
#include <stdio.h>
#include <stdlib.h>

/* concat : concaténation de deux tableaux
 * Paramètres :
 *  -  tail1 : taille du 1er tableau 
 *  -  tab1 : 1er tableau de réels (double)
 *  -  tail2 : taille du 2e tableau
 *  -  tab2 : 2e tableau de réels
 * Résultat : retourne un pointeur sur un tableau alloué dynamiquement  
 *            contenant les éléments du 1er suivis de ceux du 2e
 * (Ne pas oublier de libérer la mémoire pointée par la valeur de retour)
 */
double * concat(int tail1, double tab1[tail1],
                int tail2, double tab2[tail2]){
 /* pour provoquer une erreur, essayer :
    double * p = calloc(1000000000000,sizeof(*p));
  */
    double * p = calloc(tail1+tail2,sizeof(*p));
    
    if (p)  // en cas d'erreur, p est NULL <=> 0 <=> faux
        for (int i = 0 ; i < tail1+tail2 ; ++i)
            p[i] = (i<tail1) ? tab1[i] : tab2[i-tail1] ;
    return p;
}

/* saisir : saisie des éléments d'un tableau
 * Paramètres :
 *  -  taille : nbre d'éléments du tableau
 *  -  tabl : tableau des réels (double) à saisir
 *  -  nom : nom du tableau
 */
void saisir(int taille, double tabl[taille], char * nom){
    printf("Entrez les éléments du tableau %s\n", nom);
    for (int i=0 ; i<taille ; ++i){
        printf("%s[%d] = ",nom,i);
        scanf("%lf",tabl+i);
    }
}

int main(void){
    int tail1, tail2;
    printf("Taille du tableau A : ");
    scanf("%d",&tail1);
    double tab1[tail1];
    saisir(tail1, tab1, "A"); 
    printf("Taille du tableau B : ");
    scanf("%d",&tail2);
    double tab2[tail2];
    saisir(tail2, tab2, "B"); 
    double * tab3 = concat(tail1, tab1, tail2, tab2);
    if (!tab3){
        perror("erreur d'allocation mémoire");
        return(EXIT_FAILURE);
    }
    puts("Le tableau résultant est :");
    for (int i=0 ; i<tail1+tail2 ; ++i)
        printf("C[%d] = %g%s",i,tab3[i],i<tail1+tail2-1?", ":"\n");
    free(tab3);
    return EXIT_SUCCESS;
}

On nous demande de commencer par la saisie de deux tableaux dans le « programme principal » (la fonction main()). J’ai choisi d’utiliser des tableaux à longueur variable (VLA), qui, rappelons-nous (cf. TP4), permettent de faire une allocation dynamique automatiquement : la taille de chaque tableau est déterminée à l’exécution ; on peut donc la demander à l’utilisateur.

Nous avons la chance d’utiliser un compilateur récent qui autorise les VLA (qui n’étaient pas dans la norme du C avant 1999), cela évite d’utiliser des pointeurs (ils ne sont pas méchants mais ils peuvent être fatigants parfois :-). Cela permet surtout de ne pas s’occuper de la libération de la mémoire : l’allocation est automatique, et la libération aussi ; nous n’en sommes pas responsables puisque nous n’avons fait que déclarer un tableau et c’est le compilateur qui s’est occupé de l’allocation.

Une autre différence entre l’allocation des VLA et celle obtenue par les fonctions de la famille de malloc() est que la deuxième est faite sur le tas, alors que la première est faite sur la pile, qui est le segment de la mémoire utilisé par les fonctions pour y stocker leurs paramètres et toutes les variables locales (et qui déborde souvent quand on abuse de la récursivité). Wikipédia a un article qui explique cela clairement.
Si on veut qu’une fonction puisse allouer de la mémoire dynamiquement qui sera utilisée par le programme après qu’elle se termine, on ne peut pas le faire sur la pile (ni avec une déclaration locale habituelle ni avec des VLA) car par définition, les variables qui s’y trouvent seront libérées (dépilées) à la sortie de la fonction. D’où l’utilité de l’allocation sur le tas dans ces cas.

Pour que le programme soit assez clair et concis, la saisie des tableaux est faite avec la fonction saisir(), qui sera appelée deux fois dans main() : un appel pour chaque tableau, avec pour paramètres le tableau et sa taille (dans l’ordre inverse, rappelez-vous) et une chaîne de caractères contenant le nom du tableau.

Par contre, pour se familiariser avec elles, les fonctions d’allocation dynamique de la mémoire sont utilisées pour allouer le troisième tableau.
double * p = calloc(tail1+tail2, sizeof(*p));
L’instruction ci-dessus alloue un bloc de mémoire avec calloc() ayant une taille correspondant à (tail1+tail2) nombres. Elle est équivalente à la suivante :
double * p = malloc((tail1+tail2)*sizeof(*p));
bien que ces deux fonctions aient quand même une petite différence (vérifiez dans le manuel de malloc()) mis à part le nombre de paramètres : les valeurs contenues dans le bloc alloué par malloc() sont indéterminées, alors que calloc(), elle, s’occupe de les initialiser à zéro. Retenez cela, même si dans notre TP, cela n’a aucune importance (par contre cela a fait l’objet d’une question d’examen…).
L’opérateur unaire sizeof (bizarrement, ce n’est pas une fonction mais bien un opérateur du C, même s’il est représenté par un nom, pas par un symbole) donne une expression dont la valeur est la taille en octets du type de son opérande. On peut l’utiliser avec une expression ou un type (à entourer obligatoirement de parenthèses dans le 2e cas). Il pourrait sembler plus simple d’écrire :
double * p = calloc(tail1+tail2, sizeof(double));
Cela revient au même puisque le type de *p est justement double, mais beaucoup de programmeurs conseillent la première notation, pour deux raisons : on risque moins de se tromper avec la première et (surtout) si on change le type de p plus tard, dans la deuxième notation on risque d’oublier de changer l’opérande de sizeof, alors que dans la première il reste correct.

Pour vous exercer, vous pouvez vous amuser à inverser : utiliser les fonctions d’allocation sur le tas pour la saisie et un tableau à longueur variable pour le tableau du résultat. Mais attention à la position de vos déclarations. Remarquez ci-dessus : bien que l’allocation avec calloc() a été faite dans la fonction concat(), l’allocation automatique de tab1 et tab2 par contre se fait dans main(). La solution suivante ne fonctionnerait pas :

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

void concat(int tail1, double tab1[tail1],
            int tail2, double tab2[tail2],
                       double tab3[]     ){
    for (int i = 0 ; i < tail1+tail2 ; ++i)
        tab3[i] = (i<tail1) ? tab1[i] : tab2[i-tail1] ;
}

double * saisir(int taille, char * nom){
    printf("Entrez les éléments du tableau %s\n", nom);
    double tabl[taille];
    for (int i=0 ; i<taille ; ++i){
        printf("%s[%d] = ",nom,i);
        scanf("%lf",tabl+i);
    }
    return tabl;
}

int main(void){
    int tail1, tail2;
    printf("Taille du tableau A : ");
    scanf("%d",&tail1);
    double *tab1 = saisir(tail1, "A"); 
    printf("Taille du tableau B : ");
    scanf("%d",&tail2);
    double *tab2 = saisir(tail2, "B"); 
    double *tab3 = calloc(tail1+tail2,sizeof(*tab3));
    if (!tab3){
        perror("erreur d'allocation mémoire");
        return(EXIT_FAILURE);
    }
    concat(tail1, tab1, tail2, tab2, tab3);
    puts("Le tableau résultant est :");
    for (int i=0 ; i<tail1+tail2 ; ++i)
        printf("C[%d] = %g%s",i,tab3[i],i<tail1+tail2-1?", ":"\n");
    free(tab3);
    return EXIT_SUCCESS;
}

La fonction concat() ne pose aucun problème ici (rappelez-vous qu’un paramètre de type tableau est un pointeur en fait). Et puis la compilation se fera sans erreur. Vous aurez juste un petit avertissement (avec attention ou warning) sans importance…?

Vous pourrez exécuter ce programme mais vous aurez une surprise à la fin ; les valeurs obtenues dans le tableau tab3 seront aberrantes :

Taille du tableau A : 2
Entrez les éléments du tableau A
A[0] = 2.3
A[1] = 4.3
Taille du tableau B : 3
Entrez les éléments du tableau B
B[0] = 5.4
B[1] = 6.4
B[2] = 4.2
Le tableau résultant est :
C[0] = 3.95253e-323, C[1] = -4.57395e-42, C[2] = 3.95253e-323, C[3] = -4.57395e-42, C[4] = 6.7646e-316

Le message d’avertissement du compilateur était-il donc plus important que nous l’avions cru ? Que disait-il au fait ?

18:5: attention : cette fonction retourne l'adresse d'une variable locale [enabled by default]

La ligne 18 est celle du return tabl; de la fonction saisir(). L’avertissement confirme ce que j’ai dit plus haut : la variable tabl a été allouée dynamiquement, mais de façon automatique, sur la pile, pas sur le tas, avec le mécanisme des VLA, pas avec une fonction de type malloc(). Sa libération aussi se fait de façon automatique : elle est dépilée à la sortie de la fonction car c’était une variable locale. Son contenu est donc perdu et cela ne sert à rien de retourner son adresse !

Cette erreur n’est pas rare chez les débutants programmeurs, méfiez-vous, et puis surtout n’ignorez pas les avertissements du compilateur (et rappelez-vous de les activer tous avec l’option -Wall de gcc).

Dernier rappel : j’espère que vous avez remarqué l’appel à la fonction free() à la fin du programme. Souvenez-vous que vous devez toujours avoir autant de free() que de malloc(). Dans le programme « bugué » ci-dessus, on voit directement l’appel à calloc() mais attention aux allocations cachées : parfois elles peuvent être faites dans une fonction qui n’est pas visible, c’est pourquoi il est important de l’indiquer dans les commentaires de ce genre de fonctions et de dire qu’il faut libérer la mémoire allouée par elles. C’est ce que j’ai fait dans ma solution (la bonne) pour la fonction concat().

Encore une chose : n’oubliez pas de tester le succès de l’allocation dynamique en vérifiant si le pointeur renvoyé par malloc()/calloc()/realloc() n’est pas NULL. Essayez de remplacer tail1+tail2 par 1000000000000 dans l’appel à calloc() pour provoquer un échec d’allocation mémoire.

Publicités
One Comment
  1. benzenine permalink

    جزاكم الله خيرا و جعله في ميزان حسناتكم

    J'aime

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :