retour




Gestion de la mémoire en C : Présentation de la mémoire

Notice

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.

Introduction

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.


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 */
        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 :
  • 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.

Le tas

Exploration du tas

principes d'un allocateur memoire

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.

Allocateur minimaliste

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.

Allocateur standards

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.

La pile

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.


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.



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



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.


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


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

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

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



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



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;


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



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.



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