Gestion de la mémoire en C : Présentation de la mémoire |
Le présent document est publié sous la licence « Verbatim Copying » telle que définie par la Free Software Fondation dans le cadre du projet GNU.
Toute copie ou diffusion de ce document dans son intégralité est permise (et même encouragée), quel qu'en soit le support à la seule condition que cette notice, inclant les copyrights, soit préservée.
Copyright (C) 2002, 2003, 2004 Yann LANGLAIS, ilay.
La plus récente version en date de ce document est sise à l'url http://ilay.org/yann/articles/mem/.
Toute remarque, suggestion ou correction est bienvenue et peut être adressée à Yann LANGLAIS, ilay.org.
Le langage C est souvent considéré comme un langage difficile et peu fiable. La seconde assertion n'est pas acceptable dans la mesure où la plupart des applications, y compris les L4G et RAD sont eux mêmes écrits en C.
En fait, ces idées reçues proviennent principalement de l'utilisation intensive des pointeurs. Si le C est souvent mal considéré, c'est bien souvent à cause de la méconnaissance du concept de pointeur et de la gestion de la mémoire qui lui est associé.
Le présent document se propose de présenter ces concepts depuis leurs fondements théoriques jusqu'à leur application pratique.
Il ne s'agit pas pour autant de définir les différentes notions avec la plus grande rigueur qui soit. Mon propos n'est pas de faire de l'initiation à l'assembleur, ni à l'électronique. Mon but est de présenter la gestion de la mémoire sous un angle différent de celui généralement adopté.
L'audience visée est principalement celle des développeurs ayant déjà été initiés au langage C.
Un programme C gère la mémoire de trois façons différentes:
Exemple : #include <stdio.h> #include <stdlib.h> static int i_stat = 4; /* Stocké dans le segment data */ int i_glob; /* Stocké dans le segment bss */ int *pi_pg; /* Stocké dans le segment bss */ /* main est stocké dans le segment text de la zone de programme */ int main(int nargs, char **args) { /* paramètres nargs et args stockés dans la frame numéro 1 de la pile */ int *pi_loc; /* dans la frame 1 de la pile */ void *sbrk0; /* nécessaire pour stocker l'adresse de base avant le premier malloc */ sbrk0 = (void *) sbrk(0); if (!(pi_loc = (int *) malloc(sizeof(int) * 16))) /* réservation de 16 x sizeof(int) sur le tas */ return 1; if (!(pi_pg = (int *) malloc(sizeof(int) * 8))) { /* réservation de 8 x sizeof(int) sur le tas */ free(pi_loc); return 2; } printf("adresse de i_stat = 0x%08x (zone programme, segment data)\n", &i_stat); printf("adresse de i_glob = 0x%08x (zone programme, segment bss)\n", &i_glob); printf("adresse de pi_pg = 0x%08x (zone programme, segment bss)\n", &pi_pg); printf("adresse de main = 0x%08x (zone programme, segment text)\n", main); printf("adresse de nargs = 0x%08x (pile frame 1)\n", &nargs); printf("adresse de args = 0x%08x (pile frame 1)\n", &args); printf("adresse de pi_loc = 0x%08x (pile frame 1)\n", &pi_loc); printf("sbrk(0) (heap) = 0x%08x (tas)\n", sbrk0); printf("pi_loc = 0x%08x (tas)\n", pi_loc); printf("pi_pg = 0x%08x (tas)\n", pi_pg); free(pi_pg); free(pi_loc); return 0; } Donne sous Sparc/Solaris : adresse de i_stat = 0x00020c70 (zone programme, segment data) adresse de i_glob = 0x00020ca4 (zone programme, segment bss) adresse de pi_pg = 0x00020ca8 (zone programme, segment bss) adresse de main = 0x0001068c (zone programme, segment text) adresse de nargs = 0xffbeefa4 (pile frame 1) adresse de args = 0xffbeefa8 (pile frame 1) adresse de pi_loc = 0xffbeef4c (pile frame 1) sbrk(0) (heap) = 0x00020c00 (tas) pi_loc = 0x00020cb8 (tas) pi_pg = 0x00020d08 (tas) Parallèlement, l'utilitaire elfdump donne : index value size type bind oth ver shndx name [17] 0x00020c68 0x00000000 SECT LOCL D 0 .data [21] 0x00020c88 0x00000000 SECT LOCL D 0 .bss [47] 0x00020c70 0x00000004 OBJT LOCL D 0 .data i_stat [60] 0x00020ca4 0x00000004 OBJT GLOB D 0 .bss i_glob [68] 0x0001068c 0x000001e0 FUNC GLOB D 0 .text main [80] 0x00020ca8 0x00000004 OBJT GLOB D 0 .bss pi_pg |
On notera que la variables pi_pg est stockée dans le segment bss mais que les données
pointées par la variable sont elles stockées dans le tas.
De même, la variable pi_loc est stockée sur la pile, mais les données vers lesquelles elle pointe sont stockées sur le tas. |
A retenir :
|
Un allocateur mémoire est un ensemble de routine responsable de la gestion du tas.
Bien qu'un processus pense avoir toute la mémoire pour lui, il faut néanmoins s'assurer d'une part que cette mémoire est réellement disponible (et ne s'envolera pas toute seule lors de modifications des pages mémoires par la mmu), et d'autre part que les variables d'une fonction ne soit pas écrasées par une celles d'une autre.
Par ailleurs, les différentes sous parties d'un programme ne nécessitent pas de conserver toute la mémoire allouée pendant toute la durée de l'exécution. Par exemple, lorsque l'on referme un fichier, il est préférable libérer toute la mémoire qui lui correspond. Cet espace se verra de nouveau disponible pour ouvrir un autre document.
En résumé, l'allocateur doit se charger des taches suivantes:
La réservation de pages mémoire est normalement transparent vis à vis de l'utilisateur. Par contre, la réservation et la libération de la mémoire sont des opérations en interface avec le programmeur. Les points d'entrée de ces deux opérations sont les fonctions malloc() et free(). A ces 2 opérations viennent s'ajouter realloc(), qui permet de modifier la taille d'uen zone allouée.
Voyons tout d'abord un allocateur minimaliste qui ne libère pas réellement la mémoire, mais dont nous pourrons nous servir le cas échéant pour débugger la mémoire.
L'allocateur minimaliste ne répond pas au problème de la libération. Il a de plus l'inconvéniant d'appeler des routines systèmes (sbrk) peu performantes à chaque malloc.
Un des points les plus importants lorsque l'on parle de langage évolué est la notion de typage. Nous ne parlerons pas de la notion théorique de typage, mais de la notion pratique.
Dans la pratique, un ordinateur ne reconnait aucun type de donnée quant à son stockage en mémoire. Il n'existe pas une mémoire spécifique pour les entiers et une autre pour les caractères. La seule exception à cette règle se trouve dans les registres des microprocesseurs qui, eux, pour des raisons de performances, différencient les nombres entiers des nombres réels.
La notion de type « de bas niveau » correspond à la taille mémoire sur laquelle est encodée une entité. Le type définit donc la taille de la donnée à manipuler.
Les types simples sont les types prédéfinis par le langage C: char, short, int, long, float, double, et les pointeurs (void *). Il est à noter que quel que soit le «type» de pointeur (char *, int * double *, void *), la taille d'encodage est toujours la même. Aussi, un pointeur générique peut être appelé void *.
Ces types ont des tailles fixées pour une architecture donnée, mais varient d'une architecture à l'autre. Il semble même que la taille d'un « octet » n'ai pas toujours été de 8bits2
Il est impératif de ne pas supposer de leur taille si l'on recherche la portabilité. On pourra utiliser sizeof(type) en lieu et place de la taille supposée.
1 #include <stdio.h> 2 int main(void) { 3 printf("sizeof(char) = %d\n", sizeof(char)); 4 printf("sizeof(short) = %d\n", sizeof(short)); 5 printf("sizeof(int) = %d\n", sizeof(int)); 6 printf("sizeof(long) = %d\n", sizeof(long)); 7 printf("sizeof(long long) = %d\n", sizeof(long long));3 8 printf("sizeof(float) = %d\n", sizeof(float)); 9 printf("sizeof(double) = %d\n", sizeof(double)); 10 printf("sizeof(void *) = %d\n", sizeof(void *)); 11 printf("sizeof(char *) = %d\n", sizeof(char *)); 12 printf("sizeof(int *) = %d\n", sizeof(int *)); 13 printf("sizeof(double *) = %d\n", sizeof(double *)); 14 return 0; 15 }
donne sur linux iX86:
sizeof(char) = 1 sizeof(short) = 2 sizeof(int) = 4 sizeof(long) = 4 sizeof(long long) = 84 sizeof(float) = 4 sizeof(double) = 8 sizeof(void *) = 4 sizeof(char *) = 4 sizeof(int *) = 4 sizeof(double *) = 4
A retenir:
|
Comme nous l'avons vu dans la description de la mémoire virtuelle, la mémoire est comparable à un tableau.
On ne peut, en théorie, accéder directement qu'a une case entière dont la taille correspond à la taille de la mémoire (32bits). Il n'est donc pas possible d'accéder directement à une fraction d'une case mémoire.
Or comme nous venons de le voir, les types de donnés n'ont pas obligatoirement des tailles identiques à la taille de la mémoire.
Prennons par exemple le caractère char. Celui-ci est codé sur un seul octet. Mais, dans le cas d'une mémoire de 32bits, la taille d'une case est de 4 octets. On devra donc laisser, théoriquement, 3 octets vides.
Dans la pratique, les choses sont un peu plus complexes car même si la taille du bus mémoire est prépondérante, tous les octets sont directement numérotés. Il est donc possible, par l'entremise d'outils inclus dans les microprocesseurs et les compilateurs, d'accéder à des données de 1, 2 ou 4 octets directement.
Certaines architectures imposent cependant certaines contraintes d'alignement: les données ne peuvent être stockées qu'à des adresses multiples de la taille sur laquelle est codée ces données et au maximum, à des adresses multiples de la taille du bus.
Pour vérifier si l'architecture possède une contrainte d'alignement, il suffit de compiler et de lancer le programme suivant:
1 int main(void) { 2 struct s { char a, b, c, d, e, f, g, h, i, j;} t; 3 int *pi; 4 pi = (int *) &(t.a); 5 printf("pi = %p, i = %d\n", pi, *pi); 6 pi = (int *) &(t.b); 7 printf("pi = %p, i = %d\n", pi, *pi); 8 pi = (int *) &(t.c); 9 printf("pi = %p, i = %d\n", pi, *pi); 10 pi = (int *) &(t.d); 11 printf("pi = %p, i = %d\n", pi, *pi); 12 pi = (int *) &(t.e); 13 printf("pi = %p, f = %d\n", pi, *pi); 14 pi = (int *) &(t.f); 15 printf("pi = %p, i = %d\n", pi, *pi); 16 pi = (int *) &(t.g); 17 printf("pi = %p, i = %d\n", pi, *pi); 18 return 0; 19 }
Si le programme se termine normalement, alors l'architecture n'impose pas de contrainte. Dans le cas contraire, le programme s'arrêtera brutalement avec un signal de type bus error et génèrera un core.
Pour linux AMD Athlon, le programme tourne normalement. Sur Solaris/UltraSparc, le programme s'arrète avec un "Bus error" ligne 5.
A retenir:
|
Les types composés peuvent être définis comme des agrégats de types simples ou/et composés.
La définition de types composés se fait à l'aide du mot réservé «struct», suivi de la description des différent composants.
Ces types composés définis par « struct » fonctionnent comme les types simples en ce qui concerne l'assignation5:
1 #include <stdio.h> 2 int main(void) { 3 struct s1 {int a, b;} A, B; 4 A.a = 1, A.b = 2, B.a = 3, B.b = 4; 5 A = B; 6 printf("A.a = %d\nA.b = %d\n", A.a, A.b); 7 return 0; 8 }
donne:
A.a = 3 A.b = 4
Par contre, il est impossible de faire des comparaisons sur ces types complexes (comme par exemple A==B).
La séquence des composants tient un rôle important dans la définition des types composés. En effet en raison des contraintes d'alignement précitées, les trois structures suivantes n'utiliseront pas la même quantité de mémoire:
1 #include <stdio.h> 2 struct fin { 3 char a; 4 char b; 5 char c; 6 char d; 7 float x; 8 float y; 9 float z; 10 }; 11 struct moyen { 12 char a; 13 char b; 14 float x; 15 char c; 16 char d; 17 float y; 18 float z; 19 }; 20 struct large { 21 char a; 22 float x; 23 char b; 24 float y; 25 char c; 26 float z; 27 char d; 28 }; 29 int main(void) { 30 printf("sizeof(char) = %d\n", sizeof(char)); 31 printf("sizeof(float) = %d\n", sizeof(float)); 32 printf("4 * sizeof(char) + 3 * sizeof(float) = %d\n", 4 * sizeof(char) + 3 * sizeof(float)); 33 printf("sizeof(fin) = %d\n", sizeof(struct fin)); 34 printf("sizeof(moyen) = %d\n", sizeof(struct moyen)); 35 printf("sizeof(large = %d\n", sizeof(struct large)); 36 return 0; 37 }
donne les valeurs suivantes (linux iX86/AMD):
sizeof(char) = 1 sizeof(float) = 4 4 * sizeof(char) + 3 * sizeof(float) = 16 sizeof(fin) = 16 sizeof(moyen) = 20 sizeof(large) = 28
Dans le cas de la structure « fin », la séquence optimise la trace mémoire de la structure. Les 4 caractères sont contigus dans 1 case memoire de 32 bits, suivis de 3 float dont la taille est ici de 4 octets chacuns.
Dans le cas de la structure « large », la trace mémoire est maximale: pour chaque caractère, 4 octets sont occupés pour 1 seul de réellement utilisé.
Dans le cas de la structure « moyen », la trace mémoire n'est pas complètement optimisée et les 2 couple de « char » sont codés sur chacun 4 octets.
A retenir:
|
Les tableaux sont des zones de mémoires réservées et censées recevoir un nombre maximum préétabli de données d'un type prédéfini. Cependant, un tableau ne possède pas d'indication a priori de sa propre taille.
Par exemple :
char a[4];
va réserver statiquement une zone de mémoire de la pile de 4 fois la
taille d'un « char ».
Cette zone est valide jusqu'à la fermeture du bloc en cours.
Elle est aussi adressable depuis des fonctions appelées par le bloc en cours:
1 #include <stdio.h> 2 #include <string.h> 3 void func2(char *string) { 4 strcpy(string, "func2"); 5 } 6 char *func1() { 7 char string[10]; 8 strcpy(string, "func1"); 9 printf("func1 (a): %s\n", string); 10 func2(string); 11 printf("func1 (b): %s\n", string); 12 return string; 13 } 14 int main(void) { 15 char *s; 16 s = func1(); 17 printf("main: %s\n", s); 18 return 0; 19 }
produira quelque chose qui doit ressembler à ça:
func1 (a): func1 func1 (b): func2 main: àóÿ¿éôÿàóÿôÿ¿àóÿ¿Hôÿ¿@
Mais il se peut aussi que le programme se termine par un core dump durant l'appel au dernier printf ligne 17 sur certaines plateformes si s est NULL.
En fait, la fonction "func1()" retourne un pointeur alloué dans la frame 2 (frame correspondant à "func1()"). Or, dès que l'on sort de la fonction func1() pour revenir dans main, la frame 2 est dépilée (changement de cartographie de la mémoire virtuelle). Les valeurs qui se trouvaient dans les variables allouées sur pile sont donc remplacée de manière arbitraire.
Contraitement aux types composés, il n'est pas possible d'assigner un tableau à un autre tableau. En effet, le "type tableau" ne connait pas sa taille, il ne peut pas être manipulé comme une structure.
Le type tableau est incomplet. Il n'est en fait qu'une utilisation déguisée du concept de pointeur.
A retenir:
|
Un pointeur est une zone de mémoire qui contient ou est suceptible de contenir une adresse mémoire.
Bien qu'il existe des sous types différents de pointeurs (void *, char *, short *, int *, long *, float *, double *, pointeurs de pointeurs, pointeurs sur fonctions), tous ces sous types de pointeurs ont la même taille et sont intercheangeables.
Il exite deux opérations de base : & et *
& pourrait encore s'écrie « adresse de »
et * pourrait s'écrire « valeur de »
Il est d'ailleurs tout à fait possible d'écrire des macros:
#define addressof(x) &(x) #define valueof(x) *(x)
Exemple:
int i = 1; int *pi = NULL; pi = &i; /* pi = addressof(i) */ *pi = 2; /* valueof(pi) = 2 */
La notion de tableau est intrinsèquement liée à la notion de pointeur. En effet, si l'on considère la mémoire comme un tableau, le pointeur devient une manière d'adresser ce tableau de manière absolue, alors que le tableau au sens C est adressé de manière relative à son début par un indexe.
Un nom de tableau est
équivalent à un pointeur sur le premièr élément
du tableau:
a[0]f est équivalent à *a.
On peut aussi référencer le ième élément d'un
tableau a de deux manière:
a[i] ou *(a + i).
La définition de
tableaux multidimentionnelle passe aussi de manière implicite
par l'utilisation des pointeurs:
Un tableau bi-dimensionnel est un tableau de pointeurs sur tableaux unidimensionnels:
definition statique sur la pile:
float a[10][5];
définition dynamique et allocation:
float **b; /* allocation d'un tableau de 10 pointeurs sur (float *) */ *b = (float **) malloc(10 * sizeof(float)); for (i = 0; i < 10; i++) /* allocation de 5 float */ b[i] = (float *) malloc(5 * sizeof(float));
Les structures peuvent elles aussi être désignées par des pointeurs. Il est même possible d'accéder directement à un champs d'une structure à partir d'un pointeur sur structure. L'opérateur de membre passe alors de « . » à « -> »:
struct complex {float a; float b} c, *pc; c.a = 1., c.b = 0., pc = &c; pc->a = 2., pc->b = 1.;
Mais ce ne sont pas les seules façons de référencer des membres de structures. En effet, il existe une macro offsetof() (définie dans stddef.h) permettant de retrouver l'offset (décallage) d'un champ par rapport au premier élément de la structure qui le contient. A partir de ce décallage et en opérant les recasting qui s'imposent, il est possible d'accéder à tous les champs d'une structure :
#include <stddef.h> #include <stdio.h> #include <string.h> int main(void) { typedef struct { int a; char b[10]; double c; } S; int a_shift, b_shift, c_shift; S s; char *p; p = (char *) &s; a_shift = offsetof(S, a); b_shift = offsetof(S, b); c_shift = offsetof(S, c); printf("offset a = %d\noffset b = %d\noffset c = %d\n", a_shift, b_shift, c_shift); *((int *) (p + a_shift)) = 555; strcpy(p + b_shift, "abcdef"); *((double *) (p + c_shift)) = 3.14159; printf("s.a = %d\ns.b = \"%s\"\ns.c = %lf\n", s.a, s.b, s.c); printf("*(int *) (p+a_shift) = %d\n (p+b_shift) = \"%s\"\n*(double *) (p+c_shift) = %lf\n", *(int *) (p+a_shift), (p+b_shift), *(double *) (p+c_shift)); return 0; }
donne :
offset a = 0 offset b = 4 offset c = 16 s.a = 555 s.b = "abcdef" s.c = 3.141590 *(int *) (p+a_shift) = 555 (p+b_shift) = "abcdef" *(double *) (p+c_shift) = 3.141590
Pour plus d'exemples, reportez-vous au chapitre "Exemple récapitulatif"".
Il est possible de définir des variable « pointant » sur des fonctions.
Ces pointeurs sur fonctions sont à la base du principe de « virualisation » et, de manière plus générale, de la réutilisation de fonctions quel que soit le type des données fournies. Cette réutilisation à partir d'un typage faible, voire une absence de typage, constitue une implémentation plus élégante et plus concise que l'utisation des templates du C++.
Il est nécessaire, lors de l'usage de pointeurs sur fonctions, de définir un protocole spécifique de passage de paramètres (nombre de ces paramètres).
Le premier exemple qui vient à l'esprit lorsqu'on parle de pointeur sur fonction est la fonction qsort (dont le prototype est dè dans stdlib.h):
void qsort(void *base, size_t nmemb, size_t size, int(*compar)(const void *, const void *));
Le variable de type pointeur sur fonction s'appelle compar.
Utilisons la fonction qsort:
#include <stdlib.h> #include <stdio.h> int float_comp(float *a, float *b) { if (*a > *b) return 1; if (*a < *b) return -1; return 0; } void arr_print(float *arr) { int i; for (i = 0; i < 10; i++) printf("arr[%d] = %3.1f\n", i, arr[i]); } int main(void) { float arr[10] = {5.1, 4.2, 3.3, 1.4, 7.8, 2.0, 8.9, 9.7, 0.5, 6.6}; printf("Tableau initial:\n"); arr_print(arr); qsort(arr, 10, sizeof(float), float_comp); printf("\nTableau final:\n"); arr_print(arr); return 0; }
donne:
Tableau initial: arr[0] = 5.1 arr[1] = 4.2 arr[2] = 3.3 arr[3] = 1.4 arr[4] = 7.8 arr[5] = 2.0 arr[6] = 8.9 arr[7] = 9.7 arr[8] = 0.5 arr[9] = 6.6 Tableau final: arr[0] = 0.5 arr[1] = 1.4 arr[2] = 2.0 arr[3] = 3.3 arr[4] = 4.2 arr[5] = 5.1 arr[6] = 6.6 arr[7] = 7.8 arr[8] = 8.9 arr[9] = 9.7
Dans cet exemple, la fonction qsort utilise un pointeur sur une fonction de pomparaison. Nous avons vu comment définir la foncition de comparaison (ici float_comp) et comment la passer en paramètre à la fonction qsort.
La compilation du code précédent produit néanmoins un message d'avertissement à cause de
la différence de typage entre la fonction float_comp() et la fonction attendue par qsort.
int float_comp(void *a, void *b) { if (*(float *)a > *(float *)b) return 1; ...
qsort(arr, 10, sizeof(float), (int(*)(const void *, const void *)) float_comp);
Il est également possible de simplifier l'écriture de la seconde méthode en définissant un type de pointeur sur fonction:
typedef int (*comp_f)(const float *, const float *);
Le recast de la fonction float_comp s'écrira alors:
qsort(arr, 10, sizeof(float), (comp_t) float_comp);
Les pointeurs sur fonctions sont aussi très utils pour la définition de "classes". Les meilleurs exemples de ces techniques se trouvent dans l'implémentations des systèmes de fichiers du noyau Linux, ou encore dans l'architecture GTK/Gnome.
En ce qui concerne le noyau Linux, on pourra regarder le header linux/fs.h, et en particulier la structure address_space_operations (noyau 2.4.18):
struct address_space_operations { int (*writepage)(struct page *); int (*readpage)(struct file *, struct page *); int (*sync_page)(struct page *); /* * ext3 requires that a successful prepare_write() call be followed * by a commit_write() call - they must be balanced */ int (*prepare_write)(struct file *, struct page *, unsigned, unsigned); int (*commit_write)(struct file *, struct page *, unsigned, unsigned); /* Unfortunately this kludge is needed for FIBMAP. Don't use it */ int (*bmap)(struct address_space *, long); int (*flushpage) (struct page *, unsigned long); int (*releasepage) (struct page *, int); #define KERNEL_HAS_O_DIRECT /* this is for modules out of the kernel */ int (*direct_IO)(int, struct inode *, struct kiobuf *, unsigned long, int); };
Cette structure définit les méthodes communes à tous les systèmes de fichiers.
Pour plus d'exemples, reportez-vous au chapitre "Exemple récapitulatif"".
La désignation de données peut s'effectuer de plusieurs manières par valeur, par adresse
La désignation d'une donnée par sa valeur se fait soit en appelant le nom de la variable, si celle-ci n'est pas un pointeur vers la donnée:
int i = 1;
i est alors « mis pour » 1.
Si la variable est un
pointeur, il est nécessaire de passer par l'opératieur
« valueof() » (*):
int i = 1, *pi; pi = &i; /* pi = addressof(i) */
Alors, *pi est « mis pour » 1;
La désignation par adresse se fait soit en prenant l'adresse d'une variable par l'opérateur « addressof() » (&):
float f = 2.;
&f désigne l'adresse de la variable f.
Ou encore, si la variable est un pointeur:
float f = 2., *pf; pf = &f;
pf désigne l'adresse de f
L'appel à une fonction recopie les valeurs des paramètres sur la pile. On a donc une copie locale des données accessibles à partir de nouveaux noms de variables, ceux définis dans le prototype d'appel à la fonction. Si une modification de la valeur d'une telle variable est faite, cette modification est locale.
Si une variable passée en paramètre est un pointeur et que l'on modifie la valeur pointée:
*p = valeur; /*valueof(p) = valeur */
alors, la modification n'est plus locale:
soit le programme:
1 #include <stdio.h> 2 void func(int i, int j, int *pk) { 3 printf("&i = %p (%u), &j = %p (%u), &pk = %p (%u)\n", &i, &i, &j, &j, &pk, &pk); 4 *pk = i + j; 5 i = 3, j = 4; 6 printf("i = %d, j = %d, pk = %x (%u), *pk = %d\n", i, j, pk, pk, *pk); 7 } 8 int main(void) { 9 int a = 1, b = 2, c = 0; 10 printf("&a = %p (%u), &b = %p (%u), &c = %p (%u)\n", &a, &a, &b, &b, &c, &c); 11 printf("a = %d, b = %d, c = %d\n", a, b, c); 12 func(a, b, &c); 13 printf("a = %d, b = %d, c = %d\n", a, b, c); 14 return 0; 15 }
&a = 0xbffff4b4 (3221222580), &b = 0xbffff4b0 (3221222576), &c = 0xbffff4ac (3221222572) a = 1, b = 2, c = 0 &i = 0xbffff490 (3221222544), &j = 0xbffff494 (3221222548), &pk = 0xbffff498 (3221222552) i = 3, j = 4, pk = bffff4ac (3221222572), *pk = 3 a = 1, b = 2, c = 3
Les variables i et j sont bien locales à fonction func. La modification de leur valeur n'est que locale.
Par contre, le passage d'un pointeur en argument permet la modification de la valeur pointée.
Voici un petit exemple récapitulatif. A vous de retrouver les différentes techniques vues et
d'ajouter les commentaires.
(ce code compile sans erreur ni warning avec gcc -Wall -ansi -pedantic)
#include <stdlib.h> #include <stdio.h> void * object_new(); void * object_destroy(void *); typedef struct { int type; void * (*destroy)(void *); void * (*new)(); } object_t, *pobject_t; object_t object_defaults() { object_t o; o.type = 1; o.new = object_new; o.destroy = object_destroy; return o; } typedef struct { object_t o; int (*get)(void *); int (*set)(void *, int); int i; } int_t, *pint_t; void *object_destroy(void *po) { printf("destroy object_t\n"); if (!po) return NULL; free(po); return NULL; } void * object_new() { pobject_t po; printf("create object_t\n"); if (!(po = (pobject_t) malloc(sizeof(object_t)))) return NULL; *po = object_defaults(); return (void *) po; } int int_get(void *pi) { if (!pi) return -1; return ((pint_t) pi)->i; } int int_set(void *pi, int i) { if (!pi) return -1; return ((pint_t) pi)->i = i; } void * int_new() { pint_t pi; printf("create int_t\n"); if (!(pi = (pint_t) malloc(sizeof(int_t)))) return NULL; pi->o = object_defaults(); pi->o.type = 2; pi->o.new = int_new; pi->i = 0; pi->get = int_get; pi->set = int_set; return pi; } int main(void) { pobject_t po; pint_t pi; po = (pobject_t) (pi = int_new()); printf("po = %p\npi = %p\n", (void *) po, (void *) pi); printf("type de po: %d\n", po->type); printf("type de pi: %d\n", pi->o.type); pi->set((void *) pi, 555); printf("valeur de pi->i = %d\n", pi->i); printf("valeur de pi->get(pi) = %d\n", pi->get((void *) pi)); printf("valeur de ((pint_t) po)->i = %d\n", ((pint_t) po)->i); printf("valeur de ((pint_t) po)->get((void *) po) = %d\n", ((pint_t) po)->get((void *) po)); ((pobject_t) pi)->destroy((void *) pi); return 0; }
donne:
create int_t po = 0x8049aa0 pi = 0x8049aa0 type de po: 2 type de pi: 2 valeur de pi->i = 555 valeur de pi->get(pi) = 555 valeur de ((pint_t) po)->i = 555 valeur de ((pint_t) po)->get((void *) po) = 555 destroy object_t