CHAPITRE 8
Les pointeurs

8.0. Introduction

Il n'est pas toujours possible de prévoir au cours du développement, quelles seront les données utiles.

Exemple:
Nous ne connaissons pas toujours le nombre des  éléments d'une liste à trier.

 Il est donc nécessaire de disposer des outils pour manipuler des données dynamiques.

Certains compilateurs utilisent la créations de tableaux dynamiques (non contraints).

 Dans ce cas, l'utilisation de ces tableaux non contraints n'est pas toujours aisée pour manipuler des types de données dynamiques telles les arbres.

Les structures de données dynamiques (autres que les tableaux non contraints) constituent des outils indispensables au developpement d'un projet.

Avant d'introduire ce type de programmation, nous allons étudier la manipulation de la mémoire dynamique à l'aide de pointeurs. 

Les pointeurs constituent les fondements du stockage dynamique , facilitent la manipulation des tableaux et permettent aussi d'écrire des fonctions génériques.

8.1. Définition d'un pointeur.

Un pointeur désigne (pointe sur) une donnée.
Les mécanismes d'accès aux informations contenues dans une variable sont possibles grâce aux pointeurs.

Un pointeur ressemble à une référence d'une variable.  
La différence essentielle entre une référence d'une variable existante et un pointeur est que la référence d'une variable (existante) est à la charge du compilateur, alors que la gestion des pointeurs est du ressort du programmeur.

 Si un pointeur, comme une référence, désigne une donnée, il faut déférencer de façon explicite le pointeur afin d'avoir accès à la donnée.

 Pour les pointeurs nous disposons d'une plus grande liberté d'expression, mais évidemment les risques d'erreurs augmentent.

ATTENTION : Les pointeurs sont la causent principales des bogues dans les programmes.

Il faut être très attentif quant à leurs utilisations, et il faut concevoir soigneusement le code.

Si l'on réféchit en termes d'adresses, la manipulation des pointeurs peut être facilitée.

Nous aurons à gérer non pas des adresses physiques en mémoire, mais des adresses virtuels.
Un pointeur peut être considéré comme une variable dont la valeur droite est égale à la valeur gauche (à l'adresse) d'une autre variable.
L'initialisation des pointeurs va dans ce sens...

On écrivant des parties d'un système d'exploitation, il est quelque fois nécessaire de manipuler des adresses physiques de la machine. Mais on peut considérer que la majorité des situations consistent à considérer les pointeurs comme des adresses logiques de variables.

De plus, un pointeur est une donnée élémentaire du langage C. 

8.2. Déclaration et définition d'un pointeur.

Rappelons qu'une référence (adresse) d'une variable gérée par le compilateur.

Si v est une variable d'un certain type T, alors l'adresse (logique) mémoire de cette variable est &v.

Pour désigner une variable d'un certain type T, il faut introduire le type pointeur T* .

T* p; // peut contenir l'adresse d'une variable de type T
// par exemple:
T x;
p=&x;
et on défère p pour obtenir x :
*p est la variable x. 

Exemple : le programme suivant accède à une variable au moyen d'un pointeur.

#include <stdio.h>
void main() {
int varInt=123;
int * ptrInt = & varInt; // ptrInt désigne la variable varInt
printf("valeur =%d\n", *ptrInt);  // déférence de prtInt
*ptrInt=24;
printf("valeur = %d\n",varInt);  // nouvelle valeur de varInt
  }

Le résultat de l'exécution de ce programme donne :
valeur =123
valeur = 24

Explications :
 varInt est une variable normale qui vaut 123.
L'étoile * dans la définition de ptrInt signifie qu'il sagit d'un pointeur sur un int.
Ce dernier pointe sur la variable varInt dont l'adresse est obtenue à patir de l'opérateur &.
La valeur du pointeur est égale à l'adresse de la variable varInt : l'opérateur unaire & calcule l'adresse de son opérande.
L'opérateur * , s'il n'est pas utilisé dans une définition, déférence un pointeur, c'est à dire renvoie la valeur de la variable que ce pointeur désigne. Cette valeur peut être modifiée, telle que le fait par exemple l'instruction:
			 * ptrInt=24 ;

Les symboles * et & sont ici des opérateurs unaires (à ne pas confondre avec les opérateurs binaires multiplication et du ET binaire ). 

Les diagrammes aident à la munipulation des pointeurs : ils permettent d'illustrer une situation. Une variable est décrite par une boite associée à un nom . Lorsque que cette variable n'est pas un pointeur elle contient la valeur de la variable. A la déclaration un pointeur est une boite vide . Après la définition du pointeur, le pointeur contient l'adresse d'une variable (l'adresse réelle de cette dernière est sans importance).
Durant l'exécution d'un programme, la source principale d'erreurs vient du fait que les pointeurs n'ont pas été initialisés : il est vital que les pointeurs sont initialisés.

Règle : toute définition de pointeur doit être associée à une initialisation.

#include <stdio.h>

void main()
{ int v = 12;
  int * p = & v;

printf(" v= %d\n",*p); // valeur de la variable pointée
  printf("L'adresse en hexa de la variable v est = %0x\n" , p);
  }

A l'éxécution de ce programme, on obtient :

v= 12
L'adresse en hexa de la variable v est = 12ff7c

8.3. Synonymes de types.

Le langage C permet de définir des synonymes de types au moyen de l'instruction typedef.

Cette instruction ne crée pas un nouveau type, mais permet seulement d'améliorer la lisibilité du code C.

Syntaxe :

typedef  TypedeBase TypeSynonyme.

Exemple :

typedef  int*   PtrEntier;
int a=2 , b=4;
PtrEntier pa=&a,   pb=&b;

8.4. Pointeurs sur des constantes.

Un pointeur peut désigner aussi une constante nommée.

#include <stdio.h>
 void main() {
 const int i=1;
 const int* p =&i;
printf("%d\n",*p);
       }

 i est une constante et sa valeur ne peut pas être modifiée même à travers un pointeur.
 p pointe sur une constante nommée; la valeur de celle ne peut être modifiée.

8.5. Pointeurs constants.

On peut définir des pointeurs constants sur des données.
Un tel pointeur ne peut être qu' initialisé  et  il ne peut pas, par la suite, désigner une variable.
 
Exemple :
 int v=5;
 int *const p=&v;
// p est un pointeur qui désigne toujours v 

  p ne peut jamais désigner autre chose que v.
Ainsi si w est une variable, p=&w; n'est pas possible.

8.6. Arithmétique, opérations et tableaux de pointeurs.

Pour tout type de pointeur, les opérateurs :
++ , -- , += , -= et - sont surchargés.

Ainsi l'addition est surchargée :

La  somme d'un pointeur T* p avec un entier unsigned n donne comme résultat un pointeur de type T* ayant pour valeur 
p+n sizeof(T)
c'est à dire l'adresse (logique) obtenue en décalant p de la taille de n objets  de type T.

p

Exemple :
                   int i=3;
                   int *p=&i;

p+2

                   p=p+2;
L'expression p=p+2 équivaut à p+=2;

De même  --p , p-- , ++p,  p++ permettent de construire une valeur de pointeur en décalant (de façon préfixée ou postfixée) p de + ou - un élément .

L'expression p++ signifie p+=1; (en postfixé).

Enfin si p est q sont deux pointeurs de même type , p-q désigne un entier n tel que p soit égal q+n.

Les notions d'incrémentations et de décrémentations de pointeurs sont intéressante dans le cas où le pointeur désigne une variable isolée et montrent tout leur intérêt dans la cas où l'on utilise des tableaux.

// Exemple : affichage des lettres de l'alphabet
#include <stdio.h>

 void main() {
#define TailleTableau 27

 char Alphabet[TailleTableau], *string;

 for (string= Alphabet;
      string -Alphabet <TailleTableau-1; 
// Alphabet désigne le premier élément d'un
// tableau de caractères
        string++)
   *string='a'+(string -Alphabet);

   *string='\0';           // finir par un caractère nul
printf("%s \n", Alphabet); 
// afficher la chaîne de caractères
 } 
 Dans l'exemple précédent, string désigne le premier d'un tableau.
Le nom du tableau Alphabet est égal à l'adresse de son premier élément : 
Alphabet  & Alphabet[0]
Le pointeur permet d'avoir accès aux éléments du tableau sans utiliser l'opérateur d'indextion :
 *string = 'a';                        

signifie que l'élément pointé par pointeur reçoit la valeur 'a', c'est à dire que Alphabet[0] reçoit la valeur 'a'.

L'incrémentation se fait par une boucle for controlée par l'entier string - Alphabet qui donne le déplacement relatif de l'élément pointé du tableau par rapport au premier élément  du tableau.

Quelle est l'avantage à utiliser un pointeur plutot que d'indexer un tableau?

L'indexation d'un tableau est facile à mettre en place mais coute plus cher ( en terme de vitesse d'excution ) que la gestion de ce tableau par un pointeur.

Cependant l'indexation d'un tableau est rarement une source de problèmes et reste assez facile a maîtriser.

	Remarque importante : 
Les "débordements" des limites d'un tableau ne sont pas gérés par le compilateur : il n'y a pas d'erreur de syntaxe : le développeur est responsable de son code !
	
Parmi tous les opérateurs arithmétiques, seuls les opérations + et – ainsi que leurs variantes sont réellement significatives : ce sont les opérations qui permettent de parcourir un tableau.

Les autres opérateurs peuvent être utilisées, mais leur signification est moins claire.

8.7. Pointeurs, tableaux et paramètres de fonctions.

Les pointeurs ou le tableaux sont souvent utilisés comme des paramètres de fonctions ; il en est ainsi pour les de nombreuses fonctions bibliothèques.

Par exemple une fonction peut recevoir un tableau et un entier qui précise sa taille : un tableau , en C, ne contient pas d'informations quant à sa taille.

Le compilateur transmet par valeur un pointeur sur le premier élément du tableau.

// Exemple :
void afficher (const int Taille , const int data[ ])
 { 
    int i;  
   for (  i=0; i<Taille;  i++) printf("%d  ",data[i]);
  }

Ici le paramètre formel data est un pointeur.
Les crochets ne sont qu'une alternative à l'étoile des pointeurs.
De nombreux compilateur traiterons cette notation comme un synonyme d'un pointeur constant, c'est à dire un *const .

Dans tous les autres cas, autre que dans la définition d'un paramètre formel d'une fonction, les crochets  [ ] définissent réellement un tableau. 

/**** Exemple :
 Diverses versions pour afficher un tableau d'entiers
****/
#include <stdio.h>

#define AFFICHER_0(t) afficher_0(sizeof(t)/sizeof(int),t); 
#define AFFICHER_1(t) afficher_1(sizeof(t)/sizeof(int),t); 
#define AFFICHER_2(t) afficher_2(sizeof(t)/sizeof(int),t); 
#define AFFICHER_3(t) afficher_3(sizeof(t)/sizeof(int),t); 
#define AFFICHER_4(t) afficher_4(sizeof(t)/sizeof(int),t); 
#define NEW_LINE puts("\n");

void afficher_0 (const int Taille , const int data[]);
void afficher_1 (const int Taille , const int *const data);
void afficher_2 (const int Taille , const int *const data);
void afficher_3 (const int Taille , const int  data[]);
void afficher_4 (const int Taille , const int  data[]);

void main() {

	int TabInt[]={23,6,231,9,-45,76};

     	AFFICHER_0(TabInt)
	NEW_LINE
	AFFICHER_1(TabInt)
	NEW_LINE
	AFFICHER_2(TabInt)
	NEW_LINE
	AFFICHER_3(TabInt)
	NEW_LINE
	 AFFICHER_4(TabInt)
	NEW_LINE
}

//*******************************************************
 
// Variante 0 de afficher:
void afficher_0 (const int Taille , const int data[])
 {
int i;  
  	for (  i=0; i<Taille;  i++) printf("%d  ",data[i]);
  }
//*******************************************************
// Variante 1 de afficher.
void afficher_1 (const int Taille , const int *const data)
 {   
	int i;
	for ( i=0;i<Taille; i++) printf("%d ",data[i]); 
 }
//*******************************************************
// Variante 2 de afficher.
void afficher_2 (const int Taille , const int *const data)
 {   int i;
	for (  i=0;i<Taille; i++) printf("%d ",*(data+i)); 
 }
//*******************************************************
// Variante 3 de afficher.
void afficher_3 (const int Taille , const int  data[])
 {   const int *pdata=data;
     int i;
     for (  i=0;i<Taille; i++) printf("%d ",*pdata++);  }
//*******************************************************
//Variante 4 de afficher.
void afficher_4 (const int Taille , const int  data[])
{ 
const int *pdata;
	for ( pdata=data; pdata<&data[Taille];  pdata++)
		printf("%d ",*pdata); 
}
//*******************************************************

8.8. Fonctions renvoyant un pointeur.

Une fonction peut renvoyer une valeur d'un type quelconque, y compris un pointeur.
Il existe des problèmes liées à la durée de vie des variables : si une fonction renvoie un pointeur, la variable désignée existera-t-elle encore lorsque le pointeur sera déférencé?

/***Exemple de problèmes liés à la durée de vie d'une variable locale ***/
#include <stdio.h>
#define AFFICHER(x) printf("%d\n",x);

int * erreur(int n)
 {
int VarLocale=n;

return & VarLocale;

 // ATTENTION : retourne l'adresse d'une variable locale
 }

//**********************************************************
 void main()
 {
   int *p=erreur(2);
   int *q=erreur(10);
   AFFICHER(* p)  // affichage de 10 au lieu de 2
 }

La fonction renvoie un pointeur sur une variable automatique dont la durée de vie est inférieure à celle de l'utilisation thèorique du pointeur.

Tous les compilateurs ne releve pas cette maladresse !

Il est fréquent qu'une fonction passe en argument un pointeur et le renvoie en fin de calcul:  la fonction suppose que l'espace mémoire désigné par le pointeur est correctement alloué.

char * fonction (char * p)
{
 //
// traitement
//
 return p;}
La fonction appelante doit transmettre à fonction un paramètre réel qui est à la portée de fonction  et dont la durée de vie n'est pas arrivé à terme.

Avec cette méthode, le résultat d'une fonction peut servir de paramètre à une autre fonction.

//Exemple: concaténation de chaines.

#define PUT(str) printf("%s\n",str);
#include <stdio.h>
#include <string.h>

void main() {
const char prenom[]= "Jean";
const char nom[] = "Dupont";
const char adresse[]="3 avenue de la Mer";
const char codepostal[]="13100 Marseille";
// ici on a des chaines constantes

// Définition d'un tableau de caractère suffisamment grand
// pour contenir la concaténation de prenom et nom

char NomComplet[sizeof(prenom)+sizeof(nom)+2];

strcpy(NomComplet,prenom);
strcat(NomComplet," ");
strcat(NomComplet,nom);

	{
	char AdresseComplete[sizeof(adresse)+sizeof(codepostal)+2];

strcat(strcat(strcpy(AdresseComplete,adresse)," "), codepostal);

	PUT(NomComplet)
	PUT(AdresseComplete)
	}
}

8.9. Les chaînes de caractères et pointeurs

Les chaînes sont des tableaux de caractères avec un code de fin spécial '\0'.
Les relations entre les tableaux et les pointeurs permettent de mieux manipuler les chaînes.

8.9.1. Initiation des chaînes
Examinons l'exemple suivant :

#define PUT(str) printf("%s\n",str);
#include <stdio.h>

// Des chaînes et des caractères
// variables globales
char mot1[]={'B','o','n','j','o','u','r','\0'};
char mot2[]="Monsieur";
char *mot3="Jean";

void main() {
char mot4[] ={ 'A','u',' ','r','e','v','o','i','r','\0'};
char *mot5="Merci";
PUT(mot1)
PUT(mot2)
PUT(mot3)
PUT(mot4)
PUT(mot5)
 }

Les crochets [] définissent ici des tableaux et non pas des pointeurs.

Le nom d'un tableau est un pointeur sur l'élément d'indice 0.

L'initialisation d'un tableau et celle d'un pointeur sont différentes.

a - L'initialisation d'un tableau par une suite de littéraux caractères,  fait qu'un tableau est crée par le compilteur de dimension suffisante et que chaque littéral est recopié : ne jamais oublier le caractère de fin '\0' pour les chaînes de caractères.

b-Les définitions de mot3 et mot5 utilisent le fait qu'un littéral de type chaîne de caractères est un tableau (anonyme) de caractères.
Un littéral chaîne de caractères est adressable par un pointeur L'expression char *mot3="Jean" retourne sur mot3 l'adresse du littéral "Jean".

		mot3
		   
J	e	a	n	'\0'	

c- L'initialisation d'un tableau par un littéral chaîne de caractères utilise le mécanisme suivant:
  - le compilateur créer un nouveau tableau sur lequel le litteral est recopie terme à terme avec le caractère de fin '\0' .

Exemple : char mot2[]="Monsieur"; 
               
           mot2
		                               Variable
M	o	n	s	i	e	u	r	'\0	

M	o	n	s	i	e	u	r	'\0	
                                                             Littéral

Remarques :
1- La solution est la plus onéreuse en espace mémoire est le cas c) , car une définition de la forme
char mot2[]="Monsieur"; va créer un tableau ananyme de taille 7 caractères et un tableau mot2 aussi de 7 caractères.

2- Pour ne pas perdre la référence d'un littéral il faut rendre le pointeur constant :
  char const * mot3="Jean";
mot3 ne peut plus être modifiée par une instruction de la forme mot3=mot5;

3- Une instruction comme mot5[2]='u' modifie la chaîne mot5 qui initialement était un littéral.
Si l'on veut que les chaînes restent constantes , il faut créer des constantes nommées:
            const char const * mot5="Merci";
mot5 est maintenant un pointeur constant sur une chaîne constante : mot5 désigne un vrai littéral.

4- L'instruction :
                char c=mot5[12] est une erreur grave que le compilateur ne relevera pas.
Nous avons débordé du tableau et cette instruction se compilera et realisera quelque chose au court de l'exécution du programme : il y aura des problèmes d'accès au dela des bornes du tableau.

8.9.2. Paramètres de la ligne de commande.

La plupart des programmes sont lancés à partir d'un interpréteur de commandes.
Un programme est considéré comme une fonction qui appelle l'interpréteur de commandes.

En C la fonction invoquée par l'interpréteur de commande est  la fonction main .

Comment sont transmis les commandes à exécuter ?
Voici un exemple :
A > copy fichier1 fichier2
La commande se nomme copy .
Les paramètres sont fichier1 et fichier2 . 
L'interpréteur utilise la commande afin d'appeler la commande copy.
Comment sont transmis à la commande copy les paramètres fichier1 et fichier2 ?
Ces paramètres fichier1 et fichier2 sont transmis à la commande copy au moyen de paramètres formels de main.
La spécification de la fonction main est :
int main(int,char * [ ]);

#define PUT(str) printf("%s ",str);
#include <stdio.h>
int main(const int cptArgs, char * const vecteurArgs[])
{ int i;
  for(i=0; i < cptArgs ; i++)
   PUT(vecteurArgs[i])
   PUT("\n")
      return 0;
 }
La spécification de la fonction main est :
int main(int,char * [ ]);

Le premier argument de main est  de type int qui indique le nombre de paramètres de ligne de commande.

Le second paramètre formel de main est de type tableau de chaînes de caractères : chaque élément de ce tableau contient un paramètre de la ligne de commande.
Les éléments sont rangés dans l'ordre d'apparition dans la ligne de commande :
- l'élément 0 contient le nom de la commande 
- l'élement 1 contient le premier paramètre, ect…

8.10. Pointeurs sur des fonctions globales

Un pointeur peut désigner une fonction globale.

Déclaration :
type_ret  (*pf) (type_par1,type_parm2,...,type_parmk);

pf est la référence d’une fonction de type type_ret et dont la liste des parametres est (type_par1,type_parm2,...,type_parmk)

Exemples de déclations :
int ( * psomme ) (int,int);
char ( * last_car ) (char *);
  
#include <stdio.h>
#include <math.h>
  	typedef double (*PF_DOUBLE) (double)  ;
/* type de fonctions qui retournent un réel double et qui ont comme  paramètre formel un réel double précision */
  //*************************************************************	                              
    double ValeurOrigine(PF_DOUBLE ptrfonct)  ; 
      double acos( double x ); // Arcos
      //***************************************** Test 
     void main() {
     double v=ValeurOrigine(sin);
	 printf("%lf\n",ValeurOrigine(acos));
	 }

	// ==============================
	 // Donne la valeur de la fonction en 0
	 double ValeurOrigine(PF_DOUBLE ptrfonct) {
		 return ptrfonct(0.0);
}
       // ==============================