Що людям важко вказувати на покажчики С? [зачинено]


173

З кількості питань, розміщених тут, зрозуміло, що люди мають деякі досить фундаментальні проблеми, коли вони обертаються навколо вказівників та арифметики вказівника.

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

Отже, якщо ви боретеся з покажчиками, або ви нещодавно були, але раптом "отримали це", які аспекти покажчиків викликали у вас проблеми?


54
Snarky, завищений, непотрібний суперечливий, відповісти зерном істини: Вони були каліками --- поранені психічно, я кажу 'я ---, коли я заздалегідь заробив одну з них "виразних" мов високого рівня. Вони повинні розпочатися з програмування на голий метал, як Бог данило Буун задумав!
dmckee --- кошеня колишнього модератора

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

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

2
@Sam Saffron: Хоча я, як правило, погоджуюся, що це скоріше питання програмістів. Типово запитання типу, якщо чесно, не було б погано, якби люди були готові позначити "Я думаю, що це легко" і "Я ненавиджу бачити покажчики" як спам вони є.
jkerian

3
Хтось повинен піднести це: "Це як палець, що вказує на місяць. Не концентруйтесь на пальці, інакше ви пропустите всю ту небесну славу" - Брюс Лі
му занадто короткий

Відповіді:


86

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

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

  1. Зберігання Heap vs Stack. Просто приголомшливо, скільки людей цього не розуміють, навіть у загальному сенсі.
  2. Стек рамки. Просто загальна концепція виділеного розділу стека для локальних змінних разом із причиною, що це "стек" ... такі деталі, як зберігання місця повернення, деталі обробника винятків та попередні регістри, можна безпечно залишити, поки хтось не спробує побудувати компілятор.
  3. "Пам'ять - це пам'ять - це пам'ять". Лише змінюється, які версії операторів або скільки місця надає компілятор для певного фрагмента пам'яті. Ви знаєте, що маєте справу з цією проблемою, коли люди говорять про те, "що (примітивна) змінна X насправді є".

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

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


13
IMHO, розуміння стека та купи так само непотрібні, як і деталі процесора низького рівня. Стек і купа, як відомо, є деталями реалізації. Специфікація ISO C не містить жодної згадки про слово "стек", а також K&R.
sigjuice

4
@sigjuice: Ваші заперечення пропускають суть питання і відповіді. A) K&R C - анахронізм B) ISO C - не єдина мова з покажчиками, мої пункти 1 і 2 були розроблені проти мови, що не базується на C; 95% архітектур (не мов) там використовують купу / stack system, досить поширено, що винятки пояснюються відносно неї. D) Суть питання полягала в тому, "чому люди не розуміють покажчики", а не "як мені пояснити ISO C"
jkerian

9
@John Marchetti: Тим більше ... враховуючи, що питання було "Яка корінна проблема проблеми людей з покажчиками", я не думаю, що люди, які задають питання, пов'язані з вказівниками, будуть дуже вражені "You don" t насправді потрібно знати "як відповідь. Очевидно, вони не згодні. :)
jkerian

3
@jkerian Це може бути застарілим, але три-чотири сторінки K&R, що пояснюють покажчики, роблять це без необхідності в деталях реалізації. Знання деталей реалізації корисне з різних причин, але ІМХО не повинно бути необхідною умовою для розуміння ключових конструкцій мови.
sigjuice

3
"Взагалі допомогло надання чітких вигаданих адрес у різних місцях." -> +1
fredoverflow

146

Коли я вперше почав працювати з ними, найбільша проблема у мене була синтаксисом.

int* ip;
int * ip;
int *ip;

всі однакові.

але:

int* ip1, ip2;  //second one isn't a pointer!
int *ip1, *ip2;

Чому? тому що частина "покажчика" декларації належить до змінної, а не до типу.

А потім для відсилання речі використовується дуже схоже позначення:

*ip = 4;  //sets the value of the thing pointed to by ip to '4'
x = ip;   //hey, that's not '4'!
x = *ip;  //ahh... there's that '4'

За винятком випадків, коли вам потрібно отримати вказівник ... тоді ви використовуєте амперсанд!

int *ip = &x;

Ура за послідовність!

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

void foo(****ipppArr);

щоб викликати це, мені потрібна адреса масиву покажчиків до покажчиків на покажчики ints:

foo(&(***ipppArr));

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


21
Ваш коментар до першого, >> * ip = 4; // встановлює значення ip в '4' << неправильно. Це повинно бути // // встановлює значення речі, на яку вказує ip, на '4'
aaaa bbbb

8
Укладання занадто багато типів один на одного - погана ідея, будь-якою мовою. Ви можете знайти "foo (& (*** ipppArr));" дивно в C, але писати щось на кшталт "std :: map <std :: пара <int, int>, std :: пара <std: вектор <int>, std :: tuple <int, подвійний, std :: список <int> >>> "в C ++ також дуже складний. Це не означає, що покажчики в контейнерах C або STL в C ++ є складними. Це просто означає, що вам потрібно використовувати кращі визначення типів, щоб зробити його зрозумілим для читача вашого коду.
Патрік

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

4
Навіть читаючи цю відповідь, я спокусився дістати аркуш паперу і намалювати малюнок. У С я завжди малював картини.
Майкл Пасха

19
@Jason Яка об'єктивна міра складності є іншою, ніж у більшості людей важко?
Руперт Мадден-Абботт

52

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

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


18
@dmckee: Ну, я помиляюся? Скільки Java-програмістів могли мати справу з сегментом?
Роберт Харві

5
Невже segfaults щось спільне із зміною палиці? - програміст Java
Том Андерсон,

6
@Robert: це було задумано як справжнє доповнення. Це важкий предмет для обговорення, не завдаючи шкоди народам почуттів. І я боюся, що мій коментар спричинив той самий конфлікт, який я думав, що вам вдалося уникнути. Mea cupla.
dmckee --- кошеня колишнього модератора

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

11
@Jason: У C вказівник по суті є адресою пам'яті. Працювати з ними безпечно неможливо без розуміння архітектури машини. Дивіться en.wikipedia.org/wiki/Pointer_(computing) та boredzo.org/pointers/#definition
Роберт Харві

42

Маючи справу з покажчиками, люди, які плутаються, широко знаходяться в одному з двох таборів. Я був (є?) В обох.

array[]натовп

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

  1. Позначення масиву (індексація) з назвою масиву
  2. Позначення масиву (індексація) з назвою вказівника
  3. позначення вказівника (*) з назвою вказівника
  4. позначення вказівника (*) з назвою масиву

 

int vals[5] = {10, 20, 30, 40, 50};
int *ptr;
ptr = vals;

array       element            pointer
notation    number     vals    notation

vals[0]     0          10      *(ptr + 0)
ptr[0]                         *(vals + 0)

vals[1]     1          20      *(ptr + 1)
ptr[1]                         *(vals + 1)

vals[2]     2          30      *(ptr + 2)
ptr[2]                         *(vals + 2)

vals[3]     3          40      *(ptr + 3)
ptr[3]                         *(vals + 3)

vals[4]     4          50      *(ptr + 4)
ptr[4]                         *(vals + 4)

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

reference to a pointerІ pointer to a pointerнатовп

Це чудова стаття, яка пояснює різницю, і яку я буду цитувати і красти якийсь код у :)

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

//function prototype
void func(int*& rpInt); // I mean, seriously, int*& ??

int main()
{
  int nvar=2;
  int* pvar=&nvar;
  func(pvar);
  ....
  return 0;
}

Або, меншою мірою, щось подібне:

//function prototype
void func(int** ppInt);

int main()
{
  int nvar=2;
  int* pvar=&nvar;
  func(&pvar);
  ....
  return 0;
}

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

Тепер ми побачили синтаксис ptr-to-ptr та ref-to-ptr. Чи є переваги одного над іншим? Боюся, ні. Використання одного з обох для деяких програмістів - лише особисті переваги. Деякі, хто використовує ref-to-ptr, кажуть, що синтаксис "чистіший", а хтось, хто використовує ptr-to-ptr, наприклад, синтаксис ptr-to-ptr, стає зрозумілішим для тих, хто читає, що ви робите.

Ця складність та уявна (сміливий начебто) взаємозамінність із посиланнями, що часто є ще одним застереженням покажчиків та помилкою новоприбулих, ускладнює розуміння покажчиків. Важливо також розуміти, заради ЗАВЕРШЕННЯ, в тому , що покажчики на посилання , є незаконними в C і C ++ сплутати причини , які перенесуть вас в lvalue- rvalueсемантику.

Як зазначалося в попередній відповіді, багато разів у вас просто з’являться програми програмування, які вважають, що вони розумні в застосуванні, ******awesome_var->lol_im_so_clever()і більшість з нас, мабуть, винні писати такі злодіяння часом, але це просто не гарний код, і це, звичайно, не доцільно .

Ну ця відповідь виявилася довшою, ніж я сподівався ...


5
Я думаю, ви, можливо, дали відповідь C ++ на питання C тут ... принаймні другу частину.
detly

Покажчики на покажчики також стосуються C: p
Девід Тітаренко,

1
Ага? Я бачу вказівники на покажчики лише при проходженні масивів - ваш другий приклад насправді не застосовується до найбільш пристойного коду С. Крім того, ви перетягуєте C у безлад C ++ - посилання на C не існує.
new123456

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

2
"покажчики на посилання є незаконними на С" - більше схожі на "відсутні" :)
Кос

29

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

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


3
Так. 'int foo = 5; int * pfoo = & foo; Подивіться, наскільки це корисно? Гаразд, рухаюся по ... "Я не використовував вказівників, поки не написав власну бібліотеку подвійних списків.
Джон Лопес

2
+1. Я використовую для репетиторів студентів CS100, і так багато їхніх проблем були вирішені просто шляхом переходу покажчиків зрозумілим способом.
бензадо

1
+1 для історичного контексту. Починаючи довго після цього часу, мені цього ніколи не приходило в голову.
Лумі

26

Існує чудова стаття, яка підтверджує те, що покажчики важкі на сайті Джоела Спольського - «Небезпеки JavaSchools» .

[Відмова від відповідальності - я не Java-ненависник сам по собі .]


2
@Jason - це правда, але аргумент не заперечує.
Стів Таунсенд

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

1
@benzado - справедливий момент - мій короткий пост буде покращено, якби він прочитав «чудову статтю, яка підтримує думку про те, що покажчики важкі». З цієї статті випливає, що "наявність ступеня CS у" хорошої школи "" не є таким хорошим провісником успіху, як розробник, як це було раніше, тоді як "розуміє покажчики" (та рекурсії) все ще є.
Стів Таунсенд

1
@Steve Townsend: Я думаю, ви пропускаєте суть аргументу пана Спольського.
Ясон

2
@Steve Townsend: Пан Спольський стверджує, що школи Java виховують покоління програмістів, які не знають покажчиків та рекурсії, а не те, що покажчики важкі через поширеність шкіл Java. Як ви заявили, "є чудова стаття про те, чому це важко" і пов'язана з цією статтею, здається, у вас є остання інтерпретація. Пробачте, якщо я помиляюся.
Ясон

24

Більшість речей важче зрозуміти, якщо ти не ґрунтується на знаннях, що знаходяться «під ним». Коли я викладав CS, це стало набагато простіше, коли я запустив своїх учнів з програмування дуже простої "машини", імітованого десятковим комп'ютером з десятковими кодами, пам'ять яких складалася з десяткових регістрів і десяткових адрес. Вони вкладали б дуже короткі програми, наприклад, щоб додати серію чисел, щоб отримати загальну суму. Тоді вони зробили б один крок, щоб подивитися, що відбувається. Вони могли утримувати клавішу "enter" і спостерігати, як вона працює "швидко".

Я впевнений, що майже всі в ТАК задаються питанням, чому корисно ставати таким базовим. Ми забуваємо, як це було, не знаючи, як програмувати. Гра з таким іграшковим комп’ютером створює поняття, без яких не можна програмувати, наприклад, ідеї про те, що обчислення є поетапним процесом, використовуючи невелику кількість основних примітивів для створення програм та концепцію пам'яті змінні як місця, де зберігаються номери, у яких адреса чи ім’я змінної відрізняються від числа, яке вона містить. Існує різниця між часом, коли ви входите в програму, і часом, в який вона "працює". Мені подобається навчатись програмувати як перетинання низки "ударів зі швидкістю", таких як дуже прості програми, потім петлі та підпрограми, потім масиви, послідовні введення / виведення, потім вказівники та структура даних.

Нарешті, потрапляючи на C, вказівники плутаються, хоча K&R дуже добре спрацював із їх поясненням. Те, як я навчився їх на С, - це знати, як їх читати - справа наліво. Як коли я бачу int *pв голові, я кажу " pвказує на int". C був винайдений як один крок у порівнянні з мовою складання, і саме це мені подобається - це близько до цього "ґрунту". Покажчики, як і все інше, важче зрозуміти, якщо у вас немає цього заземлення.


1
Хороший спосіб дізнатися це - запрограмувати 8-бітні мікроконтролери. Їх легко зрозуміти. Візьміть контролери AVR Atmel; їх навіть підтримує gcc.
Ксена

@Xenu: Я згоден. Для мене це були Intel 8008 та 8051 :-)
Майк Данлаве

Для мене це був нестандартний 8-бітний комп'ютер ("Можливо") на MIT у тьмяних смугах часу.
QuantumMechanic

Майк - ти повинен отримати своїх студентів КАРДІАК :)
QuantumMechanic

1
@Quantum: CARDIAC - хороший, про це не чув. "Можливо", як я здогадуюсь, було те, що коли у Суссмана (та ін.) Люди читали книгу "Мід-Конвей" та створювали власні фішки LSI? Це було трохи після мого часу там.
Майк Данлаве

17

Я не отримав покажчиків, поки не прочитав опис у K&R. До цього моменту покажчики не мали сенсу. Я читав цілу купу речей, де люди говорили: "Не вивчайте вказівників, вони плутають і зашкодять вам голову і дадуть вам аневризми", тому я надовго відхилився від цього і створив це непотрібне повітря важкої концепції .

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

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

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


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

+1 за коментар Ніка - особливо для передачі структур.
new123456

12

Ось приклад вказівника / масиву, який дав мені паузу. Припустимо, у вас є два масиви:

uint8_t source[16] = { /* some initialization values here */ };
uint8_t destination[16];

І ваша мета - скопіювати вміст uint8_t з пункту призначення за допомогою memcpy (). Здогадайтесь, хто з наступних досягне цієї мети:

memcpy(destination, source, sizeof(source));
memcpy(&destination, source, sizeof(source));
memcpy(&destination[0], source, sizeof(source));
memcpy(destination, &source, sizeof(source));
memcpy(&destination, &source, sizeof(source));
memcpy(&destination[0], &source, sizeof(source));
memcpy(destination, &source[0], sizeof(source));
memcpy(&destination, &source[0], sizeof(source));
memcpy(&destination[0], &source[0], sizeof(source));

Відповідь (спойлер сповіщення!) - ВСІ з них. "призначення", "пункт призначення" та "& пункт призначення" [0] "мають однакове значення. "& призначення" - це інший тип, ніж два інших, але все одно це значення. Те саме стосується перестановок "джерела".

Як осторонь я особисто віддаю перевагу першій версії.


Я також віддаю перевагу першій версії (менше пунктуації).
sigjuice

++ Так само, як і я, але вам потрібно бути обережним sizeof(source), тому що якщо sourceце вказівник, sizeofвін не буде тим, що ви хочете. Я інколи (не завжди) пишу sizeof(source[0]) * number_of_elements_of_sourceпросто, щоб триматися далеко від цієї помилки.
Майк Данлаве

призначення, і пункт призначення, і пункт призначення [0] зовсім не однакові, але кожен за допомогою іншого механізму буде перетворений на ту саму порожнечу *, коли використовується в memcpy. Однак, використовуючи як аргумент sizeof, ви отримаєте два різні результати, і можливі три різні результати.
gnasher729

Я думав, що потрібна адреса оператора?
MarcusJ

7

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

Перше, що мене бентежило щодо покажчиків під час вивчення C, було просте:

char ch;
char str[100];
scanf("%c %s", &ch, str);

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

Що бентежить в цьому те, що &chнасправді означало, а також чому strце не потрібно.

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

char * x = NULL;
if (y) {
     char z[100];
     x = z;
}

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

Пізніше я дізнався mallocі про new, але вони мені справді здалися магічними генераторами пам’яті. Я нічого не знав про те, як вони можуть працювати.

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

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

Оскільки я писав C ++ для школи на наступний рік-два, я отримав багато досвіду використання покажчиків для структур даних. Тут у мене виник новий набір проблем - змішування покажчиків. У мене було б декілька рівнів покажчиків (таких речей node ***ptr;), як мене. Я би знешкодив вказівник неправильну кількість разів і врешті-решт вдався до з'ясування, скільки *мені потрібно для спроб та помилок.

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

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

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

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

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

int foo(struct frog * f, int x, int y) {
    struct leg * g = f->left_leg;
    struct toe * t = g->big_toe;
    process(t);

так що якщо я накручую тип вказівника, помилка компілятора зрозуміла, в чому проблема. Якби я:

int foo(struct frog * f, int x, int y) {
    process(f->left_leg->big_toe);

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


1
+1. Ретельний і проникливий. Я забув про scanf, але тепер, коли ви його підняли, я пам’ятаю, що був такий самий плутанина.
Джо Уайт

6

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

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

  2. Одновимірне динамічне розподілення пам'яті. Визначення одноразового розподілу пам’яті змусило мене зрозуміти поняття покажчика.

  3. Двовимірне динамічне розподілення пам'яті. З'ясування 2-D розподілу пам’яті підкріпило цю концепцію, але також навчило мене, що сам вказівник вимагає зберігання і його слід враховувати.

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

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

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

  1. Як і навіщо використовувати вказівник.
  2. Чим вони відрізняються і ще схожі на масиви.
  3. Розуміння, де зберігається інформація вказівника.
  4. Розуміння того, на що і де він вказує.

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

1
@AlexStarbuck - Я не маю на увазі, щоб це звучало нескінченно, але науковий метод - чудовий інструмент. Намалюйте собі картину про те, що, на вашу думку, може статися для конкретного сценарію. Програмуйте щось для тестування та проаналізуйте, що у вас є Чи відповідала вона тому, що ви очікували? Якщо ні, визначте, де відрізняється? Повторіть по мірі необхідності, поступово збільшуючи складність, щоб перевірити як ваше розуміння, так і розумові моделі.
Спаркі

6

У мене був "момент покажчика", який працював над деякими програмами телефонії в C. Мені довелося написати обмінний емулятор AXE10, використовуючи аналізатор протоколів, який розумів лише класичний C. Все залежало від знання покажчиків. Я спробував написати свій код без них (ей, я "вказівник" скоротив мене трохи) і не вдався до кінця.

Ключовим моментом для їх розуміння став оператор & (address). Колись я зрозумів, що це &iозначає "адресу я", то розумію це*i означає "зміст адреси, на яке я вказував", прийшло трохи пізніше. Кожного разу, коли я писав чи читав свій код, я завжди повторював, що означає «і», що означає «*», і врешті-решт я приходив їх інтуїтивно використовувати.

На жаль, мене змусили в VB, а потім на Java, тому мої знання вказівника не такі гострі, як колись, але я радий, що я "покажчик". Не вимагайте від мене використовувати бібліотеку, яка вимагає від мене розуміння * * p.


Якщо &iце адреса і *iвміст, що таке i?
Томас Ейл

2
Я якось перевантажую використання i. Для довільної змінної i, & i означає "адреса" i, я самостійно означає "вміст & i", а * i означає "ставитися до вмісту & i як до адреси, перейти до цієї адреси та повернути назад вмісту ".
Гері Роу

5

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

Так що в моєму дуже обмеженому досвіді (1 рік реального світу + 4 в коледжі) вказівники плутають мене, тому що мені ніколи не довелося реально використовувати його в чому-небудь, крім навчальних занять. І я можу співчувати студентам, які зараз починають CS з JAVA замість C або C ++. Як ви говорили, ви вчилися вказівники ще в епоху "неоліту" і, мабуть, використовуєте його ще з того часу. Для нас, більш нових людей, поняття розподілу пам’яті та виконання арифметики вказівника дійсно чуже, тому що всі ці мови абстрагували це.

PS Після прочитання реферату Спольського його опис «JavaSchools» був нічим подібним до того, що я пройшов у коледжі в Корнелі ('05 -'09). Я взяв структури та функціональне програмування (sml), операційні системи (C), алгоритми (ручка та папір), і цілий ряд інших класів, яких не вивчали в Java. Однак усі вступні класи та факультативи були зроблені в java, тому що є цінність у тому, щоб не винаходити колесо, коли ви намагаєтесь зробити щось вище рівня, ніж реалізовувати хештейн з покажчиками.


4
Чесно кажучи, враховуючи, що у вас все ще є труднощі з покажчиками, я не впевнений, що ваш досвід Корнеля суттєво суперечить статті Джоела. Очевидно, що ваш мозок є провідним у Java-мисленні, щоб переконатися.
jkerian

5
Ват? Посилання на Java (або C #, або Python, або приблизно десятки інших мов) - лише покажчики без арифметики. Розуміння покажчиків означає розуміння того, чому void foo(Clazz obj) { obj = new Clazz(); }не працює, а void bar(Clazz obj) { obj.quux = new Quux(); }мутує аргумент ...

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

1
Як це ви пройшли через клас операційних систем в C, не стаючи вільним на C? Жодних злочинів не передбачалося, я просто пам’ятаю, що мені довелося розробити просту операційну систему з нуля. Я, мабуть, тисячі разів використовував покажчики ...
Гравітація

5

Ось без відповіді. Використовуйте cdecl (або c ++ decl), щоб зрозуміти це:

eisbaw@leno:~$ cdecl explain 'int (*(*foo)(const void *))[3]'
declare foo as pointer to function (pointer to const void) returning pointer to array 3 of int

4

Вони додають додатковий вимір коду без істотних змін у синтаксисі. Подумайте над цим:

int a;
a = 5

Там тільки одна річ , щоб зміни: a. Можна писати, a = 6і результати очевидні для більшості людей. Але тепер врахуйте:

int *a;
a = &some_int;

Є дві речі, aякі є актуальними в різний час: фактичне значення a, вказівник та значення "за" вказівником. Ви можете змінити a:

a = &some_other_int;

... і some_intвсе ще десь з тим же значенням. Але ви також можете змінити те, на що вказує:

*a = 6;

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


4

Я програмував на c ++ протягом 2 років, а потім перейшов на Java (5 років) і ніколи не оглядався. Однак, коли нещодавно мені довелося використовувати якісь рідні речі, я зрозумів (з подивом), що я нічого не забув про вказівники і навіть вважаю їх простими у використанні. Це різке протиставлення тому, що я пережив 7 років тому, коли я вперше спробував зрозуміти цю концепцію. Тож, напевно, розуміння та симпатія - це питання зрілості програмування? :)

АБО

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

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


3

Покажчики важкі через непрямість.


"Кажуть, що в інформатиці немає жодної проблеми, яка не може бути вирішена ще одним рівнем непрямості" (хоча ідея, хто це сказав першим, хоча)
Архетип Павло,

Це як магія, де неправильне спрямування - це те, що бентежить людей (але це абсолютно дивовижно)
Nick T

3

Покажчики (поряд з деякими іншими аспектами роботи низького рівня) вимагають від користувача відняти магію.

Більшість програмістів високого рівня люблять магію.


3

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

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

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

Ще одна важлива річ, яку слід пам’ятати про вказівники, це те, що вони небезпечні. C - мова майстра програміста. Це передбачає, що ви знаєте, що чорт ви робите, і тим самим дає вам силу по-справжньому зіпсувати речі. Хоча деякі типи програм все ще потрібно писати на мові C, більшість програм це не так, і якщо у вас є мова, яка забезпечує кращу абстракцію різниці між об'єктом і його ручкою, то я пропоную вам використовувати його.

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


3

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

В C покажчики використовуються для вибору інших речей:

  • Визначте рекурсивні структури даних

У C ви б визначили зв'язаний список цілих чисел, як це:

struct node {
  int value;
  struct node* next;
}

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

data List = List Int List | Null

Досить простий - список або порожній, або сформований із значення та решти списку.

  • Ітерація над рядками та масивами

Ось як можна застосувати функцію fooдо кожного символу рядка на C:

char *c;
for (c = "hello, world!"; *c != '\0'; c++) { foo(c); }

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

  • Досягнення поліморфізму

Ось фактичний підпис функції, знайдений у glib :

typedef struct g_list GList;

void  g_list_foreach    (GList *list,
                 void (*func)(void *data, void *user_data),
                         void* user_data);

Ого! Це досить рот void*s. І все просто оголосити функцію, яка повторюється над списком, який може містити будь-які речі, застосувавши функцію до кожного члена. Порівняйте його з тим, як mapзаявлено в Haskell:

map::(a->b)->[a]->[b]

Це набагато простіше: mapце функція, яка приймає функцію, яка перетворює a aв a b, і застосовує її до списку as, щоб отримати список bs. Як і у функції C g_list_foreach, mapне потрібно нічого знати у своєму власному визначенні про типи, до яких воно буде застосовано.

Підсумовуючи:

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


Неправильне використання c != NULLу вашому прикладі "Здрастуй, світ" ... ти маєш на увазі *c != '\0'.
Олаф Зейберт

2

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

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


2
Виробництво покажчиків не вимагає розуміння машинного коду чи складання.
Jason

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

2

Проблема, яку я завжди мав (насамперед самоук), - це "коли" використовувати вказівник. Я можу обернути голову навколо синтаксису для побудови вказівника, але мені потрібно знати, за яких обставин слід використовувати вказівник.

Хіба я єдиний із таким мисленням? ;-)


Я це розумію. Моя відповідь якось стосується цього.
Дж. Полфер

2

Колись ... У нас було 8 бітових мікропроцесорів і всі писали в зборах. Більшість процесорів включали певний тип непрямої адресації, що використовується для переходів таблиць та ядер. Коли мови вищого рівня прийшли разом, ми додали тонкий шар абстракції і називали їх покажчиками. З роками ми все більше і більше віддаляємось від обладнання. Це не обов’язково погано. Їх називають просто мовами вищого рівня. Чим більше я можу сконцентруватися на тому, що хочу зробити, а не на деталях того, як це робиться, тим краще.


2

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

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


2

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

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

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

Принаймні, так я бачу і зараз!


Для подальшої думки ви можете визначити фіксовану кількість пам’яті для зберігання зображень / аудіо / відео, скажімо, на пристрої з обмеженою пам’яттю, але тоді вам доведеться мати справу з певним потоковим рішенням в пам’яті та поза ним.
Кріс Баррі

1

Виступаючи тут як новачок на C ++:

Система вказівників потребувала певного часу, щоб переварити не обов'язково через концепт, а через синтаксис C ++ щодо Java. Кілька речей, які я заплутав:

(1) Змінна декларація:

A a(1);

vs.

A a = A(1);

vs.

A* a = new A(1); 

і мабуть

A a(); 

є оголошенням функції, а не оголошенням змінної. В інших мовах існує лише один спосіб оголошення змінної.

(2) Амперсанд використовується декількома різними способами. Якщо це

int* i = &a;

то & a - адреса пам'яті.

OTOH, якщо є

void f(int &a) {}

тоді & a - параметр пройденого посилання.

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

(3) Відношення масиву-вказівника

Одне, що дуже важко зрозуміти, - це вказівник

int* i

може бути вказівником на int

int *i = &n; // 

або

може бути масивом до int

int* i = new int[5];

А потім просто зробити речі messier, вказівники та масив не є взаємозамінними у всіх випадках, а покажчики не можуть передаватися як параметри масиву.

Це підсумовує деякі основні фрустрації, які виникли у мене з C / C ++ та його вказівниками, на які IMO, значно ускладнюється тим, що C / C ++ має всі ці мовні особливості.


C ++ 2011 дещо покращив речі, що стосується декларацій змінних.
gnasher729

0

Я особисто не розумів вказівника навіть після закінчення навчання та після моєї першої роботи. Єдине, що я знав, це те, що він вам потрібен для пов'язаного списку, бінарних дерев та для передачі масивів у функції. Така ситуація була навіть на моїй першій роботі. Лише коли я почав давати інтерв'ю, я розумію, що концепція вказівника є глибокою та має величезну користь та потенціал. Потім я почав читати K&R і писати власну тестову програму. Вся моя ціль була орієнтована на роботу.
Цього разу я виявив, що вказівники насправді не погані, ані важкі, якщо їх навчати добре. На жаль, коли я вивчаю С у випускному, викладач не знав вказівника, і навіть у завданнях було використано менше покажчиків. На рівні випускників використання вказівника дійсно лише для створення бінарних дерев та пов'язаного списку. Таке мислення, що вам не потрібно правильно розуміти покажчики, щоб працювати з ними, вбиває ідею навчитися їх.


0

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


0

Основна проблема людей не розуміє, навіщо їм потрібні вказівники. Тому що вони не зрозумілі щодо стека та купи. Добре почати з 16-бітового асемблера для x86 з крихітним режимом пам'яті. Це допомогло багатьом людям зрозуміти стек, купу та "адресу". І байт :) Сучасні програмісти іноді не можуть сказати вам, скільки байтів потрібно для 32-бітного простору. Як вони можуть уявити покажчики?

Другий момент - позначення: ви оголошуєте вказівник як *, ви отримуєте адресу як &, а це зрозуміти для деяких людей непросто.

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

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