4 astuces pour comprendre les pointeurs en C

4 astuces pour comprendre les pointeurs en C

Les pointeurs. Le sujet semble épineux et l’utilité assez flou quand on débute le C mais à la fin de cet article vous comprendrez un peu mieux comment et où les utiliser.

I – La mémoire

La mémoire n’est pas la chose la plus compliquée à comprendre. On peut représenter ça sous la forme d’une grille où chaque case est une donnée (plus précisément un octet). Pour accéder à une valeur, on peut se servir de son adresse. Lorsque l’on définit un pointeur, on définit en fait un nombre, qui va correspondre à une adresse.

char      *address;

ici, on déclare une variable “address” qui va contenir l’adresse (le petit astérisque) d’un caractère (char). Le problème c’est que lorsque l’on déclare cette adresse, elle ne correspond à rien, ou plutôt un nombre aléatoire qui ne vous appartient sûrement pas. Pensez donc à assigner la valeur “NULL” à votre pointeur avant de l’utiliser. “NULL” correspond à l’adresse 0 de votre mémoire. On va désormais passer à l’étape de l’allocation de mémoire, la fonction malloc.

II – Malloc

Je me souviens que les premiers jours où j’utilisais cette fonction, tout me paraissait flou. Je vais essayer de vous faire comprendre au maximum ce que fait cette fonction aux allures très complexes (personnellement j’avais des appréhensions et je vais essayer de faire disparaître les vôtres). Accrochez-vous, c’est parti !

Comme je vous l’ai dit un peu plus haut, lorsque vous déclarez votre pointeur, il pointe vers une case de mémoire qui ne vous appartient pas. Malloc va vous permettre d’obtenir un certain nombre de cases en mémoire, qui vous appartiendront, que vous pourrez modifier, utiliser puis libérer (je reviendrais plus tard sur la libération).

#include  <stdio.h>
#include  <stdlib.h>

int       main()
{
char      *address;

address = NULL;
printf("Après l'initialisation : %X\n", address);
address = malloc(1);
printf("Après le malloc : %X\n", address);
}

Essayez d’exécuter ce code dans votre fonction main par exemple (en n’oubliant pas les lignes d’include au début du fichier pour utiliser respectivement printf et malloc).
Félicitations, vous venez d’acquérir votre premier octet de mémoire ! Bon j’avoue que ce n’est pas hyper fun et il y a quelques erreurs dans ce code mais nous allons tout de suite corriger le tir et lorsque l’allocation sera terminée, nous pourrons aller modifier la valeur à cette adresse.

La première erreur c’est la non-vérification d’un appel système. En effet, malloc est une fonction qui peut “rater”. Le premier réflexe à avoir lorsque l’on utilise une nouvelle fonction c’est de regarder ce que la fonction renvoie en cas d’erreur. Pour cela, vous devez lire les man disponible directement dans votre terminal en écrivant “man malloc” ou en regardant sur internet. La section “RETURN” va vous informer de ce que renvoie la fonction. Pour malloc on peut voir : “On error, these functions return NULL.”, on va donc vérifier que le résultat est différent de NULL, sinon on quitte le programme. N’oubliez pas qu’il faut toujours vérifier les appels systèmes. (petit tips : utilisez les premières lignes du man pour savoir quelles includes ajouter au début de votre programme pour pouvoir utiliser la fonction).

La seconde erreur c’est la taille du malloc. Ici nous avons alloué un octet. Par chance, un char correspond à un octet. Imaginons maintenant que nous voulions allouer des “cases de mémoire” pour un entier, soit 4 octets. La solution pourrait être d’écrire ‘4’ au lieu de ‘1’ mais il existe un mot-clef qui peut connaitre la taille d’un élément : sizeof(). Dans notre exemple, notre malloc ressemblerait à ceci :

address = malloc(sizeof(*address));

je ne vous ai pas encore parlé du “Comment accéder à notre case mémoire” mais c’est un point primordial dans la compréhension des pointeurs. Lorsque j’alloue un espace mémoire, imaginons 10 octets, je peux accéder à chaque octet un à un. Par exemple si je veux accéder à la 3ème case, j’écris address[2] (la première case étant la case numéro 0). Pour accéder à la première case, je peux écrire address[0] ou *address, *address faisant référence à la case ciblé par le pointeur donc la première. Dans le sizeof, en écrivant *address, je vais récupérer la taille du premier élément. Si c’est un char, sizeof me donnera 1, si c’est un int, sizeof me donnera 4. Ainsi, si je change mon char *address en int *address, le malloc restera fonctionnel.

Une dernière chose : si je veux allouer plusieurs cases, je fais comment ? Je multiplie sizeof(*address) par le nombre d’éléments voulus. Imaginons une chaîne de caractère, par exemple “Warrior”, nous avons 7 éléments donc on va allouer de sizeof(*address) * (7 + 1). Le “+ 1” correspond au caractère ‘\0’ toujours présent à la fin d’une chaîne et permettant de savoir où elle se termine.

Ainsi si l’on réécrit le code en réglant les erreurs, on obtient :

char      *address;

address = NULL;
address = malloc(sizeof(*address) * (7 + 1));
if (address == NULL)
  return (1);
return (0);

Voilà. Vous venez d’allouez votre première chaîne de caractère.

N’essayez pas de remplir vos cases en faisant ‘address = “Warrior”;’, souvenez-vous que address est juste un nombre correspondant à une adresse et que pour copier une chaîne dans un espace alloué, il faut parcourir les cases une à une en donnant une valeur à chaque case. Un petit exercice serait de recoder la fonction strdup. Cette fonction prend en paramètre une chaîne qu’on peut considérer comme statique n’étant pas alloué dynamiquement avec malloc, et renvoie l’adresse d’un pointeur vers une copie de cette chaîne (man strdup pour regarder le prototype de la fonction).

Vous pourrez donc faire “address = test_strdup(“Warrior”);” et vous aurez un pointeur vers une copie de votre chaîne de caractère  “Warrior” que vous pourrez modifier à votre guise.

III – Libérer la mémoire

Quelque chose que les gens oublient souvent lorsqu’on leur donne la mémoire, c’est de la rendre lorsqu’ils n’en ont plus besoin. On appelle cela des “fuites mémoire”.

Pour éviter ces fuites, il suffit d’appeler la fonction free() lorsque le pointeur n’est plus utilisé. Attention, il est impossible de free un pointeur qui a été “déplacé” (si vous tenez absolument à incrémenter la valeur de votre pointeur pour vous déplacer dans la chaîne, faites le sur une copie du pointeur pour sauvegarder le premier élément et pouvoir free votre pointeur initial).

IV – Envoyer l’adresse d’une variable pour la modifier dans une autre fonction

Utiliser les pointeurs n’implique pas forcément de “malloc”. En effet, on peut très bien déclarer un entier et envoyer son adresse à une fonction. Le prototype de la fonction va donc contenir un pointeur vers un entier et lors de l’appel, il faudra donner l’adresse de l’entier, grâce au symbole &. Voici un exemple.

#include    <stdio.h>

void        increment(int *nb)
{
   *nb = *nb + 1;
}

int         main()
{
  int       i;

  i = 0;
  printf("%d\n", i);
  increment(&i);
  printf("%d\n", i);
}

Et voilà, vous venez de changer la valeur d’un entier dans une autre fonction grâce à son adresse.

Si vous arrivez à cet endroit c’est que vous avez eu le courage de lire tout cet article sur une notion assez compliquée (sûrement la plus compliquée en C). N’hésitez pas à relire cet article, voir même à me contacter si vous avez des questions ou que vous voulez quelques idées d’exercices sur une notion de C qui vous dérange. Mon dernier conseil serait de pratiquer encore et encore et les pointeurs ne seront plus un problème pour vous.

RSS
Facebook
Facebook

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Tu as aimé l'article? Partage le blog !