UNDER WORK





Memory management in C

Notice

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.

Introduction

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

Physical memory of the computer

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.


C programs address space

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.


Figure 1 :  address space of a process.

Zone programme

The program loading area is itself composed several segments including:

Heap

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)".

Free memory

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.

Dynamic linking area

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().

Stack

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.

La mémoire 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 */
        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 :
  • La mémoire d'un processus est assimilable à un espace contigu,
  • Cet espace est organisé en différents groupes fonctionnels,
  • Les données d'un programme ne sont pas toutes stockées au même endroit en fonction de leur portée et de leur mode de réservation.

(Table des matières)


Notion de type

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.

(Table des matières)


Types simples

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:
  • D'un point de vue pratique, le type des données en mémoire est assimilable à la taille des données
  • Il ne faut jamais préjuger de la taille d'un type, mais utiliser sizeof()
  • Tous les pointeurs ont la même taille sur une architecture donnée. Cette taille est cohérente avec la taille du bus mémoire.


(Table des matières)


Alignement

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:
  • La mémoire est adressable de plusieurs manières différentes.
  • Lorsque l'architecture impose des contraintes d'alignement, l'adresse d'une donnée est un multiple de la taille des données adressées si cette taille est inférieure à la taille du bus mémoire, ou multiple de la taille du bus .
  • Nous avons vu que le typage constituait un canevas d'interprêtation de la mémoire. Réciproquement, n'importe quelle zone mémoire peut être interprètée par un type ou un autre pour peu que l'alignement soit respecté.


(Table des matières)


Types composés

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:
  • L'ordre des données définies dans une structure influe sur la taille de la mémoire utilisée afin de respecter les contraintes d'alignement (même si l'architecture n'en possède a priori pas).
  • De même que les types simples, les types complexes constituent un canevas d'interprêtation de la mémoire. Réciproquement, toute zone de mémoire respectant les contraintes d'alignement peut être interprêtée par une structure.

(Table des matières)


Tableaux

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 tableau ne connait pas sa propre taille de manière a apriori.
  • il n'est pas possible d'assigner un tableau à un autre tableau.
  • Comme nous le verrons au chapitre suivant, le tableau est un pointeur déguisé.

(Table des matières)


Notion de pointeur

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 */
   

(Table des matières)


Pointeurs et tableaux

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));

(Table des matières)


Pointeurs et structures

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"".


(Table des matières)


Pointeurs sur fonctions

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.


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"".


(Table des matières)


Désignation des données

La désignation de données peut s'effectuer de plusieurs manières par valeur, par adresse

Désignation par valeur

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;

(Table des matières)


Désignation par adresse

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


(Table des matières)


Passages de paramètres

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.


(Table des matières)


Exemple récapitulatif

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

(Table des matières)


Comment gérer la mémoire?

Allocation sur la pile

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.

Allocation dynamique

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.

(Table des matières)


Manipulation dynamique de la mémoire

Allocation de la mémoire: malloc()

Prototype:

    #include <stdlib.h>
	void *malloc(size_t size);

Comportement:

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:
  • Il est impératif de toujours vérifier que le pointeur alloué est valide.
  • Plus généralement, il est toujours nécessaire de tester un pointeur passé en argument.
  • Il ne faut jamais préjuger du contenu d'une zone mémoire après son allocation par malloc
  • Il est nécessaire d'initialiser convenablement les zones de mémoire allouées

(Table des matières)


la fonction calloc()

Prototype :
    #include <stdlib.h>
    void *calloc(size_t nmemb, size_t size);
Comportement :

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).

(Table des matières)


Désallocation de la mémoire: free()

Prototype :

    #include <stdlib.h>
    void free(void *ptr);

Comportement :

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; }

(Table des matières)


Réallocation de la mémoire : realloc()

Prototype :

    #include <stdlib.h>
    void *realloc(void *ptr, size_t size);

Comportement :

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;
    }  

(Table des matières)


Fonctions annexes: memset(), memcpy(), memmove()

Initialisation d'une zone memoire: memset()

Prototype:
    #include <string.h>
    void *memset(void *s, int c, size_t n);
Comportement:

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).

(Table des matières)


Copie de zone mémoire : memcpy()

Prototype:
    #include <string.h>
    void *memcpy(void *dest, const void *src, size_t n);
Comportement:

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:

(Table des matières)


Déplacement de zone mémoire: memmove()

Prototype:
    #include <string.h>
    void *memmove(void *dest, const void *src, size_t n);
Comportement:

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.


(Table des matières)


Gestion des chaines de caractères:

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.


Rappels

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:

strcpy()

Prototype:
    #include <string.h>
    char *strcpy(char *dest, const char *src);
Comportement:

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 # ...)


strncpy()

Prototype:
    #include <string.h>
    char *strncpy(char *dest, const char *src, size_t n);
Comportement:

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;
    }

(Table des matières)


strcat()

Prototype:
    #include <string.h>
    char *strcat(char *dest, const char *src);
Comportement:

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.


(Table des matières)


sprintf() et snprintf()

Prototype
    #include <stdio.h>
    int sprintf(char *str, const char *format, ...);
Comportement

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).

(Table des matières)


Règles de base

(Table des matières)


Erreurs liées à la mauvaise utilisation de la mémoire

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

(Table des matières)


Notes

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..

(Table des matières)


Références

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.

(Table des matières)


Index

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

A


Adressage
alignement
allocation

(Index)


B


byte

(Index)


C


calloc()

(Index)


D


double
dword

(Index)


E


(Index)


F


float
free()

(Index)


G


(Index)


H


(Index)


I


(Index)


J


(Index)


K


(Index)


L


(Index)


M


malloc()
memset()
memmove()
Mémoire

taille

type

(Index)


N


NULL

(Index)


O


offsetof()

(Index)


P


pointeur:
void *
générique
type

(Index)


Q


(Index)


R


realloc()

(Index)


S


sizeof()
strcpy()
strncpy()
strcmp()
strcat()
strncat()
strdup()
sprintf()
snprintf()
segmentation fault
segmentation violation

(Index)


T


types
taille

(Index)


U


(Index)


V


voir *

(Index)


W


word

(Index)


X


(Index)


Y


(Index)


Z


(Index)


(Table des matières)