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
Pointeur | Double 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
- Initialisation obligatoire : Assurez-vous que les pointeurs (et doubles pointeurs) sont correctement initialisés avant utilisation.
- Gestion de mémoire : Pour chaque
malloc
sur un pointeur, assurez-vous d’appelerfree
. - 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
- Allocation dynamique : Créez des structures complexes comme des tableaux 2D.
- Passage de pointeur à une fonction : Modifiez directement les pointeurs dans une fonction.
- Manipulation de chaînes de caractères : Gérez des tableaux de chaînes de manière dynamique.
- 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
- Passer l’adresse d’un pointeur pour permettre la modification dans une fonction.
- Allouer correctement la mémoire pour les doubles pointeurs et leurs éléments.
- Toujours vérifier que les allocations sont réussies (
!= NULL
). - Libérer toute mémoire allouée pour éviter les fuites mémoire.
- 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.