Theorie sur les pointeurs

Un pointeur est une variable qui stocke l’adresse d’une autre variable. On utilise le caractère * pour déclarer un pointeur et & pour obtenir l’adresse d’une variable existante. En C, il existe des pointeurs pour tous les types : int*, float*, char*… Cela signifie que * indique qu’il s’agit d’un pointeur vers une donnée d’un certain type. Le type complet est donc int*, float*, etc., à ne pas confondre avec l’opérateur * de déréférencement qui est utilisé en dehors des déclarations.

Voici quelques exemples :

int *a;
float *b;
char *c;

Chacune de ces variables occupe la même taille en mémoire (l’espace nécessaire pour stocker une adresse) et pointe vers un type distinct, chacun ayant une taille différente en mémoire. La taille d’un pointeur dépend de l’architecture (généralement : 4 octets sur une machine 32 bits, 8 octets sur une machine 64 bits), tout comme la taille des variables pointées.

  • Pour obtenir l’adresse d’une variable, on utilise l’opérateur &. Si le type de la variable est int, alors &variable devient de type int*. On peut continuer ce principe : int*int** (pointeur vers un pointeur), etc.
  • Inversement, l’opérateur * appliqué à un pointeur permet de lire ou modifier la valeur pointée. Si le type initial est int*, alors *pointeur est de type int. Attention : les types qui ne sont pas des adresses ne peuvent pas être déréférencés, cela n’a aucun sens.

Pour prendre l’adresse d’une variable, utilisez donc & :

int a = 42;     // a est stocké quelque part
int *b = &a;    // b est stocké quelque part et contient l'adresse de a
int **c = &b;   // c est stocké quelque part et contient l'adresse de b

Pour lire ou modifier la valeur à une adresse, utilisez * :

int d = *b;     // d contient la valeur de a (copie)
int *e = *c;    // e contient l'adresse de a (copie du pointeur)
int f = **c;    // e contient également la valeur de a (copie)
*d = 50;        // modifie la variable a via le pointeur
e = 42;         // modifie uniquement e, a n'a pas changé

Par contre, ces lignes n’ont aucun sens et provoqueront des erreurs :

*e = 8;         // e n'est pas un pointeur, c'est un int
&e = f;         // on ne peut pas modifier l'adresse d'une variable
int g = *f;     // f n'est pas un pointeur
int h = &a;     // types incompatibles : int vs int*
int **i = &&a;     // Cela correspond a l'operateur logique && = ET
int **j = &(&a);     // On ne peux pas appliquer & à un résultat qui n’est pas une variable.

Rappelez-vous : le type à droite et le type à gauche doivent être compatibles. Si le compilateur vous donne un avertissement avec -Wall, c’est probablement que vous avez fait une erreur.

Ces mécanismes permettent de manipuler la mémoire de façon flexible. Les pointeurs représentent des positions en mémoire, et comprendre les pointeurs, c’est comprendre où et comment lire ou modifier une information.

Mise en pratique

Base des pointeurs

Vous partirer de ce code de base :

#include <stdio.h>

int main() {
    int x = 42; // «La reponsse a l'univers»
    printf("%d\n",x); 
    return 0;
}

Déclarer et utiliser un pointeur

  1. Déclarez un pointeur px qui pointe vers x.
  2. Affichez l’adresse de x grace a %p avec un nouveau printf
  3. Affichez la valeur de x via le pointeur.
  4. Utilisez px pour changer la valeur de x à 25 : «Le paradis des souris».
  5. Affichez x directement et via le pointeur.

Échanger deux variables via des pointeurs

  1. Écrivez une fonction void swap(int* a, int* b) qui échange les valeurs pointées par a et b.
  2. Testez-la sur deux variables int a = 5, b = 8;.
void swap(int* a, int* b) {
}

Question : Pourquoi passer les variables par pointeur et non directement ?

Pointeurs et arithmétique

L’arithmétique des pointeurs permet de se déplacer en mémoire. Lorsqu’on incrémente un pointeur, il avance à l’élément suivant du type pointé. Donc il y a un calcule base sur sizeof pour savoir combien de byte l’on doit ce deplacer.

Écrivez un programme qui :

  1. Déclare un tableau int tab[5] = {10, 20, 30, 40, 50};
  2. Déclare un pointeur int *p qui pointe vers le premier élément
  3. Utilise une boucle avec l’arithmétique de pointeurs p++ a la comparaison de ponteur < pour afficher tous les éléments du tableau
  4. Affichez également les adresses de chaque élément
  5. Que donne p-tab au fur et a mesure ?

Somme d’éléments avec pointeurs

Écrivez une fonction int sum(int *tab, int taille) qui calcule la somme de tous les éléments d’un tableau en utilisant uniquement l’arithmétique de pointeurs (pas d’indexation avec []). Testez votre fonction avec le tableau {5, 10, 15, 20, 25}.

int sum(int *tab, int taille) {
    // À compléter
}

Chercher dans le cours ou votre memoire. Pour la taille est donnee en parametre ?

Trouver le maximum avec un pointeur

Écrivez une fonction void find_max(int *tab, int taille, int *max, int *indice) qui trouve la valeur maximale d’un tableau ainsi que son indice. La fonction doit modifier les valeurs pointées par max et indice pour retourner ces deux résultats.

void find_max(int *tab, int taille, int *max, int *indice) {
    // À compléter
}

int main() {
    int tableau[6] = {3, 7, 2, 9, 1, 8};
    int valeur_max, position;

    find_max(tableau, 6, &valeur_max, &position);

    printf("Maximum : %d à l'indice %d\n", valeur_max, position);
    return 0;
}

Double pointeurs

Les pointeurs vers des pointeurs (int**) sont souvent utilisés pour modifier un pointeur lui-même, pas seulement la valeur pointée.

Complétez le programme suivant

#include <stdio.h>

int main() {
    int a = 10;
    int b = 20;
    int *p = &a;    // p pointe vers a
    int **pp = &p;  // pp pointe vers p

    printf("Valeur de a : %d\n", a);
    printf("Valeur via *p : %d\n", *p);
    printf("Valeur via **pp : %d\n", **pp);

    // 1. Utilisez **pp pour changer la valeur de a à 100

    // 2. Faites pointer p vers b en utilisant *pp

    // 3. Affichez la nouvelle valeur pointée par p

    return 0;
}

Trouver l’indice du premier element equale

Écrivez une fonction void find_first(int *tab, int taille, int *max, int *indice) qui trouve la premiere occurance d’une valeur dans un. La fonction doit modifier les valeurs pointées adresse.

void find_first(int *tab, int taille, int element, int *addresse) {
    // À compléter
}

int main() {
    int valeur = 9;
    int tableau[6] = {3, 7, 2, 9, 1, 8};
    int *addresse = 0;

    find_first(tableau, 6, valeur, &position, &addresse);

        if(addresse)
           printf("%d ce trouve l'adresse %p ce qui donne\n", valeur, addresse, *addresse);
        else
           printf("%d n'est pas trouve\n", valeur);

    return 0;
}

Tableaux de pointeurs

Un tableau de pointeurs permet de stocker plusieurs adresses. Dans ce cas tab_pointeurs est unt int**

#include <stdio.h>

int main() {
    int a = 10, b = 20, c = 30;

    // Créez un tableau de 3 pointeurs vers des entiers
    int *tab_pointeurs[3];

    // Faites pointer chaque élément vers a, b, et c
    tab_pointeurs[0] = &a;
    tab_pointeurs[1] = &b;
    tab_pointeurs[2] = &c;

    // Parcourez le tableau et affichez les valeurs pointées
    for (int i = 0; i < 3; i++) {
        printf("Valeur %d : %d\n", i, *tab_pointeurs[i]);
    }

    // Modifiez les valeurs de a, b et c grace a tab_pointeurs
        // TODO

    printf("\nAprès modification :\n");
    printf("a = %d, b = %d, c = %d\n", a, b, c);

    return 0;
}

Manipulation mémoire

Ces exercices illustrent des techniques de manipulation mémoire à des fins pédagogiques. Ces pratiques peuvent causer des comportements indéfinis en C en fonction du système hôte.

Lorsque vous déclarez plusieurs variables locales consécutives, elles sont placées de manière contiguë dans la pile (stack). L’ordre exact dépend du compilateur et de l’architecture.

Exploration de la mémoire

Observez cet exemple :

#include <stdio.h>

int main() {
    int a, b, c, d, e;
    int *ptr = &c;

    *ptr = 2207;        // Olympus
    *(ptr - 2) =  8;    // Fallout 2
    *(ptr - 1) = 13;    // Fallout 1
    *(ptr + 1) = 25;    // Le paradis des souris
    *(ptr + 2) = 42;    // La réponse à l'univers

    printf("a = %d\n", a);
    printf("b = %d\n", b);
    printf("c = %d\n", c);
    printf("d = %d\n", d);
    printf("e = %d\n", e);

    return 0;
}
  1. Avant d’exécuter le programme, essayez de prédire quelles variables recevront quelles valeurs.
  2. Compilez et exécutez le programme. Les résultats correspondent-ils à vos prédictions ?
  3. Qu’est-ce qui détermine dans quelle variable chaque valeur sera stockée ?
  4. Que produirait ptr + 100 et *(ptr + 100) = 98 ?

Indice : L’arithmétique de pointeurs avance ou recule en fonction de la taille du type. ptr + 1 avance de sizeof(int) octets.

Modifiez le programme pour afficher les adresses mémoire de chaque variable

  1. Quelle est la différence en octets entre les adresses de a et b ?
  2. Les variables sont-elles stockées dans l’ordre croissant ou décroissant en mémoire ?
  3. Pourquoi ptr - 1 et ptr + 1 ne pointent-ils pas nécessairement vers b et d ?

Modifiez le programme

Remplacez les lignes

    int a, b, c, d, e;
    int *ptr = &c;

par

    int a, b;
    char c;
    int d, e;
    char *ptr = &c;

Que se passe-t-il sur toutes vos observations précédentes ?

Avez vous des observation differentes avec le code suivant ?

int main() {
    int a, b, c, d, e;
    int *ptr = &a;

    *ptr = 2207;        // Olympus
    *(ptr + 3) =  8;    // Fallout 2
    *(ptr + 4) = 13;    // Fallout 1
    *(ptr + 1) = 25;    // Le paradis des souris
    *(ptr + 2) = 42;    // La réponse à l'univers

    printf("a = %d\n", a);
    printf("b = %d\n", b);
    printf("c = %d\n", c);
    printf("d = %d\n", d);
    printf("e = %d\n", e);

    return 0;
}

Parcours complet de la zone mémoire

Écrivez un programme qui utilise un pointeur pour parcourir et initialiser toutes les variables :

#include <stdio.h>

int main() {
    int v1, v2, v3, v4, v5;

    // 1. Créez un pointeur vers v3 (la variable du milieu)
    int *ptr = &v3;

    // 2. Utilisez l'arithmétique de pointeurs pour initialiser :
    //    v1 = 10, v2 = 20, v3 = 30, v4 = 40, v5 = 50
    //    SANS utiliser les noms de variables v1, v2, v4, v5
    //    (seulement ptr avec +/- et *)

    // À compléter ici

    // 3. Vérification : affichez toutes les valeurs
    printf("v1 = %d, v2 = %d, v3 = %d, v4 = %d, v5 = %d\n", 
           v1, v2, v3, v4, v5);

    return 0;
}