Что люди находят сложными в отношении C-указателей?

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

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

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

Ответ 1

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

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

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

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

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

Ответ 2

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

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 not '4'!
x = *ip;  //ahh... there that '4'

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

int *ip = &x;

Ура для согласованности!

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

void foo(****ipppArr);

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

foo(&(***ipppArr));

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

Ответ 3

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

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

Ответ 4

Когда речь идет о указателях, люди, которые запутываются, широко входят в один из двух лагерей. Я был (я?) В обоих.

Толпа array[]

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

  • обозначение массива (индексирование) с помощью имя массива
  • обозначение массива (индексирование) с помощью имя указателя
  • обозначение указателя (*) с помощью имя указателя
  • обозначение указателя (*) с помощью имя массива

 

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/С++ путаться, не говоря уже о неопытных новичках.

Толпы 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 и С++ для путающих причин, которые приводят вас в семантику lvalue - rvalue.

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

Ну, этот ответ оказался длиннее, чем я надеялся...

Ответ 5

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

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

Ответ 6

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

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

Ответ 7

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

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

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

Ответ 8

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

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

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

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

Ответ 9

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

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

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

В стороне я лично предпочитаю первую версию.

Ответ 10

Я должен начать с того, что 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);

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

Ответ 11

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

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

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

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

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

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

Теперь вернемся к исходному вопросу. Основываясь на предыдущем списке, было несколько элементов, с которыми мне трудно было понять.

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

Ответ 12

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

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

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

Ответ 13

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

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

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

Ответ 14

Вот не ответ: Используйте cdecl (или С++ decl), чтобы понять это:

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

Ответ 15

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

ИЛИ

Указатели похожи на катание на велосипеде, как только вы выясните, как с ними работать, не забывая об этом.

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

Ответ 16

Они добавляют дополнительное измерение в код без значительного изменения синтаксиса. Подумайте об этом:

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... это может быть то, что смущает людей.

Ответ 17

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

Ответ 18

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

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

Ответ 19

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

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

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

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

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

Ответ 20

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

В 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 в b и применяет ее к списку a, чтобы получить список b 's, Как и в функции C g_list_foreach, map не нужно ничего знать в своем собственном определении о типах, к которым он будет применяться.

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

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

Ответ 21

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

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

Ответ 22

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

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

Ответ 23

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

Ответ 24

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

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

Ответ 25

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

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

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

По крайней мере, так, как я это вижу в данный момент!

Ответ 26

Говоря как новичок С++ здесь:

Система указателей потребовала времени для переваривания не обязательно из-за концепции, но из-за синтаксиса С++ относительно 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/С++ имеет все эти специфические для языка особенности.

Ответ 27

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

Ответ 28

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

Ответ 29

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

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

И последнее, что я увидел, это проблема хранения: они понимают кучу и стек, но не могут понять идею "статического".