Chapitre 3 : Les pointeurs

Ce matériel couvre les notions fondamentales des pointeurs en langage C, destinées aux étudiants en informatique ou en programmation souhaitant comprendre la gestion mémoire, l'arithmétique des pointeurs, l'allocation dynamique, ainsi que leur utilisation avec les tableaux, chaînes de caractères et structures.

D'après le document Chapitre 3 : Les pointeurs

Cet article a été rédigé automatiquement à partir du document source, puis vérifié avant publication.

Document source

Chapitre 3 : Les pointeurs

Computer Science - C Programming · PDF · 13 pages

Afficher l'aperçu du document

Consulter le document original →

Ce matériel couvre les notions fondamentales des pointeurs en langage C, destinées aux étudiants en informatique ou en programmation souhaitant comprendre la gestion mémoire, l'arithmétique des pointeurs, l'allocation dynamique, ainsi que leur utilisation avec les tableaux, chaînes de caractères et structures.

Adresse et valeur d'un objet

En mémoire centrale, chaque variable est stockée à une adresse unique, identifiée par un numéro appelé adresse. Pour accéder à une variable, il suffit de connaître l'adresse de l'octet où elle est stockée (ou celle du premier octet si la variable occupe plusieurs octets contigus).

Une Lvalue (left value) est un objet pouvant apparaître à gauche d'une affectation. Elle est caractérisée par :

  • son adresse mémoire,
  • sa valeur stockée à cette adresse.

Exemple :

int i, j;
i = 3;
j = i;

Si i est stocké à l'adresse 4831836000 et j à 4831836004, alors :

ObjetAdresseValeur
i48318360003
j48318360043

L'affectation i = j; ne modifie que les valeurs, pas les adresses. Les variables de type int occupent 4 octets, donc la valeur de i est stockée sur les octets d'adresse 4831836000 à 4831836003.

L'opérateur & permet d'obtenir l'adresse d'une variable. Par exemple, &i donne l'adresse de i. Cette expression n'est pas une Lvalue, mais une constante, on ne peut donc pas l'affecter.

Notion de pointeur

Un pointeur est un objet (Lvalue) dont la valeur est l'adresse d'un autre objet. Sa déclaration s'écrit :

type *nom_du_pointeur;

où type est le type de l'objet pointé. Par exemple :

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

Dans cet exemple, p contient l'adresse de i. La valeur de p est donc un entier représentant une adresse mémoire.

L'opérateur unaire d'indirection * permet d'accéder à la valeur pointée :

printf("*p = %d\n", *p);

affiche la valeur de i (ici 3). Modifier *p modifie directement i.

Exemple comparatif

main()
{
  int i = 3, j = 6;
  int *p1, *p2;
  p1 = &i;
  p2 = &j;
  *p1 = *p2;  // copie la valeur pointée par p2 dans celle pointée par p1
}

Après exécution, i vaut 6, j reste 6, les pointeurs n'ont pas changé.

main()
{
  int i = 3, j = 6;
  int *p1, *p2;
  p1 = &i;
  p2 = &j;
  p1 = p2;  // p1 pointe maintenant vers j
}

Après exécution, i vaut 3, mais p1 pointe vers j. Modifier *p1 modifiera alors j.

Arithmétique des pointeurs

Les opérations arithmétiques valides sur les pointeurs sont :

  • addition d'un entier à un pointeur,
  • soustraction d'un entier à un pointeur,
  • différence entre deux pointeurs du même type (résultat entier).

La somme de deux pointeurs n'est pas autorisée.

Si p est un pointeur sur un objet de type type, alors p + i désigne un pointeur dont la valeur est p incrémentée de i * sizeof(type).

Exemple

main()
{
  int i = 3;
  int *p1, *p2;
  p1 = &i;
  p2 = p1 + 1;
  printf("p1 = %ld \t p2 = %ld\n", p1, p2);
}

Affiche par exemple :

p1 = 4831835984 p2 = 4831835988

Pour un pointeur sur double :

main()
{
  double i = 3;
  double *p1, *p2;
  p1 = &i;
  p2 = p1 + 1;
  printf("p1 = %ld \t p2 = %ld\n", p1, p2);
}

Affiche :

p1 = 4831835984 p2 = 4831835992

Les opérateurs de comparaison sont aussi applicables aux pointeurs du même type.

Parcours d'un tableau avec pointeurs

#define N 5
int tab[5] = {1, 2, 6, 0, 7};
main()
{
  int *p;
  printf("\n ordre croissant:\n");
  for (p = &tab[0]; p <= &tab[N-1]; p++)
    printf(" %d \n", *p);
  printf("\n ordre decroissant:\n");
  for (p = &tab[N-1]; p >= &tab[0]; p--)
    printf(" %d \n", *p);
}

Allocation dynamique

Un pointeur doit être initialisé avant d'être utilisé avec l'opérateur d'indirection *. Sinon, il vaut la constante symbolique NULL (souvent 0).

Pour réserver dynamiquement de la mémoire, on utilise la fonction malloc :

malloc(nombre_octets)

Elle retourne un pointeur de type char * vers un espace mémoire de taille nombre_octets. Pour un pointeur sur un autre type, on effectue un cast :

#include <stdlib.h>
int *p;
p = (int*)malloc(sizeof(int));

Exemple complet :

#include <stdio.h>
#include <stdlib.h>
main()
{
  int i = 3;
  int *p;
  printf("valeur de p avant initialisation = %ld\n", p);
  p = (int*)malloc(sizeof(int));
  printf("valeur de p apres initialisation = %ld\n", p);
  *p = i;
  printf("valeur de *p = %d\n", *p);
}

Avant allocation, p vaut 0 (NULL), après allocation, p pointe vers un espace mémoire réservé. L'affectation *p = i; stocke la valeur 3 à cette adresse.

Allocation pour plusieurs objets

#include <stdio.h>
#include <stdlib.h>
main()
{
  int i = 3;
  int j = 6;
  int *p;
  p = (int*)malloc(2 * sizeof(int));
  *p = i;
  *(p + 1) = j;
  printf("p = %ld \t *p = %d \t p+1 = %ld \t *(p+1) = %d \n", p, *p, p+1, *(p+1));
}

Affiche par exemple :

p = 5368711424 *p = 3 p+1 = 5368711428 *(p+1) = 6

Fonction calloc

calloc(nb_objets, taille_objets) alloue un espace mémoire initialisé à zéro. Par exemple :

p = (int*)calloc(N, sizeof(int));

équivaut à :

p = (int*)malloc(N * sizeof(int));
for (i = 0; i < N; i++)
  *(p + i) = 0;

Libération de mémoire

Lorsque la mémoire allouée dynamiquement n'est plus utilisée, il faut la libérer avec :

free(nom_du_pointeur);

Chaque appel à malloc ou calloc doit être suivi d'un appel à free.

Pointeurs et tableaux

Pointeurs et tableaux à une dimension

Un tableau en C est un pointeur constant vers son premier élément. Par exemple :

int tab[10];

Le nom tab est un pointeur constant égal à &tab[0]. On peut parcourir un tableau avec un pointeur :

#define N 5
int tab[5] = {1, 2, 6, 0, 7};
main()
{
  int i;
  int *p;
  p = tab;
  for (i = 0; i < N; i++)
  {
    printf(" %d \n", *p);
    p++;
  }
}

L'opérateur d'indexation [] s'applique aussi aux pointeurs :

p[i] = *(p + i)

On peut donc écrire :

for (i = 0; i < N; i++)
  printf(" %d \n", p[i]);

Contrairement aux pointeurs, un tableau est un pointeur constant :

  • on ne peut pas modifier sa valeur (exemple : tab++ est interdit),
  • on ne peut pas déclarer un tableau dont la taille dépend d'une variable du programme,
  • on ne peut pas créer de tableaux bidimensionnels avec des lignes de tailles différentes.

Ces limitations disparaissent avec l'allocation dynamique.

Création dynamique d'un tableau

#include <stdlib.h>
main()
{
  int n;
  int *tab;
  ...
  tab = (int*)malloc(n * sizeof(int));
  ...
  free(tab);
}

Pour initialiser à zéro :

tab = (int*)calloc(n, sizeof(int));

Pointeurs et tableaux à plusieurs dimensions

Un tableau à deux dimensions est un tableau de tableaux, donc un pointeur vers un pointeur :

int tab[M][N];

Le nom tab est un pointeur constant vers un objet de type pointeur d'entier. tab[i] est un pointeur constant vers la ligne i.

Pour créer dynamiquement une matrice à k lignes et n colonnes :

int **tab;
tab = (int**)malloc(k * sizeof(int*));
for (i = 0; i < k; i++)
  tab[i] = (int*)malloc(n * sizeof(int));
...
for (i = 0; i < k; i++)
  free(tab[i]);
free(tab);

On peut choisir des tailles différentes pour chaque ligne :

for (i = 0; i < k; i++)
  tab[i] = (int*)malloc((i + 1) * sizeof(int));

Pointeurs et chaînes de caractères

Une chaîne de caractères est un tableau de char se terminant par le caractère nul '\0'. On peut manipuler une chaîne avec un pointeur char * :

char *chaine;
chaine = "ceci est une chaine";

On peut incrémenter ce pointeur :

chaine++;

Exemple pour compter le nombre de caractères :

#include <stdio.h>
main()
{
  int i;
  char *chaine;
  chaine = "chaine de caracteres";
  for (i = 0; *chaine != '\0'; i++)
    chaine++;
  printf("nombre de caracteres = %d\n", i);
}

La fonction standard strlen(chaine) fait de même.

Concaténation de chaînes avec pointeurs

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
main()
{
  int i;
  char *chaine1, *chaine2, *res, *p;
  chaine1 = "chaine ";
  chaine2 = "de caracteres";
  res = (char*)malloc((strlen(chaine1) + strlen(chaine2)) * sizeof(char));
  p = res;
  for (i = 0; i < strlen(chaine1); i++)
    *p++ = chaine1[i];
  for (i = 0; i < strlen(chaine2); i++)
    *p++ = chaine2[i];
  printf("%s\n", res);
}

On utilise un pointeur intermédiaire p pour ne pas perdre la référence au début de res.

Pointeurs et structures

Pointeur sur une structure

Les structures sont des Lvalues avec une adresse. On peut manipuler des pointeurs sur structures :

#include <stdlib.h>
#include <stdio.h>

struct eleve
{
  char nom[20];
  int date;
};

typedef struct eleve *classe;

main()
{
  int n, i;
  classe tab;
  printf("nombre d'eleves de la classe = ");
  scanf("%d", &n);
  tab = (classe)malloc(n * sizeof(struct eleve));
  for (i = 0; i < n; i++)
  {
    printf("\n saisie de l'eleve numero %d\n", i);
    printf("nom de l'eleve = ");
    scanf("%s", &tab[i].nom);
    printf("\n date de naissance JJMMAA = ");
    scanf("%d", &tab[i].date);
  }
  printf("\n Entrez un numero  ");
  scanf("%d", &i);
  printf("\n Eleve numero %d:", i);
  printf("\n nom = %s", tab[i].nom);
  printf("\n date de naissance = %d\n", tab[i].date);
  free(tab);
}

Pour accéder à un membre via un pointeur p sur structure :

(*p).membre ou plus simplement p->membre.

Dans l'exemple, tab[i].nom peut s'écrire (tab + i)->nom.

Structures auto-référencées

Une structure peut contenir un pointeur vers une structure du même type, utile pour les listes chaînées :

struct cellule
{
  int valeur;
  struct cellule *suivant;
};

typedef struct cellule *liste;

Insertion en tête de liste :

liste insere(int element, liste Q)
{
  liste L;
  L = (liste)malloc(sizeof(struct cellule));
  L->valeur = element;
  L->suivant = Q;
  return(L);
}

Exemple complet :

#include <stdlib.h>
#include <stdio.h>

struct cellule
{
  int valeur;
  struct cellule *suivant;
};

typedef struct cellule *liste;

liste insere(int element, liste Q)
{
  liste L;
  L = (liste)malloc(sizeof(struct cellule));
  L->valeur = element;
  L->suivant = Q;
  return(L);
}

main()
{
  liste L, P;
  L = insere(1, insere(2, insere(3, insere(4, NULL))));
  printf("\n impression de la liste:\n");
  P = L;
  while (P != NULL)
  {
    printf("%d \t", P->valeur);
    P = P->suivant;
  }
}

Arbre binaire avec structure auto-référencée

struct noeud
{
  int valeur;
  struct noeud *fils_gauche;
  struct noeud *fils_droit;
};

typedef struct noeud *arbre;

Glossaire des termes clés

  • Lvalue : Objet pouvant apparaître à gauche d'une affectation, caractérisé par une adresse et une valeur.
  • Pointeur : Variable dont la valeur est l'adresse d'un autre objet.
  • Adresse : Numéro unique identifiant un octet en mémoire.
  • Indirection (*) : Opérateur permettant d'accéder à la valeur pointée par un pointeur.
  • Allocation dynamique : Réservation d'un espace mémoire à l'exécution via malloc ou calloc.
  • Tableau : Ensemble contigu d'objets du même type, représenté par un pointeur constant vers le premier élément.
  • Structure : Type composite regroupant plusieurs membres de types variés.
  • Structure auto-référencée : Structure contenant un pointeur vers une structure du même type, utilisée pour les listes chaînées et arbres.
  • NULL : Constante symbolique représentant un pointeur ne pointant vers aucun objet.

Points clés à retenir

  • Un pointeur contient l'adresse d'un objet et permet d'accéder à sa valeur via l'indirection.
  • L'arithmétique des pointeurs permet de parcourir des tableaux en mémoire.
  • L'allocation dynamique avec malloc ou calloc est nécessaire pour réserver de la mémoire à l'exécution.
  • Un tableau est un pointeur constant, on ne peut pas modifier sa valeur ni sa taille à l'exécution.
  • Les chaînes de caractères sont des tableaux de char terminés par '\0' et manipulables avec des pointeurs.
  • Les structures peuvent être manipulées par pointeurs, facilitant la gestion dynamique et la création de structures complexes comme les listes chaînées.
  • La libération de la mémoire allouée dynamiquement est indispensable pour éviter les fuites mémoire.

Partager

Commentaires

Aucun commentaire pour le moment. Posez la première question.

Les commentaires sont relus avant publication. Votre e-mail n'est jamais affiché.

← Toutes les révisions