Что людям трудно найти в C-указателях? [закрыто]

173

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

Мне любопытно узнать почему. Они никогда не вызывали у меня серьезных проблем (хотя я впервые узнал о них еще в неолите). Чтобы лучше ответить на эти вопросы, я хотел бы знать, что людям трудно.

Итак, если вы боретесь с указателями, или вы недавно, но внезапно «получили», какие аспекты указателей вызвали у вас проблемы?

Paul
источник
54
Снарки, преувеличенные, излишне противоречивые, отвечают зерном правды: они были искалечены - искалечены, скажу я вам - благодаря тому, что сначала я приобрел один из «выразительных» языков высокого уровня. Они должны были начать с программирования на голом металле, как Бог задумал Дэниел Бун!
dmckee --- котенок экс-модератора
3
... и программист будет лучше , потому что она будет развиваться в дискуссию , несмотря на все усилия вам.
dmckee --- котенок экс-модератора
Это аргументация и субъективность, она дает N результатов, мое трудное - твое легкое. Вероятно, это будет работать на программистов, но мы пока не переносим эти вопросы, потому что сайт еще не вышел из бета-версии.
Сэм Шафран
2
@Sam Saffron: Хотя я в целом согласен с тем, что это скорее вопрос программистов. Честно говоря, было бы неплохо, если бы люди хотели пометить «я думаю, что это легко» и «я ненавижу видеть указатели» как спам они есть.
Jkerian
3
Кто-то должен поднять это: «Это как палец, указывающий на луну. Не концентрируйтесь на пальце, иначе вы пропустите всю эту небесную славу» - Брюс Ли
му слишком коротка

Ответы:

86

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

Когда я преподавал, я обнаружил, что следующие пробелы в понимании студентов являются наиболее распространенным источником проблем:

  1. Куча против хранения в стеке. Просто поразительно, сколько людей не понимают этого даже в общем смысле.
  2. Стек кадров. Просто общую концепцию выделенного раздела стека для локальных переменных, а также причину, по которой это «стек» ... такие детали, как сохранение места возврата, детали обработчика исключений и предыдущие регистры, можно безопасно оставить до тех пор, пока кто-нибудь не попытается собрать компилятор
  3. «Память - это память - это память». Приведение просто меняет, какие версии операторов или сколько места отведет компилятор для определенного куска памяти. Вы знаете, что имеете дело с этой проблемой, когда люди говорят о том, «что (примитивная) переменная X на самом деле является».

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

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

jkerian
источник
13
ИМХО, понимание стека и кучи так же не нужно, как детализация процессора низкого уровня. Стек и куча являются деталями реализации. В спецификации ISO C нет ни одного упоминания о слове «стек», как и в K & R.
sigjuice
4
@sigjuice: Ваши возражения не соответствуют сути вопроса и ответа. A) K & R C - это анахронизм. B) ISO C - не единственный язык с указателями, мои пункты 1 и 2 были разработаны на языке, не основанном на C. C) 95% архитектур (не языков) используют кучу. / стек системы, достаточно распространено, что по отношению к ней объясняются исключения. D) Суть вопроса заключалась в том, «почему люди не понимают указатели», а не «как я могу объяснить ISO C»
jkerian
9
@ Джон Маркетти: Тем более ... учитывая, что вопрос был "В чем корень проблемы людей с указателями", я не думаю, что люди, задающие вопросы, связанные с указателями, будут ужасно впечатлены "Ты не понимаешь" Т действительно нужно знать "как ответ. Очевидно, они не согласны. :)
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);

чтобы вызвать это, мне нужен адрес массива указателей на указатели на указатели целых:

foo(&(***ipppArr));

Через шесть месяцев, когда мне придется поддерживать этот код, я потрачу больше времени, пытаясь выяснить, что все это значит, чем переписывать с нуля. (да, возможно, этот синтаксис ошибочен - прошло много времени с тех пор, как я что-то делал на C.

Джефф Кнехт
источник
21
Ваш комментарий к первому, >> * ip = 4; // устанавливает значение ip в '4' << неверно. Должно быть >> // устанавливает значение вещи, на которую указывает ip, значение '4'
aaaa bbbb 26.10.10
8
Наложение слишком большого количества типов друг на друга - плохая идея на любом языке. Вы можете найти запись "foo (& (*** ipppArr));" странно в C, но писать что-то вроде "std :: map <std :: pair <int, int>, std :: pair <std :: vector <int>, std :: tuple <int, double, std :: list <int >>>> "в C ++ также очень сложен. Это не означает, что указатели в контейнерах C или STL в C ++ являются сложными. Это просто означает, что вы должны использовать лучшие определения типов, чтобы сделать его понятным для читателя вашего кода.
Патрик
20
Я искренне не могу поверить, что неправильное понимание синтаксиса является самым решительным ответом. Это самая простая часть о указателях.
Джейсон
4
Даже читая этот ответ, я испытывал желание взять лист бумаги и нарисовать картинку. В Си я всегда рисовал картинки.
Майкл Пасха
19
@ Джейсон Какая объективная мера сложности существует помимо того, что большинство людей считают трудным?
Руперт Мэдден-Эбботт
52

Правильное понимание указателей требует знания архитектуры базовой машины.

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

Роберт Харви
источник
18
@dmckee: Ну, я не прав? Сколько Java-программистов может иметь дело с segfault?
Роберт Харви
5
Являются ли segfaults как-то связаны со сдвигом палки? - Программист Java
Том Андерсон
6
@ Роберт: это было подлинное дополнение. Это сложная тема для обсуждения, не затрагивающая чувства людей. И я боюсь, что мой комментарий ускорил тот самый конфликт, который, как я думал, вам удалось избежать. Меа купла.
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 здесь ... по крайней мере, во второй части.
детально
Указатели на указатели также применимы к C: p
Дэвид Титаренко
1
Хех? Я вижу только указатели на указатели при передаче вокруг массивов - ваш второй пример не совсем применим к большинству приличного кода на Си. Кроме того, вы перетаскиваете C в беспорядок в C ++ - ссылки в C не существуют.
new123456
Вы должны иметь дело с указателями на указатели во многих законных случаях. Например, при работе с указателем на функцию, указывающую на функцию, которая возвращает указатель. Другой пример: структура, которая может содержать переменное число других структур. И многое другое ...
Дэвид Титаренко
2
«Указатели на ссылки незаконны в C» - больше похоже на «отсутствует» :)
Кос
29

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

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

Джон Боде
источник
3
Да. 'int foo = 5; int * pfoo = & foo; Видите, как это полезно? Хорошо, двигаясь дальше ... «Я действительно не использовал указатели, пока не написал свою собственную библиотеку двойных списков.
Джон Лопес
2
+1. Я использую для обучения студентов CS100, и многие из их проблем были решены путем простого наведения указателей.
Бензадо
1
+1 за исторический контекст. Начав задолго до этого, мне это никогда не приходило в голову.
Луми
26

Есть замечательная статья, подтверждающая, что на сайте Джоэла Спольски сложно найти указатели - Опасности JavaSchools .

[Отказ от ответственности - я не ненавистник Java как таковой .]

Стив Таунсенд
источник
2
@ Джейсон - это правда, но не отрицает аргумент.
Стив Таунсенд
4
Спольски не говорит, что JavaSchools - причина, по которой людям трудно найти указатели. Он говорит, что они приводят к неграмотным людям со степенью информатики.
Бензадо
1
@benzado - честно, мой краткий пост был бы улучшен, если бы он читал «отличную статью, подтверждающую, что указатели сложны». В этой статье подразумевается, что «получение степени CS в« хорошей школе »» не так хорошо предсказывает успех, как разработчик, как это было раньше, в то время как «понимание указателей» (и рекурсия) все еще остается.
Стив Таунсенд
1
@ Стив Таунсенд: Я думаю, что вы упускаете суть спора мистера Спольски.
Джейсон
2
@Steve Townsend: Мистер Спольски утверждает, что Java-школы воспитывают поколение программистов, которые не знают указателей и рекурсии, а не то, что указатели сложны из-за распространенности Java-школ. Поскольку вы заявили, что «есть отличная статья о том, почему это сложно», и ссылаетесь на эту статью, похоже, у вас последняя интерпретация. Прости меня, если я ошибаюсь.
Джейсон
24

Большинство вещей труднее понять, если вы не основаны на знаниях, которые «под ним». Когда я преподавал CS, стало намного проще, когда я начал учить своих студентов программированию очень простой «машины», имитирующего десятичный компьютер с десятичными кодами операций, чья память состояла из десятичных регистров и десятичных адресов. Они будут вставлять очень короткие программы, например, чтобы добавить ряд чисел, чтобы получить общее количество. Затем они пошагово наблюдали за происходящим. Они могли бы удерживать клавишу «Ввод» и смотреть, как она работает «быстро».

Я уверен, что почти все на SO удивляются, почему это так полезно. Мы забываем, что это было, не зная, как программировать. Игра с таким игрушечным компьютером создает концепции, без которых вы не можете программировать, такие как идея, что вычисления - это пошаговый процесс, использование небольшого количества базовых примитивов для создания программ и концепция памяти. переменные как места, где хранятся числа, в которых адрес или имя переменной отличается от числа, которое она содержит. Существует различие между временем, когда вы входите в программу, и временем, когда она «запускается». Я уподобляюсь тому, чтобы научиться программировать как пересечение ряда «скачков», таких как очень простые программы, затем циклы и подпрограммы, затем массивы, затем последовательный ввод-вывод, затем указатели и структура данных.

Наконец, при переходе к C указатели сбивают с толку, хотя K & R очень хорошо их объяснил. Способ, которым я узнал их в Си, состоял в том, чтобы знать, как читать их - справа налево. Например, когда я вижу int *pв своей голове, я говорю « pуказывает на int». C был придуман как шаг вперед по сравнению с ассемблером, и это то, что мне нравится в нем - оно близко к этой «земле». Указатели, как и все остальное, труднее понять, если у вас нет этого основания.

Майк Данлавей
источник
1
Хороший способ узнать это - запрограммировать 8-битные микроконтроллеры. Их легко понять. Возьмите контроллеры Atmel AVR; они даже поддерживаются gcc.
Ксену
@Xenu: я согласен. Для меня это были Intel 8008 и 8051 :-)
Майк Данлавей
Для меня это был обычный 8-битный компьютер («Может быть») в MIT в смутное время.
QuantumMechanic
Майк - ты должен получить своих учеников CARDIAC :)
QuantumMechanic
1
@Quantum: CARDIAC - хороший, не слышал об этом. «Может быть» - позвольте мне догадаться, было ли это, когда у Суссмана (и других) были люди, читающие книгу Мид-Конуэй и делающие свои собственные чипы LSI? Это было немного позже моего времени там.
Майк Данлавей
17

Я не получил указатели, пока не прочитал описание в K & R. До этого момента указатели не имели смысла. Я прочитал целую кучу вещей, в которых люди говорили: «Не учите указатели, они сбивают с толку и повредят вашу голову, и у вас возникнут аневризмы», поэтому я долго избегал этого и создал этот ненужный вид сложной концепции. ,

В противном случае, в основном то, что я думал, почему на земле вам нужна переменная, через которую вы должны пройти через обручи, чтобы получить значение, и если вы хотите присвоить ей что-то, вы должны были сделать странные вещи, чтобы получить значения для перехода в них. Я думал, что весь смысл переменной заключается в том, чтобы хранить значение, поэтому вопрос о том, почему кто-то хотел усложнить ситуацию, был мне не по карману. «Так что с указателем вы должны использовать *оператор, чтобы получить его значение ??? Что это за глупая переменная?» , Я думал. Бессмысленно, каламбур не предназначен.

Это было сложно, потому что я не понимал, что указатель - это адрес чего-то. Если вы объясните, что это адрес, что-то, что содержит адрес для чего-то другого, и что вы можете манипулировать этим адресом, чтобы делать полезные вещи, я думаю, это может устранить путаницу.

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

Дж. Полфер
источник
4
Если у вас ограниченные ресурсы для работы (ОЗУ, ПЗУ, ЦП), например во встроенных приложениях, указатели быстро приобретают гораздо больший смысл.
Ник Т
+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));

Ответ (Спойлер Alert!) ВСЕ из них. «destination», «& destination» и «& destination [0]» имеют одинаковое значение. «& destination» - это тип, отличный от двух других, но он все еще имеет то же значение. То же самое касается перестановок «источника».

Кроме того, я лично предпочитаю первый вариант.

Эндрю Коттрелл
источник
Я предпочитаю и первую версию (меньше знаков препинания).
sigjuice
++ Я тоже, но вам действительно нужно быть осторожным sizeof(source), потому что если sourceуказатель, sizeofон не будет тем, что вы хотите. Я иногда (не всегда) пишу sizeof(source[0]) * number_of_elements_of_sourceпросто, чтобы держаться подальше от этой ошибки.
Майк Данлавей
destination, & destination, & destination [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);

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

nategoose
источник
1
+1. Тщательно и проницательно. Я забыл про scanf, но теперь, когда вы подняли его, я помню, что у меня было такое же замешательство.
Джо Уайт
6

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

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

  2. Одномерное динамическое распределение памяти. Выяснение 1-D распределения памяти заставило меня понять концепцию указателя.

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

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

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

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

  1. Как и почему можно использовать указатель.
  2. Чем они отличаются и все же похожи на массивы.
  3. Понимание, где хранится информация указателя.
  4. Понимание того, на что и на что указывает указатель.
Спарки
источник
Эй, не могли бы вы указать мне статью / книгу / ваш рисунок / рисунок / что-нибудь, где я мог бы научиться таким же образом, который вы описали в своем ответе? Я твердо верю, что это путь к хорошей учебе, в основном к чему угодно. Глубокое понимание и хорошие ментальные модели
Александр Старбак
1
@AlexStarbuck - я не хочу, чтобы это звучало легкомысленно, но научный метод - отличный инструмент. Нарисуйте себе картину того, что, по вашему мнению, может произойти для конкретного сценария. Запрограммируйте что-нибудь, чтобы проверить это, и проанализируйте, что вы получили. Это соответствует тому, что вы ожидаете? Если нет, определите, чем отличается? Повторите при необходимости, постепенно увеличивая сложность, чтобы проверить и ваше понимание, и умственные модели.
Sparky
6

У меня был «момент указателя», работавший над некоторыми телефонными программами на C. Мне пришлось написать эмулятор обмена AXE10, используя анализатор протокола, который понимал только классический C. Все зависело от знания указателей. Я попытался написать свой код без них (эй, я был «предварительным указателем», немного ослабил меня) и потерпел неудачу.

Ключ к их пониманию, для меня, был оператор & (адрес). Когда я понял, что это &iозначает «адрес i», то понимание, что *i«содержание адреса, на который указывает i», появилось немного позже. Всякий раз, когда я писал или читал свой код, я всегда повторял, что означало «&» и что означало «*», и в конце концов я стал использовать их интуитивно.

К своему стыду, я был вынужден перейти на VB, а затем на Java, так что мои знания указателей не такие острые, как это было раньше, но я рад, что я «пост-указатель». Не проси меня использовать библиотеку, которая требует от меня понимания * * p, хотя.

Гэри Роу
источник
Если &iэто адрес и *iсодержание, то что i?
Томас Але
2
Я как бы перегружаю использование i. Для произвольной переменной i, & i означает «адрес» i, i сам по себе означает «содержимое & i», а * i означает «обрабатывать содержимое & i как адрес, перейти к этому адресу и передать обратно содержание».
Гари Роу
5

Основная проблема с указателями, по крайней мере для меня, заключается в том, что я не начинал с C. Я начал с Java. Понятие указателей было по-настоящему чуждым до тех пор, пока в колледже не было пары уроков, где я должен был знать C. Итак, я научил себя основам языка C и тому, как использовать указатели в их самом базовом смысле. Даже тогда, каждый раз, когда я читаю код на C, мне приходится искать синтаксис указателя.

Так что в моем очень ограниченном опыте (1 год в реальном мире + 4 в колледже) указатели смущают меня, потому что мне никогда не приходилось использовать его в чем-то другом, кроме школьных занятий. И я могу сочувствовать студентам, начинающим CS с JAVA вместо C или C ++. Как вы сказали, вы изучали указатели в эпоху неолита и, вероятно, используете его с тех пор. Для нас, новичков, понятие выделения памяти и выполнения арифметики указателей действительно чуждо, потому что все эти языки абстрагировали его.

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

shoebox639
источник
4
Честно говоря, учитывая, что у вас все еще есть проблемы с указателями, я не уверен, что ваш опыт работы в Корнелле существенно противоречит статье Джоэла. Очевидно, что ваш мозг подключен к Java-мышлению, чтобы понять его точку зрения.
jkerian
5
Wat? Ссылки в 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
eisbaw
источник
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... это может быть тем, что смущает людей.

detly
источник
4

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

ИЛИ

Указатели - это как езда на велосипеде, как только вы поймете, как с ними работать, вы не забудете об этом.

В целом, трудно понять или нет, вся идея указателя является ОЧЕНЬ образовательной, и я считаю, что это должен понимать каждый программист, независимо от того, программирует он на языке с указателями или нет.

Никола Йовчев
источник
3

Указатели сложны из-за косвенности.

Ясон
источник
«Говорят, что в компьютерной науке нет проблемы, которая не может быть решена еще одним уровнем косвенности» (хотя понятия не имею, кто это сказал первым)
Архетип Павел
Это похоже на магию, где неправильное направление - это то, что смущает людей (но совершенно потрясающе)
Ник Т
3

Указатели (наряду с некоторыми другими аспектами низкоуровневой работы) требуют от пользователя убрать магию.

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

Пол Натан
источник
3

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

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

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

Еще одна важная вещь, которую нужно помнить о указателях, это то, что они опасны.C является языком мастера программирования. Предполагается, что вы знаете, какого черта вы делаете, и тем самым дает вам возможность действительно все испортить. Хотя некоторые типы программ все еще должны быть написаны на C, большинство программ этого не делают, и если у вас есть язык, который обеспечивает лучшую абстракцию для различия между объектом и его дескриптором, то я предлагаю вам использовать его.

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

Samo
источник
3

Я думаю, что одна из причин, по которой указатели C трудны, состоит в том, что они объединяют несколько концепций, которые на самом деле не эквивалентны; тем не менее, поскольку все они реализованы с использованием указателей, людям может быть трудно распутать концепции.

В C указатели используются для других вещей:

  • Определить рекурсивные структуры данных

В C вы определяете связанный список целых чисел, как это:

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

Указатель существует только потому, что это единственный способ определить рекурсивную структуру данных в C, когда концепция действительно не имеет ничего общего с такими низкоуровневыми деталями, как адреса памяти. Рассмотрим следующий эквивалент в 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*. И все это только для объявления функции, которая перебирает список, который может содержать любые вещи, применяя функцию к каждому члену. Сравните это с тем, как mapобъявлено в Haskell:

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

Это гораздо проще: mapэто функция, которая берет функцию, которая преобразует a aв a b, и применяет ее к списку a's, чтобы получить список b' s '. Как и в функции C g_list_foreach, mapне нужно ничего знать в своем определении о типах, к которым она будет применяться.

Подводить итоги:

Я думаю, что указатели на C были бы намного менее запутанными, если бы люди сначала узнали о рекурсивных структурах данных, итераторах, полиморфизме и т. Д. Как об отдельных концепциях, а затем узнали, как указатели можно использовать для реализации этих идей в C , вместо того, чтобы смешивать все эти понятия объединяются в единый предмет «указатели».

оборота gcbenison
источник
Неправильное использование c != NULLв вашем примере "Hello world" ... вы имеете в виду *c != '\0'.
Олаф Сейбер
2

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

Но если человек поначалу знает языки высокого уровня (в этом нет ничего плохого - плотник использует топор. Человек, которому нужно разделить атом, использует что-то другое. Нам нужны люди, которые являются плотниками, и у нас есть люди, которые изучают атомы) и этому человеку, который знает язык высокого уровня, дается 2-минутное введение в указатели, а затем трудно ожидать, что он поймет арифметику указателей, указатели на указатели, массив указателей на строки переменного размера, массив массивов символов и т. д. Твердый фундамент низкого уровня может очень помочь.

nonopolarity
источник
2
Указатели Groking не требуют понимания машинного кода или сборки.
Джейсон
Требуется, нет. Но люди, которые понимают ассемблер, скорее всего, найдут указатели намного, намного проще, поскольку они уже установили большинство (если не все) необходимых умственных связей.
cHao
2

Проблема, с которой я всегда сталкивался (прежде всего, самоучка), - это «когда» использовать указатель. Я могу обернуть голову вокруг синтаксиса для создания указателя, но мне нужно знать, при каких обстоятельствах указатель должен использоваться.

Я единственный с таким мышлением? ;-)

Larry
источник
Я понимаю. Мой ответ вроде как с этим связан.
Дж. Полфер
2

Когда-то давно ... У нас было 8 битных микропроцессоров, и каждый писал на ассемблере. Большинство процессоров включало некоторый тип косвенной адресации, используемый для таблиц переходов и ядер. Когда появились языки более высокого уровня, мы добавили тонкий слой абстракции и назвали их указателями. С годами мы все больше отдаляемся от аппаратного обеспечения. Это не обязательно плохо. Они называются языками более высокого уровня по причине. Чем больше я могу сосредоточиться на том, что я хочу сделать, а не на деталях того, как это делается, тем лучше.

Джим С
источник
2

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

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

axilmar
источник
2

Недавно у меня только что был момент щелчка указателя, и я был удивлен, что нашел его запутанным. Больше того, что все так много говорили об этом, что я предположил, что происходит какая-то темная магия.

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

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

По крайней мере, так я сейчас вижу!

Крис Барри
источник
После этого вы могли бы определить фиксированный объем памяти для хранения изображений / аудио / видео, скажем, на устройстве с ограниченной памятью, но тогда вам придется иметь дело с каким-то решением потоковой передачи в и из памяти.
Крис Барри
1

Выступая как новичок C ++ здесь:

Система указателей заняла у меня некоторое время, чтобы переварить не обязательно из-за концепции, но из-за синтаксиса C ++ относительно Java. Несколько вещей, которые я нашел запутанными:

(1) Объявление переменной:

A a(1);

против

A a = A(1);

против

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

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

Это подводит итог некоторых основных разочарований, которые у меня были с C / C ++ и его указателями, которые, по мнению IMO, сильно усугубляются тем фактом, что C / C ++ имеет все эти специфичные для языка причуды.

Некоторые новички
источник
C ++ 2011 несколько улучшил ситуацию с объявлениями переменных.
gnasher729
0

Я лично не понимал указатель даже после окончания учебы и после моей первой работы. Единственное, что я знал, это то, что вам это нужно для связанного списка, двоичных деревьев и для передачи массивов в функции. Такая была ситуация даже на моей первой работе. Только когда я начал давать интервью, я понял, что концепция указателя глубока и имеет огромное применение и потенциал. Затем я начал читать K & R и писать собственную тестовую программу. Вся моя цель была ориентирована на работу.
В это время я обнаружил, что указатели действительно не плохие и не сложные, если их научить хорошим способом. К сожалению, когда я изучаю C на выпускном, наш учитель не знал о указателе, и даже в заданиях использовалось меньше указателей. На выпускном уровне использование указателя действительно только для создания бинарных деревьев и связанного списка. Мысль о том, что вам не нужно правильное понимание указателей, чтобы работать с ними, убивает идею их изучения.

Маной Р
источник
0

Указатели ... ха ... все, что касается указателя в моей голове, это то, что он дает адрес памяти, в котором находятся фактические значения независимо от его ссылки ... так что никакой магии в этом нет. как работают указатели .. давай, ребята ... даже в Java все ссылки ...

Касс
источник
0

Основная проблема, люди не понимают, зачем им указатели. Потому что они не имеют четкого представления о стеке и куче. Хорошо начать с 16-битного ассемблера для x86 с крошечным режимом памяти. Это помогло многим людям понять стек, кучу и «адрес». И байт :) Современные программисты иногда не могут сказать вам, сколько байтов вам нужно для адресации 32-битного пространства. Как они могут получить представление о указателях?

Второй момент - это обозначение: вы объявляете указатель как *, вы получаете адрес как &, и некоторым это непросто понять.

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

user996142
источник