web-dev-qa-db-ja.com

リンクリストにノードを追加するときにダブルポインターを使用する理由は何ですか?

以下の2つのコード例はどちらも、リンクリストの上部にノードを追加します。ただし、最初のコード例ではダブルポインターを使用していますが、2番目のコード例ではシングルポインターを使用しています

コード例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);

コード例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)

両方の戦略が機能します。ただし、リンクリストを使用するプログラムの多くは、ダブルポインターを使用して新しいノードを追加します。ダブルポインターとは何かを知っています。しかし、新しいノードを追加するのに単一のポインターで十分な場合、なぜ多くの実装が二重ポインターに依存するのでしょうか?

単一のポインターが機能しない場合があるので、二重ポインターを使用する必要がありますか?

43
a6h

一部の実装では、ポインタをポインタパラメータに渡して、新しいポインタを返す代わりに、ヘッドポインタを直接変更できるようにします。したがって、次のように書くことができます。

// 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);

ヘッドポインターへのポインターを受け取らない実装は、新しいヘッドを返す必要があり、呼び出し元はそれ自体を更新する必要があります。

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);

この関数を呼び出すときにこの割り当てを行わないと、mallocで割り当てたノードがリークし、ヘッドポインターは常に同じノードを指します。

利点は今明確になっているはずです。2番目の方法では、呼び出し元が返されたノードをヘッドポインターに割り当てるのを忘れると、悪いことが起こります。

75

特定の例では、ダブルポインターは必要ありません。ただし、たとえば、次のような操作を行う場合は、必要になる場合があります。

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

前の答えは十分ですが、「値によるコピー」の観点から考える方がずっと簡単だと思います。

ポインターを関数に渡すと、アドレス値が関数パラメーターにコピーされます。関数のスコープにより、そのコピーは戻ると消えます。

ダブルポインターを使用すると、元のポインターの値を更新できます。ダブルポインターは値によってコピーされますが、それは問題ではありません。本当に気にするのは、元のポインターを変更して、関数のスコープまたはスタックをバイパスすることです。

これがあなたの質問だけでなく、他のポインター関連の質問にも答えることを願っています。

4
user1164937

@ R。Martinho Fernandeshis answer で指摘されているように、 pointer to pointervoid Push(struct node** head, int data)の引数として使用すると、新しいポインターを返す代わりに、head関数内からPushポインターを直接変更します。

pointer to pointer を使用する代わりに、単一のポインターがコードを短縮、単純化、および高速化する理由を示すさらに別の良い例があります。 追加についてリストに新しいノードを尋ねましたが、通常は削除とリンクされたリストのノードとは対照的に、通常はポインターツーポインターを必要としません。ポインターツーポインターなしでリストからノードを削除することを実装できますが、最適ではありません。詳細を説明しました こちらこのYouTubeビデオ もご覧になることをお勧めします。これは問題に対処します。

BTW: Linus Torvaldsopinion でカウントする場合、ポインターツーポインターの使用方法を学習する方が良いでしょう。 ;-)

Linus Torvalds:(...)スペクトルの反対側では、実際にコアな低レベルのコーディングをより多くの人に理解してもらいたいです。ロックレスの名前検索のような大きくて複雑なものではなく、ポインタからポインタへの単純な使用など。たとえば、「prev」エントリを追跡することで、単一リンクのリストエントリを削除する人が多すぎます。 、次にエントリを削除するには、次のようにします

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

そして、そのようなコードを見るときはいつでも、「この人はポインターを理解していません」というだけです。そして、悲しいことに非常に一般的です。

ポインタを理解している人は、「エントリポインタへのポインタ」を使用し、それをlist_headのアドレスで初期化します。そして、リストを走査するときに、「* pp = entry-> next」を実行するだけで、条件を使用せずにエントリを削除できます。 (...)


役立つその他のリソース:

2
patryk.beza

この単純な例を見てみましょう:

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;
}

出力:

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

my_funcには次の署名があります。

void my_func(int *p);

出力を見ると、最後に、hが指す値はまだ22であり、hの値は同じですが、my_funcでは変更されています。どうして ?

さて、my_funcでは、pの値を操作していますが、これは単なるローカルポインターです。呼び出し後:

my_func(ht);

main()では、pはhが保持する値を保持します。これは、main関数で宣言されたz変数のアドレスを表します。

My_func()でpの値を変更してzの値を保持する場合、これはメモリ内の場所へのポインタであり、スペースを割り当てていますが、hの値は変更していません。渡されますが、ローカルポインターpの値のみです。基本的に、pはhの値を保持しなくなり、zが指すメモリ位置のアドレスを保持します。

ここで、例を少し変更すると、

#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;
}

次の出力があります。

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

ここで、実際にhが保持する値をmy_funcから次のようにして変更しました。

  1. 変更された関数シグネチャ
  2. main()からの呼び出し:my_func(&h);基本的に、関数のシグネチャのパラメーターとして宣言されたダブルポインターpにhポインターのアドレスを渡します。
  3. my_func()では次のようにしています:* p = z;ダブルポインターp、1レベルを逆参照しています。基本的にこれはあなたがするように翻訳されました:h = z;

Pの値は、hポインターのアドレスを保持するようになりました。 hポインターは、zのアドレスを保持します。

両方の例を取り、それらを比較することができます。そのため、質問に戻って、その関数から直接渡したポインターを変更するには、ダブルポインターが必要です。

1
bsd

観察と発見、なぜ...

私はいくつかの実験をして結論を​​出すことにしました、

OBSERVATION 1-リンクされたリストが空でない場合は、単一のポインタのみを使用して、ノードを(明らかに最後に)追加できます。

_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;
}
_

説明が簡単(基本)。メイン関数には、リストの最初のノード(ルート)を指すポインターがあります。 insert()関数では、ルートノードのアドレスを渡し、このアドレスを使用してリストの最後に到達し、それにノードを追加します。したがって、関数(メイン関数ではない)に変数のアドレスがある場合、メイン関数に反映される関数からその変数の値を永続的に変更できると結論付けることができます。

OBSERVATION 2-リストが空の場合、ノードを追加する上記の方法は失敗しました。

_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;
}
_

要素を追加し続け、最終的にリストを表示すると、リストは変更されておらず、空のままであることがわかります。私の頭に浮かんだ疑問は、この場合、ルートノードのアドレスを渡すことであり、メイン関数の永続的な変更とリストが変更されないために変更が行われない理由です。どうして?どうして?どうして?

次に、_A=NULL_を書き込むと、Aのアドレスが0になります。これは、Aがメモリ内の任意の場所を指していないことを意味します。そこで、行_A=NULL;_を削除し、挿入関数にいくつかの変更を加えました。

いくつかの変更(insert()関数は空のリストに要素を1つだけ追加できます。テスト目的でこの関数を記述しただけです)

_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;
}
_

insert()関数のルートはmain()関数のAと同じアドレスを格納しますが、root= (struct LinkedList *)malloc(sizeof(struct LinkedList));行の後にroot変更。したがって、rootinsert()関数内)とAmain()関数内)は異なるアドレスを保存します。

つまり、正しい最終プログラムは次のようになります

_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;
}
_

ただし、2つの異なる関数を挿入する必要はありません。1つはリストが空の場合、もう1つはリストが空でない場合です。物事を簡単にするダブルポインターが追加されました。

重要なことに気づいたことの1つは、ポインターがアドレスを格納し、「*」と一緒に使用すると、そのアドレスで値を与えるが、ポインター自体が独自のアドレスを持つことです。

ここに完全なプログラムがあり、後で概念を説明します。

_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;
}
_

以下は観察結果です

1。 rootはポインターAのアドレスを保管します_(&A)_、_*root_はポインターAによって保管されたアドレスを保管し、_**root_は保管されたアドレスの値を保管しますAによって。単純な言語_root=&A_、_*root= A_および_**root= *A_。

2。 _*root= 1528_と書くと、rootに格納されているアドレスの値は1528になり、rootに格納されているアドレスはポインターAのアドレスになります_(&A)_このように_A=1528_(つまり、Aに保存されているアドレスは1528です)この変更は永続的です。

_*root_の値を変更するときは常に、rootに格納されているアドレスの値を実際に変更し、_root=&A_(ポインターのアドレスA)から間接的にAまたはAに保存されているアドレス。

したがって、_A=NULL_(リストが空)_*root=NULL_の場合、最初のノードを作成し、そのアドレスを_*root_に格納します。つまり、間接的に最初のノードのアドレスをAに格納します。リストが空でない場合、ルートに格納されていたものが_*root_に格納されるため、ルートを_*root_に変更したことを除いて、すべてが単一ポインターを使用した以前の関数で行われたものと同じです。

1
roottraveller

Cでリンクリストを処理する標準的な方法は、プッシュおよびポップ機能でヘッドポインターを自動的に更新することです。

Cは「値による呼び出し」であり、パラメーターのコピーが関数に渡されます。ヘッドポインターのみを渡す場合、そのポインターに対して行ったローカル更新は呼び出し元には表示されません。 2つの回避策は

1)ヘッドポインターのアドレスを渡します。 (ヘッドポインターへのポインター)

2)新しいヘッドポインターを返し、呼び出し元に依存してヘッドポインターを更新します。

オプション1)は、最初は少しわかりにくいですが、最も簡単です。

1
WilderField

[HEAD_DATA]のような頭の記憶場所を考えてください。

2番目のシナリオでは、呼び出し元の関数のmain_headがこの場所へのポインターです。

main_head ---> [HEAD_DATA]

コードでは、ポインタmain_headの値を関数に送信しました(つまり、head_dataのメモリ位置のアドレス)関数のlocal_headにコピーしました。だから今

local_head ---> [HEAD_DATA]

そして

main_head ---> [HEAD_DATA]

どちらも同じ場所を指しますが、本質的には互いに独立しています。したがって、local_head = newnodeと書くと、あなたがしたことは

local_head-/-> [HEAD_DATA]

local_head -----> [NEWNODE_DATA]

ローカルポインタの以前のメモリのメモリアドレスを新しいメモリアドレスに置き換えただけです。 main_head(ポインター)はまだ古い[HEAD_DATA]を指します

0
Napstablook

関数のパラメーターとしてポインターを渡し、同じポインターで更新する場合は、ダブルポインターを使用します。

一方、関数のパラメーターとしてポインターを渡し、単一のポインターでキャッチすると、結果を使用するために呼び出し元の関数に結果を返す必要があります。

0
Kaushal Billore

作業ノード挿入関数を作成するのに時間がかかる場合、答えはより明白です。あなたのものではありません。

writeを頭の上に移動して前方に移動できるようにする必要があります。したがって、頭へのポインタを取得して変更するには、頭へのポインタへのポインタが必要です。

0
Blindy

カード1に自宅の住所を書き留めたとしましょう。自宅の住所を他の人に伝えたい場合は、card-1からcard-2に住所をコピーして、card-2 OR card-1を直接渡すことができます。しかし、カード1を直接渡すと、カード1で住所を変更できますが、カード2を指定した場合、カード2の住所のみを変更できますが、カード-1。

ポインターをポインターに渡すことは、カード1に直接アクセスすることに似ています。ポインターを渡すことは、アドレスの新しいコピーを作成することに似ています。

0
vishnu vardhan

ポイントは、リンクリスト内のノードを簡単に更新できるようにすることだと思います。通常、以前と現在のポインターを追跡する必要がある場合は、ダブルポインターですべてを処理できます。

#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

両方の関数にheadという名前のパラメーターがあるという事実から混乱が生じる可能性があると思います。 2つのheadは実際には異なるものです。最初のコードのheadは、ヘッドノードポインターのアドレスを格納します(ヘッドノードポインター自体は、ヘッドノード構造のアドレスを格納します)。一方、2番目のheadは、ヘッドノード構造のアドレスを直接格納します。そして、両方の関数が新しく作成されたノード(新しいヘッドである必要があります)を返すため、最初のアプローチに進む必要はないと思います。この関数の呼び出し元は、所有するヘッド参照を更新する責任があります。 2番目のものは十分で、見やすいと思います。私は2番目のもので行きます。

0
user3463521

特定の変更を行う必要があり、それらの変更が呼び出し元の関数に反映される必要がある場合を想像してください。

例:

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);
}

同様に、リストの先頭のメモリアドレスを渡します。

この方法では、ノードが追加され、ヘッドの値が変更された場合、その変更が反映され、呼び出し関数内でヘッドを手動でリセットする必要はありません。

したがって、この方法では、呼び出し関数でHeadを更新し忘れた場合に、新しく割り当てられたノードへのポインタを失うことになるため、メモリリークの可能性が減少します。

これに加えて、メモリを直接操作するため、コピーとリターンに時間が無駄にならないため、2番目のコードはより高速に動作します。

0