web-dev-qa-db-fra.com

Quelle est la raison d'utiliser un double pointeur lors de l'ajout d'un nœud dans une liste liée?

Les deux exemples de code ci-dessous ajoutent tous deux un noeud en haut de la liste liée . Mais alors que le premier exemple de code utilise un double pointeur, le second exemple de code utilise un seul pointeur

exemple de code 1:

struct node* Push(struct node **head, int data)
{
        struct node* newnode = malloc(sizeof(struct node));
        newnode->data = data;
        newnode->next = *head;
        return newnode;
}

Push(&head,1);

exemple de code 2:

struct node* Push(struct node *head, int data)
{
        struct node* newnode = malloc(sizeof(struct node));
        newnode->data = data;
        newnode->next = head;
        return newnode;
}

Push(head,1)

Les deux stratégies fonctionnent. Cependant, beaucoup de programmes qui utilisent une liste liée utilisent un double pointeur pour ajouter un nouveau nœud. Je sais ce qu'est un double pointeur. Mais si un seul pointeur suffit pour ajouter un nouveau nœud, pourquoi de nombreuses implémentations reposent-elles sur des pointeurs doubles?

Existe-t-il des cas dans lesquels un seul pointeur ne fonctionne pas, nous devons donc opter pour un double pointeur?

40
a6h

Certaines implémentations transmettent un paramètre pointeur à pointeur pour permettre de modifier directement le pointeur d'en-tête au lieu de renvoyer le nouveau. Ainsi, vous pourriez écrire:

// note that there's no return value: it's not needed
void Push(struct node** head, int data)
{
    struct node* newnode = malloc(sizeof(struct node));
    newnode->data=data;
    newnode->next=*head;
    *head = newnode; // *head stores the newnode in the head
}

// and call like this:
Push(&head,1);

L'implémentation qui ne prend pas un pointeur sur le pointeur principal doit renvoyer la nouvelle tête, et l'appelant est responsable de la mise à jour elle-même:

struct node* Push(struct node* head, int data)
{
    struct node* newnode = malloc(sizeof(struct node));
    newnode->data=data;
    newnode->next=head;
    return newnode;
}

// note the assignment of the result to the head pointer
head = Push(head,1);

Si vous n'effectuez pas cette affectation lorsque vous appelez cette fonction, vous fuierez les nœuds que vous allouez avec malloc et le pointeur d'en-tête pointe toujours vers le même nœud.

L’avantage devrait être clair maintenant: avec le second, si l’appelant oublie d’attribuer le nœud renvoyé au pointeur principal, de mauvaises choses se produiront.

69

Dans votre exemple particulier, le pointeur double n'est pas nécessaire. Cependant, cela peut être nécessaire si, par exemple, vous deviez faire quelque chose comme ceci:

struct node* Push(struct node** head, int data)
{
struct node* newnode = malloc(sizeof(struct node));
newnode->data=data;
newnode->next=*head;
//vvvvvvvvvvvvvvvv
*head = newnode; //you say that now the new node is the head.
//^^^^^^^^^^^^^^^^
return newnode;
}
4
Armen Tsirunyan

Bien que les réponses précédentes soient assez bonnes, je pense qu'il est beaucoup plus facile de penser en termes de "copie par valeur". 

Lorsque vous passez un pointeur sur une fonction, la valeur de l'adresse est copiée dans le paramètre de fonction. En raison de la portée de la fonction, cette copie disparaîtra une fois renvoyée. 

En utilisant un double pointeur, vous pourrez mettre à jour la valeur du pointeur d'origine. Le double pointeur sera toujours copié par valeur, mais cela n'a pas d'importance. Tout ce qui compte vraiment, c'est de modifier le pointeur d'origine, en contournant ainsi la portée ou la pile de la fonction.

J'espère que cela répond non seulement à votre question, mais également à d'autres questions liées au pointeur.

2
user1164937

Prenons ceci simple, par exemple:

void my_func(int *p) {
        // allocate space for an int
        int *z = (int *) malloc(sizeof(int));
        // assign a value
        *z = 99;

        printf("my_func - value of z: %d\n", *z);

        printf("my_func - value of p: %p\n", p);
        // change the value of the pointer p. Now it is not pointing to h anymore
        p = z;
        printf("my_func - make p point to z\n");
        printf("my_func - addr of z %p\n", &*z);
        printf("my_func - value of p %p\n", p);
        printf("my_func - value of what p points to: %d\n", *p);
        free(z);
}

int main(int argc, char *argv[])
{
        // our var
        int z = 10;

        int *h = &z;

        // print value of z
        printf("main - value of z: %d\n", z);
        // print address of val
        printf("main - addr of z: %p\n", &z);

        // print value of h.
        printf("main - value of h: %p\n", h);

        // print value of what h points to
        printf("main - value of what h points to: %d\n", *h);
        // change the value of var z by dereferencing h
        *h = 22;
        // print value of val
        printf("main - value of z: %d\n", z);
        // print value of what h points to
        printf("main - value of what h points to: %d\n", *h);


        my_func(h);

        // print value of what h points to
        printf("main - value of what h points to: %d\n", *h);

        // print value of h
        printf("main - value of h: %p\n", h);


        return 0;
}

Sortie:

main - value of z: 10
main - addr of z: 0x7ffccf75ca64
main - value of h: 0x7ffccf75ca64
main - value of what h points to: 10
main - value of z: 22
main - value of what h points to: 22
my_func - value of z: 99
my_func - value of p: 0x7ffccf75ca64
my_func - make p point to z
my_func - addr of z 0x1906420
my_func - value of p 0x1906420
my_func - value of what p points to: 99
main - value of what h points to: 22
main - value of h: 0x7ffccf75ca64

nous avons cette signature pour my_func:

void my_func(int *p);

Si vous regardez la sortie, à la fin, la valeur indiquée par h est toujours égale à 22 et la valeur de h est identique, bien que dans my_func elle ait été modifiée. Comment venir ?

Eh bien, dans my_func, nous manipulons la valeur de p, qui est juste un pointeur local . Après avoir appelé:

my_func(ht);

dans main (), p contiendra la valeur que h détient, qui représente l'adresse de la variable z, déclarée dans la fonction principale.

Dans my_func (), lorsque nous modifions la valeur de p pour conserver la valeur de z, qui est un pointeur sur un emplacement en mémoire, pour lequel nous avons alloué de l'espace, nous ne modifions pas la valeur de h, mais passé, mais juste la valeur du pointeur local p. Fondamentalement, p ne détient plus la valeur de h, il contiendra l'adresse d'un emplacement mémoire, que z pointe sur.

Maintenant, si nous changeons un peu notre exemple:

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

void my_func(int **p) {
    // allocate space for an int
    int *z = (int *) malloc(sizeof(int));
    // assign a value
    *z = 99;

    printf("my_func - value of z: %d\n", *z);

    printf("my_func - value of p: %p\n", p);
    printf("my_func - value of h: %p\n", *p);
    // change the value of the pointer p. Now it is not pointing to h anymore
    *p = z;
    printf("my_func - make p point to z\n");
    printf("my_func - addr of z %p\n", &*z);
    printf("my_func - value of p %p\n", p);
    printf("my_func - value of h %p\n", *p);
    printf("my_func - value of what p points to: %d\n", **p);
    // we are not deallocating, because we want to keep the value in that
    // memory location, in order for h to access it.
    /* free(z); */
}

int main(int argc, char *argv[])
{
    // our var
    int z = 10;

    int *h = &z;

    // print value of z
    printf("main - value of z: %d\n", z);
    // print address of val
    printf("main - addr of z: %p\n", &z);

    // print value of h.
    printf("main - value of h: %p\n", h);

    // print value of what h points to
    printf("main - value of what h points to: %d\n", *h);
    // change the value of var z by dereferencing h
    *h = 22;
    // print value of val
    printf("main - value of z: %d\n", z);
    // print value of what h points to
    printf("main - value of what h points to: %d\n", *h);


    my_func(&h);

    // print value of what h points to
    printf("main - value of what h points to: %d\n", *h);

    // print value of h
    printf("main - value of h: %p\n", h);
    free(h);


    return 0;
}

nous avons la sortie suivante:

main - value of z: 10
main - addr of z: 0x7ffcb94fb1cc
main - value of h: 0x7ffcb94fb1cc
main - value of what h points to: 10
main - value of z: 22
main - value of what h points to: 22
my_func - value of z: 99
my_func - value of p: 0x7ffcb94fb1c0
my_func - value of h: 0x7ffcb94fb1cc
my_func - make p point to z
my_func - addr of z 0xc3b420
my_func - value of p 0x7ffcb94fb1c0
my_func - value of h 0xc3b420
my_func - value of what p points to: 99
main - value of what h points to: 99
main - value of h: 0xc3b420

Nous avons en fait changé la valeur que h détient de my_func en procédant de la manière suivante:

  1. signature de fonction modifiée
  2. appeler de main (): my_func (& h); Fondamentalement, nous passons l'adresse du pointeur h au pointeur double p, déclaré comme paramètre dans la signature de la fonction.
  3. dans my_func () nous faisons: * p = z; nous sommes en train de déréférencer le double pointeur p, un niveau. En gros, cela a été traduit comme vous le feriez: h = z;

La valeur de p contient maintenant l'adresse du pointeur h. Le pointeur h contient l'adresse de z.

Vous pouvez prendre les deux exemples et les différencier ... Donc, pour revenir à votre question, vous avez besoin d'un double pointeur afin d'apporter des modifications au pointeur que vous êtes passé directement de cette fonction.

1
bsd

Comme @R. Martinho Fernandes a souligné dans sa réponse , utiliser le pointeur pour indiquer le pointeur comme argument dans void Push(struct node** head, int data) vous permet de modifier le pointeur head directement à partir de la fonction Push au lieu de renvoyer le nouveau pointeur.

Il existe encore un autre bon exemple qui montre pourquoi utiliser pointeur pour pointeur à la place, un seul pointeur peut raccourcir, simplifier et accélérer votre code. Vous avez posé une question sur ajout un nouveau nœud de la liste qui n'a généralement pas besoin de pointeur à pointeur contrairement à suppression le nœud de la liste à lien unique. Vous pouvez implémenter la suppression de nœud de la liste sans pointeur à pointeur, mais c'est sous-optimal. J'ai décrit les détails ici . Je vous recommande également de regarder cette vidéo YouTube qui aborde le problème.

BTW: Si vous comptez avec Linus Torvaldsopinion , vous feriez mieux d'apprendre à utiliser pointeur à pointeur. ;-)

Linus Torvalds: (...) À l'opposé, j'aimerais que davantage de gens comprennent le type de codage de bas niveau vraiment fondamental. Pas de gros trucs complexes comme la recherche de nom sans verrou, mais simplement une bonne utilisation des pointeurs à pointeurs, etc. Par exemple, j'ai vu trop de personnes supprimer une entrée de liste à lien unique en gardant une trace de l'entrée "prev" , puis supprimer l'entrée en faisant quelque chose comme

if (prev)
prev->next = entry->next;
else
list_head = entry->next;

et chaque fois que je vois un code comme celui-ci, je dis simplement "Cette personne ne comprend pas les indicateurs". Et c'est malheureusement assez commun.

Les personnes qui comprennent les pointeurs utilisent simplement un "pointeur sur le pointeur d'entrée" et l'initialisent avec l'adresse de list_head. Ensuite, lorsqu'ils parcourent la liste, ils peuvent supprimer l'entrée sans utiliser de condition, en effectuant simplement un "* pp = entry-> next". (...)


Autres ressources pouvant être utiles:

1
patryk.beza

Pensez à l'emplacement de mémoire pour la tête comme [HEAD_DATA].

Dans votre deuxième scénario, main_head de la fonction appelante est le pointeur sur cet emplacement.

main_head ---> [HEAD_DATA]

Dans votre code, il a envoyé la valeur du pointeur main_head à la fonction (c'est-à-dire l'adresse de l'emplacement mémoire de head_data) Vous l'avez copiée dans local_head dans la fonction. alors maintenant

local_head ---> [HEAD_DATA]

et

main_head ---> [HEAD_DATA]

Les deux pointent au même endroit mais sont essentiellement indépendants les uns des autres. Donc, lorsque vous écrivez local_head = newnode; Ce que vous avez fait est

local_head -/-> [HEAD_DATA]

local_head -----> [NEWNODE_DATA]

Vous avez simplement remplacé l'adresse de la mémoire précédente par une nouvelle dans le pointeur local. Le main_head (pointeur) pointe toujours sur l'ancien [HEAD_DATA]

0
Napstablook

Lorsque nous passons un pointeur en tant que paramètre dans une fonction et que nous souhaitons une mise à jour dans le même pointeur, nous utilisons un double pointeur.

D'un autre côté, si nous passons un pointeur en tant que paramètre dans une fonction et que nous l'attrapons dans un seul pointeur, nous devrons alors renvoyer le résultat à l'appel de la fonction afin d'utiliser le résultat.

0
Kaushal Billore

La réponse est plus évidente si vous prenez le temps d’écrire une fonction d’insertion de noeud active; le tien n'en est pas un.

Vous devez être capable de écrire sur la tête pour le déplacer, vous avez donc besoin d'un pointeur sur le pointeur vers la tête afin de pouvoir le déréférencer pour obtenir le pointeur sur la tête et le modifier.

0
Blindy

Disons que j'ai noté l'adresse de votre maison sur une carte-1. Maintenant, si je veux communiquer l'adresse de votre domicile à quelqu'un d'autre, je peux soit copier l'adresse de la carte 1 sur la carte 2 et donner la carte 2 OR à la carte 1 directement. Dans les deux cas, la personne connaît l'adresse et peut vous joindre. Mais lorsque je donne directement la carte-1, l'adresse peut être modifiée sur la carte-1 mais si j'ai donné la carte-2, seule l'adresse de la carte-2 peut être modifiée, mais pas sur la carte-1.

Passer un pointeur à pointeur revient à donner directement l’accès à card-1. Passer un pointeur revient à créer une nouvelle copie de l'adresse.

0
vishnu vardhan

Je pense que le fait est que cela facilite la mise à jour des nœuds dans une liste chaînée. Là où vous devriez normalement garder la trace d'un pointeur pour les précédents et actuels, vous pouvez avoir un double pointeur s'occuper de tout.

#include <iostream>
#include <math.h>

using namespace std;

class LL
{
    private:
        struct node 
        {
            int value;
            node* next;
            node(int v_) :value(v_), next(nullptr) {};
        };
        node* head;

    public:
        LL() 
        {
            head = nullptr;
        }
        void print() 
        {
            node* temp = head;
            while (temp) 
            {
                cout << temp->value << " ";
                temp = temp->next;
            }
        }
        void insert_sorted_order(int v_) 
        {
            if (!head)
                head = new node(v_);
            else
            {
                node* insert = new node(v_);
                node** temp = &head;
                while ((*temp) && insert->value > (*temp)->value)
                    temp = &(*temp)->next;
                insert->next = (*temp);
                (*temp) = insert;
            }
        }

        void remove(int v_)
        {
            node** temp = &head;
            while ((*temp)->value != v_)
                temp = &(*temp)->next;
            node* d = (*temp);
            (*temp) = (*temp)->next;
            delete d;
        }

        void insertRear(int v_)//single pointer
        {
            if (!head)
                head = new node(v_);
            else
            {
                node* temp = new node(v_);
                temp->next = head;
                head = temp;
            }
        }
};
0
user1044800

Observation et découverte, POURQUOI ...  

J'ai décidé de faire des expériences et de tirer des conclusions,

OBSERVATION 1- Si la liste chaînée n'est pas vide, nous pouvons y ajouter les nœuds (évidemment à la fin) en utilisant un seul pointeur.

int insert(struct LinkedList *root, int item){
    struct LinkedList *temp = (struct LinkedList*)malloc(sizeof(struct LinkedList));
    temp->data=item;
    temp->next=NULL;
    struct LinkedList *p = root;
    while(p->next!=NULL){
        p=p->next;
    }
    p->next=temp;
    return 0;
}


int main(){
    int m;
    struct LinkedList *A=(struct LinkedList*)malloc(sizeof(struct LinkedList));
    //now we want to add one element to the list so that the list becomes non-empty
    A->data=5;
    A->next=NULL;
    cout<<"enter the element to be inserted\n"; cin>>m;
    insert(A,m);
    return 0;
}

C'est simple à expliquer (Basic). Nous avons un pointeur dans notre fonction principale qui pointe vers le premier nœud (racine) de la liste. Dans la fonction insert(), nous passons l'adresse du nœud racine et, à l'aide de cette adresse, nous atteignons la fin de la liste et y ajoutons un nœud. Nous pouvons donc en conclure que si nous avons l'adresse d'une variable dans une fonction (et non la fonction principale), nous pouvons apporter des modifications permanentes à la valeur de cette variable à partir de cette fonction, ce qui refléterait la fonction principale.

OBSERVATION 2- / La méthode ci-dessus d'ajout de nœud a échoué lorsque la liste était vide.

int insert(struct LinkedList *root, int item){
    struct LinkedList *temp = (struct LinkedList*)malloc(sizeof(struct LinkedList));
    temp->data=item;
    temp->next=NULL;
    struct LinkedList *p=root;   
    if(p==NULL){
        p=temp;
    }
    else{
      while(p->next!=NULL){
          p=p->next;
      }
      p->next=temp;
    }
    return 0;
}



int main(){
    int m;
    struct LinkedList *A=NULL; //initialise the list to be empty
    cout<<"enter the element to be inserted\n";
    cin>>m;
    insert(A,m);
    return 0;
}

Si vous continuez à ajouter des éléments et affichez finalement la liste, vous constaterez que la liste n'a subi aucun changement et qu'elle est toujours vide… .. La question qui m'a frappé était dans ce cas aussi nous passons l'adresse de la racine noeud alors pourquoi les modifications ne se produisent pas en tant que modifications permanentes et la liste dans la fonction principale ne subit aucun changement. POURQUOI? POURQUOI? POURQUOI? 

Ensuite, j’ai observé une chose. Quand j’écris A=NULL, l’adresse de A devient 0. Cela signifie que maintenant A ne pointe pas vers un emplacement en mémoire. J'ai donc supprimé la ligne A=NULL; et apporté quelques modifications à la fonction d'insertion.

quelques modifications (en dessous de la fonction insert(), un seul élément peut être ajouté à une liste vide, cette fonction a juste été écrite à des fins de test) 

int insert(struct LinkedList *root, int item){
    root= (struct LinkedList *)malloc(sizeof(struct LinkedList));
    root->data=item;
    root->next=NULL;
    return 0;
}



int main(){
    int m;
    struct LinkedList *A;    
    cout<<"enter the element to be inserted\n";
    cin>>m;
    insert(A,m);
    return 0;
}

la méthode ci-dessus échoue également car, dans la fonction insert(), la racine stocke la même adresse que A dans la fonction main(), mais après la ligne root= (struct LinkedList *)malloc(sizeof(struct LinkedList));, l'adresse stockée dans root change. Ainsi, maintenant, root (dans la fonction insert()) et A (dans la fonction main()) stockent des adresses différentes.

Donc, le programme final correct serait,

int insert(struct LinkedList *root, int item){
    root->data=item;
    root->next=NULL;
    return 0;
}



int main(){
    int m;
    struct LinkedList *A = (struct LinkedList *)malloc(sizeof(struct LinkedList));
    cout<<"enter the element to be inserted\n";
    cin>>m;
    insert(A,m);
    return 0;
}

Mais nous ne voulons pas de deux fonctions différentes pour l'insertion, une lorsque la liste est vide et l'autre lorsque la liste n'est pas vide. Maintenant vient le double pointeur qui rend les choses faciles.

Une chose que j'ai remarquée et qui est importante est que les pointeurs enregistrent l'adresse et lorsqu'ils sont utilisés avec '*', ils donnent une valeur à cette adresse mais des pointeurs eux-mêmes ont leur propre adresse.

Maintenant, voici le programme complet et expliquer plus tard les concepts.

int insert(struct LinkedList **root,int item){
    if(*root==NULL){
        (*root)=(struct LinkedList *)malloc(sizeof(struct LinkedList));
        (*root)->data=item;
        (*root)->next=NULL;
    }
    else{
        struct LinkedList *temp=(struct LinkedList *)malloc(sizeof(struct LinkedList));
        temp->data=item;
        temp->next=NULL;
        struct LinkedList *p;
        p=*root;
        while(p->next!=NULL){
            p=p->next;
        }
        p->next=temp;
    }
    return 0;
}


int main(){
    int n,m;
    struct LinkedList *A=NULL;
    cout<<"enter the no of elements to be inserted\n";
    cin>>n;
    while(n--){
        cin>>m;
        insert(&A,m);
    }
    display(A);
    return 0;
}

Voici les observations,

1. root stocke l'adresse du pointeur A (&A), *root stocke l'adresse stockée par le pointeur A et **root stocke la valeur à l'adresse stockée par A. En langage simple, root=&A, *root= A et **root= *A.

2. si nous écrivons *root= 1528, cela signifie que la valeur de l'adresse stockée dans root devient 1528 et que l'adresse stockée dans root est l'adresse du pointeur A (&A) donc maintenant A=1528 (c'est-à-dire que l'adresse stockée dans A est 1528) et que cette modification est permanent.

chaque fois que nous modifions la valeur de *root, nous modifions effectivement la valeur de l'adresse stockée dans root et depuis root=&A (adresse du pointeur A), nous modifions indirectement la valeur de A ou l'adresse stockée dans A.

donc maintenant si A=NULL (la liste est vide) *root=NULL, nous créons donc le premier noeud et stockons son adresse à *root, c’est-à-dire que nous stockons indirectement l’adresse du premier noeud à A. Si list n'est pas vide, tout est identique à celui des fonctions précédentes utilisant un seul pointeur, sauf que nous avons changé de racine en *root puisque ce qui était stocké dans la racine est maintenant stocké dans *root.

0
roottraveller

Imaginez un cas où vous devez effectuer certaines modifications et que ces modifications doivent être reflétées dans la fonction appelante.

Exemple:

void swap(int* a,int* b){
  int tmp=*a;
  *a=*b;
  *b=tmp;
}

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

  // To ascertain that changes made in swap reflect back here we pass the memory address
  // instead of the copy of the values

  swap(&a,&b);
}

De même, nous passons l'adresse mémoire de la tête de liste.

Ainsi, si un nœud est ajouté et que la valeur de l'en-tête est modifiée, cette modification est reflétée et il n'est pas nécessaire de réinitialiser manuellement l'en-tête à l'intérieur de la fonction d'appel.

Ainsi, cette approche réduit les risques de fuite de mémoire, car nous aurions perdu le pointeur sur le nœud nouvellement alloué si nous avions oublié de réactualiser la tête dans la fonction d'appel.

A côté de cela, le second code fonctionnera plus rapidement, car la copie et le retour ne perdent pas de temps puisque nous travaillons directement avec la mémoire.

0

Le moyen standard de gérer les listes chaînées en C consiste à faire en sorte que les fonctions Push et Pop mettent automatiquement à jour le pointeur principal. 

C est "Appel par valeur", ce qui signifie que des copies de paramètres sont transmises aux fonctions. Si vous ne transmettez que le pointeur principal, l'appelant ne verra pas les mises à jour locales effectuées sur ce pointeur. Les deux solutions de contournement sont 

1) Passez l’adresse du pointeur principal. (Pointeur sur le pointeur principal)

2) Renvoyer un nouveau pointeur principal et s’appuyer sur l’appelant pour le mettre à jour. 

L'option 1) est la plus facile, même si elle est un peu déroutante au début.

0
WilderField