Comprendre les pointeurs en C
Dans le cadre de ma formation, je suis dans l'obligation de faire du C/C++. Super me direz-vous car n'ayant presque jamais fait de C de ma vie, je me lance maintenant.
Bon, je ne suis pas ici pour vous expliquer les paradigmes de la programmation avec le langage C ou C++, je vais simplement me contenter du minimum compréhensible en considérant que vous avez déjà de bonnes bases de programmation ou développement. Rien ne sera bien compliqué mais je ne vais pas rédiger des pâtés parce que j'ai pas le temps et en plus j'aurais peur de me tromper si je rentre trop dans les détails.
Si vous n'avez pas les bases nécessaires pour comprendre cet article, je ne peux que vous recommander de lire le cours de C publié sur le Site du Zéro (j'en suis encore).
1. Dire bonjour
Nous allons simplement développer un petit programme en C pur pour afficher un « Hello world! » dans la console comme ceci :
% ./hello
Hello world!
Le compilateur
Rien de bien insurmontable... Ouvrez votre éditeur de texte préféré (il n'est pas nécessaire ici d'avoir un IDE car nous allons compiler notre programme à la main avec un makefile.
Nous allons développer notre programme avec un éditeur type bloc-note, pas besoin de beaucoup plus. Cependant, assurez-vous que vous avez bien le compilateur GCC sur votre machine et si possible dans une version assez récente. Si vous utilisez un système basé sur Unix, vous devriez l'avoir par défaut.
Le code source
Nous avons besoin de créer les fichiers main.c
, hello.c
et hello.h
qui sont respectivement : le code source du programme principal, le code source de notre programme « Hello world » et les en-têtes de ce même programme.
/*
hello.h
*/
#ifndef HELLO
#define HELLO
void Hello();
#endif
/*
hello.c
*/
#include <stdio.h>
#include <stdlib.h>
void Hello() {
printf("Hello world!\n");
}
/*
main.c
*/
#include <stdio.h>
#include <stdlib.h>
#include "hello.h"
int main() {
Hello();
return EXIT_SUCCESS;
}
Bon, nous avons notre code source. Pour voir les rapports entre les fichiers, lisez et essayez de comprendre les relations de dépendance avec les inclusions (#include
) en début de fichier.
Le fichier principal est toujours main.c
.
J'aurais très bien pu directement déclarer la fonction dans le fichier main.c
en amont de la fonction principale main()
mais prenons immédiatement de bonnes habitudes pour la suite. Et puis, cela vous permettra de créer un makefile plus riche.
Faire un makefile
Typiquement, le Makefile
sera exécuté par votre compilateur. Il permet de donner les instructions de dépendance de compilation de votre programme.
Ce fichier n'est pas compliqué en soit mais encore une fois, je ne vais pas vous faire un cours détaillé sur la création de ce genre de fichiers.
Voici donc un fichier Makefile
qui sera le bon pour notre programme :
CC=gcc
CFLAGS=-W -Wall -ansi
LDFLAGS=
EXEC=hello
all: $(EXEC)
hello: hello.o main.o
$(CC) -o $@ $^ $(LDFLAGS)
main.o: hello.h
%.o: %.c
$(CC) -o $@ -c $< $(CFLAGS)
clean:
rm -rf *.o
mrpropper: clean
rm -rf $(EXEC)
Si vous n'êtes pas du tout familier avec le précédent Makefile
, je vous invite à lire ce cours, que j'ai lu pas plus tard qu'hier ^^.
Supposez que vous avez créé un répertoire hellow
dans votre dossier « home » (~/hellow/
). Placez tous les précédents fichiers dans ce répertoire et attribuez un chmod u+x makefile
pour rendre votre Makefile
exécutable.
Exécutez votre programme
Ouvrez donc votre Terminal et tapez donc make
(vous compilez le programme) :
% make
gcc -o main.o -c main.c -W -Wall -ansi
gcc -o hello hello.o main.o
Ensuite, lancez votre programme via la commande ./hello
:
% ./hello
Hello world!
Hop, voila... Ça marche !
Si vous n'avez pas saisi ce qu'il vient de se passer, je ne vous recommande pas de continuer la lecture.
Toujours vivant ? ^^
Nous allons maintenant essayer de comprendre comment fonctionne les pointeurs ; qu'est-ce donc et comment les utiliser.
2. Les pointeurs
Les pointeurs sont en fait des variables spéciales qui permettent de pointer sur une adresse mémoire. Dans les langages de plus haute niveau, on parlera de référence.
RAPPEL : lorsque vous déclarez une variable, un espace mémoire lui est alloué ; cet espace correspond en fait à une adresse (l'index d'un graaaaaand tableau) mémoire. C'est avec cette adresse que les pointeurs vont travailler.
Un pointeur est donc une variable spéciale qui prendra une adresse mémoire comme valeur.
Mise en application
Pour illustrer ce que je viens d'écrire, je vous propose de mettre en place une sorte de banc d'essai mais écrit en C. Maintenant que vous savez écrire des programmes basiques, n'allons pas perdre la main.
Reprenons notre main.c et faites les modifications nécessaires pour obtenir ceci :
/*
main.c
*/
#include <stdio.h>
#include <stdlib.h>
#include "pointer.h"
int main() {
system("clear"); /* Efface la console */
init();
return EXIT_SUCCESS;
}
Le fichier pointer.h
suivi du fichier pointer.c
:
/*
pointer.h
*/
#ifndef POINTER
#define POINTER
void init();
#endif
/*
pointer.c
*/
#include <stdio.h>
#include <stdlib.h>
#include "pointer.h"
void init() {
/*
Déclaration des variables nécessaires
*/
int variable;
int *pointeur, **pointeur2, ***pointeur3;
/*
Travail sur variable
*/
printf("-----------------\n VARIABLES\n-----------------\n\n\t");
printf("Déclaration: « int variable; »\n\n\t");
printf("Adresse mémoire: « printf(\"%%p\", &variable;); »\n\t\tRésultat: %p\n\n\t", &variable;);
printf("Valeur: « printf(\"%%d\", variable); »\n\t\tRésultat: %d\n\n\t", variable);
printf("Explications: une variable non initialisée prend une valeur aléatoire.\n\n");
printf("Appuez sur ENTREE pour la suite...");
getchar();
/*
Travail sur un pointeur
*/
printf("\n\n-----------------\n POINTEURS\n-----------------\n\n\t");
printf("Déclaration: « int *pointeur, **pointeur2, ***pointeur3; »\n\n\t");
printf("Adresses mémoire: « printf(\"p=> %%p, p2=> %%p, p3=> %%p\", &pointeur;, &pointeur2;, &pointeur3;); »\n\t\tRésultat: p=> %p, p2=> %p, p3=> %p\n\n\t", &pointeur;, &pointeur2;, &pointeur3;);
printf("Valeurs: « printf(\"p=> %%p, p2=> %%p, p3=> %%p\", pointeur, pointeur2, pointeur3); »\n\t\tRésultat: p=> %p, p2=> %p, p3=> %p\n\n\t", pointeur, pointeur2, pointeur3);
printf("Explications: un pointeur est une variable qui a une adresse mémoire comme valeur.\n\n");
printf("Appuez sur ENTREE pour la suite...");
getchar();
/*
Tests sur pointeurs
*/
printf("\n\n-----------------\n TESTS\n-----------------\n\n\t");
printf("Assignation d'une valeur à variable et affichage: « printf(\"%%d\", variable = 16); »\n\t\t");
printf("Résultat: %d\n\n\t", variable = 16);
printf("Assignation de variable au pointeur: « pointeur = &variable; »\n\t");
printf("Assignation de pointeur à pointeur2: « pointeur2 = &pointeur; »\n\t");
printf("Assignation de pointeur2 à pointeur3: « pointeur3 = &pointeur2; »\n\n\t");
pointeur = &variable;
pointeur2 = &pointeur;
pointeur3 = &pointeur2;
printf("Affichage de la valeur de variable via pointeur: « printf(\"%%d\", *pointeur); »\n\t\t");
printf("Résultat: %d\n\n\t", *pointeur);
printf("Affichage de la valeur de variable via pointeur2: « printf(\"%%d\", **pointeur2); »\n\t\t");
printf("Résultat: %d\n\n\t", **pointeur2);
printf("Affichage de la valeur de variable via pointeur3: « printf(\"%%d\", ***pointeur3); »\n\t\t");
printf("Résultat: %d\n\n\t", ***pointeur3);
printf("Autres tests:\n\t");
printf("« printf(\"%%p\", *&pointeur;); » affiche %p\n\t", *&pointeur;);
printf("« printf(\"%%p\", &*pointeur); » affiche %p\n\t\t", &*pointeur);
printf("-> donc quelque soit l'ordre des symboles: « &*pointeur <=> pointeur »\n\n\t");
printf("« printf(\"%%p\", &*pointeur2); » affiche %p\n\t", &*pointeur2);
printf("« printf(\"%%p\", **&pointeur2;); » affiche %p\n\t\t", **&pointeur2;);
printf("-> le nombre d'étoiles détermine la profondeur d'accès aux pointeurs\n\n");
}
Résultat : la révélation sur les pointeurs !
Comme vous pouvez voir, j'ai fait ce petit programme de manière assez verbeuse afin que vous puissiez comprendre comment est-ce que cela fonctionne. Il faudra aussi modifier votre makefile en remplaçant tous les « hello » par des « pointer ».
Après compilation, exécutez votre programme et vous devriez obtenir ceci :
% ./pointer
-----------------
VARIABLES
-----------------
Déclaration: « int variable; »
Adresse mémoire: « printf("%p", &variable;); »
Résultat: 0xbffff5ac
Valeur: « printf("%d", variable); »
Résultat: 6336
Explications: une variable non initialisée prend une valeur aléatoire.
Appuez sur ENTREE pour la suite...
-----------------
POINTEURS
-----------------
Déclaration: « int *pointeur, **pointeur2, ***pointeur3; »
Adresses mémoire: « printf("p=> %p, p2=> %p, p3=> %p", &pointeur;, &pointeur2;, &pointeur3;); »
Résultat: p=> 0xbffff5a8, p2=> 0xbffff5a4, p3=> 0xbffff5a0
Valeurs: « printf("p=> %p, p2=> %p, p3=> %p", pointeur, pointeur2, pointeur3); »
Résultat: p=> 0x925a0b2f, p2=> 0x1ff9, p3=> 0x0
Explications: un pointeur est une variable qui a une adresse mémoire comme valeur.
Appuez sur ENTREE pour la suite...
-----------------
TESTS
-----------------
Assignation d'une valeur à variable et affichage: « printf("%d", variable = 16); »
Résultat: 16
Assignation de variable au pointeur: « pointeur = &variable; »
Assignation de pointeur à pointeur2: « pointeur2 = &pointeur; »
Assignation de pointeur2 à pointeur3: « pointeur3 = &pointeur2; »
Affichage de la valeur de variable via pointeur: « printf("%d", *pointeur); »
Résultat: 16
Affichage de la valeur de variable via pointeur2: « printf("%d", **pointeur2); »
Résultat: 16
Affichage de la valeur de variable via pointeur3: « printf("%d", ***pointeur3); »
Résultat: 16
Autres tests:
« printf("%p", *&pointeur;); » affiche 0xbffff5ac
« printf("%p", &*pointeur); » affiche 0xbffff5ac
-> donc quelque soit l'ordre des symboles: « &*pointeur <=> pointeur »
« printf("%p", &*pointeur2); » affiche 0xbffff5a8
« printf("%p", **&pointeur2;); » affiche 0xbffff5ac
-> le nombre d'étoiles détermine la profondeur d'accès aux pointeurs
N'est pas magnifique ?
Mais attendez, ça n'est pas fini... Pourquoi ne pas appliquer les pointeurs sur des fonctions ?
3. Les pointeurs de fonctions
Avant même de rentrer dans le vif du sujet avec les pointeurs de fonctions, il faut savoir utiliser les pointeurs DANS les fonctions.
Si vous programmez ou développez avec un langage de plus haut niveau, vous avez probablement utilisé les références en paramètre de fonction ou dans des algorithmes récursifs.
En C, c'est pareil. Pour demander à ce qu'un paramètre de fonction soit un pointeur, on déclare la fonction comme ceci : void fonction(int *pointeur);
. Vous n'êtes pas obligé de typer votre paramètre en « int », vous devez le typer de la nature de votre pointeur qui lui-même devrait être typé en fonction de la cible dont il pointe.
Mise en application
Comme pour le programme précédent, vous devez créer de nouveaux fichiers et mettre à jour votre main.c
et Makefile
si vous avez choisi d'en garder qu'un seul.
Voici donc la nouvelle série de fichiers source :
/*
main.c
*/
#include <stdio.h>
#include <stdlib.h>
#include "function.h"
int main() {
system("clear");
init();
return EXIT_SUCCESS;
}
/*
function.h
*/
#ifndef FUNCTION
#define FUNCTION
void send_addr(int *);
int x3(int *);
int x2(int *);
void init();
#endif
/*
function.c
*/
#include <stdio.h>
#include <stdlib.h>
#include "function.h"
void send_addr(int *addr) {
x2(addr);
}
int x3(int *input) {
return *input *= 3;
}
int x2(int *input) {
return *input *= 2;
}
void init() {
int nb = 3;
int *pt = &nb;, (*fct)(int *) = &x3;
/* On affiche les informations de la variable */
printf("Addr nb\t\t: %p\nVal nb\t\t: %d\n\n", &nb;, nb);
/* On effectue une opération sur la variable en passant un pointeur en paramètre de fonction */
send_addr(pt);
/* On affiche les informations sur le pointeur */
printf("Addr pt\t\t: %p\nVal pt\t\t: %p\nVal targ pt\t: %d\n\n", &pt;, pt, *pt);
/* On effectue une opération sur la variable via son pointeur et une fonction-pointeur */
fct(pt);
/* On affiche les informations sur le pointeur */
printf("Addr pt\t\t: %p\nVal pt\t\t: %p\nVal targ pt\t: %d\n\n", &pt;, pt, *pt);
}
J'ai ici condensé dans cet exemple l'utilisation des pointeurs en temps que paramètre mais aussi la notion de pointeurs de fonctions.
La ligne int *pt = &nb, (*fct)(int *) = &x3;
permet de :
- Déclarer le pointeur
pt
et de lui assigner l'adresse mémoire denb
; - Déclarer le pointeur de fonction
fct
et de lui assigner la fonctionx3(int *)
.
Q : Pourquoi avoir écrit (*fct)(int *)
et non pas *fct(int*)
?
R : C'est ici une question de priorité dans le sens de l'analyse du langage. Comme en mathématiques, les parenthèses sont interprétées prioritairement lors de la lecture des termes. Si vous n'encadrez pas votre *fct
de parenthèses, alors la déclaration de pointeur de fonction échouera et votre compilateur vous crachera une belle erreur :
% make
gcc -o function.o -c function.c -W -Wall -ansi
function.c: In function ‘init’:
function.c:22: error: function ‘fct’ is initialized like a variable
function.c:22: error: nested function ‘fct’ declared but never defined
make: *** [function.o] Error 1
zsh: exit 2 make
Note : la référence sur &x3;
n'est pas obligatoire car une fonction renvoie par défaut son adresse mémoire (ou du moins l'adresse mémoire de sa première ligne d'instruction, un peu comme pour les tableaux) mais il est préférable de le laisser pour plus de clarté.
Résultat
Après un petit make suivi d'un ./function
, vous devriez obtenir ceci :
Addr nb : 0xbffff598
Val nb : 3
Addr pt : 0xbffff594
Val pt : 0xbffff598
Val targ pt : 6
Addr pt : 0xbffff594
Val pt : 0xbffff598
Val targ pt : 18
Chers lecteurs, vous venez de voir ce que sont les pointeurs. Comme quoi, ça n'est pas si sorcier que cela si ce n'est que d'avoir les idées claires.
4. Conclusion
Un petit mémo pour rappeler tous les excercices vus :
/* DECLARATION */
int variable = 1; /* Déclare une variable et assigne sa valeur à 1 */
int *pointeur = &variable; /* Déclare un pointeur et assigne sa valeur à l'adresse de variable */
int **pointeur2 = &pointeur; /* Déclare un pointeur de pointeur et lui assigne l'adresse de pointeur */
int (*fonction)() = &ma;_fonction; /* Déclare un pointeur de fonction et assigne sa valeur à l'adresse de ma_fonction */
char (*fonction2)(int *, char) = &autre;_fonction; /* Déclare un pointeur de fonction et assigne sa valeur à l'adresse de autre_fonction sachant que autre fonction à défini 2 paramètres */
/* UTILISATION */
printf("%d", variable); /* Affiche 1 */
printf("%d", *pointeur); /* Affiche 1 */
printf("%d", **pointeur2); /* Affiche 1 */
printf("%p", &variable;); /* Affiche l'adresse de variable */
printf("%p", pointeur); /* Affiche l'adresse de variable */
printf("%p", &*pointeur); /* Affiche l'adresse de variable */
printf("%p", &**pointeur2); /* Affiche l'adresse de variable */
printf("%p", &pointeur;); /* Affiche l'adresse de pointeur */
printf("%p", pointeur2); /* Affiche l'adresse de pointeur */
printf("%p", &*pointeur2); /* Affiche l'adresse de pointeur */
fonction(); /* Appelle ma_fonction */
fonction2(pointeur, "f"); /* Appelle autre_fonction */
Merci beaucoup d'en être arrivé là, comme vous avez peut-être remarqué, il m'a fallu plusieurs jours pour rédiger cet article alors n'hésitez pas à écrire vos impressions et puis, si vous voulez en savoir un peu plus, lisez ce tutoriel.