INFO517-cours4

De Wiki du LAMA (UMR 5127)
Révision datée du 10 novembre 2008 à 17:30 par Lvaux (discussion | contributions) (Création de la page)
(diff) ← Version précédente | Voir la version actuelle (diff) | Version suivante → (diff)
Aller à la navigation Aller à la recherche

Représentation de la mémoire: pointeurs.

Les exemples vus en cours

Appel par adresse: <source lang="c">

  1. include <stdio.h>

void inc (int *i) { ++(*i) ; }

int main () { int n = 0 ; printf("%d\n",n) ; inc(&n) ; printf("%d\n",n) ; return n ; } </source>

Rappel (tableaux): <source lang="c">

  1. include <stdio.h>

void inc (int t[]) { t[0]++ ; }

int main () { int t[] = { 0 } ; printf("%d\n",t[0]) ; inc(t) ; printf("%d\n",t[0]) ; return t[0] ; } </source>

Les tableaux sont des pointeurs particuliers: <source lang="c">

  1. include <stdio.h>

void aff (char *s) { while (*s != '\0') { putchar(*s) ; s++ ; } }

int main () { aff("bateau\n") ; return 0 ; } </source>

Affichage des éléments d'un tableau d'entiers (c'est pareil): <source lang="c">

  1. include <stdio.h>

/* affiche les n premiers éléments de t */ void aff (int *t, int n) { putchar('{') ; for (;n>0;n--) { printf("%d",*t) ; if (n>1) putchar(',') ; t++ ; } putchar('}') ; putchar('\n') ; }

int main () { int t[] = {1,7,3,4,0,-1} ; aff(t,3) ; return 0 ; } </source>

Attention: rien ne contrôle le fait qu'on est bien dans le tableau. C'est-à-dire que ce qui suit est un programme C valide, qui compile et s'exécute: <source lang="c">

  1. include <stdio.h>

/* affiche les n premiers éléments de t */ void aff (int *t, int n) { putchar('{') ; for (;n>0;n--) { printf("%d",*t) ; if (n>1) putchar(',') ; t++ ; } putchar('}') ; putchar('\n') ; }

int main () { int t[] = {1,7,3,4,0,-1} ; aff(t,7) ; // PROBLÈME ! return 0 ; } </source> On obtient par exemple:

 {1,7,3,4,0,-1,-1077769696}

Toutefois, il ne faut pas pousser le bouchon trop loin: on finit par écrire dans des zones qui ne sont pas allouées en mémoire. Ce programme, bien que valide en C, plante: <source lang="c">

  1. include <stdio.h>

/* affiche les n premiers éléments de t */ void aff (int *t, int n) { putchar('{') ; for (;n>0;n--) { printf("%d",*t) ; if (n>1) putchar(',') ; t++ ; } putchar('}') ; putchar('\n') ; }

int main () { int t[] = {1,7,3,4,0,-1} ; aff(t,32000) ; // PROBLÈME ! return 0 ; } </source> C'est notre deuxième erreur de segmentation: on y reviendra, en précisant où sont les choses en mémoire.

On peut afficher les adresses d'un peu tout. Ici, par exemple, celles des arguments des fonctions qui se retrouvent sur la pile. <source lang="c">

  1. include <stdio.h>

void f (char a, char b) { printf("char:\n") ; printf("&a: %u\n&b: %u\n", &a,&b) ; printf("a: %u\nb: %u\n", a,b) ; }

void g (int a, int b) { printf("int:\n") ; printf("&a: %u\n&b: %u\n", &a,&b) ; printf("a: %u\nb: %u\n", a,b) ; }

void h (long double a, long double b) { printf("long double:\n") ; printf("&a: %u\n&b: %u\n", &a,&b) ; printf("a: %Lf\nb: %Lf\n", a,b) ; }

int main () { f(0,1) ; g(0,1) ; h(0,1) ; return 0 ; } </source> À l'exécution, on remarque que l'écart entre ces adresses change (et aussi, qu'elles ne sont pas forcément dans l'ordre attendu):

 char:
 &a: 3216755060
 &b: 3216755056
 a: a
 b: b
 int:
 &a: 3216755072
 &b: 3216755076
 a: 97
 b: 98
 long double:
 &a: 3216755072
 &b: 3216755084
 a: 97.000000
 b: 98.000000

En effet, les types considérés ont des tailles différentes. La primitive sizeof donne (en octets) l'espace mémoire nécessaire pour stocker une donnée du type considéré: <source lang="c">

  1. include <stdio.h>

void aff_taille (const char nom[], size_t taille) { printf("%16s : %2u\n",nom,taille) ; }


/* Affiche les tailles de types à taille fixe les plus courants */ int main () {

printf("Liste de tailles :\n") ; aff_taille("void",sizeof(void)) ; aff_taille("char",sizeof(char)) ; aff_taille("short int",sizeof(short int)) ; aff_taille("int",sizeof(int)) ; aff_taille("long int",sizeof(long int)) ; aff_taille("float",sizeof(float)) ; aff_taille("double",sizeof(double)) ; aff_taille("long double",sizeof(long double)) ; aff_taille("void*",sizeof(void*)) ; aff_taille("int*",sizeof(int*)) ; aff_taille("long double*",sizeof(long double*)) ;

return 0 ; } </source> Sur ma machine, on obtient la sortie:

 Liste de tailles :
             void :  1
             char :  1
        short int :  2
              int :  4
         long int :  4
            float :  4
           double :  8
      long double : 12
            void* :  4
             int* :  4
     long double* :  4

Noter que la taille des pointeurs est toujours la même (et c'est celle du type int).

Le type du pointeur renseigne le compilateur sur le décalage d'adresse à produire lors d'opérations arithmétiques: <source lang="c">

  1. include <stdio.h>

int main () { char *s ; int *p ; long double *q ;

int i ;

for (i=0;i<4;++i) { ++s ; printf("s+%d : %u\n",i,s) ; }

for (i=0;i<4;++i) { ++p ; printf("p+%d : %u\n",i,p) ; }

for (i=0;i<4;++i) { ++q ; printf("q+%d : %u\n",i,q) ; }

return 0 ;

} </source> On observe que les écarts reflètent bien la taille du type annoncé (1, 4 et 12 ici):

 s+0 : 3086073265
 s+1 : 3086073266
 s+2 : 3086073267
 s+3 : 3086073268
 p+0 : 134513757
 p+1 : 134513761
 p+2 : 134513765
 p+3 : 134513769
 q+0 : 3216681396
 q+1 : 3216681408
 q+2 : 3216681420
 q+3 : 3216681432

Revenons sur l'allocation des tableaux avec le code suivant: <source lang="c">

  1. include <stdio.h>
  1. define TAILLE 1024

int t1 [TAILLE] ; int t2 [1] ; int t3 [TAILLE] ; int t4 [1] ;

void aff_point (int *p) { printf("%u = %x -> %i\n",p,p,*p) ; }

int * tab_nouv (int n) { int t[TAILLE] ; int i ; for (i=0; i<TAILLE; ++i) t[i]=n ; return t ; }

/* Affiche les adresses des tableaux statiques et de deux "tableaux" créés par

* tab_nouv() */

int main () { int *t5 = tab_nouv(4) ; int *t6 = tab_nouv(2) ; printf("Tableaux statiques:\n") ; aff_point(t1) ; aff_point(t2) ; aff_point(t3) ; aff_point(t4) ; printf("Tableaux dynamiques (?):\n") ; aff_point(t5) ; aff_point(t6) ; return 0 ; } </source> L'intention de l'auteur est visiblement de retourner un tableau « frais » à chaque appel de tab_nouv. C'est un peu raté, car la sortie de ce programme ressemble à:

 Tableaux statiques:
 134522656 = 804a720 -> 0
 134518496 = 80496e0 -> 0
 134518528 = 8049700 -> 0
 134522624 = 804a700 -> 0
 Tableaux dynamiques (?):
 3219749988 = bfe97c64 -> 2
 3219749988 = bfe97c64 -> 2

Les adresses renvoyées par tab_nouv sont les mêmes et le contenu est écrasé. C'est bien normal: ces tableaux sont alloués sur la pile, puis immédiatement oubliés à la sortie de la fonction.

On souhaite donc faire de l'allocation dynamique: c'est le rôle de la fonction void *malloc(size_t taille) qui renvoie l'adresse de début d'une zone de mémoire allouée, d'une taille au moins égale à taille. On pourra donc réécrire: <source lang="c"> int * tab_nouv (int n) { int *t = malloc(TAILLE*sizeof(int)) ; // la taille est donnée en octets !

       int i ;
       for (i=0; i<TAILLE; ++i)
               t[i]=n ;
       return t ;

} </source> On peut vérifier que la nouvelle version fonctionne bien sur la sortie:

 Tableaux statiques:
 134522720 = 804a760 -> 0
 134518560 = 8049720 -> 0
 134518592 = 8049740 -> 0
 134522688 = 804a740 -> 0
 Tableaux dynamiques:
 134529032 = 804c008 -> 4
 134533136 = 804d010 -> 2

Notez que les adresses rendues par malloc sont proches de celles des variables statiques.


Application à la lecture de lignes de longueur non bornée: <source lang="c">

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

// On alloue par blocs de talle MORCEAU.

  1. define MORCEAU 1024

/* Lit une ligne en entrée _quelle que soit sa taille_

* et la stocke dans *p (p est un pointeur sur une chaîne).
* Renvoie le nombre de caractère lus (-1 en cas d'erreur). */

int lit_ligne (char **p) { char c ; char *s ; int pos, blocs, i ;

// La position courante dans la chaîne est i+MORCEAU*blocs. pos = blocs = i = 0 ;

// On alloue le premier morceau. s = malloc ((++blocs)*MORCEAU*sizeof(char)) ; if (s == NULL) return -1 ;

while ((c=getchar())!=EOF && c!='\n') { s[pos] = c ; if (i==MORCEAU-1) { // on déborderait en faisant ++i if ((s=realloc(s,(++blocs)*MORCEAU*sizeof(char))) == NULL) { return -1 ; // plus de mémoire : on quitte } i=0 ; } else { ++i; } ++pos ; }

// si c == '\n' il faut l'ajouter

if (c=='\n') { s[pos] = c ; // on pourrait déborder en ajoutant le '\0': on gère ça if (i==MORCEAU-1) { if ((s=realloc(s,(pos+1)*sizeof(char))) == NULL) return -1 ; // plus de mémoire : on quitte } ++pos ; }

s[pos]='\0' ; *p=s ; return pos ;


}

/* Lit des lignes sur l'entrée et les affiche à l'envers. */ int main () { char *s ; int ret ; while ((ret=lit_ligne(&s))>0) { if (s[ret-1]=='\n') ret--; for (;ret>0;--ret) putchar(s[ret-1]) ; putchar('\n') ; free(s) ; } return ret ; } </source> La fonction void *realloc(void *p, size_t taille) demande de changer la taille de la zone allouée par malloc pointée par p, en en conservant le contenu.

La fonction void free(void *p) libère la zone de mémoire allouée par malloc ou realloc et la rend au système: on a fini de s'en servir. C'est essentiel, car on alloue autant de zones que de lignes lues: sans l'appel à free en fin de boucle, on conserverait en mémoire toutes les lignes alors que seule la dernière nous intéresse.