Каковы препятствия для понимания указателей и что можно сделать для их преодоления?

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

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

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

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

Я добавил код Delphi ниже и некоторые комментарии там, где это необходимо. Я выбрал Delphi, так как мой другой основной язык программирования, C #, не показывает такие вещи, как утечки памяти, таким же образом.

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

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


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

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

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

В памяти будут некоторые накладные расходы, связанные с распределением дома, я проиллюстрирую это ниже следующим образом:

 --- [ttttNNNNNNNNNN] ---
      ^ ^
      |  |
      |  + - массив FName
      |
      + - накладные расходы

Область «tttt» является служебной, обычно это будет больше для разных типов времени выполнения и языков, например 8 или 12 байтов. Крайне важно, чтобы любые значения, хранящиеся в этой области, никогда не изменялись ничем иным, чем распределителем памяти или основными системными подпрограммами, или вы рискуете сбой программы.


Выделить память

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

Другими словами, предприниматель выберет место.

 THouse.Create('My house'); 

Макет памяти:

 --- [ttttNNNNNNNNNN] ---
     1234Мой дом

Хранить переменную с адресом

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

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

Макет памяти:

     час
     v
 --- [ttttNNNNNNNNNN] ---
     1234Мой дом

Копировать значение указателя

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

Примечание. Обычно это понятие, что у меня есть большая проблема, объясняющая людям, два указателя не означают два объекта или блоки памяти.

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

Освобождение памяти

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

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

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

Макет памяти:

     h <- +
     v + - до свободной
 --- [ttttNNNNNNNNNN] --- |
     1234My дом <- +

     h (теперь нигде не опускается) <- +
                                 + - после бесплатного
 ---------------------- |  (заметьте, память все равно может
     xx34My дом <- + содержит некоторые данные)

Висячие указатели

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

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

Использование h после вызова .Free может работать, но это просто удача. Скорее всего, он потерпит неудачу, на месте клиентов, в середине критической операции.

     h <- +
     v + - до свободной
 --- [ttttNNNNNNNNNN] --- |
     1234My дом <- +

     h <- +
     v + - после бесплатного
 ---------------------- |
     xx34My дом <- +

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


Утечка памяти

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

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

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

Схема памяти после первого размещения:

     час
     v
 --- [ttttNNNNNNNNNN] ---
     1234Мой дом

Схема памяти после второго выделения:

                        час
                        v
 --- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
     1234Мой дом 5678Мой дом

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

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

После выполнения этого метода в наших переменных нет места, что адрес в доме существует, но дом все еще там.

Макет памяти:

     h <- +
     v + - перед потерей указателя
 --- [ttttNNNNNNNNNN] --- |
     1234My дом <- +

     h (теперь нигде не опускается) <- +
                                 + - после потери указателя
 --- [ttttNNNNNNNNNN] --- |
     1234My дом <- +

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


Освобождение памяти, но сохранение (теперь недействительной) ссылки

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

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

Иногда вы даже можете обнаружить, что на соседнем адресе есть довольно большой дом, который занимает три адреса (Main Street 1-3), и ваш адрес переместится в середину дома. Любые попытки обработать эту часть большого дома с тремя адресами в качестве одного маленького дома могут также терпеть неудачу.

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

Здесь дом был разорван, через ссылку в h1 , и, хотя h1 был очищен, h2 все еще имеет старый, устаревший адрес. Доступ к дому, который больше не работает, может работать или не работать.

Это вариант висячего указателя выше. См. Его макет памяти.


Переполнение буфера

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

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

Таким образом, этот код:

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

Схема памяти после первого размещения:

                         h1
                         v
 ----------------------- [ttttNNNNNNNNNN]
                         5678Мой дом

Схема памяти после второго выделения:

     h2 h1
     ст
 --- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN]
     1234Мой другой дом где-то
                         ^ --- + - ^
                             |
                             + - перезаписан

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


Связанные списки

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

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

Здесь мы создаем ссылку из нашего дома в нашу каюту. Мы можем следить за цепочкой, пока в доме нет ссылки NextHouse , что означает, что она последняя. Чтобы посетить все наши дома, мы могли бы использовать следующий код:

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

Схема памяти (добавлена ​​NextHouse как ссылка в объекте, отмеченная четырьмя LLLL на диаграмме ниже):

     h1 h2
     ст
 --- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
     1234Дом + 5678Cabin +
                    |  ^ |
                    + -------- + * (без ссылки)

В основном, что такое адрес памяти?

Адрес памяти в базовых терминах - это просто номер. Если вы считаете память большим массивом байтов, то первый байт имеет адрес 0, следующий адрес 1 и т. Д. Вверх. Это упрощено, но достаточно хорошо.

Итак, этот макет памяти:

     h1 h2
     ст
 --- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
     1234Мой дом 5678Мой дом

Возможно, эти два адреса (самый левый - адрес 0):

  • h1 = 4
  • h2 = 23

Это означает, что наш связанный список выше мог бы выглядеть так:

     h1 (= 4) h2 (= 28)
     ст
 --- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
     1234Дом 0028 5678Cabin 0000
                    |  ^ |
                    + -------- + * (без ссылки)

Типично хранить адрес, который «указывает нигде» как нулевой адрес.


В основном, что такое указатель?

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

В моем первом classе Comp Sci мы выполнили следующее упражнение. Конечно, это был лекционный зал с примерно 200 учениками в нем …

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

Джон встает

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

Салли встает, указывает на Джона

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

Билл встает, указывает на Джона

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

Сэм встает

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

Билл теперь указывает на Сэма.

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

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

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

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

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

Почему указатели являются ведущим фактором путаницы для многих новых, и даже старых, студентов уровня колледжа на языке C / C ++?

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

Существуют ли какие-либо инструменты или мыслительные процессы, которые помогли вам понять, как указатели работают с переменной, функцией и за пределами уровня?

Адреса боксов. Я помню, когда я учился программировать BASIC в микрокомпьютерах, были эти красивые книги с играми в них, и иногда вам приходилось выставлять ценности на конкретные адреса. У них была фотография кучки ящиков, поэтапно помеченных 0, 1, 2 … и было объяснено, что в них могут входить только одна маленькая вещь (байт), и их было много – некоторые компьютеры было целых 65535! Они были рядом друг с другом, и все они имели адрес.

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

Для дрели? Создайте структуру:

 struct { char a; char b; char c; char d; } mystruct; mystruct.a = 'r'; mystruct.b = 's'; mystruct.c = 't'; mystruct.d = 'u'; char* my_pointer; my_pointer = &mystruct.b; cout << 'Start: my_pointer = ' << *my_pointer << endl; my_pointer++; cout << 'After: my_pointer = ' << *my_pointer << endl; my_pointer = &mystruct.a; cout << 'Then: my_pointer = ' << *my_pointer << endl; my_pointer = my_pointer + 3; cout << 'End: my_pointer = ' << *my_pointer << endl; 

Тот же пример, что и выше, кроме C:

 // Same example as above, except in C: struct { char a; char b; char c; char d; } mystruct; mystruct.a = 'r'; mystruct.b = 's'; mystruct.c = 't'; mystruct.d = 'u'; char* my_pointer; my_pointer = &mystruct.b; printf("Start: my_pointer = %c\n", *my_pointer); my_pointer++; printf("After: my_pointer = %c\n", *my_pointer); my_pointer = &mystruct.a; printf("Then: my_pointer = %c\n", *my_pointer); my_pointer = my_pointer + 3; printf("End: my_pointer = %c\n", *my_pointer); 

Вывод:

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

Возможно, это объясняет некоторые из основ на примере?

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

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

Учебник по указателям и массивам в C: Глава 3 – Указатели и строки

 int puts(const char *s); 

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

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

Даже если вы разработчик VB .NET или C # (как я) и никогда не использую небезопасный код, все равно стоит понять, как работают указатели, или вы не поймете, как работают ссылки на объекты. Тогда у вас будет общее ошибочное представление о том, что передача объектной ссылки на метод копирует объект.

Я нашел «Учебник по указателям и массивам в C» Теда Дженсена – отличный ресурс для изучения указателей. Он разделен на 10 уроков, начиная с объяснения того, какие указатели (и для чего они предназначены) и заканчивая указателями функций. http://home.netcom.com/~tjensen/ptr/cpoint.htm

Двигаясь оттуда, руководство Beej по программированию в сети учит API-интерфейсам Unix, из которого вы можете начать делать действительно забавные вещи. http://beej.us/guide/bgnet/

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

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

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

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

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

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

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

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

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

Джоэл Спольский делает несколько хороших моментов в понимании указателей в своем Руководстве по партизанским вопросам для интервьюирования статьи:

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

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


Задайте сцену :

Рассмотрите парковку с 3 пробелами, эти пробелы пронумерованы:

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

В некотором роде это похоже на ячейки памяти, они последовательны и непрерывны. Подобно массиву. Прямо сейчас в них нет автомобилей, поэтому это похоже на пустой массив ( parking_lot[3] = {0} ).


Добавить данные

Автостоянка никогда не остается пустой надолго … если бы это было сделано, это было бы бессмысленно, и никто не построил бы. Итак, скажем так, как день движется по лотам, заполняется 3 автомобилями, синим автомобилем, красным автомобилем и зеленой машиной:

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

Эти автомобили все одного типа (автомобиль), поэтому один из способов думать об этом заключается в том, что наши автомобили – это какие-то данные (скажем, int ), но они имеют разные значения ( blue , red , green , это может быть цветное enum )


Введите указатель

Теперь, если я отведу вас на эту стоянку и попрошу вас найти синий автомобиль, вы протяните один палец и используйте его, чтобы указать на синий автомобиль в месте 1. Это похоже на то, чтобы взять указатель и присвоить его адресу памяти ( int *finger = parking_lot )

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


Переназначение указателя

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

Указатель физически не изменился, это все еще ваш палец, только данные, которые он показывал мне, изменились. (адрес «парковочного места»)


Двойные указатели (или указатель на указатель)

Это работает и с несколькими указателями. Я могу спросить, где указатель, который указывает на красный автомобиль, и вы можете использовать другую руку и указывать пальцем на первый палец. (это как int **finger_two = &finger )

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


Висячий указатель

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

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

Ваш указатель все еще указывает на то, где был красный автомобиль, но больше нет. Скажем, новый автомобиль тянет туда … оранжевый автомобиль. Теперь, если я снова спрошу вас: «Где красный автомобиль», вы все еще указываете туда, но теперь вы ошибаетесь. Это не красный автомобиль, это оранжевый.


Арифметика указателя

Хорошо, так что вы все еще указываете на второе место парковки (теперь занятое оранжевой машиной)

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

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

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

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

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

Ни одно из них не является истинным. Pointers are actually fairly simple concepts, as long as you stick to what the C++ language has to say about them and don’t imbue them with attributes that “usually” turn out to work in practice, but nevertheless aren’t guaranteed by the language, and so aren’t part of the actual concept of a pointer.

I tried to write up an explanation of this a few months ago in this blog post — hopefully it’ll help someone.

(Note, before anyone gets pedantic on me, yes, the C++ standard does say that pointers represent memory addresses. But it does not say that “pointers are memory addresses, and nothing but memory addresses and may be used or thought of interchangeably with memory addresses”. The distinction is important)

The problem with pointers is not the concept. It’s the execution and language involved. Additional confusion results when teachers assume that it’s the CONCEPT of pointers that’s difficult, and not the jargon, or the convoluted mess C and C++ makes of the concept. So vast amounts of effort are poored into explaining the concept (like in the accepted answer for this question) and it’s pretty much just wasted on someone like me, because I already understand all of that. It’s just explaining the wrong part of the problem.

To give you an idea of where I’m coming from, I’m someone who understands pointers perfectly well, and I can use them competently in assembler language. Because in assembler language they are not referred to as pointers. They are referred to as addresses. When it comes to programming and using pointers in C, I make a lot of mistakes and get really confused. I still have not sorted this out. Let me give you an example.

When an api says:

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

what does it want?

it could want:

a number representing an address to a buffer

(To give it that, do I say doIt(mybuffer) , or doIt(*myBuffer) ?)

a number representing the address to an address to a buffer

(is that doIt(&mybuffer) or doIt(mybuffer) or doIt(*mybuffer) ?)

a number representing the address to the address to the address to the buffer

(maybe that’s doIt(&mybuffer) . or is it doIt(&&mybuffer) ? or even doIt(&&&mybuffer) )

and so on, and the language involved doesn’t make it as clear because it involves the words “pointer” and “reference” that don’t hold as much meaning and clarity to me as “x holds the address to y” and “this function requires an address to y”. The answer additionally depends on just what the heck “mybuffer” is to begin with, and what doIt intends to do with it. The language doesn’t support the levels of nesting that are encountered in practice. Like when I have to hand a “pointer” in to a function that creates a new buffer, and it modifies the pointer to point at the new location of the buffer. Does it really want the pointer, or a pointer to the pointer, so it knows where to go to modify the contents of the pointer. Most of the time I just have to guess what is meant by “pointer” and most of the time I’m wrong, regardless of how much experience I get at guessing.

“Pointer” is just too overloaded. Is a pointer an address to a value? or is it a variable that holds an address to a value. When a function wants a pointer, does it want the address that the pointer variable holds, or does it want the address to the pointer variable? I’m confused.

I think that what makes pointers tricky to learn is that until pointers you’re comfortable with the idea that “at this memory location is a set of bits that represent an int, a double, a character, whatever”.

When you first see a pointer, you don’t really get what’s at that memory location. “What do you mean, it holds an address ?”

I don’t agree with the notion that “you either get them or you don’t”.

They become easier to understand when you start finding real uses for them (like not passing large structures into functions).

The reason it’s so hard to understand is not because it’s a difficult concept but because the syntax is inconsistent .

  int *mypointer; 

You are first learned that the leftmost part of a variable creation defines the type of the variable. Pointer declaration does not work like this in C and C++. Instead they say that the variable is pointing on the type to the left. In this case: * mypointer is pointing on an int.

I didn’t fully grasp pointers until i tried using them in C# (with unsafe), they work in exact same way but with logical and consistent syntax. The pointer is a type itself. Here mypointer is a pointer to an int.

  int* mypointer; 

Don’t even get me started on function pointers…

I could work with pointers when I only knew C++. I kind of knew what to do in some cases and what not to do from trial/error. But the thing that gave me complete understanding is assembly language. If you do some serious instruction level debugging with an assembly language program you’ve written, you should be able to understand a lot of things.

I like the house address analogy, but I’ve always thought of the address being to the mailbox itself. This way you can visualize the concept of dereferencing the pointer (opening the mailbox).

For instance following a linked list: 1) start with your paper with the address 2) Go to the address on the paper 3) Open the mailbox to find a new piece of paper with the next address on it

In a linear linked list, the last mailbox has nothing in it (end of the list). In a circular linked list, the last mailbox has the address of the first mailbox in it.

Note that step 3 is where the dereference occurs and where you’ll crash or go wrong when the address is invalid. Assuming you could walk up to the mailbox of an invalid address, imagine that there’s a black hole or something in there that turns the world inside out 🙂

I think that the main reason that people have trouble with it is because it’s generally not taught in an interesting and engaging manner. I’d like to see a lecturer get 10 volunteers from the crowd and give them a 1 meter ruler each, get them to stand around in a certain configuration and use the rulers to point at each other. Then show pointer arithmetic by moving people around (and where they point their rulers). It’d be a simple but effective (and above all memorable) way of showing the concepts without getting too bogged down in the mechanics.

Once you get to C and C++ it seems to get harder for some people. I’m not sure if this is because they are finally putting theory that they don’t properly grasp into practice or because pointer manipulation is inherently harder in those languages. I can’t remember my own transition that well, but I knew pointers in Pascal and then moved to C and got totally lost.

I don’t think that pointers themselves are confusing. Most people can understand the concept. Now how many pointers can you think about or how many levels of indirection are you comfortable with. It doesn’t take too many to put people over the edge. The fact that they can be changed accidently by bugs in your program can also make them very difficult to debug when things go wrong in your code.

I think it might actually be a syntax issue. The C/C++ syntax for pointers seems inconsistent and more complex than it needs to be.

Ironically, the thing that actually helped me to understand pointers was encountering the concept of an iterator in the c++ Standard Template Library . It’s ironic because I can only assume that iterators were conceived as a generalization of the pointer.

Sometimes you just can’t see the forest until you learn to ignore the trees.

The confusion comes from the multiple abstraction layers mixed together in the “pointer” concept. Programmers don’t get confused by ordinary references in Java/Python, but pointers are different in that they expose characteristics of the underlying memory-architecture.

It is a good principle to cleanly separate layers of abstraction, and pointers do not do that.

The way I liked to explain it was in terms of arrays and indexes – people might not be familiar with pointers, but they generally know what an index is.

So I say imagine that the RAM is an array (and you have only 10-bytes of RAM):

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

Then a pointer to a variable is really just the index of (the first byte of) that variable in the RAM.

So if you have a pointer/index unsigned char index = 2 , then the value is obviously the third element, or the number 4. A pointer to a pointer is where you take that number and use it as an index itself, like RAM[RAM[index]] .

I would draw an array on a list of paper, and just use it to show things like many pointers pointing to the same memory, pointer arithmetic, pointer to pointer, and so on.

I don’t see what is so confusing about pointers. They point to a location in memory, that is it stores the memory address. In C/C++ you can specify the type the pointer points to. Например:

 int* my_int_pointer; 

Says that my_int_pointer contains the address to a location that contains an int.

The problem with pointers is that they point to a location in memory, so it is easy to trail off into some location you should not be in. As proof look at the numerous security holes in C/C++ applications from buffer overflow (incrementing the pointer past the allocated boundary).

Post office box number.

It’s a piece of information that allows you to access something else.

(And if you do arithmetic on post office box numbers, you may have a problem, because the letter goes in the wrong box. And if somebody moves to another state — with no forwarding address — then you have a dangling pointer. On the other hand — if the post office forwards the mail, then you have a pointer to a pointer.)

Not a bad way to grasp it, via iterators.. but keep looking you’ll see Alexandrescu start complaining about them.

Many ex-C++ devs (that never understood that iterators are a modern pointer before dumping the language) jump to C# and still believe they have decent iterators.

Hmm, the problem is that all that iterators are is in complete odds at what the runtime platforms (Java/CLR) are trying to achieve: new, simple, everyone-is-a-dev usage. Which can be good, but they said it once in the purple book and they said it even before and before C:

Indirection.

A very powerful concept but never so if you do it all the way.. Iterators are useful as they help with abstraction of algorithms, another example. And compile-time is the place for an algorithm, very simple. You know code + data, or in that other language C#:

IEnumerable + LINQ + Massive Framework = 300MB runtime penalty indirection of lousy, dragging apps via heaps of instances of reference types..

“Le Pointer is cheap.”

Some answers above have asserted that “pointers aren’t really hard”, but haven’t gone on to address directly where “pointer are hard!” comes from. Some years back I tutored first year CS students (for only one year, since I clearly sucked at it) and it was clear to me that the idea of pointer is not hard. What’s hard is understanding why and when you would want a pointer .

I don’t think you can divorce that question – why and when to use a pointer – from explaining broader software engineering issues. Why every variable should not be a global variable, and why one should factor out similar code into functions (that, get this, use pointers to specialize their behaviour to their call site).

Just to confuse things a bit more, sometimes you have to work with handles instead of pointers. Handles are pointers to pointers, so that the back end can move things in memory to defragment the heap. If the pointer changes in mid-routine, the results are unpredictable, so you first have to lock the handle to make sure nothing goes anywhere.

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 talks about it a bit more coherently than me. 🙂

Every C/C++ beginner has the same problem and that problem occurs not because “pointers are hard to learn” but “who and how it is explained”. Some learners gather it verbally some visually and the best way of explaining it is to use “train” example (suits for verbal and visual example).

Where “locomotive” is a pointer which can not hold anything and “wagon” is what “locomotive” tries pull (or point to). After, you can classify the “wagon” itself, can it hold animals,plants or people (or a mix of them).

  • C - Разница между «char var » и «char * var»?
  • Разница между std :: reference_wrapper и простым указателем?
  • указатель на массив c ++
  • C Программирование: malloc () внутри другой функции
  • Как сравнить две функции для равенства указателей в последней неделе?
  • Ошибка сегментации при изменении строки с помощью указателей?
  • Как передать функцию-член, где ожидается функция free?
  • Сколько уровней указателей у нас есть?
  • Почему я не могу преобразовать 'char **' в 'const char * const *' в C?
  • Указатели на C: когда использовать амперсанд и звездочку?
  • Почему я должен использовать указатель, а не сам объект?
  • Interesting Posts
    Давайте будем гением компьютера.