Вказівка ​​на уточнення вказівника


142

Я дотримувався цього підручника про те, як працює покажчик на покажчик .

Дозвольте мені процитувати відповідний уривок:


    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;

Тепер ми можемо встановити

    int **ipp = &ip1;

і ippвказує на ip1які вказує i. *ippє ip1, і **ippє i, або 5. Ми можемо проілюструвати ситуацію за допомогою наших знайомих позначень поля та стрілки, як це:

введіть тут опис зображення

Якщо тоді ми скажемо

    *ipp = ip2;

ми змінили вказівник, на який вказує ipp(тобто ip1), щоб містити копію ip2, так що вона ( ip1) тепер вказує на j:

введіть тут опис зображення


Моє запитання: Чому на другому малюнку ippвсе ще вказується, ip1але ні ip2?


6
Будь ласка, додайте тип, ippколи визначатимете його, щоб ваше запитання було повним ;-)
змо

1
Бо ippвказівка ​​на ip1так *ipp = ip2само, як іip1 = ip2
Гріеш Чаухан

1
Чи можемо ми припинити писати зірочки, що знаходяться далеко від загостреного типу? int **ippнабагато менш інтуїтивно зрозумілий, ніж int** ipp, особливо, якщо це **ippозначає зовсім інша річ поза деклараціями типу.
Darkhogg

4
@Darkhogg мені int **ippздається досить інтуїтивно зрозумілим. Це означає, що я роблю . Що правда. **ippint
Ітіса

2
@ user54609 значення та особиста інтуїтивність, крім того, врахуйте це: тип ippє int**, тому просто напишіть int**замість магічних розумінь " impdereference is int" .
Manu343726

Відповіді:


143

Забудьте на секунду про аналогію вказівки. Що вказівник насправді містить, це адреса пам'яті. Оператор &"адреса" - тобто повертає адресу в пам'яті об'єкта. *Оператор дає об'єкт покажчик посилається, тобто даного покажчика , що містить адресу, він повертає об'єкт за цією адресою пам'яті. Отже, коли ви робите *ipp = ip2, ви *ippотримуєте об'єкт за адресою, в ippякій є, ip1а потім присвоюєте ip1значення, збережене в ньому ip2, яке є адресою j.

Просто
& -> Адреса
*-> Значення при


14
& і * ніколи не були такими легкими
Рей

7
Я вважаю, що основне джерело плутанини пояснюється неоднозначністю оператора *, який під час оголошення змінної використовується для вказівки на те, що змінна, власне, є вказівником на певний тип даних. Але, з іншого боку, він також використовується у операторах для доступу до вмісту змінної, на яку вказує вказівник (оператор перенаправлення).
Лукас А.

43

Тому що ви змінили значення, на яке вказувало ippне значення ipp. Отже, ippдосі вказує на ip1(значення ipp), ip1значення тепер те саме, що ip2і значення ', тому вони обидва вказують j.

Це:

*ipp = ip2;

те саме, що:

ip1 = ip2;

11
Можливо, варто вказати на різницю між int *ip1 = &iі *ipp = ip2;, тобто, якщо ви вилучите intз першого твердження, тоді завдання виглядають дуже схоже, але *в двох випадках відбувається щось дуже різне.
Кроуман

22

Як і більшість питань для початківців у тезі C, на це питання можна відповісти, повернувшись до перших принципів:

  • Вказівник - це своєрідне значення.
  • Змінна містить значення.
  • &Оператор перетворює змінну в покажчик.
  • *Оператор перетворює покажчик в змінну.

(Технічно я повинен сказати "lvalue" замість "змінної", але я вважаю, що більш чітко описати мінливі місця зберігання як "змінні".)

Отже, у нас є змінні:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

Змінна ip1 містить покажчик. &Оператор перетворюється iв покажчик та що значення покажчика присвоюється ip1. Так ip1 міститься вказівник на i.

Змінна ip2 містить покажчик. &Оператор перетворюється jв покажчик та покажчик присвоюється ip2. Так ip2 міститься вказівник на j.

int **ipp = &ip1;

Змінна ippмістить покажчик. &Оператор включає змінну ip1в покажчик та що значення покажчика присвоюється ipp. Так ippміститься вказівник на ip1.

Давайте підведемо підсумок до цього часу:

  • i містить 5
  • j містить 6
  • ip1містить "вказівник на i"
  • ip2містить "вказівник на j"
  • ippмістить "вказівник на ip1"

Зараз ми кажемо

*ipp = ip2;

*Оператор повертає покажчик назад в змінний. Ми отримуємо значення ipp, яке "вказує на ip1та перетворюємо його на змінну. Яка змінна? ip1Звичайно!

Тому це просто інший спосіб сказати

ip1 = ip2;

Таким чином, ми отримуємо значення ip2. Що це? "вказівник на j". Це значення покажчика ми присвоюємо ip1, тому ip1тепер "вказівник на j"

Ми змінили лише одне: значення ip1:

  • i містить 5
  • j містить 6
  • ip1містить "вказівник на j"
  • ip2містить "вказівник на j"
  • ippмістить "вказівник на ip1"

Чому ippвсе-таки вказується ip1і ні ip2?

Змінна змінюється, коли ви призначаєте її. Порахувати завдання; не може бути більше змін змінних, ніж є призначення! Ви починаєте шляхом присвоєння i, j, ip1, ip2і ipp. Потім ви призначаєте *ipp, що, як ми бачили, означає те саме, що "призначити ip1". Оскільки ви не призначили ippвдруге, він не змінився!

Якщо ви хочете змінитись, ippвам доведеться фактично призначити ipp:

ipp = &ip2;

наприклад.


21

сподіваюся, цей фрагмент коду може допомогти.

#include <iostream>
#include <stdio.h>
using namespace std;

int main()
{
    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;
    int** ipp = &ip1;
    printf("address of value i: %p\n", &i);
    printf("address of value j: %p\n", &j);
    printf("value ip1: %p\n", ip1);
    printf("value ip2: %p\n", ip2);
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
    *ipp = ip2;
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
}

він виводить:

введіть тут опис зображення


12

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

Як і все на вашому комп’ютері, покажчики - це цифри . Назва "покажчик" - це просто фантазійний спосіб сказати "змінну, що містить адресу".

Тому дозвольте мені порушити речі, пояснивши, як насправді працює комп'ютер.

У нас є int, він має ім'я iта значення 5. Це зберігається в пам'яті. Як і все, що зберігається в пам'яті, йому потрібна адреса, інакше ми не зможемо її знайти. Скажімо, він iзакінчується за адресою 0x12345678, а його товариш jзі значенням 6 закінчується відразу після нього. Якщо припустити 32-розрядний процесор, де int - 4 байти, а покажчики - 4 байти, то змінні зберігаються у фізичній пам'яті так:

Address     Data           Meaning
0x12345678  00 00 00 05    // The variable i
0x1234567C  00 00 00 06    // The variable j

Тепер ми хочемо вказати на ці змінні. Ми створюємо один вказівник на int int* ip1, і один int* ip2. Як і все в комп'ютері, ці змінні вказівника також виділяються десь у пам'яті. Давайте припустимо, що вони опиняються на наступних суміжних адресах пам'яті, одразу після цього j. Встановлюємо покажчики, які містять адреси виділених раніше змінних: ip1=&i;("скопіюйте адресу i в ip1") і ip2=&j. Що відбувається між рядками:

Address     Data           Meaning
0x12345680  12 34 56 78    // The variable ip1(equal to address of i)
0x12345684  12 34 56 7C    // The variable ip2(equal to address of j)

Отже, ми отримали лише кілька байтових фрагментів пам'яті, що містять числа. Містічних чи магічних стрілок ніде не видно.

Насправді, лише переглянувши дамп пам'яті, ми не можемо сказати, чи містить адреса 0x12345680 intабо int*. Різниця полягає в тому, як наша програма вирішує використовувати вміст, збережений за цією адресою. (Завдання нашої програми насправді просто сказати процесору, що робити з цими номерами.)

Тоді ми додамо ще один рівень непрямості за допомогою int** ipp = &ip1;. Знову ми просто отримуємо шматок пам’яті:

Address     Data           Meaning
0x12345688  12 34 56 80    // The variable ipp

Шаблон здається знайомим. Ще один фрагмент у 4 байти, що містить число.

Тепер, якби у нас був дамп пам'яті вищенаведеної вигаданої невеликої ОЗУ, ми могли б вручну перевірити, куди ці вказівки вказують. Ми заглядаємо, що зберігається за адресою ippзмінної, і знаходимо вміст 0x12345680. Звичайно, це адреса, де ip1зберігається. Ми можемо зайти на цю адресу, перевірити вміст там і знайти адресу i, і, нарешті, ми можемо перейти до цієї адреси і знайти номер 5.

Отже, якщо ми візьмемо вміст ipp *ipp, ми отримаємо адресу змінної вказівника ip1. Написавши, *ipp=ip2ми копіюємо ip2 в ip1, він еквівалентний ip1=ip2. В будь-якому випадку ми отримаємо

Address     Data           Meaning
0x12345680  12 34 56 7C    // The variable ip1
0x12345684  12 34 56 7C    // The variable ip2

(Ці приклади були наведені для великого ендіанського процесора)


5
Хоча я погоджуюсь з вами, є цінність у мисленні покажчиків як абстрактних, загадкових сутностей. Будь-яка конкретна реалізація покажчиків - це лише цифри, але стратегія реалізації, яку ви накреслюєте, не є вимогою реалізації, це лише загальна стратегія. Покажчики не повинні бути такого ж розміру, як int, покажчики не повинні бути адресами у плоскій моделі віртуальної пам'яті тощо; це лише деталі реалізації.
Ерік Ліпперт

@EricLippert Я думаю, що можна зробити цей приклад більш абстрактним, не використовуючи фактичні адреси пам'яті або блоки даних. Якщо це була таблиця, в якій зазначалося щось на зразок location, value, variableмісця розташування 1,2,3,4,5та значення A,1,B,C,3, відповідне уявлення про вказівники можна було легко пояснити без використання стрілок, які за своєю суттю заплутані. Незалежно від того, яку реалізацію ви вибрали, значення існує в якомусь місці, і це фрагмент головоломки, який затуманюється при моделюванні зі стрілками.
MirroredFate

@EricLippert На мій досвід, більшість потенційних програмістів на C, які мають проблеми з розумінням покажчиків, - це ті, кого годували абстрактними, штучними моделями. Абстракція не є корисною, оскільки вся мета мови С сьогодні полягає в тому, щоб вона була близькою до апаратних засобів. Якщо ви навчаєтесь C, але не маєте наміру писати код близько до обладнання, ви витрачаєте свій час . Java тощо - це набагато кращий вибір, якщо ви не хочете знати, як працюють комп'ютери, а просто програмування високого рівня.
Лундін

@EricLippert І так, можуть існувати різні незрозумілі реалізації покажчиків, де вказівники не обов'язково відповідають адресам. Але малювання стрілок не допоможе зрозуміти, як вони працюють. У якийсь момент вам доведеться залишити абстрактне мислення і перейти до апаратного рівня, інакше вам не слід використовувати C. Існує багато куди більш підходящих сучасних мов, призначених для суто абстрактного програмування високого рівня.
Лундін

@Lundin: Я теж не великий шанувальник діаграм зі стрілками; Поняття стрілки як даних є складним. Я вважаю за краще думати про це абстрактно, але без стрілок. &Оператор змінної дає монету, що представляє цю змінну. *Оператор на цій монеті дає вам назад змінну. Не потрібно стрілок!
Ерік Ліпперт

8

Помітьте завдання:

ipp = &ip1;

результати, на які ippслід вказати ip1.

тож, ippщоб зазначити ip2, ми повинні змінити аналогічним чином,

ipp = &ip2;

чого ми, очевидно, не робимо. Натомість ми змінюємо значення за адресою, на яку вказує ipp.
Роблячи наступне

*ipp = ip2;

ми просто замінюємо значення, збережене в ip1.

ipp = &ip1, Значить *ipp = ip1 = &i,
тепер *ipp = ip2 = &j.
Отже, *ipp = ip2по суті таке ж, як ip1 = ip2.


5
ipp = &ip1;

Пізніше призначення не змінило значення ipp. На це все ще вказує ip1.

Те, що ви робите *ipp, т. Е. З ip1, не змінює того факту, на який ippвказує ip1.


5

Моє запитання: Чому на другому малюнку ipp все ще вказує на ip1, але не на ip2?

Ви розмістили приємні фотографії, я спробую зробити приємне мистецтво Ascii:

Як @ Роберт-С-Барнс сказав у своїй відповіді: забудьте про покажчики та що вказує на що, але подумайте з точки зору пам’яті. В основному, це int*означає, що вона містить адресу змінної та an int**містить адресу змінної, яка містить адресу змінної. Тоді ви можете використовувати алгебру вказівника для доступу до значень або адрес: &fooзасобів address of fooі *fooзасобів value of the address contained in foo.

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

Отже, ось пам'ять вашої програми (спрощена для прикладу):

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [   |   |   |   |   ]

коли ви робите свій початковий код:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

ось як виглядає ваша пам'ять:

name:    i   j ip1 ip2
addr:    0   1   2   3
mem : [  5|  6|  0|  1]

там ви можете бачити ip1та ip2отримувати адреси iта, jі ippдосі їх не існує. Не забувайте, що адреси - це цілі числа, що зберігаються із спеціальним типом.

Потім ви заявляєте та визначаєте ippтакі, як:

int **ipp = &ip1;

тож ось ваша пам’ять:

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  0|  1|  2]

а потім ви змінюєте значення, вказане адресою, що зберігається ipp, яка є адресою, збереженою у ip1:

*ipp = ip2;

пам'ять програми є

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  1|  1|  2]

Примітка: як int*це особливий тип, я вважаю за краще завжди уникати оголошення кількох покажчиків на одній лінії, оскільки я думаю, що int *x;або int *x, *y;позначення можуть вводити в оману. Я вважаю за краще писатиint* x; int* y;

HTH


з , наприклад, початкове значення ip2має бути 3НЕ 4.
Dipto

1
ой, я просто змінив пам'ять, щоб вона відповідала порядку декларування. Я думаю, що я це виправив?
змо

5

Бо коли ти кажеш

*ipp = ip2

ви говорите "об'єкт, на який вказує ipp", щоб вказати напрямок пам'яті, який ip2вказує.

Ви не говорите, ippщоб вказувати ip2.


4

Якщо ви додасте *вказівник оператора перенаправлення, ви перенаправляєте його з покажчика на об'єкт із загостреним об'єктом.

Приклади:

int i = 0;
int *p = &i; // <-- N.B. the pointer declaration also uses the `*`
             //     it's not the dereference operator in this context
*p;          // <-- this expression uses the pointed-to object, that is `i`
p;           // <-- this expression uses the pointer object itself, that is `p`

Тому:

*ipp = ip2; // <-- you change the pointer `ipp` points to, not `ipp` itself
            //     therefore, `ipp` still points to `ip1` afterwards.

3

Якщо ви хочете ippвказати на це ip2, вам доведеться сказати ipp = &ip2;. Однак це ip1ще не вказуватиме на це i.


3

В самому початку ви ставите,

ipp = &ip1;

Тепер знешкодження це як,

*ipp = *&ip1 // Here *& becomes 1  
*ipp = ip1   // Hence proved 

3

Розглянемо кожну змінну, представлену так:

type  : (name, adress, value)

тому ваші змінні повинні бути представлені так

int   : ( i ,  &i , 5 ); ( j ,  &j ,  6); ( k ,  &k , 5 )

int*  : (ip1, &ip1, &i); (ip1, &ip1, &j)

int** : (ipp, &ipp, &ip1)

В якості значення ippIS &ip1так в inctruction:

*ipp = ip2;

змінює значення в адресі &ip1на значення ip2, що означає ip1, що змінюється:

(ip1, &ip1, &i) -> (ip1, &ip1, &j)

Але ippвсе-таки:

(ipp, &ipp, &ip1)

Тож значення ippstill, &ip1що означає, що воно все ще вказує ip1.


1

Тому що ви змінюєте вказівник на *ipp. Це означає

  1. ipp (змінна назва) ---- зайти всередину.
  2. всередині ipp- адреса ip1.
  3. тепер *ippтак перейдіть (адреса всередину) ip1.

Зараз ми в ip1. *ipp(Тобто ip1) = ip2.
ip2містять адресу j.so ip1контенту буде замінити містити від ip2 (тобто адреса J), ми не змінюємо ippзміст. ЦЕ ВОНО.


1

*ipp = ip2; означає:

Призначити ip2змінну, на яку вказує ipp. Отже, це рівнозначно:

ip1 = ip2;

Якщо ви хочете, щоб адреса ip2зберігалася в ipp, просто зробіть:

ipp = &ip2;

Тепер ippвказує на ip2.


0

ippможе містити значення (тобто вказувати на) вказівник на об'єкт типу вказівника . Коли ви робите

ipp = &ip2;  

то ippмістить адресу змінної (покажчика)ip2 , яка є ( &ip2) типу вказівника на покажчик . Тепер стрілка ippв другому малюнку буде вказувати на ip2.

Вікі говорять: оператор є оператор разименовать працює на змінному покажчик, і повертає л-значення (змінний) еквівалентно значення в покажчику адресу. Це називається перенаправленням покажчика.
*

Застосування *оператора на ippдерефренції до l-значення вказівника наint тип. Відхилене l-значення *ippмає вказівник наint тип , воно може містити адресу intданих типу. Після заяви

ipp = &ip1;

ippтримає адресу ip1і *ippтримає адресу (вказує на) i. Ви можете сказати, що *ippце псевдонім ip1. І те, **ippі *ip1інше є псевдонімом i.
Роблячи

 *ipp = ip2;  

*ippі ip2обидва вказують на одне місце, але ippвсе ще вказують на ip1.

Що *ipp = ip2;ж на самому справі є те , що він копіює вміст ip2(адреса j) до ip1(як це *ippє псевдонімом ip1), по суті робить обидва покажчика ip1і ip2вказує на той же об'єкт ( j).
Отже, на другій фігурі стрілка ip1та ip2вказує на jтой час, ippпоки ще вказується на те ip1, що жодних змін не робиться для зміни значенняipp .

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