TP 9: Les Pointeurs en C

 

Définition:

Un pointeur est une position mémoire qui peut contenir l'adresse d'une autre position mémoire. Généralement cette position mémoire est une variable qui contient l'adresse d'une autre variable. Si un pointeur P contient l'adresse d'une variable A, on dit que  'P pointe vers A'.

En C, chaque pointeur est limité à contenir l'adresse d'une variable d'un type donné.

 

Déclaration d'un pointeur

 

                   <type>   *<nomPointeur>;

 

<nomPointeur> est le nom de la variable pointeur qui vas contenir l'adresse d'une variable de type <type>.

 

Exemple:

 

int   *p;

 

p est le nom d'un pointeur qui peut contenir l'adresses d'une variable de type entier.

 

Les opérateurs de base

 

Lors du travail avec des pointeurs, nous avons besoin

- d'un opérateur 'adresse de': & pour obtenir l'adresse d'une variable.

- d'un opérateur 'contenu de': * pour accéder au contenu d'une adresse.

- d'une syntaxe de déclaration pour pouvoir déclarer un pointeur.

 

L'opérateur 'adresse de' : &

  & <NomVariable>    fournit l'adresse de la variable <NomVariable>

Exemple:

Dans l'exemple ci-dessous on a 2 variables de type entier (int) i et g et une autre variable p qui est un pointeur vers un entier (int *).

A l'etape 0 est declarée le pointeur p comme étant un pointeur vers un entier. A cette etape là, p contient n'importe quoi (certains compilateurs C initialisent un pointeur à 0 (NULL) lors de sa déclaration mais il ne faut pas se fier à celà; il faut toujours initialiser un pointeur apres sa déclaration). Aussi les 2 variables i et g sont initialisées respectivement à 25 et 48.

A l'etape 1 dans p on met l'adresse de i et à l'etape 2 on met dans p l'adresse de g.

 

int main(void)

{

       int  *p;        /* Etape 0 */

       int    i = 25 ;

       int    g = 48 ;

       p  =   &i ;     /* Etape  1 */

       p  =  &g ;     /* Etape  2 */

}

 

Ci-dessous est montré l’etat de la Mémoire Centrale (MC) aux 2 étapes 1 et 2 où par exemple 824 est l'adresse en MC de la variable i et 800 est l'adresse en MC de la variable g (Adresses et Valeurs des variables sont montrées en décimale mais en MC tout est en binaire).

L'opérateur 'contenu de' : *

*<NomPointeur>  désigne le contenu de l'adresse référencée par le pointeur  <NomPointeur>

Comme montré ci-dessous,  si p contient l'adresse de i alors (*p) désigne i c'est a dire la variable pointée par p.

 

Donc si on ecrit: p = &i;

alors si on veut mettre i dans g on peut écrire: g = i;

ou bien on peut écrire: g = *p;

car *p c'est tout simplement i.

 

Et si on veut mettre 12 dans i on peut écrire: i = 12;

ou bien on peut écrire: *p = 12;

car *p c'est tout simplement i.

 

int main(void)

{

       int  *p;

       int    i = 25 ;

       int g;

 

       p = &i;

       g = *p;       /* Etape  1 */

       *p = 12;     /* Etape  2 */

}

 

Ci-dessous est montré l’etat de la mémoire centrale aux 2 etapes 1 et 2.

Les opérations élémentaires sur pointeurs

En travaillant avec des pointeurs, nous devons observer les règles suivantes:

 

Priorité de * et &

- Les opérateurs * et & ont la même priorité que les autres opérateurs unaires (la négation !, l'incrémentation ++, la décrémentation --). Dans une même expression, les opérateurs unaires *, &, !, ++, -- sont évalués de droite à gauche.

 

Exemple  Après l'instruction    P = &X;  les expressions suivantes, sont équivalentes:

Y = *P+1

 

             Y = X+1

 

*P = *P+10

 

             X = X+10

 

*P += 2

 

             X += 2

Y=++*P 

 

             

Y=++X  (incrémente puis affecte)

 

 

 

 

 

 

 

 

                                   

Y=(*P)++                    Y=X++   (affecte puis incrémente)

 

Dans le dernier cas, les parenthèses sont nécessaires. Comme les opérateurs unaires * et ++ sont évalués de droite à gauche, sans les parenthèses le pointeur P serait incrémenté, non pas l'objet sur lequel P pointe. On peut uniquement affecter des adresses à un pointeur.

Le pointeur NULL

La valeur NULL est utilisée pour indiquer qu'un pointeur ne pointe 'nulle part’.
Remarque : Les pointeurs sont aussi des variables et peuvent être utilisés comme telles. 
Soient P1 et P2 deux pointeurs sur int, alors l'affectation P1 = P2; copie le contenu de P2 vers P1 alors P1 pointe sur le
même objet que P2

 

Pointeurs et tableaux

 

Comme nous l'avons déjà constaté, le nom d'un tableau représente l'adresse de son premier élément. En d'autre termes: &tableau[0] et tableau sont une seule et même adresse.

Il faut retenir donc que :

-       le nom d'un tableau est un pointeur constant sur le premier élément du tableau. 

Soit A le nom d’un tableau alors :

A+0 désigne &A[0]

A+1 désigne &A[1]

A+i désigne &A[i]

 

-       Si P est un pointeur sur un tableau A,  l’instruction P=Aest équivalente à &A[0]  alors :

P désigne &A[0]

P+1 désigne &A[1]

P+2 désigne &A[2]

P+i désigne &A[i]

 et *P  désigne le contenu de l’élément pointé par P.

*P         désigne A[0]

*(P+1)   désigne A[1]       

*(P+2)   désigne A[2]            

*(P+i)    désigne A[i]

-       Si P pointe sur une composante quelconque d'un tableau, alors P+1 pointe sur la composante suivante

Plus généralement,

 

P+i

pointe sur la ième composante derrière P et

 

 

P- i

pointe sur la ième composante devant P.

 

Exemple

 

En déclarant un tableau A de type int et un pointeur P sur int :    int A[10];   int *P;

 

Ainsi, après les instructions :  

 P = A;   le pointeur P pointe sur A[0]  est équivalent à P=&A[0] et P=A+0

*P = 3;   le contenu de P reçoit 3          est équivalent à A[0]=3

 

Important :

Il existe toujours une différence essentielle entre un pointeur et le nom d'un tableau:

- Un pointeur est une variable, donc des opérations comme P = A ou P++ sont permises.

- Le nom d'un tableau est une constante, donc des opérations comme A = P ou A++ sont impossibles, au même titre que 3++.

Exemple

Les deux programmes suivants copient les éléments positifs d'un tableau T dans un deuxième tableau POS.

 

Formalisme tableau

#include  <stdio.h>

main()
{ 
  int T[10] ,n, Pos[10], i, j ;
  do {
        printf("Veuillez entrer N entre 1 et 10 ..\n");
        scanf("%d",&n);
  } while ( (n < 1) || (n > 10)) 
  for(i=0;i<n;i++) 
scanf("%d", &T[i]);

j = 0;
  for (i=0 ; i<n ; i++)
{
        if (T[i]>0) 
{
Pos[j] = T[i];
j++;
}
}
  for(i=0; i<j; i++) 
printf("%d ", Pos[i]);
}

Formalisme pointeur

#include  <stdio.h>

main()
{
  int T[10] ,n, Pos[10], i, j, *p1, *p2 ;
  do {
      printf("Veuillez entrer N entre 1 et 10 ..\n");
      scanf("%d",&n);
  } while ( (n < 1) || (n > 10)) ;
  p1 = T ;
  for(i=0;i<n;i++, p1++) 
scanf("%d", p1);
  p1 = T; 
p2 = Pos;
  for (j=0,i=0 ; i<n ; i++)
{
      if (*(p1+i)>0) 
      { 
*(p2+j) = *(p1+i);
j++;
}
}
  for(i=0; i<j; i++) 
printf("%d ", *(p2+i));
}

 

Arithmétique des pointeurs

Affectation par un pointeur sur le même type

Soient P1 et P2 deux pointeurs sur le même type de données, alors l'instruction P1=P2; fait pointer P1 sur le même objet que P2.

 

- Addition et soustraction d'un nombre entier

Si P pointe sur l'élément A[i] d'un tableau, alors

 

P+n

pointe sur A[i+n]

 

P-n

pointe sur A[i-n]

Exemple :  p=A ; ou bien  p=&A[0]

                   p=p+9  à p pointe sur A[9]

                   p=p-1 à p pointe sur A[8]

 

- Incrémentation et décrémentation d'un pointeur

Si P pointe sur l'élément A[i] d'un tableau, alors après l'instruction

 

P++;

P pointe sur A[i+1]

 

P+=n;

P pointe sur A[i+n]

 

P--;

P pointe sur A[i-1]

 

P-=n;

P pointe sur A[i-n]

         

Exemple : Initialiser un tableau avec des 1

int  t[10], i ;

for(i=0 ;i<10 ;i++)

t[i]=1 ; // oubien  *(t+i)=1

int t[10], *p;

for(p=t ;p<t+10 ;p++)

     *p=1;

 

Domaine des opérations

L'addition, la soustraction, l'incrémentation et la décrémentation sur les pointeurs sont seulement définies à l'intérieur d'un tableau. Si l'adresse formée par le pointeur et l'indice sort du domaine du tableau, alors le résultat n'est pas défini.

 

- Soustraction de deux pointeurs

Soient P1 et P2 deux pointeurs qui pointent dans le même tableau:

P1-P2

fournit le nombre de composantes comprises entre P1 et P2.

Le résultat de la soustraction P1-P2 est

 

- négatif,

si P1 précède P2

 

- zéro,

si P1 = P2

 

- positif,

si P2 précède P1

 

- indéfini,

si P1 et P2 ne pointent pas dans le même tableau

 

Plus généralement, la soustraction de deux pointeurs qui pointent dans le même tableau est équivalente à la soustraction des indices correspondants.

 

- Comparaison de deux pointeurs

On peut comparer deux pointeurs par <, >, <=, >=, ==, !=.

La comparaison de deux pointeurs qui pointent dans le même tableau est équivalente à la comparaison des indices correspondants. (Si les pointeurs ne pointent pas dans le même tableau, alors le résultat est donné par leurs positions relatives dans la mémoire).

 

 

Pointeurs et chaînes de caractères

 

a) Affectation

On peut attribuer l'adresse d'une chaîne de caractères constante à un pointeur sur char:

Exemple :   char *C; // C est un pointeur sur 1 ou plusieurs caractères (chaine)

                    C = "Ceci est une chaîne de caractères constante";

 

Nous pouvons lire cette chaîne constante (p.ex: pour l'afficher), mais il n'est pas recommandé de la modifier, parce que le résultat d'un programme qui essaie de modifier une chaîne de caractères constante n'est pas prévisible.

 

b) Initialisation

Un pointeur sur char peut être initialisé lors de la déclaration si on lui affecte l'adresse d'une chaîne de caractères constante:       char *B = "Bonjour !";

Attention !  Il existe une différence importante entre les deux déclarations:

 
char A[ ] = "Salem !";   /* un tableau  */
char *B  = "Salem !";   /* un pointeur */

 

A est un tableau qui a exactement la grandeur pour contenir la chaîne de caractères et la terminaison '\0'. Les caractères de la chaîne peuvent être changés, mais le nom A va toujours pointer sur la même adresse en mémoire.

 

B est un pointeur qui est initialisé de façon à ce qu'il pointe sur une chaîne de caractères constante stockée quelque part en mémoire. Le pointeur peut être modifié et pointer sur autre chose. La chaîne constante peut être lue, copiée ou affichée, mais pas modifiée.

 

c) Modification

Si nous affectons une nouvelle valeur à un pointeur sur une chaîne de caractères constante, nous risquons de perdre la chaîne constante. D'autre part, un pointeur sur char a l'avantage de pouvoir pointer sur des chaînes de n'importe quelle longueur (allocation dynamique sera vue plus tard):

 

Exemple :    char *A = "Petite chaîne";

                     char *B = "Deuxième chaîne un peu plus longue";
                     A = B;   

Maintenant A et B pointent sur la même chaîne; la "Petite chaîne" est perdue:

 

Important : Les affectations discutées ci-dessus ne peuvent pas être effectuées avec des tableaux de caractères:

Exemple :

  char A[45] = "Petite chaîne";
  char B[45] = "Deuxième chaîne un peu plus longue";
  char C[30];
  A = B;    /* IMPOSSIBLE -> ERREUR !!! */  (A est une adresse constante)
  C = "Bonjour !"; /* IMPOSSIBLE -> ERREUR !!! */ (C aussi est une constante)

Dans cet exemple, nous essayons de copier l'adresse de B dans A, respectivement l'adresse de la chaîne constante dans C. Ces opérations sont impossibles et illégales parce que l'adresse représentée par le nom d'un tableau reste toujours constante.

 

Pour changer le contenu d'un tableau, nous devons changer les composantes du tableau l'une après l'autre ou déléguer cette charge à une fonction de <string> (strcpy(s1,s2) ).

 

Pointeurs et tableaux à deux dimensions

 

Exemple :   Le tableau M à deux dimensions est défini comme suit:

 
int M[4][10] = {{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
                {10,11,12,13,14,15,16,17,18,19},
                {20,21,22,23,24,25,26,27,28,29},
                {30,31,32,33,34,35,36,37,38,39}};

 

Le nom du tableau M représente l'adresse du premier élément du tableau et pointe sur le tableau M[0] qui a la valeur:   {0,1,2,3,4,5,6,7,8,9}.  (M représente &M[0])

L'expression (M+1) est l'adresse du deuxième élément du tableau et pointe sur M[1] qui a la valeur:                {10,11,12,13,14,15,16,17,18,19}. (M+1 représente &M[1])

 

Explication

Au sens strict du terme, un tableau à deux dimensions est un tableau unidimensionnel dont chaque composante est un tableau unidimensionnel. Ainsi, le premier élément de la matrice M est le vecteur {0,1,2,3,4,5,6,7,8,9}, le deuxième élément est {10,11,12,13,14,15,16,17,18,19} et ainsi de suite.

L'arithmétique des pointeurs qui respecte automatiquement les dimensions des éléments conclut logiquement que: M+Idésigne l'adresse du tableau M [I]

 

&M[0][0]  équivalent à M[0]+0   équivalent à  *(M+0)+0

&M[0][1]  équivalent à M[0]+1   équivalent à  *(M+0)+1

&M[1][0]  équivalent à M[1]+0   équivalent à  *(M+1)+0

&M[i][j]   équivalent à M[i]+j   équivalent à  *(M+i)+j

 

M[0][0]  équivalent à *(M[0]+0)   équivalent à  *(*(M+0)+0)

M[0][1]  équivalent à *(M[0]+1)   équivalent à  *(*(M+0)+1)

M[1][0]  équivalent à *(M[1]+0)   équivalent à  *(*(M+1)+0)

M[i][j]  équivalent à *(M[i]+j)   équivalent à  *(*(M+i)+j)

 

Important : M n’est pas de type int *, mais c’est un pointeur sur des blocs (lignes) donc si on désire pointer un pointeur P sur une matrice il faut faire une conversion forcée comme suit :

Int *p ;   p=M ; ß faux

P=(int*) M ; convertir M qui est un pointeur sur un tableau en un pointeur sur un int   

 

 

 

Tableaux de pointeurs

Déclaration :                            <Type> *<NomTableau>[<N>];

Exemple :       double *A[10];

Déclare un tableau de 10 pointeurs sur des réels du type double dont les adresses et les valeurs ne sont pas encore définies. 

Remarque

Le plus souvent, les tableaux de pointeurs sont utilisés pour mémoriser de façon économique des chaînes de caractères de différentes longueurs.

 

Pointeurs et structures

Exemple :

typedef  struct  { int Jour, Mois, Annee; } Date;

Date d;

Date *Ptr_Date;    /* Ptr_Date pointe sur des objets de type Date */

Ptr_Date = &d;

/* Il est alors possible d'utiliser ce pointeur de la façon suivante : */

 

(*Ptr_Date).jour=12;

/* ou bien */

Ptr_Date->Jour = 12;

 

 

Allocation Dynamique de Mémoire

 

Introduction

Si nous générons des données pendant l'exécution d’un programme, il nous faut des moyens pour réserver et libérer de la mémoire au fur et à mesure que nous en avons besoin. Nous parlons alors de l'allocation dynamique de la mémoire.

 

Déclaration statique de données

Chaque variable dans un programme a besoin d'un certain nombre d'octets en mémoire. Jusqu'ici, la réservation de la mémoire s'est déroulée automatiquement par l'emploi des déclarations des données. Dans tous ces cas, le nombre d'octets à réserver était déjà connu pendant la compilation. Nous parlons alors de la déclaration statique des variables.

 

Exemple :   int T[10] ; char Mot[30] ; …

 

Allocation dynamique

Problème

Souvent, nous devons travailler avec des données dont nous ne pouvons pas prévoir le nombre et la grandeur lors de la programmation. Ce serait alors un gaspillage de réserver toujours l'espace maximal prévisible. Il nous faut donc un moyen de gérer la mémoire lors de l'exécution du programme.

 

Exemple1 : int *T ; char * Mot ; …

 

Exemple2 : Nous voulons lire 10 phrases (de différentes tailles) au clavier et les mémoriser en utilisant un tableau de pointeurs sur char. Nous déclarons ce tableau de pointeurs par:   char  *TEXTE[10];

Pour les 10 pointeurs, nous avons besoin de 10*p octets. Ce nombre est connu dès le départ et les octets sont réservés automatiquement. Il nous est cependant impossible de prévoir à l'avance le nombre d'octets à réserver pour les phrases elles-mêmes qui seront introduites lors de l'exécution du programme ...

La réservation de la mémoire pour les 10 phrases peut donc seulement se faire pendant l'exécution du programme. Nous parlons dans ce cas de l'allocation dynamique de la mémoire.

 

La fonction malloc

La fonction malloc de la bibliothèque <stdlib.h> nous aide à localiser et à réserver de la mémoire au cours d'un programme.

La fonction malloc( <N> ) fournit l'adresse d'un bloc en mémoire de <N> octets libres ou la valeur NULL s'il n'y a pas assez de mémoire.

 

Exemple :

Supposons que nous ayons besoin d'un bloc en mémoire pour un texte de 4000 caractères. Nous disposons d'un pointeur T sur char (char *T).

Alors l'instruction:   T = malloc(4000); fournit l'adresse d'un bloc de 4000 octets libres et l'affecte à T. S'il n'y a plus assez de mémoire, T obtient la valeur NULL.

 

La fonction free

 

Si nous n'avons plus besoin d'un bloc de mémoire que nous avons réservé à l'aide de malloc, alors nous pouvons le libérer à l'aide de la fonction free de la bibliothèque <stdlib.h>.

 
free( <Pointeur> ) 
Libère le bloc de mémoire désigné par le <Pointeur>; n'a pas d'effet si le pointeur a la valeur NULL.

Exemple 1: Soit le programme suivant qui alloue un enregistrement de type Date, demande à l’utilisateur de donner des valeurs et à la fin libère l’enregistrement alloué.

 

#include <stdio.h>

#include <stdlib.h>

 

typedef struct

{

            int jour;

            int mois;

            int annee;

}  Date;

 

main()

{

            Date *p;

           

            p = (Date *) malloc(sizeof(Date));

           

            if (p != NULL)

            {

                        printf("Donner jour, mois et annee ...\n");

                        scanf("%d %d %d", &p->jour, &p->mois, &p->annee);

                       

                        printf(" Jour et mois et annee sont : %d-%d-%d \n", p->jour, p->mois, p->annee);

                        free(p);

            }

            else

              printf("Problem allocation memoire ...\n");

             

            system("pause");

            return 0;

}

 

Exemple 2: Soit le programme suivant qui demande à l'utilisateur de donner un entier n ensuite alloue de l'espace pour n entiers et demande à l'utilisateur de donner  n valeurs entières et les met dans l'espace memoire alloué.

 

#include <stdio.h>

#include <stdlib.h>

 

int main()

{

    int i, n, *p, *p1;

 

    printf("Combien d'entiers vous voulez rentrer\n");

    scanf("%d", &n);

    p = malloc(n * sizeof(int));

    if (p == NULL)

    {

        printf("Probleme Allocation de %d octets\n", n * sizeof(int));

        return 0;

    }

 

    p1 = p;

    for (i = 1; i <= n; i++)

    {

        printf("Donner entier %d\n", i);

        scanf("%d", p1);

        p1++;

    }

 

    p1 = p;

    printf("Liste des Elements Rentres:\n");

    for (i = 0; i < n; i++)

    {

        printf("%d ", *p1);

        p1++;

    }

    printf("\n");

 

    free(p);

    return 0;

}

 

La fonction calloc

La fonction calloc de la bibliothèque <stdlib.h> nous aide à localiser et à réserver de la mémoire au cours d'un programme et initialise tous les octets de la zone alloué à 0.

La fonction calloc(nbrElements, tailleElement) fournit l'adresse d'un bloc en mémoire de (nbrElements * tailleElement) octets libres ou la valeur NULL s'il n'y a pas assez de mémoire.

 

La différence entre malloc() et calloc() est que calloc() initialise tous les octest de la zone alloué à 0 alors que malloc() ne fait pas cela.

 

 

La fonction realloc

La fonction realloc(ptr, taille) de la bibliothèque <stdlib.h> essaye de donner une nouvelle taille à la zone mémoire pointée par ptr et qui a été précédemment allouée par malloc() ou calloc().

La function retourne le pointeur vers la nouvelle zone de mémoire allouée ou bien NULL dans le cas d’un echec.