Banière du site

[koala01.free.fr]->Tutoriaux->Principes de Programmation ->Les pointeurs

Image d'imprimante   image d'enveloppe

16.1 Pointeur Kessako?

Les pointeurs sont des entités assez particulières.

Leur but n'est pas, comme les autres variables, de garder une équivalence nom=valeur, mais bien de garder l'«adresse mémoire à laquelle se trouve» une variable donnée.

fleche haut

16.2 Quel avantage?

Comme je l'ai déjà signalé, le gros problème auquel on se trouve confronté quand on veut créer une fonction, c'est qu'elle ne peut renvoyer qu'une seule donnée d'un type bien précis…

La grosse question est donc de savoir comment faire pour qu'une fonction soit en mesure de renvoyer une valeur (de réussite, par exemple) mais aussi de modifier la valeur d'une ou de plusieurs variables.

Bien sûr, il est toujours envisageable d'utiliser une variable globale, mais il est généralement déconseillé d'y avoir recours:

Par contre, si dans une fonction, on passe en parametre (nommons le P) «l'adresse à laquelle se trouve la variable» et que dans la fonction on utilise de manière systématique «l'adresse mémoire pointée par P», une fois sorti de la fonction, la variable dont l'adresse avait été donnée en parametre aura bel et bien été modifiée.

Un autre énorme avantage, c'est qu'ils permettent de faire des liens entre différentes valeurs dont les adresses sont inconnues car réservées de manière dynamique.

Mais ne brûlons pas les étapes, commençons par

fleche haut

16.3 la théorie des pointeurs

Un pointeur contiendra donc une donnée qui correspond à l'adresse mémoire à laquelle se trouve une autre donnée.

La premiere chose qu'il faut savoir, c'est que le type que l'on déclare pour un pointeur doit être le même (sauf cas exceptionnel) que le type de la variable dont on lui donne l'adresse.

Pour rappel, chaque type de donnée de base donne en fait une indication concernant le nombre de bits que la variable est supposée contenir…

Si donc, on déclare une variable de type "entier" (qui, classiquement, selon les compilateurs, prendra 16 ou 32 bits), et qu'on venait à essayer de la faire entrer dans une variable de type caractere ASCII (qui ne contient que 8 bits)… il nous faudrait un chausse pied pour y faire rentrer le tout(ou accepter l'idée d'avoir deux à quatre caractères vraissemblablement non affichables).

Si donc, on souhaîte utiliser un pointeur pour contenir l'adresse d'une variable de type entier, il faudra que le pointeur soit lui même de type entier.

En créant une variable (ici de type entier, mais elle pourrait être de n'importe quel autre type, y compris, d'un type que vous définiriez vous même) nommée numero, et un pointeur sur la variable nommé ptr, nous pourrons déjà commencer à comprendre le fonctionnement.

Comme numero est un entier, rien n'empeche que nous lui donnions une valeur quelconque avec un tout simple

numero = 10 |

Nous allons définir la valeur de ptr comme étant l'«adresse de numero»…

La plupart des langages utilisant l'esperluette "&" pour signaler qu'il s'agit de l'adresse mémoire de, mon petit langage gardera cette convention.

le code à écrire sera donc du genre de

ptr = &numero |

En utilisant le pointeur, il est parfaitement possible de modifier la valeur de numero, simplement, il faut indiquer qu'il s'agit de "la valeur pointée par" ptr.

La plupart des langages utilisant l'étoile pour signaler qu'il s'agit de l'adresse pointée par, gardons encore cette convention.

Le code suivant aura donc comme résultat de donner la valeur 12 à numero.

*ptr = 12 |

Il sera enfin tout à fait possible, vu que toutes les valeurs sont numériques à la base, d'incrémenter la valeur de ptr.

Le code

ptr++ |

aura donc comme effet de donner à ptr la valeur de l'adresse mémoire qui suit (selon l'exemple) celle à laquelle se trouve notre variable numero.

Il va de soi qu'il s'agit d'être particulièrement attentif lorsque l'on modifie ainsi une valeur de pointeur.

En effet, si on a déclaré un tableau de N éléments d'un type donné et un pointeur sur le premier élément, l'incrémentation du pointeur aura comme résultat direct de faire pointer notre pointeur vers l'élément suivant du tableau (du fait que l'incrémentation par 1 du pointeur lui signifie en fait d'incrénter l'adresse "d'une fois le nombre de bits nécessaire au type que tu gère").

Par contre, le gros risque apparait lorsque le pointeur repère une variable qui n'est pas un tableau.

Incrémenter le pointeur revient alors à le faire pointer… sur une adresse dont on ignore le contenu (mais que le pointeur concidérera néanmoins comme étant du type dont il a la charge).

Il n'est donc pas impossible qu'un pointeur de type entier pointe vers une adresse à laquelle se trouvent deux à quatre caractère ASCII et qu'il les considère comme étant un seul et unique entier… avec les incohérences que cela engendre (cf: le chapitre un ne vaut pas un de la page sur les variables).

Avec un peu de chance, la nouvelle adresse ne contiendra que des "crasses" laissée par l'exécution d'un programme dont l'exécution a été arrêtée depuis des heures, mais avec un peu de malchance, elle contiendra une variable (qui risque alors d'être modifiée et de donner un résultat aléatoire), voire, carrément, une instruction (car, rappelons le encore une fois, les instructions utilisées par le processeur ne sont que des valeurs sous la forme binaire).

fleche haut

16.4 La théorie de l'allocation dynamique

Il est tout à fait possible d'utiliser un pointeur qui pointe vers une variable que l'on n'aurait pas déclarée au préalable.

Pour ce faire, il s'agira simplement de s'assurer que le pointeur pointera vers un élément qui sera alloué en mémoire de manière dynamique.

Il ne faudra cependant pas perdre de vue le fait que vous êtes seul responsable de la libération en fin d'exécution de la mémoire dont vous aurez demandé une allocation dynamique .

Nous pouvons donc créer un pointeur sur un entier, que nous appellerons ptr.

Le code reste tout à fait identique, à savoir

entier *ptr |

Pour l'instant, ptr pointe sur… tout et n'importe quoi… (cf. la mise en garde "ne soyez pas suicidaire")

En effet, bien que le programme sache que c'est un pointeur qui est sensé pointer vers un entier, d'abord, il n'y a pour l'instant aucune adresse mémoire qui est réservée à cet entier,mais, en plus, personne n'est en mesure de savoir la valeur qui se trouve réellement dans le pointeur.

Si l'adresse mémoire qui est donnée au pointeur n'a pas encore été utilisée, la valeur sera de 0 (NULL), mais si un programme dont l'exécution a été arrêtée il y a deux heures a utilisé cette adresse mémoire… allez savoir la valeur qu'il contient…

La seule chose dont on soit sûr, c'est que l'adresse mémoire du pointeur était disponible avant que l'on déclare le pointeur.

La deuxième étape est donc d'initialiser le pointeur de manière à ce qu'il pointe vers un élément du type attendu.

Pour ce faire, il s'agira surtout de demander l'allocation d'un espace mémoire capable de contenir une valeur de type entier, et de spécifier que ptr reçoit la valeur de cette adresse mémoire.

L'allocation dynamique d'une adresse mémoire peut prendre plusieurs syntaxes différentes, mais, dans le langage que j'ai mis au point, il s'agira simplement d'utiliser le terme Allouer, suivi du type de valeur à allouer et, si besoin en est, du nombre d'élément entre crochets (car il est tout à fait possible d'allouer l'espace mémoire nécessaire pour un tableau)

L'instruction Allouer renvoyant un pointeur vers l'adresse mémoire qu'elle vient d'allouer, il suffira de récupérer cette valeur dans le pointeur prévu à cet effet.  En voici le code

ptr=Allouer entier |

Si le but avait été d'allouer un tableau de dix entiers, le code en aurait été

ptr=Allouer entier[10] | 

Nous disposons maintenant d'un pointeur de type entier qui pointe vers une adresse mémoire qui est en mesure d'accepter des valeurs entières.

La seule différence par rapport à la première manière que j'ai expliquée dans cette page, c'est… qu'on ne dispose pas d'un nom de variable pour accéder directement à la valeur de l'entier.

N'allez pourtant pas croire qu'il n'y aie pas moyen d'accéder à cette valeur… ce qui serait malheureux…

Simplement, pour accéder à la valeur de l'entier, il s'agira de demander la valeur «de l'élément pointé par ptr».

Le code

*ptr = 10 |

aura donc pour effet d'affecter la valeur 10 à notre entier, alors que le code

ptr++

aura toujours pour effet de faire pointer ptr vers l'adresse mémoire qui suit celle à laquelle se trouve la valeur de notre entier.

Impératifs de l'allocation dynamique

Lorsque vous décidez de gérer dynamiquement l'allocation de la mémoire, il y a deux impératifs stricts à respecter:

fleche haut

16.5 vérifier l'allocation

On ne peut en effet jamais être tout à fait sûr qu'une allocation de mémoire dynamique se soit bien déroulée…

En effet, si vous même n'avez l'habitude de lancer qu'une application à la fois, il n'est pas impossible qu'une autre personne ait l'habitude d'en avoir des dizaines en fonctionnement au même moment…

Il n'est pas non plus impossible que votre programme doive tourner sur un système qui ne dispose que de peu de mémoire…

Or, toutes les applications qui tournent monopolisent une certaine quantité de mémoire, plus ou moins importante.

Si donc, vous effectuez une allocation dynamique de mémoire, elle ne réussira que dans le cas où le système arrive à trouver un espace contigu suffisant non utilisé…

Essayez donc d'imaginer la tête que vous tireriez si l'on vous demandait d'effectuer des actions quelconques sur un objet… inexistant.

De la même manière qu'il vous serait impossible d'entreprendre ces actions, il sera tout à fait impossible au processeur de le faire (ou du moins, sans provoquer un résultat pour le moins aberrant).

Il est donc primordial de veiller à ce que les actions à entreprendre sur une variable allouée dynamiquement ne soient entreprises que dans le cas où l'allocation se sera bien déroulée.

Pour vous en assurer, il suffira simplement de vérifier que le pointeur vers votre donnée ne soit pas égal à 0(NULL), car si l'allocation échoue, la valeur renvoyée par Allouer est égale à 0(NULL).

fleche haut

16.6 Libération de la mémoire

En quelques mots, on peut exprimer la manière de voir les choses du compilateur sous la forme de «Si vous êtes suffisament téméraire pour gérer vous même l'allocation de mémoire, c'est à vous à veiller à ce qu'elle soit libérée… "It isn't my problem"»

Bien qu'il ne s'agisse pas forcément d'un acte téméraire, autant le dire tout de suite, il s'agit d'un acte impliquant bien plus de responsabilités que tout ce qui a été vu jusqu'à présent.

Le principal problème vient du fait que rien dans le système n'est prévu pour libérer la mémoire que vous aurez vous meme allouée dynamiquement…

Même le fait de quitter l'application ne provoquera sans doute pas la libération de la mémoire, si du moins vous ne prenez pas la peine de la libérer explicitement.

Le résultat ne se fera pas attendre:

Les premières exécutions du programme que vous aurez créé aboutiront sans problèmes majeurs, mais… toute la mémoire que vous aurez oublié de libérer explicitement restera considérée comme utilisée, et donc, comme indisponible pour l'exécution suivante.

Nous serons alors face à ce qu'il est convenu d'appeler une «fuite de la mémoire».

D'exécutions en exécutions du programme, c'est tout le système qui se retrouvera à chaque fois avec un peu moins de ressources.

Il s'en suit qu'après un nombre d'exécution dépendant des ressources système ET de la quantité de mémoire perdue à chaque exécution, fatalement, le système se retrouvera dans une situation critique du fait qu'il ne dispose plus de suffisemment de ressources pour rester stable.

Arrivé à ce point, non seulement l'application risque de donner des résultats aberrants, mais c'est carrément tout le système qui risque de "planter" et de forcer à un redémarrage du système, avec toute la perte d'informations que cela peut impliquer.

Il est donc primordial, dès le moment où vous utiliser l'allocation dynamique de mémoire (et les cas qui le nécessitent sont bien plus nombreux qu'il n'y parrait), de veiller à libérer toutes les adresses mémoires qui auront été allouées dynamiquement.

Dans le langage d'apprentissage que j'ai mis au point, la commande est finalement toute simple: il s'agit de Liberer suivit du nom de la variable dont il faut libérer la mémoire.

Nota:La libération de la mémoire allouée dynamiquement n'a pour seul effet QUE de rendre la mémoire disponible pour un usage ultérieur.

Cela n'affecte en rien la valeur de l'adresse mémoire pointée par un pointeur qui pointerait vers la variable que l'on vient de libérer

Comme l'adresse mémoire que l'on vient de libérer risque d'être utilisée par la suite pour autre chose, il est largement préférable d'«annuler» le pointeur en le faisant pointer vers une adresse mémoire empêchant les manipulations inoportunes.

Le meilleur moyen d'y parvenir est de faire pointer le pointeur vers l'adresse 0 (NULL) qui est une valeur facilement testable et qui est une adressse mémoire "de garage".

Nota: L'adresse mémoire 0(NULL) a comme seul avantage le fait d'être facilement repérable.

Cependant, ce n'est nullement le gage d'une sécurité sans faille: Il faut en effet savoir que l'adresse qui s'écrit en hexadécimal 0000 h est la première de toutes les adresses, qu'elle est suivie de 0001 h et de toutes les autres…

De plus, le premier kilo octet (celui qui se trouve de 0000 h à 0400 h) est… utilisée (par les interuptions BIOS), et que toutes les suivantes ont des chances de l'être aussi.

Enfin, il faut bien rester conscient du fait que l'utilisation d'une variable structurée fait que les données qui la composent se trouvent l'une à la suite de l'autre…

L'accès aux données se fait sur base du nombre de bits (ou d'octets) de chaque champs de la structure, l'un à la suite de l'autres.

De ce fait, l'acces à Structure.DonnéeN signifiera simplement "tu pars de l'adresse mémoire à laquelle se trouve Structure (nommons la départ), et tu va chercher le nombre de bits correspondant au type de DonnéeN à l'adresse (depart+Nombre de bit pour les données précédentes)"

Si la donnée à laquelle on essaie d'accéder se trouve après 20 caractères dans la structure, cela signifie que, même en ayant défini l'adresse de départ sur 0(NULL), essayer d'accéder à cette donnée reviendrait à lui dire que "tu trouvera les infos recherchées à 0014 h"…

Le simple accès en récupération de valeur risque alors de donner des résultats pour le moins aberrant, alors que l'accès en modification de la valeur risque clairement de faire quarrément "planter le système".

fleche haut

16.7 Un code revu et corrigé

De manière à respecter ce qui précède, voici un code adapté en vue d'assurer un maximum de sécurité (pour une fois, je vous ferai grâce du nassichneiderman correspondant)

Principale()
{
  //déclaration du pointeur
	entier *pointeur |
	pointeur=Allouer entier |
	si(pointeur≠NULL)
	{
//l'allocation a résussi, le travail se fait ici
	//donnons une valeur à notre entier
		*pointeur=10|
	//suite d'instructions
	//(...)
	//n'oublions pas de libérer le pointeur
		Liberer pointeur |
	}
	sinon
	{
//l'allocation a échoué, il peut être sympa de prévenir l'utilisateur
	}
}

Nota:Un tel code n'aura une raison d'être que dans le cas d'une gestion dynamique de la mémoire…

Rien n'empeche en effet d'utiliser un pointeur pour accéder à une variable créée de manière tout à fait statique

fleche haut

16.8 Les pointeurs de tableaux

Quand on déclare un tableau de dix entiers (par exemple) sous la forme de

entier tab[10] |

Bien sûr, le système réserve l'espace mémoire nécessaire à l'introduction de dix valeurs entières successives (c'est à dire à dix fois 32 bits, généralement)…

Bien sur, l'utilisation d'un code du genre

tab[3]=10 |

agira directement sur l'élément concerné (ici, le quatrième)…

Cependant, il faut bien être conscient du fait que tab, sans indice existe aussi… et qu'il sera en réalité un… pointeur sur le tableau d'entiers.

De cette manière, nous pourrions, même si cela complique quelque peu la gestion des informations, avoir un code qui ressemblerait à ceci

//déclaration d'un tableau d'entiers
	entier tab[10] |
//déclaration d'un pointeur de type entier
	entier *pointeur |
//pointeur recoit l'adresse mémoire de tab
	pointeur = tab |
//pointeur recoit l'adresse mémoire du troisième élément
	pointeur+=2 |
//on détermine la valeur de tab[2] en utilsant le pointeur
	*pointeur=15 |
//pointeur recoit l'adresse mémoire de l'élément suivant (tab[3])
	pointeur ++ |
//on détermine la valeur de tab[3] en utilsant le pointeur
	*pointeur=20 |

qui aurait pour conséquence directe de donner la valeur 15 au troisième élément de notre tableau et la valeur 20 au quatrième.

Un code similaire fonctionnera d'ailleurs également très bien

//déclaration d'un tableau d'entiers
	entier tab[10] |
//déclaration d'un pointeur de type entier
	entier *pointeur |
//pointeur recoit l'adresse mémoire du troisième élément
	pointeur = tab+2 |
//une autre solution aurait été celle-ci
	pointeur = &tab[2] |
//on détermine la valeur de tab[2] en utilsant le pointeur
	*pointeur=20 |

fleche haut

16.9 Les tableaux de pointeurs

Une autre possibilité qui nous est offerte de jouer avec les pointeurs, c'est de créer un tableau qui contienne un nombre donné de pointeurs, qui pointeront eux-même chacun vers une adresse mémoire réservée en vue de prendre une valeur d'un type donné.

Cette manière de travailler présentera, entre autre, l'avantage de nous affranchir de l'obligation de jouer avec des tableaux "carrés" en ce que chaque élément de tableau contient exactement le même nombre d'adresse mémoire que celui qui se trouve à côté.

Un autre gros avantage est que, sur les systèmes qui ne disposent que de peu de ressources, il sera sans doute plus facile de trouver dix emplacements d'un nombre restreint d'octets contigus que une fois la somme de tous ces octets pris ensembles…

Evidemment, l'utilisation de tableaux de pointeurs nécessite une gestion dynamique de la mémoire de chacun de ces pointeurs.

Un petit code pour comprendre

Imaginons que nous ayions besoin de dix chaines de 20 caractères chacunes (n'allons pas tout de suite chercher trop de difficulté petit gif sympa)

Voici, en substance, ce qu'il nous faudrait faire:

//déclarons un tableau de dix pointeur 
	caractere *tabptr[10]|
//il nous faudra un comtpeur pour gérer le tout
	entier compteur |
//une petite boucle d'allocation des pointeurs
	pour(compteur=0 à 9)
	{
		tabptr[compteur]=Allouer caractere[20] |
//tabptr[compteur] prend bel et bien la valeur du pointeur de tableau renvoyé
//par l'instruction d'allocation
//normalement, il faudrait vérrifier ici si l'allocation a réussi
	}
//il ne nous reste plus qu'à utiliser ces 10 chaines de caractères
	instruction1
	instruction2
	(...)
//avant de quitter, n'oublions pas de libérer la mémoire
//une peite boucle fera cela très bien
	pour(compteur=0 à 9)
	{
		libere tabptr[compteur] |
//par sécurité, on place le pointeur sur une adresse "voie de garage"
		tabptr[compteur]=NULL |
	}

fleche haut

16.10 Et les pointeurs de pointeurs

Un pointeur de pointeur n'est jamais qu'un pointeur un peu particulier en cela qu'il contiendra l'adresse de la mémoire à laquelle le système trouvera… un pointeur du type donné…

fleche haut

16.11 Pour que faire?

Il est parfois souhaitable de créer une fonction qui modifiera, non pas la valeur d'une variable, mais bel et bien completement un pointeur…

Cela peut prendre plusieurs formes, comme le fait de modifier la taille d'un tableau, ou tout autre…

Le problème étant toujours le même (une fonction ne peut renvoyer qu'une seule valeur), la solution sera elle aussi toujours la même (transmettre un pointeur vers la valeur à modifier en parametre de la fonction)…

Seulement, comme la valeur à modifier est elle même déjà un pointeur, ben, le parametre à passer devra bel et bien être… un pointeur de pointeur…

fleche haut

16.12 Leur gestion

Tout ce qui a trait à la gestion dynamique des pointeurs s'applique bien évidemment à la gestion dynamique des pointeurs de pointeurs:

fleche haut

16.13 Et le reste

Finalement, la seule différence entre un pointeur de pointeur et un pointeur, c'est que le pointeur de pointeur pointe… sur un pointeur…

Concrètement, si on déclare une variable de type entier (nommons la "var" pour l'exemple), un pointeur de type entier (nommons le ptr pour l'exemple) qui prend l'adresse de var comme valeur (qui pointe donc sur var) et un pointeur de pointeur de type entier (nommons le pptr pour l'exemple) qui prend l'adresse de ptr comme valeur (qui pointe donc sur ptr), on arrive aux possibilité suivantes:

en modifiant la valeur de ce qui est pointé par ce qui est pointé par pptr, on modifie la valeur même de var (**ptr=15 aura un résultat identique à var=15)

en incrémentant la valeur de ce qui est pointé par pptr, ptr aura une valeur qui pointera sur l'adresse qui suit l'adresse de var (*pptr++ aura un résultat identique à ptr++)

en incréemenant enfin la valeur de pptr, pptr pointera sur l'adresse mémoire qui suit celle à laquelle se trouve ptr

Pour ceux qui n'auraient pas forcément tout suivi, le shéma ci-dessous devrait permettre de fixer les choses

shéma du principe d'utilisation des pointeur

shéma du principe d'utilisation des pointeur

fleche haut

16.14 Avant de passer à la suite

Si vous avez réussi à lire toute cette page, et surtout à la comprendre sans *trop* vous taper la tête contre les murs (je vous jure pourtant avoir fait tout mon possible pour qu'elle reste compréhensible), vous avez théoriquement assimilé suffisemment de choses pour passer à l'étape suivante, qui traitera des pointeurs dans les structures et des possibilités qu'ils offrent.

Cependant, si tout n'est pas parfaitement clair pour vous, il est très largement conseillé de recommencer la lecture à tête reposée (voire, pourquoi pas, d'imprimer cette page pour en faire votre livre de chevet l'espace d'un jour ou deux), car la suite risque fort d'être encore plus ténébreuse.

image d'imprimante   image de mail   fleche haut

Evaluation donnée par les visiteurs
Cette page a été évaluée 6 fois et a obtenu une moyenne de compréhensible, sans plus
Mon appréciation sur la compréhensibilitéde cette page est:
  • incompréhensible
  • mal expliquée
  • compréhensible, sans plus
  • bien expliquée
  • très bien expliquée

fleche haut

[koala01.free.fr]->Tutoriaux->Principes de Programmation ->Les pointeurs

Copyright (©) 2005 (Philippe Dunski)

Ce cours est libre, vous pouvez le redistribuer et/ou le modifier selon les termes de la Licence Publique Générale GNU publiée par la Free Software Foundation (version 2 ou bien toute autre version ultérieure choisie par vous).

Ce cours est distribué car potentiellement utile, mais SANS AUCUNE GARANTIE, ni explicite ni implicite, y compris les garanties de commercialisation ou d'adaptation dans un but spécifique. Reportez-vous à la Licence Publique Générale GNU pour plus de détails.

Cependant, l'auteur apprécierait grandement que vous lui fassiez part de toute modification apportée à son contnu

Vous pouvez le contacter par mail à l'adresse koala01@free.fr

Vous pouvez trouver une adaptation française de la licence GNU/GPL à l'URL http://www.linux-france.org/article/these/gpl.html