Programmation C/Pointeurs
Dans cette section, nous allons présenter un mécanisme permettant de manipuler les adresses, les pointeurs. Un pointeur a pour valeur l'adresse d'un objet C d'un type donné (un pointeur est typé). Ainsi, un pointeur contenant l'adresse d'un entier sera de type "pointeur vers entier".
Usage et déclaration.
L'opérateur & permet de connaitre l'adresse d'une variable, on dira aussi la référence. Toute déclaration de variable occupe un certain espace dans la mémoire de l'ordinateur. La référence permet de savoir où cet emplacement se trouve. En simplifiant à l'extrême, on peut considérer la mémoire d'un ordinateur comme une gigantesque table d'octets. Quand on déclare une variable de type codice_1, elle sera allouée à un certain emplacement (ou dit autrement : un indice, une adresse ou une référence) dans cette table. Un pointeur permet simplement de stocker une référence, il peut donc être vu comme un nombre allant de 0 à la quantité maximale de mémoire dont dispose votre ordinateur (moins un, pour être exact).
Un pointeur occupera habituellement toujours la même taille (occupera la même place en mémoire), quel que soit l'objet se trouvant à cet emplacement. Il s'agit en général de la plus grande taille directement gérable par le processeur : sur une architecture 32bits, elle sera de 4 octets, sur une architecture 64bits, 8 octets, etc. Le type du pointeur ne sert "qu'à" renseigner comment sont organisées les données suivant l'adresse référencée par le pointeur. Ce code, par exemple, affiche la référence d'une variable au format hexadécimal :
int i; 
printf("%p\n", &i);
Pouvoir récupérer l'adresse n'a d'intérêt que si on peut manipuler l'objet pointé. Pour cela, il est nécessaire de pouvoir déclarer des pointeurs, ou dit autrement un objet pouvant contenir des références. Pour cela on utilise l'étoile (codice_2) entre le type et le nom de la variable pour indiquer qu'il s'agit d'un pointeur :
T * pointeur, * pointeur2, /* ..., */ * pointeurN;
Déclare les variables "pointeur", "pointeur2", ..., "pointeurN" de type pointeur vers le type "T". À noter la bizarrerie du langage à vouloir associer l'étoile à la variable et non au type, qui oblige à répéter l'étoile pour chaque variable.
/* Ce code contient une déclaration volontairement confuse */
int * pointeur, variable;
Cet exemple de code déclare un "pointeur sur un entier de type codice_1" et une variable de type "codice_1". Dans un vrai programme, il est rarement possible d'utiliser des noms aussi triviaux, aussi il est recommandé de séparer la déclaration des variables de celles des pointeurs (ou d'utiliser l'instruction codice_5, qui, elle, permet d'associer l'étoile au type), la lisibilité du programme sera légèrement améliorée.
Il est essentiel de bien comprendre ce qui a été déclaré dans ces exemples. Chaque pointeur peut contenir une référence sur un emplacement de la mémoire (un indice dans notre fameuse table). On peut obtenir une référence (ou un indice) avec l'opérateur codice_6 (ou allouer une référence soi-même avec des "fonctions" dédiées, c.f la section suivante). Cet opérateur transforme donc une "variable" de type "codice_7" en un pointeur de type "codice_8". Insistons sur le terme variable, car évidemment des expressions telles que 'codice_9' ou 'codice_10' n'ont aucun sens, dans la mesure où les constantes et expressions du langage n'occupent aucun emplacement susceptible d'intéresser votre programme.
Ce code, par exemple, affiche la référence d'une variable dans un format défini par l'implémentation (qui peut être hexadécimal, ou une combinaison "segment:offset", par exemple) :
int i;
printf("%p\n", &i);
Il ne faut pas oublier que, comme toutes les variables locales en C, un pointeur est à l'origine non initialisé. Une bonne attitude de programmation est de s'assurer que lorsqu'il ne pointe pas vers un objet valide, sa valeur est mise à zéro (ou codice_11, qui est déclaré entre autre dans codice_12).
L'arithmétique des pointeurs.
L'arithmétique associée aux pointeurs est sans doute ce qui a valu au C sa réputation « d'assembleur plus compliqué et plus lent que l'assembleur ». On peut très vite construire des expressions incompréhensibles avec les opérateurs disponibles. Dans la mesure du possible, il est conseillé de se limiter à des expressions simples, quitte à les décomposer, car la plupart des compilateurs savent très bien optimiser un code C.
Déréférencement.
Le "déréférencement" ou "indirection" est l'opération la plus simple sur les pointeurs. Comme son nom l'indique, il s'agit de l'opération réciproque au référencement (codice_6). L'opérateur associé est l'étoile (codice_2), qui est aussi utilisé pour déclarer un type pointeur. Cet opérateur permet donc de transformer un pointeur de type "T *", en un objet de type "T", les opérations affectant l'objet pointé :
int variable = 10;
int * pointeur = &variable; 
Ici, codice_15 contient une adresse valide, celle de codice_16 ; son déréférencement est donc possible. Par contre, si codice_15 était une variable locale non initialisée, son déréférencement provoquerait à coup sûr un arrêt brutal de votre programme.
Vous obtiendrez le même résultat, si codice_15 est initialisé à codice_11. Cette adresse est invalide et toute tentative de déréférencement se soldera par un arrêt du programme.
Arithmétique de base.
L'arithmétique des pointeurs s'apparente à celle des entiers, mais il est important de comprendre la distinction entre ces deux concepts.
Les opérations arithmétiques permises avec les pointeurs sont :
Addition / soustraction d'une valeur entière à un pointeur (on avance / recule d'un nombre de cases mémoires égal à la taille du type "T") : le résultat est donc un pointeur, de même type que le pointeur de départ.
Il faut bien faire attention avec ce genre d'opération à ne pas sortir du bloc mémoire, car le C n'effectuera aucun test pour vous. Considérez l'exemple suivant :
/* Parcours les éléments d'un tableau */
int tableau[N];
int * p;
for (p = tableau; p < &tableau[N]; p ++)
 /* ... */
Normalement un tableau de N cases permet d'être itéré sur les indices allant de 0 à N - 1, inclusivement. L'expression codice_20 fait référence la case mémoire non allouée immédiatement après le plus grand indice, donc potentiellement source de problème. Toutefois, par exception pour le premier indice après le plus grand, C garantit que le résultat de l'expression soit bien défini. Bien sûr, il ne faut pas déréférencer ce pointeur.
À noter qu'à l'issue de la boucle, codice_21 pointera sur la N+1ème case du tableau, donc hors de l'espace alloué. Le C autorise tout à fait ce genre de pratique, il faut juste faire attention à ne pas déréférencer le pointeur à cet endroit.
Soustraction de deux pointeurs de même type (combien d'objet de type "T" y a t-il entre les deux pointeurs) : le résultat est donc un entier, de type codice_22.
int autre_tableau[3];
int tableau[10];
int * p = &tableau[5]; /* p pointe sur le 6e élément du tableau */
int * q = &tableau[3]; /* q pointe sur le 4e élément du tableau */
ptrdiff_t diff1 = p - q; /* diff1 vaut 2 */
ptrdiff_t diff2 = q - p; /* diff2 vaut -2 */
q = &autre_tableau[2];
ptrdiff_t diff3 = p - q; /* Erreur ! */
Dans cet exemple, les deux premières soustractions sont définies, car codice_21 et codice_24 pointent sur des éléments du même tableau. La troisième soustraction est indéfinie, car on utilise des adresses d'éléments de tableaux différents.
Notons que l'opérateur codice_25 s'applique toujours à une opérande de type entier et une autre de type pointeur. Lorsqu'on écrit codice_26, il y a en fait une conversion de tableau à pointeur avec l'application de l'opérateur codice_25. On peut donc bien sûr utiliser l'opérateur codice_25 avec un pointeur pour opérande :
int a;
int b;
int * p = &a; /* On peut accéder à la valeur de 'a' via 'p[0]' ou '*p' */
/* p[1] est indéfini - n'espérez pas accéder à la valeur de b depuis l'adresse de a */
Arithmétique avec effet de bord.
C'est sans doute ce qui a donné des sueurs froides à des générations de programmeurs découvrant le C : un usage « optimisé » de la priorité des opérateurs, le tout imbriqué dans des expressions à rallonge. Par exemple 'codice_29', pour copier une chaine de caractères.
En fait, en décomposant l'instruction, c'est nettement plus simple qu'il ne parait. Par exemple :
int i;
int * entier;
i = *entier++; /* i = *(entier++); */
Dans ce cas de figure, l'opérateur d'incrémentation ayant priorité sur celui de déréférencement, c'est celui-ci qui sera appliqué en premier. Comme il est postfixé, l'opérateur ne prendra effet qu'à la fin de l'expression (donc de l'affectation). La variable "i" sera donc tout simplement affectée de la valeur pointée par "entier" et après cela le pointeur sera incrémenté. Voici les différents effets suivant les combinaisons de ces deux opérateurs :
i = *++entier; /* Incrémente d'abord le pointeur, puis déréférence la nouvelle adresse pointée */
i = ++*entier; /* Incrémente la valeur pointée par "entier", puis affecte le résultat à "i" */
i = (*entier)++; /* Affecte la valeur pointée par "entier" et incrémente cette valeur */
On peut évidemment complexifier les expressions à outrance, mais privilégier la compacité au détriment de la clarté et de la simplicité dans un hypothétique espoir d'optimisation est une erreur de débutant à éviter.
Le pointeur codice_30.
Ce pointeur est un cas particulier. Il permet de pointer sur un type quelconque. Il est notamment utilisé dans la fonction malloc():
void * malloc(int n);
En pratique, il faut penser à transformer ce pointeur pour qu'il devienne utilisable, même si certains compilateurs acceptent de l'utiliser directement:
// Allocation avec conversion
int * p; // Pointeur p sur le type int
p = (int *) malloc(sizeof(int) * 10); // Allocation de 10 int, soit 20 octets
// Allocation sans conversion
int * p; // Pointeur p sur le type int
p = malloc(sizeof(int) * 10); // Allocation de 10 int, soit 20 octets
Tableaux dynamiques.
Un des intérêts des pointeurs et de l'allocation dynamique est de permettre de décider de la taille d'une variable au moment de l'exécution, comme par exemple pour les tableaux. Ainsi pour allouer un tableau de n entiers (n étant connu à l'exécution), on déclare une variable de type pointeur sur entier à laquelle on alloue une zone mémoire correspondant à n entiers :
int * alloue_tableau(int n, size_t taille)
 return malloc(n * taille);
/* Ailleurs dans le programme */
int * tableau = alloue_tableau(256, sizeof *tableau);
if (tableau != NULL)
 /* opérations sur le tableau */
 free( tableau );
Cet exemple alloue un tableau de 256 cases. Bien que la variable soit un pointeur, il est dans ce cas permis d'accéder aux cases de 0 à 255, soit entre les adresses codice_31 et codice_32, incluses.
Tableaux dynamiques à deux dimensions.
Tout comme on pouvait allouer des tableaux statiques à plusieurs dimensions, on peut allouer des tableaux dynamiques à plusieurs dimensions. Pour ce faire, on commence là-aussi par déclarer un pointeurs approprié : un pointeur sur des pointeurs (etc.) sur des types. Pour déclarer un tableau dynamique d'entiers à deux dimensions :
int ** matrice;
L'allocation d'un tel objet va se dérouler en plusieurs étapes (une par étoile), on alloue d'abord l'espace pour un tableau de pointeurs vers entier. Ensuite, on alloue pour chacun de ces tableaux l'espace pour un tableau d'entiers. Si on veut une matrice 4x5 :
int i;
int ** matrice = malloc(sizeof *matrice * LIGNES);
for (i = 0; i < LIGNES; i++)
 matrice[i] = malloc(sizeof **matrice * COLONNES);
Il ne faut jamais oublier de libérer la mémoire allouée précédemment. Ainsi, à tout appel de codice_33 doit correspondre un appel de codice_34
Pour libérer l'espace alloué ci-dessus, on procède de manière inverse, en commençant par libérer chacune des lignes du tableau, puis le tableau lui même :
for(i = 0; i < LIGNES; i++)
 free(matrice[i]);
free(matrice);
Tableaux dynamiques à trois dimensions.
Voici maintenant un exemple d'une fonction créant un tableau à trois dimensions avec surtout, et c'est très important, les tests des valeurs de retour des fonctions codice_33 :
int *** malloc_3d(int nb_tableau, int lignes, int colonnes)
 int i;
 int j;
 int***t = malloc(sizeof(*t) * nb_tableau); 
 /* première dimension */
 if (t==NULL) 
 printf ("Impossible d'initialiser avec malloc\n" ); 
 exit (-1);