Des pointeurs et des hommes (2)
Et nous voilà repartis pour un nouveau chapitre dans notre découverte des pointeurs ! Maintenant que vous avez compris ce qu’est au final un pointeur, j’espère que tout cela vous fait beaucoup moins peur sur le côté théorique de la chose … Et oui, tout ça pour ça !
Pourtant le pointeur, malgré sa simplicité, est un concept fondamental du C. C’est vraiment l’un des points du langage qui le rendent, à mon humble avis, si proche de la machine et si passionnant ! Je vous propose donc de continuer notre parcours en regardant comment on les utilise sur un petit exemple de fonction.
Une fonction pourtant si simple …
Imaginons que nous écrivions un petit programme appelant une fonction d’affichage d’entier, que nous appellerons dis_moi_des_mots_doux:
#include <stdio.h>
void dis_moi_des_mots_doux( int ) ;
int
main()
{
int A ;
A = 69 ;
dis_moi_des_mots_doux( A ) ;
return 0 ;
}
void
dis_moi_des_mots_doux( int n )
{
printf("Oh ... un %d ! Comme c'est gentil !!!\n", n ) ;
}
On peut vérifier que notre fonction marche parfaitement :
$gcc -o thefunk main.c
$./thefunk
Oh ... un 69 ! Comme c'est gentil !!!
La valeur 69 est parfaitement transmise à la fonction dis_moi_des_mots_doux, qui l’affiche. Jusque là tout va bien !
Fort de ce succès, nous décidons d’ajouter la fonction remise_a_zero qui, comme vous l’aurez sûrement deviné, devra remettre à zéro la variable transmise. La plupart des débutants vont écrire une fonction ressemblant à celle-ci :
#include <stdio.h>
void dis_moi_des_mots_doux( int ) ;
void remise_a_zero(int) ;
int
main()
{
int A ;
A = 69 ;
dis_moi_des_mots_doux( A ) ; // J'affiche A
remise_a_zero( A ) ; // Je remets A à zéro
dis_moi_des_mots_doux( A ) ; // Je réaffiche A
return 0 ;
}
void
dis_moi_des_mots_doux( int n )
{
printf("Oh ... un %d ! Comme c'est gentil !!!\n", n ) ;
}
void
remise_a_zero( int k )
{
k = 0 ;
}
Malheureusement, lorsque l’on teste cette fonction, notre déception est immense :
$gcc -o thefunk_v2 main_v2.c
$./thefunk_v2
Oh ... un 69 ! Comme c'est gentil !!!
Oh ... un 69 ! Comme c'est gentil !!!
A n’a pas changé de valeur.
Et c’est tout à fait normal !!! Car A elle-même n’est jamais transmis lors de l’appel de la fonction. C’est une copie de sa valeur qui est envoyée à la fonction.
Pour expliquer ce qui se passe, je vous propose deux manières de voir la chose : graphiquement, et en assembleur. Vous allez voir que c’est très intuitif.
Lorsqu’on appelle une fonction, les opérations suivantes sont effectuées, dans l’ordre :
- On évalue l’expression entre parenthèses ( C’est à dire on cherche sa valeur numérique )
- On saute à l’emplacement de la fonction
- Une variable locale est créée ( ici n dans la fonction dis_moi_des_mots_doux )
- La variable est initialisée avec notre valeur numérique.
Graphiquement, pour le premier appel de fonction, cela donne l’exécution suivante :
Le problème, vous vous en doutez, est à l’appel de la fonction remise_a_zero, qui essaie de modifier A.
Voici les étapes suivantes de l’exécution de notre programme :
Et oui, étant donné que seule une copie de la valeur contenue dans A est transmise à remise_a_zero, la variable A reste inchangée. Cela ne peut pas fonctionner de cette façon ! (trop dur pour nous !)
Voyons maintenant “the real thing”, et plongeons nous dans l’assembleur. Si cette étape vous fait peur, vous pouvez la sauter sans aucun problème : rendez-vous au chapitre suivant, un peu plus loin !
Compilons, désassemblons, et visualisons le code de notre programme :
$gcc -g -o thefunk_v2 mainv2.c --static
$objdump -S ./thefunk_v2
( Si vous n’avez pas les outils nécessaires, vous pouvez récupérer ce code assembleur ici
[...]
int
main()
{
40174d: 55 push %rbp
40174e: 48 89 e5 mov %rsp,%rbp
401751: 48 83 ec 10 sub $0x10,%rsp
int A ;
A = 69 ;
401755: c7 45 fc 45 00 00 00 movl $0x45,-0x4(%rbp)
dis_moi_des_mots_doux( A ) ; // J'affiche A
40175c: 8b 45 fc mov -0x4(%rbp),%eax
40175f: 89 c7 mov %eax,%edi
401761: e8 1b 00 00 00 call 401781 <dis_moi_des_mots_doux>
remise_a_zero( A ) ; // Je remets A à zéro
401766: 8b 45 fc mov -0x4(%rbp),%eax
401769: 89 c7 mov %eax,%edi
40176b: e8 35 00 00 00 call 4017a5 <remise_a_zero>
dis_moi_des_mots_doux( A ) ; // Je réaffiche A
401770: 8b 45 fc mov -0x4(%rbp),%eax
401773: 89 c7 mov %eax,%edi
401775: e8 07 00 00 00 call 401781 <dis_moi_des_mots_doux>
return 0 ;
40177a: b8 00 00 00 00 mov $0x0,%eax
}
40177f: c9 leave
401780: c3 ret
0000000000401781 <dis_moi_des_mots_doux>:
void
dis_moi_des_mots_doux( int n )
{
401781: 55 push %rbp
401782: 48 89 e5 mov %rsp,%rbp
401785: 48 83 ec 10 sub $0x10,%rsp
401789: 89 7d fc mov %edi,-0x4(%rbp)
printf("Oh ... un %d ! Comme c'est gentil !!!\n", n ) ;
40178c: 8b 45 fc mov -0x4(%rbp),%eax
40178f: 89 c6 mov %eax,%esi
401791: 48 8d 3d 70 f8 07 00 lea 0x7f870(%rip),%rdi # 481008 <_IO_stdin_used+0x8>
401798: b8 00 00 00 00 mov $0x0,%eax
40179d: e8 5e 84 00 00 call 409c00 <_IO_printf>
}
4017a2: 90 nop
4017a3: c9 leave
4017a4: c3 ret
00000000004017a5 <remise_a_zero>:
void
remise_a_zero( int k )
{
4017a5: 55 push %rbp
4017a6: 48 89 e5 mov %rsp,%rbp
4017a9: 89 7d fc mov %edi,-0x4(%rbp)
k = 0 ;
4017ac: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
}
4017b3: 90 nop
4017b4: 5d pop %rbp
4017b5: c3 ret
[...]
A étant une variable locale, elle est créée dans la pile. Ceci est effectué en agrandissant ladite pile grâce à la l’instruction suivante :
401751: 48 83 ec 10 sub $0x10,%rsp
La pile est augmentée de 16 octets en changeant la valeur de %rsp ( le stack pointer ). Ceci est largement suffisant pour stocker un int (4 octets).
La valeur 69 en (0x45 en hexadécimal) est ensuite stockée dans A, qui est à l’adresse rbp-4 (donc dans la pile, et plus spécifiquement dans le frame de main():
401755: c7 45 fc 45 00 00 00 movl $0x45,-0x4(%rbp)
Voyons maintenant comment se fait l’appel de la fonction remise_a_zero dans la fonction main :
401766: 8b 45 fc mov -0x4(%rbp),%eax
401769: 89 c7 mov %eax,%edi
40176b: e8 35 00 00 00 call 4017a5 <remise_a_zero>
La valeur de A est tout d’abord chargée dans le registre %eax depuis la pile. Elle est ensuite copiée dans le registre %edi puis l’exécution “saute” à l’adresse 4017a5, où se trouve la fonction remise_a_zero.
Ce passage dans %eax puis dans %edi peut surprendre : pourquoi ne pas mettre directement la valeur de A dans %edi ? Et bien tout simplement parce que l’expression entre parenthèses est d’abord évaluée, et ensuite seulement l’appel se fait.
Ainsi, on pourrait avoir des choses comme :
remise_a_zero( 2*A+3 ) ;
Dans ce cas, il faudrait d’abord évaluer l’expression (2*A+3), puis appeler remise_a_zero. Le compilateur a donc généré un code en deux étapes :
- Evaluation de l’expression avec %eax (ici elle est triviale)
- Appel de la fonction ( La norme nous dit que le premier argument doit être mise dans %edi )
Quoi qu’il en soit, ni A ni sa position en mémoire ne sont transmises à la fonction : seulement une copie de sa valeur !
Pointeurs et fonctions
Les choses sont un peu plus claires maintenant : la fonction remise_a_zero ne fonctionne pas parce qu’aucune variable n’est “transmise” : seulement une copie de sa valeur.
C’est là que les pointeurs entrent en jeu : au lieu de transmettre la valeur de A, nous allons transmettre son adresse, afin que la fonction appelée puisse savoir où écrire. Et les adresses, nous les manipulons avec ce nouveau type de variable que nous avons vu la dernière fois : le pointeur !
Allons-y et modifions le main(), afin de transmettre l’adresse de A à notre fonction. On utilisera tout simplement l’opérateur & que nous avions vu la dernière fois :
int
main()
{
int A ;
A = 69 ;
dis_moi_des_mots_doux( A ) ;
remise_a_zero( &A ) ; // <= La magie est ici !!!
dis_moi_des_mots_doux( A ) ;
return 0 ;
}
Puisque nous ne transmettons plus un entier mais une adresse d’entier, il va nous falloir adapter remise_a_zero :
void
remise_a_zero( int* k)
{
*k = 0 ; // Ne pas oublier l'opérateur * ici !!!
}
La variable locale k est désormais un pointeur, qui contiendra donc une adresse.
Pour écrire un zéro à cette adresse, on utilise l’opérateur * comme vu la dernière fois. Encore une fois, ceci veut dire :
- Je lis k
- k contient une adresse
- J’écris 0 à cette adresse
Le code complet, ça donne ça :
#include <stdio.h>
void dis_moi_des_mots_doux( int ) ;
void remise_a_zero(int*) ;
int
main()
{
int A ;
A = 69 ;
dis_moi_des_mots_doux( A ) ;
remise_a_zero( &A ) ;
dis_moi_des_mots_doux( A ) ;
return 0 ;
}
void
dis_moi_des_mots_doux( int n )
{
printf("Oh ... un %d ! Comme c'est gentil !!!\n", n ) ;
}
void
remise_a_zero( int* k )
{
*k = 0 ;
}
Quoi ? Vous voulez un autre petit dessin ? Vraiment ?
Mais heuuuuuuu … Ça prend trois plombes à faireeeeeeuuuuu !!!!!!!!!!!!!!
Bon, comme je vous aime bien, le voici :
Et ça devrait fonctionner ! On teste ça de suite :
$gcc -o thefunk_v3 main_v3.c
$./thefunk_v3
Oh ... un 69 ! Comme c'est gentil !!!
Oh ... un 0 ! Comme c'est gentil !!!
Je crois que nous pouvons appeler ceci une victoire !
Encore une fois, allons voir le code assembleur. Ceux qui veulent peuvent encore une fois sauter au prochain chapitre :)
[...]
int
main()
{
40174d: 55 push %rbp
40174e: 48 89 e5 mov %rsp,%rbp
401751: 48 83 ec 10 sub $0x10,%rsp
401755: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
40175c: 00 00
40175e: 48 89 45 f8 mov %rax,-0x8(%rbp)
401762: 31 c0 xor %eax,%eax
int A ;
A = 69 ;
401764: c7 45 f4 45 00 00 00 movl $0x45,-0xc(%rbp)
dis_moi_des_mots_doux( A ) ;
40176b: 8b 45 f4 mov -0xc(%rbp),%eax
40176e: 89 c7 mov %eax,%edi
401770: e8 31 00 00 00 call 4017a6 <dis_moi_des_mots_doux>
remise_a_zero( &A ) ;
401775: 48 8d 45 f4 lea -0xc(%rbp),%rax
401779: 48 89 c7 mov %rax,%rdi
40177c: e8 49 00 00 00 call 4017ca <remise_a_zero>
dis_moi_des_mots_doux( A ) ;
401781: 8b 45 f4 mov -0xc(%rbp),%eax
401784: 89 c7 mov %eax,%edi
401786: e8 1b 00 00 00 call 4017a6 <dis_moi_des_mots_doux>
return 0 ;
40178b: b8 00 00 00 00 mov $0x0,%eax
}
401790: 48 8b 55 f8 mov -0x8(%rbp),%rdx
401794: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
40179b: 00 00
40179d: 74 05 je 4017a4 <main+0x57>
40179f: e8 dc 62 04 00 call 447a80 <__stack_chk_fail>
4017a4: c9 leave
4017a5: c3 ret
[...]
void
remise_a_zero( int* k )
{
4017ca: 55 push %rbp
4017cb: 48 89 e5 mov %rsp,%rbp
4017ce: 48 89 7d f8 mov %rdi,-0x8(%rbp)
*k = 0 ;
4017d2: 48 8b 45 f8 mov -0x8(%rbp),%rax
4017d6: c7 00 00 00 00 00 movl $0x0,(%rax)
}
4017dc: 90 nop
4017dd: 5d pop %rbp
4017de: c3 ret
4017df: 90 nop
[...]
Si la création de A n’a pas beaucoup changée (A est toujours en pile), La pile contient maintenant également un canari. Ce n’est pas l’objet du cours d’aujourd’hui, on en fera donc abstration.
Par contre, l’affectation de la valeur 69 à A est toujours réalisée de la même façon :
401764: c7 45 f4 45 00 00 00 movl $0x45,-0xc(%rbp)
L’appel de la fonction remise_a_zero a cependant bien changé :
remise_a_zero( &A ) ;
401775: 48 8d 45 f4 lea -0xc(%rbp),%rax
401779: 48 89 c7 mov %rax,%rdi
40177c: e8 49 00 00 00 call 4017ca <remise_a_zero>
On utilise non plus l’instruction mov, mais l’instruction lea. Et celle-ci va stocker l’adresse de A (%rbp-0xc) dans %rax.
Vous le voyez, plus de mensonge : tout se passe bien comme prévu !
Le mot de la fin
Ce chapitre est déjà bien long, alors nous allons repousser quelques-uns des points que je voulais traiter ici à une prochaine fois. Pour conclure, je voudrais juste souligner combien le C peut être clair quand il s’agit des fonctions.
Si vous voyez une fonction déclarée de cette façon-ci :
int fonction_mystere ( int A, double* B, int *C ) ;
Vous savez qu’après un appel à cette fonction, le premier paramètre transmis n’a pas pû être modifié par celle-ci, alors que les deux suivants oui ! Cela permet de “compartimenter” ses variables, et de garder un certain control même si vous utilisez des fonctions en provenance de librairies obscures.
Par contre, il va falloir faire très attention à ne pas utiliser des adresses invalides et on a très vite fait de se tirer une balle dans le pied ! Les typedefs, notamment, peuvent cacher des pointeurs à votre regard aiguisé.
Je vous dis à bientôt, et n’hésitez pas à m’envoyer vos questions !
Rancune.