Memory management in C |
The present document is published under terms of the (GNU Free Documentation License).
Diffusion and usage of this document is greatly encouraged, provided the present notice and the name of the author are sited.
Please notify me any intent of mercantile exploitation of this doctument, in part or as a whole.
The last version of this document is located at the folling URL:
http://ilay.org/yann/articles/mem/index.en.html
Any remarque, suggestion or correction is welcomed and can be addressed to
Yann LANGLAIS,
ilay.org.
The C language is often considered as difficult and unreliable. The second assertion is unacceptable since a lot of 4th generation languages and RADs are themselves written with C.
These clichés mainly result from the intensive pointer usage. Misconsideration often comes from pointer concept and associated memory management misunderstanding.
The present document tries to present these concepts from their theoreticle base up to their practicle application.
However, purpose is not to define all notions in the most rigoreous possible way. The purpose is to present my way of considering memory management.
The reader is assumed to have prior experience in C programming.
(index)
The memory of a computer can be seen in first approximation as a long and continuous array. The width of this array is expressed in "bits".
Generally, this size is subdivided. Endeed, considering a width of 32bits, since a common type (the char) is coded on one byte (8bits), it seems interesting to be able to store and access 4 diffent bytes independently within our 32bits in order to save memory.
This 32bits array is generally subdivided in 8 and 16 bits.
Each subdivision (byte) is numbered. This numbering corresponds to memory addresses.
The organisation previously described does not apply "as is" to process address space. A process address space is virtual. A process can even use more memory than physically available RAM by using RAM and other kind of storage such as swap space located on hard drive.
The operating system helped by some processors (especially the MMU or Memory Management Unit) agregate different resources (RAM, swap, hard drive,...) to provide a vast continuous an homogeneous array to processes.
The mecanisms creating this illusion are fairly complex and spread outside the scope of this document but the matter is well documented1.
However, one must note that the memory array slots are not all really present (i.e. do
not point to existing storage). In fact, only the memory in use is really mapped or
bound to existing resources.
For each process, the address space spread from
0x00000000 to
0xFFFFFFFF.
Adresses lower than the loading area of a program text segment is reserved and cannot be
accessed for writing operations.
Addresses above the value given by the PAGE_OFFSET
macro is reserved for the kernel (kernel mapping of process addresses).
Address space is filled up with respect to system dependant and hardware dependent rules.
The program loading area is itself composed several segments including:
Then comes the heap. Heap is the memory dynamically allocated by the malloc() function call. The heap grows toward higher addresses. Heap base address is given by "sbrk(0)".
Above the heap, comes free memory. This unallocated memory is consumed by the
heap.
Any memory allocation or reservation is done by malloc(amount) which calls sbrk(AlreadyAllocated + amount) where AlreadyAllocated + amount is the total amount of memory
already reserved above sbrk(0) plus the required amount to reserve.
Part of the memory is reserved for dynamic linking with shared libraries. Shared libraries linked to an executable at linking time are loaded when the executable is loaded. Shared libraries such as plugins or external modules are loaded at runtime with the dlopen().
The last area in user space is the Stack. By opposition to the heap, the stack
starts at the highest address (theoretically at address
PAGE_OFFSET-1).
Stack then grow towards lower addresses.
The stack is composed of boxes or frames. Each frame correspond to a function.
The main() function is the box numbered 1
(the second on the stack since main()
is also called). If in turn main()
calls the foo(), a frame "numbered" 2 will be
added on "top" of frame "1".
When returning to main(), frame "2" will be destroyed (unstacked/unmapped/unbounded).
A frame contains :
When return from a fonction, the associated frame is/may be unmaped. The address previously used by the frame may then contain radom data.
XXXXXXXXXXXXXXXXXXXXXXXx
En corrolaires :
La pile est réservée statiquement. Il est impossible de changer dynamiquement sa taille.
Une pile est alloué localement :
La pile représente la proportion de la mémoire de travail utilisée temporairement pour un traitement particulier.
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 */ 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%8x (zone programme, segment data)\n", &i_stat); printf("adresse de i_glob = 0x%8x (zone programme, segment bss)\n", &i_glob); printf("adresse de pi_pg = 0x%8x (zone programme, segment bss)\n", &pi_pg); printf("adresse de main = 0x%8x (zone programme, segment text)\n", main); printf("adresse de nargs = 0x%8x (pile frame 1)\n", &nargs); printf("adresse de args = 0x%8x (pile frame 1)\n", &args); printf("adresse de pi_loc = 0x%8x (pile frame 1)\n", &pi_loc); printf("sbrk(0) = 0x%8x (tas)\n", sbrk(0)); printf("pi_loc = 0x%8x (tas)\n", pi_loc); printf("pi_pg = 0x%8x (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) = 0x00022cb0 (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 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
Lorsque l'on a affaire à des constantes, ou des variables bien définies et fixées en terme de taille, et a usage local seulement, l'allocation sur la pile est appropriée.
Lorsque les variables ont des tailles non fixées ou encore ne sont pas à usage local, il est sûr d'utiliser l'allocation dynamique.
#include <stdlib.h> void *malloc(size_t size);
Malloc prend en argument la taille de la zone mémoire désirée et retourne le pointeur sur la zone allouée ou 0 (pointeur nul) s'il n'y a pas assez d'espace mémoire contigu.
Une zone mémoire fraichement allouée par malloc n'est a priori pas initialisée et peut contenir n'importe quoi (données aléatoires provenant d'anciennes applications ayant utilisé le même espace, ou données aléatoires).
Il est donc nécessaire de l'initialiser avec des valeurs appropriées (par exemple 0) en fonction du type de donées à inserer dans ces zones allouées et / ou en fonction des règles de programmation utilisées. On pourra initialiser l'ensemble de la zone avec la fonction memset(), recopier des zones existantes avec memcpy(), ou encore initialiser les blocs en fonction de leur découpage (types, structures, tableaux, ...).
A retenir:
|
#include <stdlib.h> void *calloc(size_t nmemb, size_t size);
A la différence de malloc(), calloc() alloue un tableau de nmemb élements ayant chacun size pour taille. La mémoire allouée est initialisée à 0.
Pour le reste, calloc() fonctionne comme malloc(): si la mémoire a bien été réservée et initialisée, calloc() retourne le pointeur sur la zone. Sinon, calloc() returne 0 (pointeur nul).
#include <stdlib.h> void free(void *ptr);
La fonction free() annule la réservation d'une zone de mémoire pointée par ptr, allouée par malloc() ou calloc() ou par toute autre fonction réservant de la mémoire, comme strdup() qui duplique une chaine de caractères.
Si l'on tente de libérer un espace mémoire qui n'a pas été allouée par malloc() ou calloc(), directement ou par le biais d'une fonction appelant malloc() ou calloc(), la fonction free() enverra un signal 11 (SIGSEGV « Invalid memory reference ») qui terminera brutalement le programme.
La fonction free() n'écrase pas la mémoire de la zone.
Il est fortement recommandé d'annuler le pointeur qui vient d'être libéré par free(), afin de ne pas tenter d'adresser un espace qui pourra être réutilisé. On pourra par exemple réécrire une fonction:
void *my_free(void *ptr) { if (ptr) free(ptr); return NULL; }
et l'utiliser de la manière suivante:
ptr = my_free(ptr);
ou encore écrire la macro qui s'utilise de la même manière que free():
#define FREE(x) { if (x) free(x); x = NULL; }
#include <stdlib.h> void *realloc(void *ptr, size_t size);
La fonction realloc() prend en argument le pointeur sur la zone de mémoire (ptr) dont il faut modifier la taille, et la nouvelle taille désirée (size). Si l'opération s'est bien passée, realloc() returne le pointeur sur la zone mémoire.
Attention:
il est fort possible que le nouveau pointeur soit
différent de l'ancien! En effet, si la zone mémoire
doit être augmentée, et que la zone en cours n'est pas
assez grande pour supporter la nouvelle taille, realloc() allouera
une nouvelle zone de mémoire contigue et recopiera
complètement l'ancienne zone.
Il est donc impératif de ne pas adresser directement par un pointeur un espace mémoire dont la taille doit/peut être modifiée par realloc:
Les zones en rouge sont des zones de mémoire occupées. La zone jaune est la zone de mémoire que nous avons réservé et modifié par realloc. Si nous conservons un pointeur sur la zone avant le réalloc, ce pointeur ne sera plus utilisable après car in adressera une zone non réservée et contenant des données aléatoires.
Si l'opération échoue, realloc() retourne 0, mais ne déasalloue pas la zone d'origine. Il est impératif de ne pas réassigner le même pointeur immédiatement avec le résultat de realloc() au risque de perdre toutes les données en mémoire et de ne jamais pouvoir les libérer.
Ce qu'il ne faut pas faire:
ptr = realloc(ptr, newsize);
Ce qu'il faut faire:
{ void *p; if ((p = realloc(ptr, newsize))) { /* la réallocation s'est bien passée , l'affectation est sûre: */ ptr = p; } else { /* la réallocation a échoué, le processus ne peut pas continuer mais la mémoire est encore utilisée: */ free (ptr); } return NULL; }
#include <string.h> void *memset(void *s, int c, size_t n);
memset() prend en argument le pointeur sur la zone allouée (s) à initialiser, la valeur à affecter à toute la zone mémoire (c), et le nombre d'octets devant subir le traitement (n).
Il est à noter que seul le premier octet de la valeur d'initialisation est pris en compte.
memset retourne le pointeur d'entrée (s).
#include <string.h> void *memcpy(void *dest, const void *src, size_t n);
memcpy prend en entrée le pointeur zone de mémoire allouée de destination, le poinrteur sur la zone de mémoire cible, et le nombre d'octets devant être copiés.
memcpy retourne le pointeur de destination (dest).
Attention:
#include <string.h> void *memmove(void *dest, const void *src, size_t n);
La fonction memmove() déplace le contenu d'une zone mémoire vers une autre. Elle prend en argument le pointeur vers la zone mémoire allouée vers laquelle déplacer le contenu mémoire (dest), le pointeur vers la zone de départ de la copie (src) et le nombre d'octets à déplacer. Elle retourne le pointeur vers la zone de destination (dest).
Contrairement à memcpy(), il est possible que les 2 zones se supperposent.
La manipulation de chaines de caractères est une source de nombreux problèmes dont sont victime autant les programmeurs débutants que les chevronnés. Ces quelques lignes permettrons peut être à certains d'éviter quelques écueils.
Une chaine de caractères est un tableau d'octets terminé par un octet nul (ou caractère nul = 0 = '\0').
Si l'on veut réserver la mémoire pour la chaine « bonjour, monde », il est nécessaire de penser que cette chaine doit en fait s'écrire « bonjour, monde\0 » et réserver l'espace en conséquence.
La fonction strlen() retourne la taille d'une chaine de caractère sans son terminateur.
L'indexe d'un tableau commence toujours par 0 et non par 1. Aussi, lorsque l'on réserve 50 chars pour une chaine, le caractère nul devra se trouver au plus à l'index 49.
Certaines fonctions nécessitent une attention toute particulière:
#include <string.h> char *strcpy(char *dest, const char *src);
La fonction strcpy recopie le contenu de la chaine src vers la chaine allouée pointée par dest et retourne le pointeur vers la chaine de destination (le caractère nul est inclus dans la copie).
Mais attention: il est
nécessaire au préalable de s'être assuré
que la taille réservé dans dest est supérieure
ou égale à la taille de la chaine pointée par
src.
Si ce n'est pas le cas, nous risquons d'avoir un écrasement
d'une zone mémoire.
Cet écrasement peut modifier des
données stockées sur la pile, ou pire, écraser
une partie du programme lui même puisque la définition
de la pile précède le code dans la mémoire.
Ce défaut est la base de la technique de piratage la plus
répendue: le pirate fournit au programme une chaine assez
longue pour empiéter sur le code de la fonction et contient un
autre code au format binaire qui sera exécuté au lieu
du code de la fonction (voir Linux Mag # ...)
#include <string.h> char *strncpy(char *dest, const char *src, size_t n);
strncpy() prend les mêmes arguments que strcpy() plus n, le nombre maximal d'octets à recopier.
Mais, là encore, attention car strncpy() recopie au plus n caractères sans pour autant tester que le dernier caractère copié est bien NULL. Il est donc nécessaire de s'assurer après un strncpy() que le dernier caractère est bien nul:
char* mystrncpy(char *dest, char *src, size_t n) { if (!dest) return 0; if (!src || n <= 1) return *dest = 0; strncpy(dest, src, n); dest[n - 1] = 0; return dest; }
#include <string.h> char *strcat(char *dest, const char *src);
strcat ajoute à la chaine dest la chaine src. Il est nécessaire d'être sûr d'avoir réservé assez de place dans la chaine dest pour recevoir la chaine src en plus de son contenu. De même que pour strncpy, il existe une variation permettant de limiter la longueur de la chaine globale:
char *strncat(char *dest, const char *src, size_t n)
strncat() ne recopie que les n premiers octets de src à la fin de dest et ajoute 0 à la fin, 0 n'étant pas pris en compte dans n.
Pour ne pas faire de débordement mémoire, il faut donc que n soit égal à la taille réservée de dest, diminué de strlen(dest) et encore diminué de 1 pour le caractère nul final.
#include <stdio.h> int sprintf(char *str, const char *format, ...);
Cette fonction « imprime » remplit une chaine de caractères allouée (str) à partir d'un format (format) et, eventuellement, des données (...) et retourne le nombre de caractères de la nouvelle chaine (caractère nul final non compris). Une fois encore, aucun test n'est effectué sur la taille maximale de la chaine de caractères de destination. Pour cette fonction aussi, il existe une « parade »: la fonction snprintf().
int snprintf(char *str, size_t size, const char *format, ...);
snprintf() tronque la nouvelle chaine « str » à size caractères y compris le caractère nul final. Dans le cas où la chaine str est inférieure à size, la fonction retourne la taille de str. Sinon, la taille de str est size - 1 et la fonction retourne -1.
Il existe d'autres groupes de fonctions avec des propriétés similaires, telles que strcmp et strncmp, strdup et strndup, vsprintf et vsnprintf (les équivalent de sprintf et snprintf mais dont le le dernier argument est une va_list (voir man vsprintf et man stdarg).
Description des erreurs:
Passage d'un argument par valeur
Utilisation d'une variable non initialisée
Adressage sur la pile
Utilisation d'un pointeur null / pas de test de malloc()
Dépassement de l'espace alloué (leak) strcpy, strcat, sprintf ..., problème de pointeur (pointeur libéré mais pas remis à 0
free() sans malloc()
malloc() sans free() (ou allocation de mémoire par une fonction sans désallocation -strdup()-),
Adressage d'une zone réallouée
Adressage d'une zone non alignée (pour les architectures avec contraintes fortes d'alignement).
Symptomes:
Affichage de chaines de caractères erronnés
Données invalides, corrompues ou aléatoires
boucles trop longues/trop courtes
Plantage sur free()
Plantage sur malloc()
Plantage pendant l'adressage d'un élément d'une structure
Plantage à un endroit donné avec ou sans débugger
Plantage à un endroit donné durant l'execution et absence de plantage sous débugger ou plantage à un autre endroit sous débugger
Bus error
Problème d'alignement
Segmentation fault
1
voir "Linux Kernel Hacker's Guide" à l'url http://en.tldp.org/LDP/khg/HyperNews/get/khg.html
ou encore "the Linux Kerneli" http://en.tldp.org/LDP/khg/HyperNews/get/khg.html
2Brian W. Kernigan et Dennis M. Richie, dans "The C Programming Language » font état au chapitre 2.2, page 34 de la première édition, 1978, d'une taille de 9bits pour les « char » sur les machines de type Honeywell 6000
3Le type « long long » a été introduit dans la version ANSI C99 des spécification ANSI pour le langage C
4Ces adresses sont données à titre d'exemple. Elle peuvent varier d'un programme à un autre et d'une machine à une autre.
4Cette fonctionnalité n'était pas présente à la base, mais prévue (cf. Brian W. Kernighan & Dennis M. Richie dans « The C Programming Language » paragraphe 14.1 de la page 209 de première édition 1978):
« Other opterations, such as assigning from or to it or passing it as a parameter, draw an error message. In the future, it is expected that these operations, but not necessarily others, will be allowed »
« Les autres opérations, comme l'assignation depuis ou vers une structure, ou le passage d'une structure en paramètre [d'une fonction] génèrent un message d'erreur. Il est vraissemblable que ces opérations, mais pas nécessairement d'autres, viendront à être autorisées »
Cette fonctionnalité est apparue avec le standard ANSI C89..
The C Programming Language, Brian W. Kernigan et Dennis M. Richie, ISBN 0131101633, 1978, Prentice-Hall
LE LANGAGE C, NORME ANSI, Brian W. Kernigan et Dennis M. Richie, ISBN 2100051164, 2ème édition, 1997, Dunod.
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)
(Index)