Langage C/C++

Guide Complet : Double Pointeur en C

Les doubles pointeurs (ou pointeurs de pointeurs) en C sont des variables qui contiennent l’adresse d’un pointeur. Ils permettent de manipuler des pointeurs de manière flexible, et sont souvent utilisés dans des contextes avancés comme la gestion dynamique de mémoire ou la manipulation de tableaux 2D. Ce guide explique les concepts fondamentaux et leur utilisation, avec des exemples corrigés.


1. Déclaration et Syntaxe

Un double pointeur est déclaré en ajoutant un second astérisque (**) lors de la définition.

Syntaxe :

type **nom_double_pointeur;

Exemple :

int **doublePtr; // Déclare un double pointeur vers un int

Le type de doublePtr est un pointeur vers un pointeur qui pointe vers un int.


2. Concept Fondamental

Un double pointeur est utile lorsque :

  • Vous voulez pointer vers une autre variable qui est déjà un pointeur.
  • Vous devez modifier l’adresse contenue dans un pointeur à partir d’une fonction.

3. Allocation Dynamique et Double Pointeur

L’un des usages courants des doubles pointeurs est l’allocation dynamique d’un tableau 2D.

Exemple : Allocation dynamique d’un tableau 2D

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

int main() {
    int rows = 3, cols = 4;
    int **matrix = malloc(rows * sizeof(int *)); // Allocation pour les lignes

    for (int i = 0; i < rows; i++) {
        matrix[i] = malloc(cols * sizeof(int)); // Allocation pour les colonnes
    }

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

    // Libération de la mémoire
    for (int i = 0; i < rows; i++) {
        free(matrix[i]); // Libère chaque ligne
    }
    free(matrix); // Libère les pointeurs des lignes

    return 0;
}

4. Passage de double pointeur à une fonction

Un double pointeur permet de modifier l’adresse d’un pointeur passé en paramètre.

Exemple : Modification d’un pointeur dans une fonction

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

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

int main() {
    int *p = NULL;

    allouerMemoire(&p); // Passe l'adresse du pointeur p
    printf("Valeur allouée : %d\n", *p); // Affiche 42

    free(p); // Libère la mémoire
    return 0;
}

5. Manipulation de Tableaux 2D

Un tableau 2D peut être représenté à l’aide de doubles pointeurs.

Exemple : Manipulation simple d’un tableau 2D

#include <stdio.h>

int main() {
    int array[2][3] = {{1, 2, 3}, {4, 5, 6}};
    int *ptr1 = array[0];
    int **ptr2 = &ptr1;

    printf("Accès via double pointeur : %d\n", **ptr2); // Affiche 1
    return 0;
}

6. Double Pointeur et Chaînes de Caractères

Les doubles pointeurs sont souvent utilisés pour manipuler des tableaux de chaînes de caractères.

Exemple : Tableaux de chaînes de caractères

#include <stdio.h>

int main() {
    char *noms[] = {"Alice", "Bob", "Charlie"};
    char **ptr = noms;

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

7. Différences entre Pointeur et Double Pointeur

PointeurDouble Pointeur
Contient l’adresse d’une variable.Contient l’adresse d’un pointeur.
Utilisé pour manipuler directement des variables.Utilisé pour manipuler des pointeurs.
Accès via un astérisque (*).Accès via deux astérisques (**).

8. Précautions avec les Doubles Pointeurs

  1. Initialisation obligatoire : Assurez-vous que les pointeurs (et doubles pointeurs) sont correctement initialisés avant utilisation.
  2. Gestion de mémoire : Pour chaque malloc sur un pointeur, assurez-vous d’appeler free.
  3. Vérification des pointeurs NULL : Avant d’accéder à un double pointeur, vérifiez que les pointeurs sont non nuls.

9. Cas Pratique : Liste Chaînée avec Double Pointeur

Les doubles pointeurs sont utiles pour manipuler une liste chaînée, notamment pour insérer un élément en tête.

Exemple : Insertion en tête

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

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

void insererEnTete(Node **head, int valeur) {
    Node *nouveau = malloc(sizeof(Node));
    nouveau->data = valeur;
    nouveau->next = *head;
    *head = nouveau; // Modifie le pointeur head dans la fonction
}

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

int main() {
    Node *head = NULL;

    insererEnTete(&head, 3);
    insererEnTete(&head, 5);
    insererEnTete(&head, 7);

    afficherListe(head);

    return 0;
}

10. Résumé des Applications

  1. Allocation dynamique : Créez des structures complexes comme des tableaux 2D.
  2. Passage de pointeur à une fonction : Modifiez directement les pointeurs dans une fonction.
  3. Manipulation de chaînes de caractères : Gérez des tableaux de chaînes de manière dynamique.
  4. Structures dynamiques : Implémentez des structures comme des listes chaînées.

Voici des exemples corrigés et commentés pour illustrer l’utilisation correcte des doubles pointeurs en C. Ces exemples couvrent différents cas pratiques.


Exemple 1 : Allouer et modifier un pointeur avec un double pointeur

Code incorrect :

#include <stdlib.h>
void allouerMemoire(int *ptr) {
    ptr = malloc(sizeof(int)); // Change une copie locale de ptr
    *ptr = 42; // Cela provoque un comportement indéfini si malloc échoue
}

int main() {
    int *p = NULL;
    allouerMemoire(p); // p reste NULL après l'appel
    printf("Valeur : %d\n", *p); // Erreur de segmentation
    return 0;
}

Correction :

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

void allouerMemoire(int **ptr) {
    *ptr = malloc(sizeof(int)); // Alloue de la mémoire à l'adresse du pointeur
    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
    } else {
        printf("Allocation échouée\n");
    }

    return 0;
}

Exemple 2 : Allocation dynamique d’un tableau 2D

Code incorrect :

int main() {
    int **tableau = malloc(3 * sizeof(int *)); // Allocation pour les lignes
    for (int i = 0; i < 3; i++) {
        tableau[i] = malloc(4 * sizeof(int)); // Allocation pour les colonnes
    }
    tableau[3][0] = 10; // Dépassement de mémoire, 3 est hors limites
    return 0;
}

Correction :

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

int main() {
    int rows = 3, cols = 4;
    int **tableau = malloc(rows * sizeof(int *)); // Allocation pour les lignes
    if (tableau == NULL) {
        printf("Échec de l'allocation mémoire\n");
        return 1;
    }

    for (int i = 0; i < rows; i++) {
        tableau[i] = malloc(cols * sizeof(int)); // Allocation pour les colonnes
        if (tableau[i] == NULL) {
            printf("Échec de l'allocation mémoire\n");
            return 1;
        }
    }

    // Initialisation et affichage
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            tableau[i][j] = i * cols + j;
            printf("%d ", tableau[i][j]);
        }
        printf("\n");
    }

    // Libération de la mémoire
    for (int i = 0; i < rows; i++) {
        free(tableau[i]); // Libère chaque ligne
    }
    free(tableau); // Libère les pointeurs des lignes

    return 0;
}

Exemple 3 : Modification d’un tableau avec un double pointeur

Code incorrect :

void remplirTableau(int *tab) {
    tab[0] = 10; // Modifie une copie locale du tableau
}

int main() {
    int *tableau = malloc(5 * sizeof(int));
    remplirTableau(tableau); // Le tableau n'est pas initialisé correctement
    printf("%d\n", tableau[0]); // Résultat indéfini
    free(tableau);
    return 0;
}

Correction :

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

void remplirTableau(int **tab, int taille) {
    *tab = malloc(taille * sizeof(int)); // Alloue la mémoire
    if (*tab != NULL) {
        for (int i = 0; i < taille; i++) {
            (*tab)[i] = i * 2; // Initialise le tableau
        }
    }
}

int main() {
    int *tableau = NULL;
    remplirTableau(&tableau, 5); // Passe l'adresse du pointeur

    if (tableau != NULL) {
        for (int i = 0; i < 5; i++) {
            printf("%d ", tableau[i]); // Affiche : 0 2 4 6 8
        }
        printf("\n");
        free(tableau); // Libère la mémoire
    }

    return 0;
}

Exemple 4 : Gestion d’un tableau de chaînes de caractères

Code incorrect :

void remplirNoms(char **noms) {
    noms[0] = "Alice"; // Erreur : mémoire potentiellement non allouée
}

int main() {
    char **noms = NULL;
    remplirNoms(noms); // noms reste NULL
    printf("%s\n", noms[0]); // Comportement indéfini
    return 0;
}

Correction :

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

void remplirNoms(char ***noms, int taille) {
    *noms = malloc(taille * sizeof(char *)); // Alloue pour le tableau de chaînes
    if (*noms == NULL) {
        printf("Échec de l'allocation mémoire\n");
        return;
    }

    (*noms)[0] = strdup("Alice"); // Alloue et copie la chaîne
    (*noms)[1] = strdup("Bob");
    (*noms)[2] = strdup("Charlie");
}

int main() {
    char **noms = NULL;
    remplirNoms(&noms, 3);

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

    return 0;
}

Exemple 5 : Manipulation d’une liste chaînée avec double pointeur

Code incorrect :

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

void ajouterEnTete(Node *head, int valeur) {
    Node *nouveau = malloc(sizeof(Node));
    nouveau->data = valeur;
    nouveau->next = head;
    head = nouveau; // Modifie uniquement une copie locale
}

int main() {
    Node *head = NULL;
    ajouterEnTete(head, 10); // head reste NULL
    return 0;
}

Correction :

#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) {
        printf("Échec de l'allocation mémoire\n");
        return;
    }
    nouveau->data = valeur;
    nouveau->next = *head;
    *head = nouveau; // Modifie le pointeur réel
}

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); // Affiche : 30 -> 20 -> 10 -> NULL

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

    return 0;
}

Résumé des Correctifs

  1. Passer l’adresse d’un pointeur pour permettre la modification dans une fonction.
  2. Allouer correctement la mémoire pour les doubles pointeurs et leurs éléments.
  3. Toujours vérifier que les allocations sont réussies (!= NULL).
  4. Libérer toute mémoire allouée pour éviter les fuites mémoire.
  5. Utiliser des outils comme valgrind pour vérifier l’utilisation correcte de la mémoire.

Ces exemples corrigés et optimisés montrent comment manipuler efficacement les doubles pointeurs tout en évitant 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 *