В чем причина использования двойного указателя при добавлении узла в связанный список? - PullRequest
43 голосов
/ 01 сентября 2011

Оба приведенных ниже примера кода добавляют узел вверху связанного списка. Но тогда как в первом примере кода используется двойной указатель, во втором примере кода используется один указатель

пример кода 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)

Обе стратегии работают. Однако многие программы, использующие связанный список, используют двойной указатель для добавления нового узла. Я знаю, что такое двойной указатель. Но если для добавления нового узла достаточно одного указателя, почему многие реализации полагаются на двойные указатели?

Есть ли случаи, когда один указатель не работает, поэтому нам нужно использовать двойной указатель?

Ответы [ 13 ]

75 голосов
/ 01 сентября 2011

Некоторые реализации передают указатель на параметр указателя, чтобы разрешить непосредственное изменение указателя заголовка вместо возврата нового. Таким образом вы могли бы написать:

// 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, и указатель заголовка всегда будет указывать на один и тот же узел.

Преимущество должно быть очевидным: со вторым, если вызывающая сторона забывает назначить возвращенный узел указателю головы, произойдут плохие вещи.

4 голосов
/ 26 февраля 2018

Хотя предыдущие ответы достаточно хороши, я думаю, что гораздо проще думать с точки зрения «копирования по значению».

Когда вы передаете указатель на функцию, значение адреса копируется в параметр функции. Из-за объема функции эта копия исчезнет, ​​как только вернется.

Используя двойной указатель, вы сможете обновить исходное значение указателя. Двойной указатель все равно будет скопирован по значению, но это не имеет значения. Все, что вас действительно волнует, - это изменение исходного указателя, обходя тем самым область действия или стек функции.

Надеюсь, что это ответ не только на ваш вопрос, но и на другие вопросы, связанные с указателями.

4 голосов
/ 01 сентября 2011

В вашем конкретном примере нет необходимости в двойном указателе.Однако это может понадобиться, если, например, вам нужно сделать что-то вроде этого:

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;
}
1 голос
/ 05 марта 2018

Стандартный способ обработки связанных списков в C состоит в том, чтобы функции push и pop автоматически обновляли указатель заголовка.

C - «Вызов по значению», что означает, что копии параметров передаются в функции. Если вы передадите только указатель заголовка, любое локальное обновление, которое вы сделаете для этого указателя, не будет видно вызывающей стороне. Два обходных пути:

1) Передать адрес указателя головы. (Указатель на указатель головы)

2) Вернуть новый указатель головы и положиться на вызывающего, чтобы обновить указатель головы.

Вариант 1) самый простой, хотя сначала немного запутанный.

1 голос
/ 18 февраля 2018

Как @ R.Мартиньо Фернандес указал в свой ответ , используя указатель на указатель в качестве аргумента в void push(struct node** head, int data), позволяющий изменять указатель head непосредственно из push функциивместо возврата нового указателя.

Есть еще один хороший пример, который показывает, почему использование указателя на указатель вместо одного указателя может сократить, упростить и ускорить ваш код.Вы спрашивали о добавлении нового узла в список, который, вероятно, обычно не нуждается в указателе-указателе в отличие от удаления узла из односвязного списка.Вы можете реализовать удаление узла из списка без указателя на указатель, но это неоптимально.Я описал детали здесь .Я также рекомендую вам посмотреть это видео на YouTube , которое решает проблему.

Кстати: если вы подсчитаете с Линус Торвальдс мнение , вы былучше узнать, как использовать указатель на указатель.; -)

Линус Торвальдс: (...) На противоположном конце спектра я бы хотел, чтобы больше людей понимали действительно базовый низкоуровневый тип кодирования.Не большие и сложные вещи, такие как поиск по имени без блокировки, но просто хорошее использование указателей на указатели и т. Д. Например, я видел слишком много людей, которые удаляли односвязную запись списка, отслеживая запись «prev»и затем, чтобы удалить запись, сделав что-то вроде

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

и всякий раз, когда я вижу такой код, я просто говорю: «Этот человек не понимает указатели».И это, к сожалению, довольно часто.

Люди, которые понимают указатели, просто используют «указатель на указатель записи» и инициализируют его адресом list_head.И затем, проходя по списку, они могут удалить запись без использования каких-либо условий, просто выполнив «* pp = entry-> next».(...)


Другие полезные ресурсы:

1 голос
/ 07 февраля 2017

Давайте возьмем это просто, например:

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, которое представляет адрес переменной z, объявленной в главной функции.

В my_func (), когда мы меняем значение p для хранения значения z, которое является указателем на место в памяти, для которого мы выделили пространство, мы не меняем значение h, что мы передали, но только значение локального указателя р. По сути, 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); В основном мы передаем адрес указателя h двойному указателю p, объявленному как параметр в сигнатуре функции.
  3. в my_func () мы делаем: * p = z; мы разыменовываем двойной указатель p, один уровень. По сути, это переводится так, как вы это делаете: h = z;

Значение p теперь содержит адрес указателя h. Указатель h содержит адрес z.

Вы можете взять оба примера и сравнить их. Итак, возвращаясь к вашему вопросу, вам нужен двойной указатель, чтобы внести изменения в указатель, который вы передали прямо из этой функции.

1 голос
/ 10 июня 2016

Наблюдение и находка, ПОЧЕМУ ...

Я решил провести несколько экспериментов и сделать какой-то вывод,

НАБЛЮДЕНИЕ 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;
}

Его легко объяснить (Basic). В нашей главной функции есть указатель, который указывает на первый узел (корень) списка. В функции insert() мы передаем адрес корневого узла и, используя этот адрес, достигаем конца списка и добавляем в него узел. Таким образом, мы можем заключить, что если у нас есть адрес переменной в функции (не основной функции), мы можем сделать постоянные изменения в значении этой переменной из этой функции, что отражается в основной функции.

НАБЛЮДЕНИЕ 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() функция может добавлять только один элемент в пустой список, только что написала эту функцию для целей тестирования)

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() root сохраняет тот же адрес, что и A в функции main(), но после строки root= (struct LinkedList *)malloc(sizeof(struct LinkedList)); адрес, сохраненный в root, изменяется. Таким образом, теперь root (в функции insert()) и A (в функции main()) хранят разные адреса.

Таким образом, правильная конечная программа будет

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

Но нам не нужны две разные функции для вставки: одна, когда список пуст, и другая, когда список не пуст. Теперь приходит двойной указатель, который делает вещи проще.

Одна вещь, которую я заметил, что важно, это то, что указатели хранят адрес и при использовании с '*' они дают значение по этому адресу, но указатели сами имеют свой адрес.

Теперь вот полная программа и позже объясним понятия.

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.

0 голосов
/ 16 мая 2018

Допустим, я записал ваш домашний адрес на карточке-1.Теперь, если я хочу сообщить ваш домашний адрес кому-то другому, я могу либо скопировать адрес с карты 1 на карту 2 и дать карту 2, либо я могу дать карту 1 напрямую.В любом случае человек узнает адрес и сможет связаться с вами.Но когда я даю карточку-1 напрямую, адрес можно изменить на карточке-1, но если я дал карточку-2, можно изменить только адрес на карточке-2, но не на карточке-1.

Передачауказатель на указатель аналогичен предоставлению доступа к карте-1 напрямую.Передача указателя аналогична созданию новой копии адреса.

0 голосов
/ 07 октября 2017

Подумайте об области памяти для головы, как [HEAD_DATA].

Теперь в вашем втором сценарии 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 голосов
/ 28 мая 2017

Когда мы передаем указатель в качестве параметра в функцию и хотим обновить его в том же указателе, мы используем двойной указатель.

С другой стороны, если мы передадим указатель в качестве параметра в функцию и перехватим его в единственном указателе, то для возврата результата придется вернуть результат в вызывающую функцию.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...