« INFO517-cours4 » : différence entre les versions
(Création de la page) |
mAucun résumé des modifications |
||
Ligne 1 : | Ligne 1 : | ||
Séance 4 du Cours-TD de [[INFO517|Programmation C]]. |
|||
Représentation de la mémoire: pointeurs. |
Représentation de la mémoire: pointeurs. |
||
Dernière version du 18 novembre 2008 à 22:55
Séance 4 du Cours-TD de Programmation C.
Représentation de la mémoire: pointeurs.
Les exemples vus en cours
Appel par adresse: <source lang="c">
- 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">
- 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">
- 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">
- 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">
- 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">
- 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">
- 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">
- 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">
- 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">
- include <stdio.h>
- 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>
- include <stdio.h>
// On alloue par blocs de talle MORCEAU.
- 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.