Le langage C - mémoire, tableaux, pointeurs

Stockage des informations en mémoire

Lorsqu'on déclare une variable, le programme (lors de son exécution) réserve la quantité de mémoire nécessaire pour enregistrer le contenu de la variable. L'unité de base de la mémoire est l'octet (huit bits) et selon le type de variable, il faut plus ou moins d'octets: un caractère (type char) occupera typiquement 1 octet, un entier 2 à 4, un réel en simple précision 4 octets, un réel en double précision peut par exemple prendre 8 octets).

Tableaux

Il est possible d'utiliser des séries de variables du même type, c'est ce qu'on appelle des tableaux. Un tableau est déclaré en indiquant le nombre d'éléments qu'il va contenir:

int i[100] ;  /*    tableau de 100 entiers    */
float x[25] ; /*    tableau de 25 réels       */

La taille doit être spécifiée par un entier positif, on ne peut utiliser de variable. Lorsqu'on ne connait pas a priori la taille du tableau à utiliser, il faut faire appel à l'allocation dynamique.

Pour utiliser un élément d'un tableau, on y fait référence par son indice placé entre crochets, les indices commencant toujours à 0:

i[0] = ...
i[1] = ...
.
.
.
i[99] = ...

/!\ Attention /!\

Attention piège: le dernier élément d'un tableau à 100 éléments a l'indice 99. Le piège est méchant car l'écriture dans l'élément numéro 100 conduit à un comportement totalement imprévisible du programme: plantage immédiat, plantage aléatoire/différé, pas de plantage, mais peut-être faux résultats, ... !

On peut utiliser une variable entière comme indice, en prenant garde de ne pas dépasser les valeurs permises (de 0 à la taille du tableau moins 1):

int i ;                /*    compteur de boucle    */
float x[25] ;          /*    tableau de 25 réels   */
for (i=0 ; i<25 ; i++) /*    boucle de 0 à 24 inclus    */
{
    x[i] = ...         /*    chaque élément est affecté */
}

Pointeurs

Un pointeur est un type très particulier de variable, puisqu'il est déstiné à contenir une adresse mémoire. L'interêt de ce type de variable apparaît dans plusieurs cas:

En effet, comme nous l'avons déjà signalé, lorsqu'on appelle une fonction avec des arguments, ce sont des copies des arguments qui sont passés à la fonction. Ceci empèche la modification des variables à l'intérieur d'une fonction. L'idée est d'envoyer l'adresse de la variable dont on veut modifier le contenu. Même si c'est une copie de l'adresse que la fonction reçoit, ce qu'il y a à cette adresse correspond bien à la variable qu'on veut modifier. De même, si l'on veut envoyer à une fonction un objet de grande taille, on peut se contenter de lui envoyer son adresse mémoire, la fonction saura alors où se trouve l'objet, et pourra l'utiliser.

Déclaration des pointeurs

Un pointeur se déclare à l'aide du symbole * qui se place entre le type de la variable et le nom de la variable:

int *adr_entier ;

représente un pointeur sur une variable entière. Après la déclaration, le pointeur ne 'pointe' sur aucune variable. Il faut lui affecter une 'destination':

int  *adr_entier, x ;
x = 5 ;
adr_entier = &x ;

on a ici une variable entière x à laquelle on a affecté la valeur 5, et on fait pointer le pointeur adr_entier sur cette variable en utilisant le symbole &, qui, placé devant une variable, permet d'obtenir l'adresse mémoire de la variable. Ainsi, le pointeur adr_entier contient l'adresse mémoire de la variable x.

[schéma mémoire]

Sur cet exemple, nous avons représenté la mémoire de l'ordinateur comme une suite de 'cases'; chaque case a une adresse qui est indiquée en dessous. La variable x est (par exemple) stockée à partir de la case numéro 105 (mais vu qu'il s'agit d'un entier de type int, la variable occupe les cases 105, 106, 107 et 108, soit 4 octets en tout), qui contient donc la valeur 5. La variable adr_entier est (par exemple) stockée à la case 502 (le pointeur occupe 8 cases dans notre exemple). Elle contient l'adresse de la variable x, donc 105. Sur un même ordinateur, un pointeur occupe toujours la même quantité de cases (typiquement 4 ou 8), que ce soit un pointeur vers un entier, vers un caractère, vers un flottant de double précision ou vers une grande structure de données. Ce qui compte, c'est qu'il doit pouvoir stocker une adresse. C'est le compilateur qui sait combien de place prend une adresse, et qui, comme pour les types entiers et flottants, réserve la place qu'il faut quand vous déclarez un pointeur.

Modification de variables par une fonction

Comme nous l'avons vu dans le paragraphe sur les fonctions, la modification du contenu d'une variable passée comme argument à une fonction n'a pas de portée en dehors de la fonction. Le seul moyen de modifier le contenu d'une variable par l'appel d'une fonction consiste à passer non pas la variable elle-même mais l'adresse de la variable à la fonction. Dans ce cas, la fonction a connaissance de l'endroit dans la mémoire où se trouve stockée la variable, et peut donc la modifier. L'argument de la fonction est alors un pointeur sur une variable du type que l'on souhaite modifier.

void doubler (float *adr_x)   /* fonction qui double la valeur a l'adresse donnee */
{
  (*adr_x) = (*adr_x) * 2.0 ; /* le contenu de l'adresse est double ici */
  return ;                    /* on ne retourne rien (void) */
}

int main (void)
{
    float z ;                 /* variable reelle        */
    z = 5.0 ;
    doubler (&z) ;            /* on essaye de doubler z */
    printf ("%f\n", z) ;      /* z vaut maintenant 10   */
    return 1 ;
}

Dans cet exemple, la fonction doubler demande un argument qui est un pointeur sur une variable de type float. Cette fonction fait une opération sur le contenu de l'adresse, et non pas sur l'adresse elle-même, c'est pourquoi on utilise *adr_x et non pas adr_x. En effet, adr_x est un pointeur (type float *), c'est-à-dire une adresse mémoire d'une variable de type float. L'expression *adr_x représente le contenu de cette adresse, ce qui est bien la variable à modifier. Noter l'usage des parenthèses autour de (*adr_x) qui permettent une meilleure lisibilité (par rapport au symbole de mutliplication).

L'appel de la fonction doubler dans le main se fait en passant l'adresse de la variable sur laquelle on veut effectuer l'opération. La variable z étant une variable de type float, on passe son adresse mémoire &z à la fonction.

Exemple

Affichez, compilez et executez le programme

prog11.c : Modification de variables par une fonction / passage d'arguments par adresse.

Auteur(s) : Anciens, A. Daerr. Dernière modification : Wed Nov 22 01:08:55 2006. [valid. XHTML]