2.3- Les pointeurs


2.3.1- Qu'est ce qu'un pointeur ?

2.3.2- Création dynamique d'objet

2.3.3- Dangers de la gestion directe de la mémorie


2.3.1- Qu'est ce qu'un pointeur ? Declaration et utilisation

Un pointeur est une variable particulière, c'est une variable qui contient une adresse de variable dans la mémoire vive. La mémoire vive est divisée en emplacemens d'un octet numérotés séquentiellement :
Numérotation des emplacements ->34353637383940
10100001101110110101010110110001

Dans le tableau représentant la mémoire vive, on a mis une variable codé sur 4 entiers. C'est par exemple un entier long. Dans ce cas, On dira que l'adresse de la variable long est 34. Les adresses sont codées sous forme hexadécimale. Soit une adresse apparaîtra comme 0x22ff74 par exemple.
Pour voir apparaître une adresse :

#include <iostream>

int main()
	{
	int a;
	std::cout << &a; 	
	return 0;
	}

Opérateur &

Pour obtenir l'adresse d'une variable, on utilise l'opérateur &. Soit le code suivant :

Exemple pointeurs
#include 

int main()
	{
	int i=0;
	std::cout << "L'adress de i : " << &i;
	}

Déclarer une variable pointeur : *

Soit la déclaration suivante :

int *a=0;

Ce code déclare a non pas comme une variable entière, mais comme un pointeur sur un entier de type int. Donc a recevra l'adresse d'une variable de type int. On peut tester la taille réservée à un pointeur en mémoire vive :

int *a;
std::cout << "La taille pour socker un pointeur : " << sizeof(a);

On constate qu'un pointeur est stocké sur 4 octets. Noter : 24*8=168, cf le fait que l'on présente les adresses codées sur 8 hexadécimaux. Il est bon de déclarer les variables pointeurs en les indexant par p, et donc, pour l'exemple précédent, de déclarer int *pA; Ceci permet de distinguer les variables.

Dangers de la manipulation des pointeurs

Comme un pointeur est une variable, il a un espace dédié en mémoire. Lors de sa déclaration, ce qui est stocké à cet espace est imprévisible. La valeur de la variable, ie l'adresse stockée est donc imprévisible. On risque de produire une erreur grave en utilisant l'adresse stockée, par exemple en modifiant le contenu stocké à cette adresse. En effet, l'information contenue à cette adresse peut être fondamentale pour le fonctionnement du système d'exploitation. Pour éviter ce type de désagrement majeur, une bonne habitude consiste à affecter une valeur au pointeur dès sa déclaration. Deux solutions pour affecter une valeur à un pointeur : soit on affecte le pointeur nul, soit on affecte l'adresse d'une variable.

double x=7;
double *pteur1=0;
double *pteur2=&x;
std::cout << pteur1 << " et " << pteur2;

A la troisième ligne du code, on affecte au pointeur sur double pteur l'adresse de la variable de type double x. L'opérateur & permet d'obtenir l'adresse d'une variable. Attention, il faut que la variable dont on récupère l'adresse soit du même type que le type sur lequel pointe le pointeur.

Indirection

L'indirection consiste à accéder la valeur contenue à l'adresse enregistrée dans un pointeur. On parle aussi de déréferencement d'un pointeur. Dans le cas d'une variable standard, recupérer son contenu n'est pas difficile :

double y;
double x=7;
y=x;
Dans ce qui précède, on récupère le contenu de x dans y. Pour accéder la valeur d'un pointeur :
double y;
double x=7;
double *pZ=&x;
y=*pZ;

L'opérateur d'adressage indirect (*) devant le nom d'une variable signifie "Valeur stockée à l'adresse".
double x=5;
double *pX=&x;
*pX=7;
cout << "La valeur de x : " << x;

Ici la valeur de x est 7 après la manipulation. On modifie la valeur de x en modifiant la valeur stocké à l'adresse de x, cette adresse étant stockée dans pX. Noter une autre écriture équivalente mais décomposée :
double x=5;
double *pX;
pX=&x;
*pX=7;
cout << "La valeur de x : " << x;
Noter que lorsque l'affectation d'un pointeur se fait en même temps que la déclaration, elle se fait double *pX=&x; alors que l'affectation du pointeur hors de la déclaration se fait : *pX=&x;
Note, on ne peut pas manipuler de pointeur qui ne contient pas d'adresse de variable.
double x=5;
double *pX;
*pX=7;
cout << "La valeur de x : " << x;
Ce code provoquera une erreur à l'exécution. Précisement à l'exécution de *pX=7.

Usage des pointeurs

Les pointeurs sont utilisés :

Les usages seront développés et exemplifiés plus tard.


2.3.2- Création dynamique d'objet


new et delete


Il est possible de déclarer des variables pour tous les blocs de données :
void methode1()
	{
	Action a();
	int j;
		for(int i=0;i<7;i++)
		{
		Action a2();
		int j2;
		}
	}
Dans ce cas, les variables a et j sont locales à methode1() : c'est à dire qu'elle ne sont valables que dans le bloc d'instruction (l'espace entre les deux accolades) qui correspond à la définition de la fonction. Par ailleurs, les deux variables a2 et j2 sont locales au bloc d'instruction de la boucle. Par ailleurs, si on spécifie une fonction qui prend un objet comme paramètre :
void variation(Action a,int j)
	{
	....
	}
Dans ce cas, le passage des deux objets se fait par valeur : la machine crée un objet local à la fonction, de même qu'un entier local à la fonction. De sorte que l'objet modifié sera celui qui est local à la fonction. Pour que l'objet soit modifié, on va utiliser le passage de pointeur sur des objets. C'est à dire qu'on définira plutôt une fonction :
int main()
	{
	Action a;
	variation(&a,5);
	}

void variation(Action *a,int j)
	{
	....
	}
Une autre solution va être de créer des objets de manière dynamique :
int main()
	{
	Action *a;
	a=new Action();
	variation(a,5);
	}

void variation(Action *a,int j)
	{
	....
	}

Dans le cas où les variables sont créées comme des variables locales à un bloc d'instruction, elles sont gérées dans la partie de la mémoire vive appellée la pile : dès que l'on sort du bloc d'instruction, la mémoire est libérée. en utilisant le mot clé new + constructeur, on crée un objet dans la partie de la mémoire vive qui est appellée tas. Dans l'exemple précédent, on crée un pointeur qui ne pointe sur rien au début, puis on demande à la machine de réserver de l'espace dans la mémoire libre, plus précisement un espace de taille Action et on affecte l'adresse de cet espace au pointeur a. Cette mémoire vive n'est pas liberée à la fin des blocs d'instructions : il faut gérer soi même la libération de la mémoire :
Action *a=new Action();
delete a;
libère la mémoire vive dans le tas.
Noter qu'ici l'instruction est new Action(); parce qu'il n'y a pas d'autre constructeur que le constructeur par défaut défini. Si par exemple seul le constructeur Action(int,int) a été défini dans la déclaration de la classe, il faudra :
int i=0;
int j=5;
Action *a=new Action(i,j);

Les types primitifs


Une démarche similaire est possible pour les types primitifs.
int *pInt;
pInt=new int;
Ou bien sûr :
int *pInt=new int;
On a réservé une zone de mémoire du tas de taille int ici. delete permet là aussi de libérer de l'espace.
delete pInt;
Note : en C, cela se faisait avec l'opérateur malloc pour memory allocation qui permettait d'allouer de la mémoire. Pour savoir quelles tailles sont réservées pour chaque type :
#include 

class A{
      public : 
        int k;
        double e;
	A(int i,int j);
	void doubleK();
};

A::A(int j,int a)
        {
	k=j;        
        e=a; 
	}

void A::doubleK()
	{
	k=k*2;
	}
         
int main()
	{
    A *varA=new A(5,7);
    std::cout << sizeof(A) << "\n";
    std::cout << sizeof(int);
    }

Forme d'acces

Pour accéder aux attributs et aux fonctions d'un objet stocké dans la pile, par exemple à partir de l'exemple précédent :
varA->k=5;
varA->doubleK();
int j=varA->k;

Destruction

L'utilisation du mot clé delete sur un objet implique l'appel d'une fonction destructeur. Elle-existe par défaut, à l'instar du constructeur par défaut. A l'instar de la modification du constructeur, il est possible de modifier le code du destructeur :
Ex :Les objets dans la pile, les objets dans le tas
#include 

using namespace::std;

class A
	{
	public :
	int num;
	~A();
	A(int j);
	donneNum();
	};

A::A(int j)
	{
	num=j;
    	cout << "\nAppel du constructeur de A pour l'objet de num :  " << num;
	}

A::donneNum()
	{
	cout << "\nLe chat de num  " << num;
	}


A::~A()
	{
	cout << "\nAppel du destructeur de A pour l'objet de num :  " << num;
	}

int main()
	{
	cout << "\nCreation de l'objet objA : dans la pile";
	A objA(1);
	cout << "\nOn reserve un espace mémoire pour un objet de type A dans le tas";
	A *pA=new A(5);
	
	delete pA;
	return 0;
	}
Note, soit la classe :
class B{
public :
	B();
	~B();
private : 
	int *att1_B;
	int *att2_B;

};

B::B()
	{
	att1_B=new int;
	att2_B=new int
	}

B::~B()
	{
	delete att1_B;
	delete att2_B;
	}


Le pointeur this

Il s'agit d'un pointeur toujours défini de manière implicite. Le pointeur this est passé de manière implicite aux fonctions d'une classe. Il est possible de l'utiliser de manière explicite. Ceci n'a aucun intérêt dans l'exemple suivant, mais les chapitres suivant feront montre d'usages important.
Ex
#inlcude 
classe Rectangle{
public :
	Rectangle();
	~Rectangle();
	void defLongueur(int longueur);
	int lireLongueur() const;
	void defLargeur(int largeur);
	int lireLargeur() const;
private : 
	int saLongueur;
	int saLargeur;	
};

Rectangle::Rectangle()
	{
	saLargeur=5;
	saLongueur=10;
	}

Rectangle::~Rectangle()
	{
	}

Rectangle::defLongueur(int l)
	{
	this->saLongueur=l;
	}

Rectangle::defLargeur(int l)
	{
	this->saLargeur=l;
	}

int main()
	{
	Rectangle theRect;
	cout << "theRect mesure " << theRect.lireLongueur() << " cm de long.\n";
	cout << "theRect mesure " << theRect.lireLargeur() << " cm de largeur.\n";
	theRect.defLongueur(20);
	theRect.defLargeur(10);
	cout << "theRect mesure " << theRect.lireLongueur() << " cm de long.\n";
	cout << "theRect mesure " << theRect.lireLargeur() << " cm de largeur.\n";
	return 0;
	}

2.3.3- Dangers de la gestion directe de la mémorie

Fuite de mémoire

Réaffecter une valeur à un pointeur alors que celui est l'unique référence d'un espace mémoire fait perdre la référence d'un espace mémoire que la machine considère comme affecté. Par exemple :

int *pX=new int;
pX=new int;
On a perdu la référence sur le premier espace mémoire en faisant cela, mais la machine ne considère pas cet espace comme disponible

Danger des pointeurs

Il ne faut pas utiliser un pointeur pour lequel on a libéré l'espace mémoire delete. Une bonne méthode consiste à affecter la valeur nulle à un pointeur après avoir liberé l'espace mémoire qu'il pointe, sinon, si on réaccède cet espace mémoire, on risque de provoquer une erreur.

2.3.4- Tableaux et opérations sur pointeurs


Un tableau

#include 
 
int main()
    {
    int tab[5];
    int *a=tab;
    }
Ce code ne provoque pas d'erreur : c'est qu'un tableau d'entiers est un pointeur sur entier. En créant un tableau de 5 entiers, on réserve 5 espaces de taille int en mémoire, on retient l'adresse du premier.

Opérations sur les pointeurs

On peut reprendre l'exemple précédent et ajouter 1 à un pointeur :
#include 


         
int main()
	{
    int tab[5];
    int *a=tab;
    
    tab[0]=0;
    tab[1]=1;
    tab[2]=2;
    tab[3]=3;
    tab[4]=4;
    int temp=*(tab+1);
    std::cout << temp;
    }
En ajoutant 1 à un pointeur sur entier d'adresse x, on obtient un pointeur sur x+taille d'entier. On dispose donc de deux moyens pour parcourir le tableau :
#include 
    
int main()
	{
    int tab[5];
    int *a=tab;
    
    tab[0]=0; tab[1]=1; tab[2]=2; tab[3]=3; tab[4]=4;
             for(int i=0;i<5;i++)
             std::cout << *(tab+i) << " ";
    std::cout << "\n";
             for(int i=0;i<5;i++)
             std::cout << tab[i] << " ";
    }

Fonctions qui renvoient un pointeur

int *f1()
	{
	}

Ici la fonction f1 renvoie un pointeur sur entier.

Pointeur sur pointeur

Il est bien sur possible de définir des pointeur sur pointeurs :
int **t;

Ici t est un pointeur sur pointeur d'entier. Nous ne rentrons pas plus dans les détails ici.