Які бар'єри для розуміння покажчиків і що можна зробити для їх подолання? [зачинено]


449

Чому вказівники є таким провідним фактором плутанини для багатьох нових, навіть навіть старих, студентів на рівні C або C ++? Чи є якісь інструменти чи мислені процеси, які допомогли вам зрозуміти, як покажчики працюють на змінній, функції та за її межами?

Які речі хорошої практики можна зробити, щоб довести когось до рівня "А-а-а-а, я це зрозумів", не забивши їх у загальній концепції? В основному, бурити як сценарії.


20
Теза цього питання полягає в тому, що покажчики важко зрозуміти. Питання не підтверджує, що покажчики зрозуміти важче, ніж будь-що інше.
bmargulies

14
Можливо, мені чогось не вистачає (тому що я кодую в мовах GCC'd), але я завжди думав, чи покажчики в пам'яті як ключові-> значення структури. Оскільки дорого обходити велику кількість даних у програмі, ви створюєте структуру (значення) і обходите її вказівником / посиланням (ключем), оскільки ключ є значно меншим представленням більшої структури. Важка частина полягає в тому, коли вам потрібно порівняти два покажчики / посилання (ви порівнюєте ключі або значення), що вимагає додаткової роботи, щоб пробити дані, що містяться в структурі (значення).
Еван Плейс

2
@ Wolfpack'08 "Мені здається, пам'ять у адресі завжди буде int." - Тоді вам повинно здатися, що нічого не має типу, оскільки всі вони просто біти в пам'яті. "Власне, тип вказівника - це тип вару, на який вказує вказівник" - Ні, тип вказівника вказує на тип вару, на який вказує вказівник, - що природно і має бути очевидним.
Джим Балтер

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

8
Коротше кажучи, студенти, мабуть, не розуміють, оскільки не розуміють правильно, або взагалі, як працює пам'ять комп'ютера в цілому, а конкретно C-модель пам'яті . Ця книга « Програмування з нуля» дає дуже хороший урок з цих тем.
Аббафей

Відповіді:


745

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

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

Я додав код Delphi внизу, а також деякі коментарі, де це доречно. Я вибрав Delphi, оскільки інша моя основна мова програмування, C #, не демонструє таких же речей, як витоки пам'яті.

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

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


Припустимо, клас THouse, який використовується нижче, виглядає приблизно так:

type
    THouse = class
    private
        FName : array[0..9] of Char;
    public
        constructor Create(name: PChar);
    end;

Коли ви ініціалізуєте об'єкт будинку, ім'я, яке надається конструктору, копіюється у приватне поле FName. Є причина, яку він визначає як масив фіксованого розміру.

У пам’яті буде деякий накладний зв’язок, пов’язаний з виділенням будинку, я проілюструю це нижче так:

--- [ttttNNNNNNNNNN] ---
     ^ ^
     | |
     | + - масив FName
     |
     + - накладні

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


Виділіть пам’ять

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

Іншими словами, підприємець обере місце.

THouse.Create('My house');

Макет пам'яті:

--- [ttttNNNNNNNNNN] ---
    1234 мій будинок

Зберігайте змінну з адресою

Запишіть адресу свого нового будинку на аркуші паперу. Цей папір послужить вашим посиланням на ваш будинок. Без цього аркуша паперу ви загубитесь і не можете знайти будинок, якщо ви вже не в ньому.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...

Макет пам'яті:

    год
    v
--- [ttttNNNNNNNNNN] ---
    1234 мій будинок

Скопіюйте значення вказівника

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

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

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1
    v
--- [ttttNNNNNNNNNN] ---
    1234 мій будинок
    ^
    h2

Звільнення пам'яті

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

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    h := nil;

Тут я спочатку будую будинок і отримую його адресу. Потім я щось роблю додому (використовую його, ... код, залишений як вправа для читача), а потім звільняю його. Нарешті я очищаю адресу зі своєї змінної.

Макет пам'яті:

    h <- +
    v + - перед безкоштовною
--- [ttttNNNNNNNNNN] --- |
    1234 мій будинок <- +

    h (зараз вказує нікуди) <- +
                                + - після безкоштовного
---------------------- | (зауважте, пам’ять все ще може бути
    xx34Мий будинок <- + містить деякі дані)

Вивісні покажчики

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

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    ... // forgot to clear h here
    h.OpenFrontDoor; // will most likely fail

Використання hпісля дзвінка .Free може спрацювати, але це лише чиста удача. Швидше за все, це не вдасться в місці покупців в середині критичної операції.

    h <- +
    v + - перед безкоштовною
--- [ttttNNNNNNNNNN] --- |
    1234 мій будинок <- +

    h <- +
    v + - після вільного
---------------------- |
    xx34Мий будинок <- +

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


Витік пам'яті

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

var
    h: THouse;
begin
    h := THouse.Create('My house');
    h := THouse.Create('My house'); // uh-oh, what happened to our first house?
    ...
    h.Free;
    h := nil;

Тут ми перекреслили вміст hзмінної з адресою нового будинку, але старий все ще стоїть ... десь. Після цього коду неможливо дістатися до цього будинку, і він залишиться стояти. Іншими словами, виділена пам'ять залишатиметься виділеною доти, доки програма не закриється, і в цей момент операційна система зірве її.

Макет пам'яті після першого розподілу:

    год
    v
--- [ttttNNNNNNNNNN] ---
    1234 мій будинок

Макет пам'яті після другого розподілу:

                       год
                       v
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNN] --- [ttttNNNNNNNNN]
    1234 мій будинок 5678 мій будинок

Більш поширений спосіб отримати цей метод - просто забути звільнити щось, замість того, щоб перезаписати його, як зазначено вище. У термінах Delphi це відбудеться за допомогою наступного методу:

procedure OpenTheFrontDoorOfANewHouse;
var
    h: THouse;
begin
    h := THouse.Create('My house');
    h.OpenFrontDoor;
    // uh-oh, no .Free here, where does the address go?
end;

Після виконання цього методу в наших змінних немає місця, що адреса до будинку існує, але будинок все ще там.

Макет пам'яті:

    h <- +
    v + - до втрати покажчика
--- [ttttNNNNNNNNNN] --- |
    1234 мій будинок <- +

    h (зараз вказує нікуди) <- +
                                + - після втрати покажчика
--- [ttttNNNNNNNNNN] --- |
    1234 мій будинок <- +

Як бачите, старі дані залишаються недоторканими в пам'яті і не використовуються розподільником пам'яті. Алокатор відслідковує, які області пам’яті були використані, і не використовуватиме їх повторно, якщо ви не звільните її.


Звільнення пам'яті, але збереження (тепер недійсної) посилання

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

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

Іноді ви навіть можете виявити, що на сусідній адресі встановлено досить великий будинок, який займає три адреси (Головна вулиця 1-3), а ваша адреса йде в середину будинку. Будь-які спроби трактувати цю частину великого 3-адресного будинку як єдиний маленький будинок також можуть жахливо зазнати краху.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1.Free;
    h1 := nil;
    h2.OpenFrontDoor; // uh-oh, what happened to our house?

Тут будинок був зруйнований через посилання в h1, і, хоча також h1був очищений, h2досі має стару застарілу адресу. Доступ до будинку, який вже не стоїть, може не працювати.

Це зміна звисаючого вище вказівника. Дивіться його макет пам'яті.


Перекриття буфера

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

З цієї причини я вибрав масив фіксованого розміру. Щоб встановити сцену, припустимо, що другий будинок, який ми виділимо, чомусь буде розміщений перед першим у пам’яті. Іншими словами, другий будинок матиме нижчу адресу, ніж перший. Крім того, вони виділяються прямо поруч.

Таким чином, цей код:

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := THouse.Create('My other house somewhere');
                         ^-----------------------^
                          longer than 10 characters
                         0123456789 <-- 10 characters

Макет пам'яті після першого розподілу:

                        h1
                        v
----------------------- [ttttNNNNNNNNN]
                        5678Мий будинок

Макет пам'яті після другого розподілу:

    h2 h1
    vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNN]
    1234Мой інший будинок десьбудинок
                        ^ --- + - ^
                            |
                            + - перезаписати

Частина, яка найчастіше спричинить збій - це перезапис важливих частин збережених даних, які насправді не повинні бути випадковими змінами. Наприклад, можливо, це не буде проблемою, що частини імені h1-house були змінені внаслідок збоїв програми, але перезапис накладних даних об'єкта, швидше за все, вийде з ладу при спробі використання зламаного об'єкта, як буде перезапис посилань, які зберігаються до інших об'єктів в об'єкті.


Пов'язані списки

Коли ви слідуєте за адресою на аркуші паперу, ви потрапляєте до будинку, а в цьому будинку є ще один аркуш паперу з новою адресою на ньому, для наступного будинку в ланцюжку тощо.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;

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

var
    h1, h2: THouse;
    h: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;
    ...
    h := h1;
    while h <> nil do
    begin
        h.LockAllDoors;
        h.CloseAllWindows;
        h := h.NextHouse;
    end;

Макет пам’яті (додано NextHouse як посилання на об’єкт, відмічений чотирма LLLL на схемі нижче):

    h1 h2
    vv
--- [ttttNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNLLLL]
    1234Дома + 5678Кабін +
                   | ^ |
                   + -------- + * (немає посилання)

В основному, що таке пам'ять?

Адреса пам'яті - це в основному просто число. Якщо ви вважаєте пам'ять як великий масив байтів, перший байт має адресу 0, наступний - адресу 1 і так далі вгору. Це спрощено, але досить добре.

Отже, цей макет пам'яті:

    h1 h2
    vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNN] --- [ttttNNNNNNNNN]
    1234 мій будинок 5678 мій будинок

Можливо, є ці дві адреси (крайній лівий - адреса 0):

  • h1 = 4
  • h2 = 23

Що означає, що наш зв'язаний список вище може виглядати так:

    h1 (= 4) h2 (= 28)
    vv
--- [ttttNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNLLLL]
    1234Дома 0028 5678Кабін 0000
                   | ^ |
                   + -------- + * (немає посилання)

Типово зберігати адресу, яка "не вказує нікуди", як нульову адресу.


В основному, що таке покажчик?

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


59
Перевищення буфера є веселим. "Сусід повертається додому, розкриває череп, ковзаючи по вашій мотлоху, і подає позов до вас у небуття".
gtd

12
Це приємне пояснення концепції. Концепція НЕ річ, яку я вважаю заплутаною щодо покажчиків, тому весь цей нарис був трохи марний.
Бретон

10
Але щойно запитав, що ви вважаєте заплутаним у вказівниках?
Лассе В. Карлсен

11
Я переглядав цю публікацію кілька разів, відколи ви написали відповідь. Ваше роз'яснення з кодом є прекрасним, і я вдячний, що ви переглянули його, щоб додати / уточнити більше думок. Браво Лассе!
Девід Макгрю

3
Немає можливості, щоб одна сторінка тексту (незалежно від того, скільки вона тривала) може підсумувати кожен нюанс управління пам’яттю, посиланнями, покажчиками тощо. Оскільки за це проголосували 465 людей, я б сказав, що він служить достатньо хорошим початкова сторінка інформації. Чи є чому навчитися? Звичайно, коли це не так?
Лассе В. Карлсен

153

У моєму першому класі Comp Sci ми зробили наступну вправу. Зрозуміло, це була лекційна зала, де було близько 200 студентів ...

Професор пише на дошці: int john;

Джон встає

Професор пише: int *sally = &john;

Саллі встає, вказує на Джона

Професор: int *bill = sally;

Білл встає, вказує на Джона

Професор: int sam;

Сем встає

Професор: bill = &sam;

Зараз Білл вказує на Сема.

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


5
Я не думаю, що я помилився. Мій намір полягав у тому, щоб змінити значення вказаної змінної з Джона на Сем. Трохи складніше представити їх людям, оскільки, схоже, ви змінюєте значення обох вказівників.
Tryke

24
Але причина, яка це бентежить, полягає в тому, що це не так, як Джон встав зі свого місця, а потім Сем сів, як ми можемо собі уявити. Це більше схоже на те, що Сем підійшов і встромив руку в Джон і клонував програмування Сем в тіло Джона, як плетіння Уго в перезавантаженій матриці.
Бретон

59
Більше, як Сем займає посаду Джона, і Джон плаває по кімнаті, поки він не натикається на щось критичне і не спричиняє сегментації.
just_wes

2
Я особисто вважаю цей приклад зайвим складним. Мій професор сказав мені, щоб я вказував на світло і сказав: "ваша рука - вказівник на світловий об'єкт".
Celeritas

Проблема цього типу прикладів полягає в тому, що вказівники на X і X не є однаковими. І це не зображається з людьми.
Ісаак Некіттепас

124

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


15
Мені це дуже подобається. Не важко помітити, що двічі виписування гіперпосилання не призводить до появи двох веб-сайтів (так само, як int *a = bне робиться двох копій *b).
detly

4
Це насправді дуже інтуїтивно, і те, з чим кожен повинен бути у стані. Хоча існує маса сценаріїв, коли ця аналогія розпадається. Хоча чудово для швидкого вступу. +1
Брайан Віґгінгтон

Посилання на сторінку, яка відкривається двічі, зазвичай створює два майже повністю незалежні екземпляри цієї веб-сторінки. Я думаю, що гіперпосилання, можливо, може бути хорошою аналогією конструктору, але не вказівнику.
Utkan Gezer

@ThoAppelsin Не обов'язково правда, якщо, наприклад, ви відкриваєте статичну веб-сторінку HTML, ви отримуєте доступ до одного файлу на сервері.
dramzy

5
Ти це переосмислюєш. Гіперпосилання вказує на файли на сервері, саме в цьому є аналогія.
dramzy

48

Причина, що, як видається, вказує на те, що багато людей бентежить, полягає в тому, що вони здебільшого мало архітектури комп’ютера або їх зовсім немає. Оскільки багато хто, здається, не мають уявлення про те, як реально реалізуються комп'ютери (машина) - робота в C / C ++ здається чужою.

Підготовка полягає у тому, щоб попросити їх реалізувати просту віртуальну машину на основі байт-коду (будь-якою мовою, яку вони обрали, python для цього чудово працює) з набором інструкцій, орієнтованим на операції з покажчиками (завантаження, зберігання, пряма / непряма адресація). Потім попросіть їх написати прості програми для цього набору інструкцій.

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


2
Цікаво. Не маю ідеї, як почати робити це з цього питання. Будь-які ресурси для спільного використання?
Кароліс

1
Я згоден. Наприклад, я навчився програмувати в складанні перед C і знаючи, як працюють регістри, вивчити покажчики було легко. Насправді, не багато було навчання, все вийшло дуже природно.
Мілан Бабушків

Візьміть базовий процесор, скажіть, що запускає газонокосарки або мийки для посуду та виконайте це. Або дуже-дуже базовий підмножина ARM або MIPS. Обидва мають дуже простий ISA.
Даніель Голдберг

1
Можливо, варто зазначити, що цей освітній підхід обстоював / практикує сам Дональд Кнут. Мистецтво комп’ютерного програмування Кнут описує просту гіпотетичну архітектуру і просить студентів реалізувати рішення для розв'язання задач гіпотетичною мовою складання для цієї архітектури. Як тільки це стало практично здійсненним, деякі студенти, що читають книги Кнута, насправді реалізують його архітектуру як VM (або використовують наявну реалізацію) і фактично запускають їх рішення. ІМО - це чудовий спосіб вчитися, якщо у вас є час.
WeirdlyCheezy

4
@Luke Я не думаю, що це так просто зрозуміти людям, які просто не можуть зрозуміти покажчики (або, точніше кажучи, непряме взагалі). Ви в основному припускаєте, що люди, які не розуміють покажчиків на C, зможуть почати вивчати складання, розуміти основну архітектуру комп’ютера та повертатися до C із розумінням покажчиків. Це може бути правдою для багатьох, але, згідно з деякими дослідженнями, здається, що деякі люди, по суті, не можуть зрозуміти опосередкованість, навіть в принципі (я все ще вважаю, що дуже важко повірити, але, можливо, мені просто пощастило зі своїми "студентами" ").
Луань

27

Чому вказівники є таким провідним фактором плутанини для багатьох нових і навіть старих студентів рівня коледжу мовою C / C ++?

Концепція заповнювача мінливих змінних - відображає щось, чого ми навчаємо в школі - алгебру. Не існує існуючої паралелі, яку можна провести, не розуміючи, як фізично розміщується пам'ять у комп'ютері, і ніхто не думає про подібні речі, поки вони не мають справу з речами низького рівня - на рівні зв'язку C / C ++ / byte .

Чи є якісь інструменти чи мислені процеси, які допомогли вам зрозуміти, як покажчики працюють на змінній, функції та за її межами?

Коробки адреси. Я пам'ятаю, коли я вчився програмувати BASIC на мікрокомп'ютери, були ці гарні книги з іграми в них, і іноді доводилося тикати значення в конкретні адреси. У них було зображення кути коробок, поступово позначених позначками 0, 1, 2 ... і було пояснено, що в ці скриньки може поміститися лише одна маленька річ (байт), і їх було багато - деякі комп’ютери мали цілих 65535! Вони були поруч, і всі вони мали адресу.

Які речі хорошої практики можна зробити, щоб довести когось до рівня "А-а-а-а, я це зрозумів", не забивши їх у загальній концепції? В основному, бурити як сценарії.

Для дрилі? Складіть структуру:

struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;
cout << 'Start: my_pointer = ' << *my_pointer << endl;
my_pointer++;
cout << 'After: my_pointer = ' << *my_pointer << endl;
my_pointer = &mystruct.a;
cout << 'Then: my_pointer = ' << *my_pointer << endl;
my_pointer = my_pointer + 3;
cout << 'End: my_pointer = ' << *my_pointer << endl;

Той самий приклад, що і вище, крім C:

// Same example as above, except in C:
struct {
    char a;
    char b;
    char c;
    char d;
} mystruct;

mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;

printf("Start: my_pointer = %c\n", *my_pointer);
my_pointer++;
printf("After: my_pointer = %c\n", *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c\n", *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c\n", *my_pointer);

Вихід:

Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u

Можливо, це пояснює деякі приклади на прикладі?


+1 за "без розуміння того, як фізично закладена пам'ять". Я прийшов до С із мови асемблерної мови, і концепція покажчиків була дуже природною та легкою; і я бачив, як люди, які мають лише мову вищого рівня, намагаються зрозуміти це. Щоб гірше було синтаксис заплутаним (функціональні вказівники!), Тож вивчення поняття та синтаксису одночасно є рецептом неприємностей.
Брендан

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

Так, це схоже на алгебру (хоча алгебри мають додаткову точку зрозумілості у незмінності своїх "змінних"). Але приблизно половина людей, яких я знаю, не розуміє алгебри на практиці. Це просто не обчислює їх. Вони знають усі ті "рівняння" та приписи, щоб дійти до результату, але застосовують їх дещо випадково та незграбно. І вони не можуть продовжити їх за власним призначенням - це просто якась незмінна, незручна для них чорна скринька. Якщо ви розумієте алгебру і вмієте ефективно її використовувати, ви вже випереджаєте пакет - навіть серед програмістів.
Луань

24

Причиною, що мені важко було зрозуміти покажчики, спочатку є те, що багато пояснень включають багато лайно щодо проходження посилання. Все це робить плутати питання. Коли ви використовуєте параметр вказівника, ви все одно переходите за значенням; але значення трапляється як адреса, а не, скажімо, int.

Хтось уже пов’язав цей підручник, але я можу виділити момент, коли я почав розуміти покажчики:

Навчальний посібник щодо покажчиків та масивів на C: Глава 3 - Покажчики та рядки

int puts(const char *s);

На даний момент ігноруйте Переданий const.параметр puts()- це вказівник, тобто значення вказівника (оскільки всі параметри в C передаються за значенням), а значення вказівника - адреса, на яку він вказує, або, просто , адреса. Таким чином, коли ми пишемо так, puts(strA);як ми бачили, ми передаємо адресу strA [0].

Щойно я прочитав ці слова, хмари розступилися і промінь сонячного світла огорнув мене розумінням вказівника.

Навіть якщо ви розробник VB .NET або C # (як я) і ніколи не використовуєте небезпечний код, все одно варто зрозуміти, як працюють покажчики, або ви не зрозумієте, як працюють посилання на об’єкти. Тоді у вас з'явиться загальне, але помилкове поняття, що передача посилання на об'єкт на метод копіює об'єкт.


Залишає мене цікавитись, у чому сенс мати покажчик. У більшості кодових блоків, з якими я стикаюся, вказівник бачиться лише в його декларації.
Wolfpack'08

@ Wolfpack'08 ... що ?? Який код ви дивитесь ??
Кайл Странд

@KyleStrand Дозвольте мені подивитися.
Wolfpack'08

19

Я знайшов «Підручник з покажчиків та масивів у С» Теда Дженсена відмінним ресурсом для вивчення покажчиків. Він розділений на 10 уроків, починаючи з пояснення, що таке покажчики (і для чого вони потрібні), і закінчуючи функціональними покажчиками. http://home.netcom.com/~tjensen/ptr/cpoint.htm

Переходячи звідти, Посібник Beej з мережевого програмування навчає API розеток Unix, з якого можна починати робити дуже цікаві речі. http://beej.us/guide/bgnet/


1
Я другий підручник Теда Дженсена. Це розбиває покажчики до рівня деталізації, це не надто детально, жодна книга, яку я читав, не робить. Надзвичайно корисно! :)
Дейв Галлахер

12

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

Я брав участь у системах, де у нас були структури, спрямовані на інші структури, спрямовані на інші структури. Деякі з цих структур також містили вбудовані структури (а не покажчики на додаткові структури). Ось де вказівники стають дійсно заплутаними. Якщо у вас є кілька рівнів непрямості, і ви починаєте закінчувати такий код:

widget->wazzle.fizzle = fazzle.foozle->wazzle;

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

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

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


10

Я думав би додати аналогію до цього списку, що мені було дуже корисно при поясненні покажчиків (ще в той день) як викладач з інформатики; спочатку давайте:


Встановити сцену :

Розглянемо паркінг з 3 місцями, ці місця пронумеровані:

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |

Певним чином це схоже на місця пам'яті, вони є послідовними та суміжними .. начебто масиву. Зараз у них немає автомобілів, тож це як порожній масив ( parking_lot[3] = {0}).


Додайте дані

Автостоянка ніколи не залишається порожньою ... якби це було, це було б безглуздо і ніхто не будував би її. Тож скажімо, коли день руху по партії заповнюється 3 машинами, синім автомобілем, червоним автомобілем та зеленим автомобілем:

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |

Ці машини все той же тип (автомобіль) , так що один спосіб думати про те , що наші машини які - то дані (скажімо int) , але вони мають різні значення ( blue, red, green, які можуть бути кольору enum)


Введіть вказівник

Тепер, якщо я заведу вас на цю автостоянку, і попрошу знайти мені синю машину, ви протягніть один палець і використовуєте її, щоб вказати на синій автомобіль на місці 1. Це як би взяти вказівник і призначити його на адресу пам'яті ( int *finger = parking_lot)

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


Переназначення вказівника

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

Вказівник фізично не змінився, це все одно ваш палець, просто змінилися дані, які він мені показував. (адреса "місця для паркування")


Подвійні вказівники (або вказівник на покажчик)

Це працює і з більш ніж одним покажчиком. Я можу запитати, де вказівник, який вказує на червоний автомобіль, і ви можете скористатися іншою рукою і вказати пальцем на перший палець. (це як int **finger_two = &finger)

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


Висячий покажчик

Тепер скажемо, що ти дуже схожий на статую, і ти хочеш нескінченно тримати руку, вказуючи на червону машину. Що робити, якщо той червоний автомобіль їде геть?

   1     2     3
-------------------
| o=o |     | o=o |
| |B| |     | |G| |
| o-o |     | o-o |

Ваш покажчик все ще вказує , де червоний автомобіль ні , але не більше. Скажімо, нова машина під’їжджає туди… помаранчева машина. Тепер якщо я ще раз запитаю вас, "де червона машина", ви все одно вказуєте туди, але тепер ви помиляєтесь. Це не червона машина, це помаранчевий.


Арифметика вказівника

Гаразд, ви все ще вказуєте на друге місце для паркування (зараз займає автомобіль Orange)

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |

Ну, у мене зараз нове питання ... Я хочу знати колір машини на наступному паркувальному місці. Ви можете бачити, що ви вказуєте на місце 2, тож ви просто додаєте 1 і вказуєте на наступне місце. ( finger+1), тепер, оскільки я хотів дізнатися, які дані там, ви повинні перевірити це місце (не лише пальцем), щоб ви могли відмітити вказівник ( *(finger+1)), щоб побачити, що там присутній зелений автомобіль (дані в цьому місці )


Просто не використовуйте слово "подвійний вказівник". Покажчики можуть вказувати на що завгодно, тому, очевидно, ви можете мати вказівники, що вказують на інші вказівники. Вони не є подвійними покажчиками.
gnasher729

Я думаю, що це не вистачає того, що самі «пальці», щоб продовжити вашу аналогію, кожен «займає місце для паркування». Я не впевнений, що люди мають труднощі з розумінням покажчиків на високому рівні абстрагування вашої аналогії, це розуміння того, що вказівники - це змінні речі, які займають місця пам'яті, і наскільки це корисно, що, схоже, ухиляється від людей.
Еммет

1
@Emmet - Я не погоджуюся з тим, що є набагато більше, що можна було б перейти до покажчиків WRT, але я читав питання: "without getting them bogged down in the overall concept"як розуміння на високому рівні. І до вашої точки зору: "I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"- ви були б дуже здивовані, скільки людей не розуміють покажчиків навіть на цей рівень
Майк

Чи є якась заслуга в поширенні аналогії автомобільного пальця на людину (одним або декількома пальцями - і генетичну аномалію, яка може дозволити кожному з них вказувати в будь-якому напрямку!) Сіла в одну з машин, вказуючи на інший автомобіль (або нахилившись над вказівкою на пустир поруч із партією як на "неініціалізований вказівник"; або ціла рука викреслила, вказуючи на ряд пробілів, як "фіксований розмір [5] масив покажчиків" або згорнувшись у долоні "нульовим покажчиком" що вказує десь, де відомо, що НІКОЛИ автомобіля немає) ... 8-)
SlySven

ваше пояснення було помітним і хорошим для початківця.
Ятендра Ратхор

9

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

Складність, принаймні, з якою я стикався в минулому і коли я бачив інших, полягає в тому, що управління покажчиками в C / C ++ може бути беззаперечним.


9

Приклад навчального посібника з хорошим набором діаграм чудово допомагає з розумінням покажчиків .

Джоель Спольський робить деякі хороші моменти щодо розуміння покажчиків у своїй статті « Посібник з інтерв'ю» .

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


8

Проблема з покажчиками полягає не в концепції. Це виконання та мова, що стосується мови. Додаткові результати плутанини, коли викладачі припускають, що поняття вказівників складно, і не жаргон, чи перекручені безлад C і C ++ складають цю концепцію. Настільки великі зусилля спрямовані на пояснення концепції (як, наприклад, у прийнятій відповіді на це запитання), і це майже просто витрачається на когось, як я, тому що я вже все це розумію. Це просто пояснення неправильної частини проблеми.

Щоб дати вам уявлення, звідки я родом, я - хтось, хто чудово розуміє покажчики, і можу їх грамотно використовувати в мові асемблера. Тому що в мові асемблера вони не називаються покажчиками. Їх називають адресами. Що стосується програмування та використання покажчиків на С, то я роблю багато помилок і дуже плутаюсь. Я досі цього не розбирав. Дозвольте навести вам приклад.

Коли api каже:

int doIt(char *buffer )
//*buffer is a pointer to the buffer

чого воно хоче?

воно може захотіти:

число, що представляє адресу до буфера

(Щоб сказати це, я кажу doIt(mybuffer), чи doIt(*myBuffer)?)

число, що представляє адресу до адреси до буфера

(це doIt(&mybuffer)чи doIt(mybuffer)чи doIt(*mybuffer)?)

число, що представляє адресу до адреси до адреси до буфера

(можливо, це doIt(&mybuffer). чи це doIt(&&mybuffer)? чи навіть doIt(&&&mybuffer))

і так далі, а мова, що займається цим, не робить це зрозумілим, оскільки вона включає слова "вказівник" та "посилання", які не мають для мене стільки значущості та ясності, як "x має адресу у" та " для цієї функції потрібна адреса у ". Відповідь додатково залежить від того, з чого саме починається хек "mybuffer" і що з цим має намір робити. Мова не підтримує рівні гніздування, які зустрічаються на практиці. Як і коли мені потрібно вручити "вказівник" на функцію, яка створює новий буфер, і він змінює вказівник на точку на нове місце буфера. Чи дійсно хоче вказівник чи вказівник на вказівник, тож він знає, куди звернутися, щоб змінити вміст вказівника. Більшу частину часу я просто повинен здогадуватися, що означає "

"Покажчик" просто занадто перевантажений. Чи вказівник адресу до значення? або це змінна, яка містить адресу до значення. Коли функція хоче вказівника, чи хоче вона адресу, яку містить змінна вказівник, чи вона хоче адресу змінної вказівника? Я збентежений.


Я бачив, як це пояснювалося так: якщо ви бачите декларацію вказівника на кшталт double *(*(*fn)(int))(char), результатом оцінювання *(*(*fn)(42))('x')буде а double. Ви можете зняти шари оцінки, щоб зрозуміти, якими мають бути проміжні типи.
Бернд Джендрісек

@BerndJendrissek Не впевнений, що я дотримуюся. Який результат оцінювання (*(*fn)(42))('x') тоді?
Бретон

ви отримуєте річ (назвемо її x), де, якщо оціните *x, ви отримуєте дубль.
Бернд Джендрісек

@BerndJendrissek Це має пояснити щось про покажчики? Я не розумію. Який твій погляд? Я зняв шар і не отримав нової інформації про проміжні типи. Що це пояснює про те, яку конкретну функцію збирається прийняти? Що це стосується будь-чого?
Бретон

Може бути, повідомлення в цьому поясненні (і це не моє, якщо б я міг знайти , де я вперше побачив це), щоб думати про це менше в плані того , що fn є і більш з точки зору того, що ви можете зробити зfn
Бернд Jendrissek

8

Я думаю, що головним бар’єром на шляху розуміння покажчиків є погані викладачі.

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

І звичайно, що їх важко зрозуміти, небезпечно і напівмагічно.

Жодне з яких не відповідає дійсності. Покажчики насправді є досить простими поняттями, якщо ви дотримуєтесь того, що мова C ++ має про них сказати, і не переймайте їх атрибутами, які, як правило, "працюють на практиці, але, тим не менш, не гарантуються мовою" , і тому не є частиною фактичної концепції вказівника.

Я намагався написати пояснення цьому кілька місяців тому в цій публікації в блозі - сподіваюся, це комусь допоможе.

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


Зрештою, нульовий покажчик не вказує на нульову адресу в пам’яті, навіть якщо це значення «C» дорівнює нулю. Це абсолютно окрема концепція, і якщо ви попрацюєте з нею неправильно, ви можете вирішити (і перенаправити) те, чого ви не очікували. У деяких випадках це може бути навіть нульова адреса в пам'яті (особливо зараз, коли адресний простір зазвичай рівний), але в інших може бути опущено як невизначене поведінка оптимізуючим компілятором або отримати доступ до якоїсь іншої частини пам'яті, пов'язаної з "нулем" для даного типу вказівника. Настає веселість.
Луань

Не обов'язково. Потрібно вміти моделювати комп’ютер у голові, щоб покажчики мали сенс (а також налагоджувати інші програми). Не всі можуть це зробити.
Thorbjørn Ravn Andersen

5

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

Коли ви вперше бачите вказівник, ви насправді не отримуєте того, що знаходиться в цій пам'яті. "Що ви маєте на увазі, він містить адресу ?"

Я не погоджуюся з думкою, що "ти їх або отримуєш, або не знаєш".

Їх стає легше зрозуміти, коли ви починаєте знаходити для них реальні можливості (наприклад, не передаючи великі структури функціям).


5

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

   int *mypointer;

Ви вперше дізнаєтесь, що крайня ліва частина створення змінної визначає тип змінної. Оголошення вказівника не працює так у C та C ++. Натомість вони кажуть, що змінна вказує на тип зліва. У цьому випадку: *міпоінтеркатор вказує на int.

Я не повністю зрозумів покажчики, поки не спробував їх використовувати в C # (з небезпечним), вони працюють точно так само, але з логічним і послідовним синтаксисом. Вказівник - сам тип. Тут mypointer - вказівник на int.

  int* mypointer;

Навіть не запускайте мене з функціональних покажчиків ...


2
Насправді, обидва ваші фрагменти є дійсними C. Це дуже багато років стилю С, що перший зустрічається частіше. Другий, наприклад, трохи частіше зустрічається у C ++.
RBerteig

1
Другий фрагмент насправді не справляється зі складнішими деклараціями. І синтаксис не такий "непослідовний", як тільки ви зрозумієте, що права частина декларації вказівника показує вам, що потрібно зробити вказівнику, щоб отримати щось, тип якого - специфікатор атомного типу зліва.
Бернд Джендрісек

2
int *p;має просте значення: *pце ціле число. int *p, **ppозначає: *pі **ppє цілими числами.
Майлз Рут

@MilesRout: Але саме в цьому проблема. *pі **ppє НЕ цілими числами, тому що ви ніколи не ініціалізовані pабо ppабо *ppв точку до чого - або. Я розумію, чому деякі люди вважають за краще дотримуватися граматики цього, особливо тому, що деякі крайні випадки та складні випадки вимагають від вас цього (хоча, все-таки, ви можете тривільно обійти це питання у всіх випадках, про які я знаю) ... але я не думаю, що ці випадки важливіші, ніж те, що викладання правильного вирівнювання вводить в оману для новачків. Не кажучи вже про якусь потворну! :)
Гонки легкості на орбіті

@LightnessRacesinOrbit Викладання правильного вибору далеко не вводить в оману. Це єдиний правильний спосіб її навчання. НЕ викладаючи це вводить в оману.
Майлз Рут

5

Я міг би працювати з покажчиками, коли знав лише C ++. Я якось знав, що робити в деяких випадках, а що не робити з спроб / помилок. Але річ, яка дала мені повне розуміння, - це мова монтажу. Якщо ви робите серйозну налагодження рівня інструкцій з написаною вами мовою програми складання, ви повинні мати можливість зрозуміти багато речей.


4

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

Наприклад, слідуючи за пов'язаним списком: 1) почніть з паперу з адресою 2) Перейдіть до адреси на папері 3) Відкрийте поштову скриньку, щоб знайти новий аркуш паперу з наступною адресою на ньому

У лінійному зв’язаному списку остання поштова скринька не містить нічого (кінець списку). У кругопов'язаному списку остання поштова скринька має адресу першої поштової скриньки в ній.

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


Неприємне ускладнення з аналогією номера поштової скриньки полягає в тому, що хоча винайдена Деннісом Річі мова визначає поведінку з точки зору адрес байтів і значень, збережених у цих байтах, мова, визначена стандартом C, пропонує "оптимізувати" реалізацію для використання поведінкової модель, яка є більш складною, але визначає різні аспекти моделі способами, неоднозначними, суперечливими та неповними.
supercat

3

Я думаю, що головна причина, що люди мають проблеми з цим, полягає в тому, що це, як правило, не викладається цікаво та захоплююче. Мені хотілося б, щоб лектор здобув 10 добровольців з натовпу і дав їм лінійку на 1 метр кожен, змусив їх стояти навколо у певній конфігурації та використовувати лінійки, щоб наводити один на одного. Потім покажіть арифметику вказівника, переміщаючи людей навколо (і куди вони вказують своїх лінійок). Це був би простий, але ефективний (і перш за все пам’ятний) спосіб показу концепцій, не надто занурюючись в механіку.

Опинившись на C та C ++, для деяких людей це стає складніше. Я не впевнений, чи це тому, що вони, нарешті, вкладають теорію, що вони не належним чином сприймаються на практиці, або тому, що маніпуляція з покажчиком суттєво важче для цих мов. Я не можу так добре пригадати свій власний перехід, але я знав покажчики в Паскалі, а потім перейшов на С і зовсім загубився.


2

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


2

Я думаю, що це насправді може бути проблемою синтаксису. Синтаксис C / C ++ для покажчиків здається непослідовним і складнішим, ніж це має бути.

За іронією долі, річ, яка насправді допомогла мені зрозуміти покажчики, зустрічалася з концепцією ітератора в програмі c ++ бібліотеці стандартних шаблонів . Іронічно, тому що я можу лише припустити, що ітератори були задумані як узагальнення вказівника.

Іноді ти просто не можеш побачити ліс, поки не навчишся ігнорувати дерева.


Проблема здебільшого в синтаксисі декларації С. Але використання вказівника, звичайно, було б простіше, якби (*p)це було (p->), і, таким чином, у нас було б p->->xзамість неоднозначного*p->x
MSalters

@MSalters О боже, ти жартуєш, правда? Тут немає невідповідностей. a->bпросто означає (*a).b.
Майлз Рут

@Miles: Дійсно, і за цією логікою * p->xкошти , в * ((*a).b)той час як *p -> xкошти (*(*p)) -> x. Змішування операторів префікса та постфікса призводить до неоднозначного розбору.
MSalters

@MSalters немає, оскільки пробіл не має значення. Це як би сказати, що 1+2 * 3повинно бути 9.
Майлз Рут

2

Плутанина походить від безлічі шарів абстракції, змішаних разом у концепції "покажчика". Програмістів не плутають звичайні посилання на Java / Python, але покажчики відрізняються тим, що вони розкривають характеристики основної архітектури пам'яті.

Хороший принцип - чітко розділяти шари абстракції, і покажчики цього не роблять.


1
Цікавим є те, що покажчики С насправді не викривають жодної характерної риси основної архітектури пам'яті. Єдині відмінності між посиланнями Java та вказівниками на C полягають у тому, що у вас можуть бути складні типи, що включають покажчики (наприклад, int *** або char * ( ) (void * )), є арифметика вказівників для масивів та покажчиків на членів структури, наявність void * та подвійність масиву / покажчика. Крім цього, вони працюють точно так само.
jpalecek

Гарна думка. Це робить арифметика вказівника та можливість переповнення буфера - вибивання з абстракції шляхом виривання із відповідної області пам'яті.
Джошуа Фокс

@jpalecek: Досить просто зрозуміти, як працюють покажчики на реалізаціях, які документують їх поведінку з точки зору основної архітектури. Казати - foo[i]означає йти до певного місця, рухатися вперед на певну відстань і бачити, що там є. Що ускладнює речі, це набагато складніший додатковий шар абстракції, який Стандарт додав виключно на користь компілятора, але моделює речі таким чином, що погано підходить для потреб програміста і компілятор потребує так.
supercat

2

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

Тому я кажу, що оперативна пам'ять є масивом (і у вас є лише 10-байт оперативної пам'яті):

unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };

Тоді вказівник на змінну насправді є лише індексом (першого байта) цієї змінної в ОЗП.

Отже, якщо у вас є вказівник / індекс unsigned char index = 2, то значення, очевидно, є третім елементом або числом 4. Вказівник на вказівник - це те, де ви берете це число і використовуєте його як сам індекс, якRAM[RAM[index]] .

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


1

Номер поштової скриньки.

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

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


1

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

Багато колишніх розробників C ++ (які ніколи не розуміли, що ітератори - це сучасний покажчик, перш ніж скинути мову), переходять до C # і все ще вважають, що мають гідних ітераторів.

Хм, проблема полягає в тому, що все те, що є ітераторами, суперечить тому, що платформи виконання (Java / CLR) намагаються досягти: нове, просте, все-на-використання-розробник. Що може бути добре, але вони сказали це один раз у фіолетовій книзі, і вони сказали це ще до і перед C:

Непрямий.

Дуже потужна концепція, але ніколи так, якщо ви робите це до кінця .. Ітератори корисні, оскільки вони допомагають при абстрагуванні алгоритмів, інший приклад. І час компіляції - це місце для алгоритму, дуже простого. Ви знаєте код + дані або іншою мовою C #:

IErumerable + LINQ + Massive Framework = 300МБ покарання за час виконання покарання паршивих, перетягування програм через купу примірників типів ..

"Le Pointer - це дешево".


4
Що це стосується нічого?
Ніл Вільямс

... що ви намагаєтесь сказати, окрім "статичного зв’язку - це найкраща річ" та "я не розумію, як працює щось інше, ніж те, що я раніше дізнався"?
Луань

Луаане, ти не міг знати, чого можна навчитися, розбираючи JIT в 2000 році? Що це закінчується в таблиці стрибків, починаючи з таблиці вказівників, як показано в 2000 році в Інтернеті в ASM, тож не розуміючи нічого іншого, можна отримати інший сенс: уважне читання - важлива навичка, спробуйте ще раз.
rama-jka toti

1

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

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


0

Я не бачу, що так бентежить вказівники. Вони вказують на місце в пам'яті, тобто воно зберігає адресу пам'яті. У C / C ++ ви можете вказати тип, на який вказує вказівник. Наприклад:

int* my_int_pointer;

Каже, що my_int_pointer містить адресу до місця, яке містить int.

Проблема з покажчиками полягає в тому, що вони вказують на місце в пам'яті, тому легко знайти місце в якомусь місці, в якому ви не повинні знаходитись. Як доказ подивіться на численні отвори безпеки в додатках C / C ++ від переповнення буфера (збільшуючи покажчик повз виділену межу).


0

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

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 розповідає про це трохи більш послідовно, ніж я. :-)


-1: Ручки не є покажчиками на покажчики; вони не є покажчиками в жодному сенсі. Не плутайте їх.
Ян Голдбі

"Вони не є вказівниками в будь-якому сенсі" - гм, я прошу різнитися.
SarekOfVulcan

Вказівник - це місце пам'яті. Ручка - це будь-який унікальний ідентифікатор. Це може бути вказівник, але він також може бути індексом масиву або будь-яким іншим з цього приводу. Посилання, яке ви надали, - це лише особливий випадок, коли ручка є вказівником, але це не повинно бути. Дивіться також parashift.com/c++-faq-lite/references.html#faq-8.8
Ян Голдбі

Це посилання не підтримує ваше твердження про те, що вони не є вказівниками в жодному сенсі - "Наприклад, ручки можуть бути Фред **, де вказівники на Фред * вказівники ..." Я не думаю -1 було справедливим.
SarekOfVulcan

0

У кожного початківця C / C ++ є одна і та ж проблема, і ця проблема виникає не тому, що "покажчики важко вивчити", а "хто і як пояснює це". Деякі студенти збирають це усно, деякі візуально, і найкращий спосіб пояснити це - використовувати приклад «тренування» (костюми для словесного та наочного прикладу).

Де "локомотив" - це вказівник, який нічого не може вмістити, а "вагон" - це те, що "локомотив" намагається тягнути (або вказувати). Після цього ви можете класифікувати сам "вагон", чи може він вміщувати тварин, рослин або людей (або їх суміш).

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