Что такого сложного в указателях / рекурсии? [закрыто]


20

В опасностях java-школ Джоэл рассказывает о своем опыте работы в Penn и о трудностях, связанных с ошибками сегментации. Он говорит

[Сегфоуты трудны до тех пор, пока вы] «не сделаете глубокий вдох и действительно попытаетесь заставить свой ум работать на двух разных уровнях абстракции одновременно».

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

По какой-то причине Джоэл считает эти понятия основными для способности программистов абстрагироваться. Я не хочу брать на себя слишком много. Итак, что же такого сложного в указателях / рекурсии? Примеры были бы хорошими.


31
Перестань беспокоиться о том, что Джоэл может думать о тебе. Если вы находите рекурсию легкой, это хорошо. Не все так делают.
FrustratedWithFormsDesigner

6
Рекурсия проста по определению (функция, которая вызывает self), но знание того, когда ее использовать и как заставить ее работать, является сложной частью.
JeffO

9
Подать заявку на работу в Fog Creek и дайте нам знать, как оно идет. Мы все очень заинтересованы в вашей саморекламе.
Джоэл Этертон

4
@ P.Brian.Mackey: Мы не недоразумение. Вопрос на самом деле ничего не задает. Это вопиющая самореклама. Если вы хотите узнать, что Джоэл спрашивает об указателях / рекурсии, спросите его: team@stackoverflow.com
Джоэль Этертон

19
Дубликат этого вопроса ?
Оз

Ответы:


38

Я впервые заметил, что указатели и рекурсия были трудными в колледже. Я прошел несколько типичных курсов первого курса (один был C и Assembler, другой был в Схеме). Оба курса начинались с сотен студентов, многие из которых имели многолетний опыт программирования на уровне средней школы (обычно BASIC и Pascal, в те дни). Но как только указатели были введены в курс C, и рекурсия была введена в курсе Scheme, огромное количество студентов - возможно, даже большинство - были полностью сбиты с толку. Это были дети, которые написали много кода раньше и у них не было никаких проблем, но когда они ударяли по указателям и рекурсии, они также врезались в стену с точки зрения их когнитивных способностей.

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

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

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


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

14
если бы это было естественно, вам бы не пришлось тренироваться. : P
Джоэл Спольски

1
Хорошая мысль :), но разве нам не нужны тренировки по математике, логике, физике и т. Д., Которые в более широком смысле наиболее естественны. Интересно, что немногие программисты имеют какие-либо проблемы с синтаксисом языков, но он полон рекурсии.
Инго

1
В моем университете первый курс начался с функционального программирования и рекурсии почти сразу, задолго до введения мутации и тому подобного. Я обнаружил, что некоторые студенты без опыта понимают рекурсию лучше, чем те, у кого есть опыт. Тем не менее, самая верхняя часть класса была составлена ​​из людей с большим опытом.
Тихон Джелвис

2
Я думаю, что неспособность понять указатели и рекурсию связана с а) общим уровнем IQ и б) плохим математическим образованием.
Quant_dev

23

Рекурсия - это не просто «функция, которая сама себя вызывает». Вы по-настоящему не поймете, почему рекурсия трудна, пока вы не начнете рисовать кадры стека, чтобы выяснить, что пошло не так с вашим анализатором рекурсивного спуска. Часто у вас есть взаимно рекурсивные функции (функция A вызывает функцию B, которая вызывает функцию C, которая может вызывать функцию A). Может быть очень трудно выяснить, что пошло не так, когда вы N стековых фреймов глубоко во взаимно-рекурсивной серии функций.

Что касается указателей, опять же концепция указателей довольно проста: переменная, которая хранит адрес памяти. Но опять же, когда что-то пойдет не так с вашей сложной структурой данных void**указателей, которые указывают на разные узлы, вы поймете, почему это может быть сложно, когда вы пытаетесь выяснить, почему один из ваших указателей указывает на адрес мусора.


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

Взаимная рекурсия между многими функциями по сути такая же, как goto.
звездный синий

2
@starblue, не совсем - так как каждый стек создает новые экземпляры локальных переменных.
Чарльз Сальвиа,

Ты прав, только хвостовая рекурсия такая же как и goto.
Starblue

3
@wnoise int a() { return b(); }может быть рекурсивным, но это зависит от определения b. Так что это не так просто, как кажется ...
альтернатива

14

Java поддерживает указатели (они называются ссылками) и поддерживает рекурсию. Так что на первый взгляд его аргументация кажется бессмысленной.

На самом деле он говорит о способности к отладке. Указатель Java (err, reference) гарантированно указывает на допустимый объект. Указатель переменного тока не И хитрость в программировании на C, при условии, что вы не используете такие инструменты, как valgrind , заключается в том, чтобы выяснить, где именно вы облажали указатель (это редко в точке, найденной в трассировке стека).


5
Указатели сами по себе являются деталью. Использование ссылок в Java не сложнее, чем использование локальных переменных в C. Даже смешивать их так, как это делают реализации на Лиспе (атом может быть целым числом ограниченного размера, или символом, или указателем), не сложно. Становится все труднее, когда язык допускает локальные или ссылочные данные того же рода, с другим синтаксисом, и действительно сложный, когда язык допускает арифметику указателей.
Дэвид Торнли

@ Дэвид - как это связано с моим ответом?
Anon

1
Ваш комментарий о поддерживающих Java указателях.
Дэвид Торнли

«где вы облажались указатель (это редко в точке, найденной в трассировке стека).» Если вам повезло получить трассировку стека.
Омега Центавра

5
Я согласен с Дэвидом Торнли; Java не поддерживает указатели, если я не могу сделать указатель на указатель на указатель на указатель на int. Что, может быть, я полагаю, я мог бы сделать, как 4-5 классов, каждый из которых ссылается на что-то другое, но это действительно указатели или это уродливый обходной путь?
альтернатива

12

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

Точно так же каждый раз, когда кто-то использует функцию Фибоначчи для иллюстрации рекурсии, я хочу кричать. Это плохой пример, потому что итеративная версия не сложна в написании и работает по крайней мере так же хорошо или лучше, чем рекурсивная, и она не дает реального представления о том, почему рекурсивное решение было бы полезным или желательным. Быстрая сортировка, обход дерева и т. Д. гораздо лучшие примеры того, почему и как рекурсия.

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


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

Если у вас есть ссылка на Quicksort, реализованную в Fortran IV, я бы хотел ее увидеть. Не говоря о том, что этого нельзя сделать - на самом деле, я внедрил его в BASIC около 30 лет назад - но мне было бы интересно это увидеть.
Anon

Я никогда не работал в Fortran IV, но я реализовал некоторые рекурсивные алгоритмы в реализации Fortran 77 для VAX / VMS (был крюк, позволяющий сохранить цель goto как переменную особого типа, чтобы вы могли писать GOTO target) , Я думаю, что мы должны были создать наши собственные стеки времени выполнения. Это было достаточно давно, и я не могу вспомнить подробности.
Джон Боде

8

Есть несколько трудностей с указателями:

  1. Псевдоним Возможность изменения значения объекта с использованием разных имен / переменных.
  2. Нелокальность Возможность изменения значения объекта в контексте, отличном от того, в котором оно объявлено (это также происходит с аргументами, передаваемыми по ссылке).
  3. Несоответствие времени жизни Время жизни указателя может отличаться от времени жизни объекта, на который он указывает, и это может привести к неверным ссылкам (SEGFAULTS) или мусору.
  4. Указатель Арифметика . Некоторые языки программирования допускают манипулирование указателями как целыми числами, а это означает, что указатели могут указывать куда угодно (включая самые неожиданные места при наличии ошибки). Чтобы правильно использовать арифметику указателей, программист должен знать размеры памяти объектов, на которые он указывает, и об этом стоит подумать.
  5. Приведение типов Возможность приведения указателя из одного типа в другой позволяет перезаписывать память объекта, отличного от предназначенного.

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

Pair* make_pair(int a, int b)
{
    Pair p;
    p.a = a;
    p.b = b;
    return &p;
}

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

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

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

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

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


Использование этой функции вернет действительный указатель, но переменная находится в области, превышающей область, в которой была вызвана функция, поэтому указатель может (предположим, будет) признан недействительным при использовании malloc.
правостороннее

4
@Radek S: Нет, не будет. Он вернет недопустимый указатель, который в некоторых средах работает некоторое время, пока что-то еще не перезапишет его. (На практике это будет стек, а не куча. malloc()Скорее всего , это сделает любая другая функция.)
wnoise

1
@Radeck В примере функции указатель указывает на память, которая, как гарантирует язык программирования (в данном случае C), будет освобождена после возврата из функции. Таким образом, возвращаемый указатель указывает на мусор . Языки со сборкой мусора поддерживают объект живым, пока на него ссылаются в любом контексте.
Апалала

Кстати, у Rust есть указатели, но без этих проблем. (когда не в небезопасном контексте)
Sarge Borsch

2

Указатели и рекурсия являются двумя отдельными животными, и существуют разные причины, по которым каждый из них считается «трудным».

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

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

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


1

Я даю П. Брайану +1, потому что я чувствую, что он это делает: рекурсия - это настолько фундаментальная концепция, что тому, у кого есть малейшие трудности с этим, лучше подумать о поиске работы в mac donalds, но тогда даже есть рекурсия:

make a burger:
   put a cold burger on the grill
   wait
   flip
   wait
   hand the fried burger over to the service personel
   unless its end of shift: make a burger

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


6
Это отшельник, который, возможно, зацикливается.
Майкл К

6
Извините, зацикливание возможно является хвостовой рекурсией :)
Ingo

3
@Ingo: :) Функциональный фанатик!
Майкл К

1
@ Майкл - да, конечно! Но я думаю, можно утверждать, что рекурсия является более фундаментальной концепцией.
Инго

@ Инго: Вы могли бы, действительно (ваш пример демонстрирует это хорошо). Тем не менее, по какой-то причине людям тяжело с этим в программировании - мы, кажется, хотим этого goto topпо какой-то причине IME.
Майкл К

1

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

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

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

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


1
  DATA    |     CODE
          |
 pointer  |   recursion    SELF REFERENTIAL
----------+---------------------------------
 objects  |   macro        SELF MODIFYING
          |
          |

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

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


-1

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

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

Аналогично с рекурсией. Это может быть очень мощным способом организации хитрых вещей - путем помещения хитрости в скрытую структуру данных (стек).

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.