Як працює покажчик покажчиків на C?


171

Як працюють покажчики на покажчики в C? Коли ви використовували б їх?


43
Ні, не домашнє завдання .... просто хотів знати .. тому я бачу це багато, коли я читаю код C.

1
Вказівник на покажчик - це не окремий випадок чогось, тому я не розумію, що ви не розумієте про недійсність **.
akappa

для 2D-масивів найкращим прикладом є аргументи командного рядка "prog arg1 arg2", які зберігаються char ** argv. І якщо абонент не хоче виділити пам'ять (викликана функція виділить пам'ять)
результирує

1
У вас є хороший приклад використання "вказівника на покажчик" в Git 2.0: дивіться мою відповідь нижче
VonC

Відповіді:


359

Припустимо, 8-розрядний комп'ютер з 8-бітовими адресами (і, таким чином, лише 256 байт пам'яті). Це частина цієї пам'яті (цифри вгорі - це адреси):

  54   55   56   57   58   59   60   61   62   63   64   65   66   67   68   69
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
|    | 58 |    |    | 63 |    | 55 |    |    | h  | e  | l  | l  | o  | \0 |    |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+

Тут ви можете побачити, що з адреси 63 починається рядок "привіт". Тож у цьому випадку, якщо це єдине виникнення "привіт" у пам'яті,

const char *c = "hello";

... визначає cяк вказівник на рядок (тільки для читання) рядка "привіт", і, таким чином, містить значення 63. воно cповинно бути збережене десь: у прикладі вище в місці 58. Звичайно, ми не можемо вказувати лише на символи , а також на інші покажчики. Наприклад:

const char **cp = &c;

Тепер cpвказує на c, тобто містить адресу c(що становить 58). Ми можемо піти ще далі. Поміркуйте:

const char ***cpp = &cp;

Тепер cppзберігається адреса cp. Отже, воно має значення 55 (на підставі наведеного вище прикладу), і ви здогадалися: воно зберігається за адресою 60.


Щодо того, чому користуються вказівниками на покажчики:

  • Ім’я масиву зазвичай дає адресу його першого елемента. Отже, якщо масив містить елементи типу t, посилання на масив має тип t *. Тепер розглянемо масив масивів типу t: природно, посилання на цей 2D масив матиме тип (t *)*= t **, а значить, вказівник на покажчик.
  • Хоча масив рядків звучить одновимірним, він насправді є двовимірним, оскільки рядки - це масиви символів. Отже: char **.
  • Функції fпотрібно прийняти аргумент типу, t **якщо він повинен змінити змінну типу t *.
  • Багато інших причин, які тут занадто багато, щоб їх перелічити.

7
так, хороший приклад .. я розумію, що вони є ... але як і коли ними користуватися важливіше ... тепер ..

2
Стефан зробив непогану роботу, відтворивши, в основному, діаграму в мові програмування Керніган і Річі. Якщо ви програмуєте на C і не маєте цієї книги, і вас цікаво з паперовою документацією, я настійно пропоную вам її отримати, (досить) скромні витрати окупляться дуже швидко продуктивністю. Це, як правило, дуже чітко в своїх прикладах.
Дж. Полфер

4
char * c = "привіт" має бути const char * c = "привіт". Також максимум оманливо сказати, що "масив зберігається як адреса першого елемента". Масив зберігається як ... масив. Часто його назва дає вказівку на його перший елемент, але не завжди. Щодо покажчиків на покажчики, я б просто сказав, що вони корисні, коли функція повинна змінювати покажчик, переданий як параметр (тоді ви переходите вказівник на покажчик замість цього).
Бастієн Леонард

4
Якщо я неправильно тлумачу цю відповідь, це виглядає неправильно. c зберігається в 58 і вказує на 63, cp зберігається в 55 і вказує на 58, а cpp не представлений на діаграмі.
Танатос

1
Виглядає добре. Незмінною проблемою було все, що заважало мені сказати: Чудовий пост. Само пояснення було чудовим. Перехід до голосування. (Можливо, stackoverflow потрібно переглянути вказівники?)
Танатос,

46

Як працюють покажчики на покажчики в C?

Спочатку вказівник - це змінна, як і будь-яка інша змінна, але вона містить адресу змінної.

Вказівник на покажчик - це змінна, як і будь-яка інша змінна, але яка містить адресу змінної. Ця змінна як раз і є вказівником.

Коли ви використовували б їх?

Ви можете використовувати їх, коли вам потрібно повернути вказівник на деяку пам'ять на купі, але не використовувати значення повернення.

Приклад:

int getValueOf5(int *p)
{
  *p = 5;
  return 1;//success
}

int get1024HeapMemory(int **p)
{
  *p = malloc(1024);
  if(*p == 0)
    return -1;//error
  else 
    return 0;//success
}

І ви називаєте це так:

int x;
getValueOf5(&x);//I want to fill the int varaible, so I pass it's address in
//At this point x holds 5

int *p;    
get1024HeapMemory(&p);//I want to fill the int* variable, so I pass it's address in
//At this point p holds a memory address where 1024 bytes of memory is allocated on the heap

Існують і інші способи використання, як і аргумент main () кожної програми C має вказівник на покажчик для argv, де кожен елемент містить масив символів, які є параметрами командного рядка. Ви повинні бути обережними, хоча, коли ви використовуєте покажчики покажчиків для вказівки на двовимірні масиви, краще замість цього використовувати вказівник на двовимірний масив.

Чому це небезпечно?

void test()
{
  double **a;
  int i1 = sizeof(a[0]);//i1 == 4 == sizeof(double*)

  double matrix[ROWS][COLUMNS];
  int i2 = sizeof(matrix[0]);//i2 == 240 == COLUMNS * sizeof(double)
}

Ось приклад вказівника на двовимірний масив, виконаний правильно:

int (*myPointerTo2DimArray)[ROWS][COLUMNS]

Ви не можете використовувати вказівник на двовимірний масив, хоча ви хочете підтримувати змінну кількість елементів для ROWS та COLUMNS. Але коли ви знаєте заздалегідь, ви будете використовувати двовимірний масив.


32

Мені подобається цей "реальний світ" приклад коду вказівника на використання вказівника, в Git 2.0, виконувати 7b1004b :

Колись Лінус сказав:

Я дійсно бажаю, щоб більше людей зрозуміли дійсно кодування низького рівня кодування. Не великі, складні речі, такі як пошук без замкових імен, але просто хороше використання покажчиків на покажчики тощо.
Наприклад, я бачив занадто багато людей, які видаляють запис, пов’язаний окремо із списку, відслідковуючи запис "prev" , а потім видалити запис, зробивши щось подібне

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

і коли я бачу такий код, я просто переходжу "Ця людина не розуміє покажчиків". І це, на жаль, досить часто.

Люди, які розуміють покажчики, просто використовують " вказівник на вхідний покажчик " та ініціалізують його з адресою list_head. А потім, переходячи список, вони можуть видалити запис, не використовуючи жодних умовних умов, просто зробивши

*pp =  entry->next

http://i.stack.imgur.com/bpfxT.gif

Застосовуючи це спрощення, ми можемо втратити 7 рядків від цієї функції, навіть додаючи 2 рядки коментарів.

-   struct combine_diff_path *p, *pprev, *ptmp;
+   struct combine_diff_path *p, **tail = &curr;

Кріс вказує у коментарі до відео у 2016 році " Проблема подвійного покажчика Лінуса Торвальдса " Філіпа Баука .


Кумар вказує в коментарі до блогу " Лінус про розуміння покажчиків ", де Гриша Трубецький пояснює:

Уявіть, що у вас пов'язаний список, визначений як:

typedef struct list_entry {
    int val;
    struct list_entry *next;
} list_entry;

Потрібно перебрати його від початку до кінця та видалити конкретний елемент, значення якого дорівнює значенню to_remove.
Більш очевидний спосіб це зробити:

list_entry *entry = head; /* assuming head exists and is the first entry of the list */
list_entry *prev = NULL;

while (entry) { /* line 4 */
    if (entry->val == to_remove)     /* this is the one to remove ; line 5 */
        if (prev)
           prev->next = entry->next; /* remove the entry ; line 7 */
        else
            head = entry->next;      /* special case - first entry ; line 9 */

    /* move on to the next entry */
    prev = entry;
    entry = entry->next;
}

Ми робимо вище:

  • ітерація над списком до вступу NULL, це означає, що ми дійшли до кінця списку (рядок 4).
  • Коли ми стикаємося із записом, який ми хочемо видалити (рядок 5),
    • ми присвоюємо значення поточного наступного вказівника попередньому,
    • таким чином усуваючи поточний елемент (рядок 7).

Вище є окремий випадок - на початку ітерації немає попереднього запису ( prevє NULL), і щоб видалити перший запис у списку, вам слід змінити саму головку (рядок 9).

Що сказав Лінус, це те, що вищезазначений код можна спростити, зробивши попередній елемент вказівником на покажчик, а не просто на вказівник .
Потім код виглядає приблизно так:

list_entry **pp = &head; /* pointer to a pointer */
list_entry *entry = head;

while (entry) {
    if (entry->val == to_remove)
        *pp = entry->next;

    pp = &entry->next;
    entry = entry->next;
}

Вищевказаний код дуже схожий на попередній варіант, але зауважте, як нам більше не потрібно спостерігати за окремим випадком першого елемента списку, оскільки ppце не NULLна початку. Простий і розумний.

Також хтось із цієї теми прокоментував, що причина цього краща - *pp = entry->nextце атомна. Це, звичайно, НЕ атомний .
Вищенаведений вираз містить два оператори зриву ( *і ->) та одне призначення, і жодна з цих трьох речей не є атомною.
Це поширена помилка, але , на жаль , майже нічого в C ніколи не слід вважати атомними ( в тому числі ++і --операторів)!


4
Це допоможе зрозуміти краще - grisha.org/blog/2013/04/02/linus-on-understanding-pointers
kumar

@kumar хороша довідка. я включив його у відповідь для більшої наочності.
VonC

Це відео було для мене важливим для розуміння вашого прикладу. Зокрема, я почувався розгубленим (і войовничим), поки не намалював схему пам'яті і не простежив хід програми. Але це все ще здається мені таємничим.
Кріс

@Chris Чудове відео, дякую, що згадуєте про це! Я включив ваш коментар у відповідь для більшої наочності.
VonC

14

Під час висвітлення покажчиків на курсі програмування в університеті нам дали два підказки, як почати вивчати їх. Першим було переглянути Pointer Fun With Binky . Друге - було подумати про перехід Очних птахів з «Любляни Керролл» через « Зеркальне скло»

"Ви сумуєте", - тривожно промовив Лицар: - Дозвольте заспівати вам пісню, щоб потішити вас.

"Це дуже довго?" - спитала Аліса, бо вона того дня почула багато віршів.

- Давно, - сказав лицар, - але це дуже, дуже красиво. Усі, хто чує мене, це співають - або це приносить сльози їхнім очам, або ж - "

"Або ще що?" - сказала Аліса, бо Лицар зробив раптову паузу.

"Або ще ні, ви знаєте. Назва пісні називається "Зірки очей". "

"О, так називається пісня, чи не так?", - сказала Аліса, намагаючись відчути інтерес.

"Ні, ви не розумієте", - сказав Лицар, трохи розчулено. «Саме так називається ім’я. Назва справді - "У віці людина у віці". "

"Тоді я повинен був сказати:" Ось як називається пісня "?" Аліса поправила себе.

"Ні, ви не повинні: це зовсім інша річ! Пісня називається "Шляхи та засоби": але це лише те, що вона називається, ви знаєте! "

"Ну, що ж це за пісня?" - сказала Аліса, яка до цього часу була зовсім збентежена.

"Я прийшов до цього", - сказав Лицар. "Пісня насправді -" Сидіти біля воріт ": і мелодія - це власний винахід".


1
Мені довелося прочитати цей уривок пару разів ... +1, щоб змусити мене думати!
Рубен Штайнс

Ось чому Льюїс Керролл не звичайний письменник.
метароза

1
Отже ... пішло б так? ім'я -> 'The Aged Aged Man' ->
call


7

Коли потрібна посилання на покажчик. Наприклад, коли ви бажаєте змінити значення (адресу, вказану на) змінної вказівника, оголошеної в області викликової функції всередині викликаної функції.

Якщо ви передаєте один вказівник в якості аргументу, ви будете змінювати локальні копії вказівника, а не оригінальний покажчик в області виклику. За допомогою вказівника на вказівник ви змінюєте останнє.


Добре пояснено для частини "Чому"
Rana Deep

7

Вказівник на покажчик також називається ручкою . Одне використання для нього часто - це коли об'єкт можна переміщувати в пам'яті або вилучати. Один з них часто відповідає за блокування та розблокування використання об'єкта, щоб він не переміщувався під час доступу до нього.

Він часто використовується в середовищі з обмеженою пам'яттю, тобто в Palm OS.

Посилання computer.howstuffworks.com >>

www.flippinbits.com Посилання >>


7

Розглянемо рисунок і програму нижче, щоб краще зрозуміти це поняття .

Подвійна діаграма вказівника

Відповідно до рисунка, ptr1 - це єдиний покажчик, який має адресу змінної num .

ptr1 = #

Аналогічно, ptr2 - покажчик на покажчик (подвійний вказівник), який має адресу вказівника ptr1 .

ptr2 = &ptr1;

Вказівник, який вказує на інший покажчик, відомий як подвійний вказівник. У цьому прикладі ptr2 - подвійний покажчик.

Значення з наведеної діаграми:

Address of variable num has : 1000
Address of Pointer ptr1 is: 2000
Address of Pointer ptr2 is: 3000

Приклад:

#include <stdio.h>

int main ()
{
   int  num = 10;
   int  *ptr1;
   int  **ptr2;

   // Take the address of var 
   ptr1 = &num;

   // Take the address of ptr1 using address of operator &
   ptr2 = &ptr1;

   // Print the value
   printf("Value of num = %d\n", num );
   printf("Value available at *ptr1 = %d\n", *ptr1 );
   printf("Value available at **ptr2 = %d\n", **ptr2);
}

Вихід:

Value of num = 10
Value available at *ptr1 = 10
Value available at **ptr2 = 10

5

це вказівник на значення адреси вказівника. (це жахливо я знаю)

в основному, це дозволяє передати вказівник на значення адреси іншого вказівника, так що ви можете змінити, де інший вказівник вказує з підфункції, наприклад:

void changeptr(int** pp)
{
  *pp=&someval;
}

вибачте, я знаю, що це було досить погано. Спробуйте прочитати, ем, це: codeproject.com/KB/cpp/PtrToPtr.aspx
Люк Шафер

5

У вас є змінна, яка містить адресу чогось. Це вказівник.

Потім у вас є ще одна змінна, яка містить адресу першої змінної. Це вказівник на покажчик.


3

Вказівник на вказівник - це, ну, вказівник на вказівник.

Важливим прикладом someType ** є двовимірний масив: у вас є один масив, заповнений покажчиками на інші масиви, тому коли ви пишете

dpointer [5] [6]

Ви отримуєте доступ до масиву, який містить вказівники до інших масивів у його 5-му положенні, отримуєте вказівник (нехай fpointer його ім'я), а потім отримують доступ до 6-го елемента масиву, на який посилається цей масив (так, fpointer [6]).


2
покажчики на покажчики не слід плутати з масивами rank2, наприклад, int x [10] [10], де ви пишете x [5] [6], ви отримуєте доступ до значення в масиві.
Піт Кіркхем

Це лише приклад, коли пустота ** доречна. Вказівник на покажчик - це лише вказівник, який вказує на, ну, вказівник.
akappa

1

Як це працює: це змінна, яка може зберігати інший покажчик.

Коли ви використовуєте їх: Багато хто використовує один з них, якщо ваша функція хоче побудувати масив і повернути його абоненту.

//returns the array of roll nos {11, 12} through paramater
// return value is total number of  students
int fun( int **i )
{
    int *j;
    *i = (int*)malloc ( 2*sizeof(int) );
    **i = 11;  // e.g., newly allocated memory 0x2000 store 11
    j = *i;
    j++;
    *j = 12; ;  // e.g., newly allocated memory 0x2004 store 12

    return 2;
}

int main()
{
    int *i;
    int n = fun( &i ); // hey I don't know how many students are in your class please send all of their roll numbers.
    for ( int j=0; j<n; j++ )
        printf( "roll no = %d \n", i[j] );

    return 0;
}


0

Там так багато корисних пояснень, але я не знайшов лише короткий опис, так що ..

В основному вказівник - це адреса змінної. Короткий підсумковий код:

     int a, *p_a;//declaration of normal variable and int pointer variable
     a = 56;     //simply assign value
     p_a = &a;   //save address of "a" to pointer variable
     *p_a = 15;  //override the value of the variable

//print 0xfoo and 15 
//- first is address, 2nd is value stored at this address (that is called dereference)
     printf("pointer p_a is having value %d and targeting at variable value %d", p_a, *p_a); 

Також корисну інформацію можна знайти в темі, що означає довідник та відсторонення

І я не настільки впевнений, коли корисні покажчики можуть бути корисними, але, як правило, їх потрібно використовувати, коли ви робите певний розподіл пам'яті вручну / динаміку - malloc, calloc тощо.

Тож сподіваюся, що це також допоможе з’ясувати проблематику :)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.