Langage C/C++

Guide Complet : Pointeur de Pointeur en C

Un pointeur de pointeur (ou double pointeur) en C est une variable qui contient l’adresse d’un pointeur, lequel pointe lui-même vers une autre variable. Les pointeurs de pointeurs sont utilisés pour gérer des structures complexes, comme des tableaux 2D, pour manipuler des pointeurs dans des fonctions, et pour gérer dynamiquement la mémoire.


1. Définition et Syntaxe

Un pointeur de pointeur est déclaré en ajoutant deux astérisques (**) dans sa définition.

Syntaxe :

type **nom_du_pointeur;

Exemple :

int **ptr; // ptr est un pointeur vers un pointeur d'entiers

Dans cet exemple, ptr peut contenir l’adresse d’un autre pointeur qui, lui, pointe vers un entier.


2. Principe Fondamental

Pour comprendre le fonctionnement, imaginons trois niveaux de mémoire :

  1. Variable : Contient la valeur réelle.
  2. Pointeur : Contient l’adresse de la variable.
  3. Pointeur de pointeur : Contient l’adresse du pointeur.

Exemple de base :

#include <stdio.h>

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

    printf("Valeur de a : %d\n", a);
    printf("Adresse de a (contenue dans p) : %p\n", (void *)p);
    printf("Adresse de p (contenue dans pp) : %p\n", (void *)pp);
    printf("Valeur de a via pp : %d\n", **pp); // Double déréférencement
    return 0;
}

3. Applications des Pointeurs de Pointeurs

3.1 Modification d’un pointeur dans une fonction

Un pointeur de pointeur est souvent utilisé pour modifier un pointeur dans une fonction.

Exemple :

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

void allouerMemoire(int **ptr) {
    *ptr = malloc(sizeof(int)); // Alloue de la mémoire pour un entier
    if (*ptr != NULL) {
        **ptr = 42; // Initialise la valeur pointée
    }
}

int main() {
    int *p = NULL;

    allouerMemoire(&p); // Passe l’adresse de p
    if (p != NULL) {
        printf("Valeur : %d\n", *p); // Affiche 42
        free(p); // Libère la mémoire
    }
    return 0;
}

3.2 Allocation Dynamique d’un Tableau 2D

Les pointeurs de pointeurs sont utilisés pour créer des tableaux 2D dynamiques.

Exemple :

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

int main() {
    int rows = 3, cols = 4;
    int **matrix = malloc(rows * sizeof(int *)); // Alloue un tableau de pointeurs

    for (int i = 0; i < rows; i++) {
        matrix[i] = malloc(cols * sizeof(int)); // Alloue chaque ligne
    }

    // Initialisation
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = i + j;
        }
    }

    // Affichage
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }

    // Libération de la mémoire
    for (int i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);

    return 0;
}

3.3 Tableaux de chaînes de caractères

Les pointeurs de pointeurs sont utilisés pour gérer des tableaux de chaînes de caractères.

Exemple :

#include <stdio.h>

int main() {
    char *noms[] = {"Alice", "Bob", "Charlie"};
    char **ptr = noms; // Pointeur vers un tableau de chaînes

    for (int i = 0; i < 3; i++) {
        printf("%s\n", ptr[i]); // Affiche chaque chaîne
    }
    return 0;
}

3.4 Manipulation d’une liste chaînée

Les pointeurs de pointeurs permettent d’insérer des éléments dans une liste chaînée.

Exemple :

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

typedef struct Node {
    int data;
    struct Node *next;
} Node;

void ajouterEnTete(Node **head, int valeur) {
    Node *nouveau = malloc(sizeof(Node));
    if (nouveau != NULL) {
        nouveau->data = valeur;
        nouveau->next = *head;
        *head = nouveau;
    }
}

void afficherListe(Node *head) {
    while (head != NULL) {
        printf("%d -> ", head->data);
        head = head->next;
    }
    printf("NULL\n");
}

int main() {
    Node *head = NULL;

    ajouterEnTete(&head, 10);
    ajouterEnTete(&head, 20);
    ajouterEnTete(&head, 30);

    afficherListe(head);

    // Libération de la mémoire
    Node *temp;
    while (head != NULL) {
        temp = head;
        head = head->next;
        free(temp);
    }

    return 0;
}

4. Précautions à Prendre

  1. Initialisation : Assurez-vous que les pointeurs et pointeurs de pointeurs sont correctement initialisés avant usage. int **pp = NULL;
  2. Vérification des allocations : Vérifiez toujours que malloc ne retourne pas NULL.
  3. Libération de mémoire : Libérez chaque niveau de mémoire alloué pour éviter les fuites. free(pointeur[i]); // Libère chaque ligne free(pointeur); // Libère les pointeurs des lignes
  4. Évitez les accès invalides : Assurez-vous que les pointeurs pointent vers des zones valides avant de les déréférencer.

5. Résumé des Avantages

AvantagesDescription
Manipulation flexiblePermet de modifier des pointeurs à l’intérieur des fonctions.
Gestion de structures complexesSimplifie la gestion des tableaux 2D ou des listes chaînées.
Optimisation mémoirePrend en charge des allocations dynamiques pour des besoins variés.

Cas Particuliers des Pointeurs de Pointeurs en C

Les pointeurs de pointeurs en C sont des outils puissants pour manipuler des structures complexes, mais leur utilisation peut présenter des cas particuliers qui nécessitent une attention particulière. Ce guide couvre ces situations, avec des explications et des exemples.


1. Pointeur de Pointeur NULL

Un pointeur de pointeur peut être NULL, comme n’importe quel autre pointeur. Il est essentiel de vérifier son état avant de l’utiliser pour éviter des comportements indéfinis.

Exemple :

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

void verifierPointeur(int **pp) {
    if (pp == NULL || *pp == NULL) {
        printf("Le pointeur ou le contenu du pointeur est NULL\n");
    } else {
        printf("Valeur : %d\n", **pp);
    }
}

int main() {
    int *p = NULL;
    int **pp = &p;

    verifierPointeur(pp); // Affiche : Le pointeur ou le contenu du pointeur est NULL

    int x = 42;
    p = &x;
    verifierPointeur(pp); // Affiche : Valeur : 42

    return 0;
}

2. Pointeur Dangling (Dangling Pointer)

Un pointeur de pointeur peut devenir dangling (non valide) si la mémoire à laquelle il fait référence est libérée.

Exemple de comportement problématique :

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

int main() {
    int *p = malloc(sizeof(int));
    int **pp = &p;

    *p = 100;
    free(p); // Libère la mémoire pointée par p

    printf("Valeur après free : %d\n", **pp); // Comportement indéfini

    return 0;
}

Solution :

Toujours réinitialiser les pointeurs après libération.

free(p);
p = NULL; // Évite le problème

3. Tableaux Dynamiques de Pointeurs

Les tableaux dynamiques impliquent souvent l’utilisation de pointeurs de pointeurs. Cependant, des erreurs de manipulation peuvent provoquer des dépassements de mémoire ou des fuites.

Exemple : Mauvaise manipulation d’un tableau dynamique

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

int main() {
    int **tableau = malloc(3 * sizeof(int *));
    for (int i = 0; i < 3; i++) {
        tableau[i] = malloc(4 * sizeof(int)); // Alloue chaque ligne
    }

    tableau[3][0] = 10; // Erreur : dépassement, tableau a 3 lignes (indices 0 à 2)

    // Libération partielle
    free(tableau[0]);
    free(tableau); // Fuite de mémoire pour tableau[1] et tableau[2]

    return 0;
}

Solution correcte :

Vérifiez toujours les limites et libérez toute la mémoire.

for (int i = 0; i < 3; i++) {
    free(tableau[i]);
}
free(tableau);

4. Pointeurs de Pointeurs dans les Fonctions

Passer un pointeur de pointeur à une fonction pour modifier un pointeur peut entraîner des erreurs si les pointeurs ne sont pas correctement initialisés.

Exemple de mauvaise utilisation :

#include <stdio.h>

void modifierPointeur(int **pp) {
    *pp = malloc(sizeof(int)); // Si pp est NULL, comportement indéfini
    **pp = 42;
}

int main() {
    int **pp = NULL; // Non initialisé
    modifierPointeur(pp); // Erreur : accès à une adresse invalide
    return 0;
}

Solution :

Assurez-vous que le pointeur est initialisé avant de le manipuler.

int main() {
    int *p = NULL;
    int **pp = &p; // pp pointe vers p
    modifierPointeur(pp);
    printf("Valeur : %d\n", *p); // Affiche 42
    free(p); // Libère la mémoire
    return 0;
}

5. Tableaux de Chaînes de Caractères

Les tableaux de chaînes de caractères impliquent des pointeurs de pointeurs, et leur gestion peut être complexe.

Exemple avec erreur :

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

int main() {
    char **noms = malloc(3 * sizeof(char *));
    noms[0] = "Alice"; // Correct, mais non alloué dynamiquement
    noms[1] = "Bob";
    noms[2] = "Charlie";

    free(noms[0]); // Erreur : "Alice" n'a pas été alloué avec malloc
    free(noms);

    return 0;
}

Solution correcte :

Allouez chaque chaîne dynamiquement.

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

int main() {
    char **noms = malloc(3 * sizeof(char *));
    noms[0] = strdup("Alice");
    noms[1] = strdup("Bob");
    noms[2] = strdup("Charlie");

    for (int i = 0; i < 3; i++) {
        printf("%s\n", noms[i]); // Affiche chaque nom
        free(noms[i]); // Libère chaque chaîne
    }
    free(noms); // Libère le tableau de pointeurs

    return 0;
}

6. Conversion entre Pointeurs et Tableaux

Un tableau 2D est souvent utilisé comme une matrice en C, mais il peut être manipulé avec des pointeurs de pointeurs, ce qui peut créer de la confusion.

Exemple : Erreur dans la conversion tableau-pointeur

#include <stdio.h>

void afficherMatrice(int **mat, int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", mat[i][j]); // Comportement indéfini si la mémoire n'est pas contiguë
        }
        printf("\n");
    }
}

int main() {
    int mat[2][2] = {{1, 2}, {3, 4}};
    afficherMatrice((int **)mat, 2, 2); // Conversion incorrecte, les pointeurs ne sont pas compatibles

    return 0;
}

Solution :

Si vous utilisez un tableau statique, transmettez son pointeur.

void afficherMatrice(int mat[2][2], int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", mat[i][j]);
        }
        printf("\n");
    }
}

Si la matrice est allouée dynamiquement, utilisez un double pointeur correctement configuré.


7. Détection des Fuites de Mémoire

Les erreurs fréquentes avec les pointeurs de pointeurs incluent les fuites de mémoire. Utilisez des outils comme Valgrind pour vérifier.

Exemple d’analyse avec Valgrind :

valgrind --leak-check=full ./programme

Résumé des Cas Particuliers

CasPrécaution / Solution
Pointeur NULLToujours vérifier avant d’utiliser un double pointeur.
Dangling PointerRéinitialisez les pointeurs à NULL après libération.
Tableaux dynamiquesVérifiez les limites et libérez chaque niveau de mémoire.
Passer un double pointeurAssurez-vous que les pointeurs sont initialisés correctement.
Tableaux de chaînesAllouez dynamiquement les chaînes si vous utilisez free.
Conversion tableau/pointeurFaites attention à la compatibilité des pointeurs et tableaux.

Ces cas particuliers montrent l’importance de gérer les pointeurs de pointeurs avec soin pour éviter les erreurs courantes en C.

Autres articles

Pointeurs en C - Exercices Corrigés avec...
Ce guide propose des exercices corrigés sur les pointeurs en...
Read more
La Vérité sur les Tableaux et les...
Les tableaux et les pointeurs sont au cœur du langage...
Read more
Guide : Déclarer un Pointeur en C...
Cet article vous montre comment déclarer un pointeur en C...
Read more

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *