Каковы барьеры для понимания указателей и что можно сделать, чтобы их преодолеть?
Почему указатели являются таким ведущим фактором путаницы для многих новых и даже старых студентов уровня колледжа в C или C++? Существуют ли какие-либо инструменты или мыслительные процессы, которые помогли вам понять, как работают указатели на уровне переменной, функции и за ее пределами?
Что можно сделать с хорошей практикой, чтобы привести кого-то к уровню "Ах, я понял", не увязнув в общей концепции? В основном, тренировка как сценарии.
28 ответов
Указатели - это концепция, которая поначалу многих может сбить с толку, особенно когда речь идет о копировании значений указателей и сохранении ссылок на один и тот же блок памяти.
Я обнаружил, что лучшая аналогия - рассматривать указатель как лист бумаги с адресом дома и блоком памяти, на который он ссылается как на настоящий дом. Таким образом, все виды операций могут быть легко объяснены.
Я добавил немного кода Delphi внизу и несколько комментариев, где это уместно. Я выбрал Delphi, поскольку мой другой основной язык программирования, C#, не демонстрирует такие вещи, как утечки памяти, таким же образом.
Если вы хотите узнать только концепцию указателей высокого уровня, вам следует игнорировать части, помеченные как "Расположение памяти" в приведенном ниже объяснении. Они предназначены для того, чтобы привести примеры того, как память может выглядеть после операций, но они имеют более низкоуровневый характер. Однако, чтобы точно объяснить, как на самом деле работают переполнения буфера, было важно, чтобы я добавил эти диаграммы.
Отказ от ответственности: для всех намерений и целей, это объяснение и примеры макетов памяти значительно упрощены. Там будет больше накладных расходов и гораздо больше деталей, которые вам нужно знать, если вам нужно иметь дело с памятью на низкоуровневой основе. Однако для целей объяснения памяти и указателей это достаточно точно.
Давайте предположим, что используемый ниже класс THouse выглядит следующим образом:
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;
Здесь я сначала строю дом, и получаю его адрес. Затем я делаю что-то для дома (использую его, код, оставленный в качестве упражнения для читателя), а затем освобождаю это. Наконец я очищаю адрес из моей переменной.
Расположение памяти:
ч <- + v + - перед свободным --- [ttttNNNNNNNNNN] --- | 1234Мой дом <- + h (теперь нигде не указывает) <- + + - после бесплатно ---------------------- | (обратите внимание, память может еще xx34Мой дом <- + содержит некоторые данные)
Висячие указатели
Вы говорите своему предпринимателю разрушить дом, но забыли стереть адрес с вашего листа бумаги. Когда позже вы посмотрите на лист бумаги, вы забыли, что дома больше нет, и отправляетесь навестить его с ошибочными результатами (см. Также часть о недействительной ссылке ниже).
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
... // forgot to clear h here
h.OpenFrontDoor; // will most likely fail
С помощью h
после звонка .Free
может сработать, но это просто удача. Скорее всего, он потерпит неудачу на месте клиента в середине критической операции.
ч <- + v + - перед свободным --- [ttttNNNNNNNNNN] --- | 1234Мой дом <- + ч <- + v + - после бесплатно ---------------------- | xx34Мой дом <- +
Как вы можете видеть, 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;
После выполнения этого метода в наших переменных не будет места, где указан адрес дома, но дом все еще там.
Расположение памяти:
ч <- + v + - до потери указателя --- [ttttNNNNNNNNNN] --- | 1234Мой дом <- + h (теперь нигде не указывает) <- + + - после потери указателя --- [ttttNNNNNNNNNN] --- | 1234Мой дом <- +
Как видите, старые данные остаются нетронутыми в памяти и не будут повторно использоваться распределителем памяти. Распределитель отслеживает, какие области памяти были использованы, и не будет использовать их повторно, пока вы не освободите его.
Освобождение памяти, но сохранение (теперь недействительной) ссылки
Снесите дом, сотрите один из кусочков бумаги, но у вас также есть другой листок бумаги со старым адресом на нем, когда вы идете по адресу, вы не найдете дом, но вы можете найти что-то похожее на руины одного.
Возможно, вы даже найдете дом, но это не тот дом, которому вы изначально дали адрес, и поэтому любые попытки использовать его так, как будто он принадлежит вам, могут ужасно потерпеть неудачу.
Иногда вы можете даже обнаружить, что на соседнем адресе настроен довольно большой дом, который занимает три адреса (главная улица 1-3), и ваш адрес идет в середину дома. Любые попытки трактовать эту часть большого 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Дом + 5678Кабина + | ^ | +--------+ * (без ссылки)
В общих чертах, что такое адрес памяти?
Адрес памяти в основных терминах просто число. Если вы воспринимаете память как большой массив байтов, самый первый байт имеет адрес 0, следующий - адрес 1 и т. Д. Вверх. Это упрощено, но достаточно хорошо.
Итак, это макет памяти:
h1 h2 ст --- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN] 1234Мой дом 5678Мой дом
Может иметь эти два адреса (самый левый - это адрес 0):
- h1 = 4
- h2 = 23
Это означает, что наш приведенный выше список может выглядеть так:
h1 (= 4) h2 (= 28) ст ---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL] 1234Дом 0028 5678Кабина 0000 | ^ | +--------+ * (без ссылки)
Обычно адрес, который "нигде не указывает", является нулевым адресом.
В общих чертах, что такое указатель?
Указатель - это просто переменная, содержащая адрес памяти. Обычно вы можете попросить язык программирования дать вам его номер, но большинство языков программирования и сред выполнения стараются скрыть тот факт, что число находится под ним, просто потому, что само число на самом деле не имеет никакого значения для вас. Лучше всего рассматривать указатель как черный ящик, т.е. вы на самом деле не знаете или не заботитесь о том, как это на самом деле реализовано, до тех пор, пока оно работает.
В моем первом классе Comp Sci мы выполнили следующее упражнение. Конечно, это был лекционный зал с примерно 200 студентами...
Профессор пишет на доске: int john;
Джон встает
Профессор пишет: int *sally = &john;
Салли встает, указывает на Джона
Профессор: int *bill = sally;
Билл встает, указывает на Джона
Профессор: int sam;
Сэм встает
Профессор: bill = &sam;
Билл теперь указывает на Сэма.
Я думаю, вы поняли идею. Я думаю, что мы потратили на это около часа, пока не рассмотрели основы назначения указателей.
Аналогия, которую я нашел полезной для объяснения указателей, - это гиперссылки. Большинство людей могут понять, что ссылка на веб-странице "указывает" на другую страницу в Интернете, и если вы сможете скопировать и вставить эту гиперссылку, они оба будут указывать на одну и ту же исходную веб-страницу. Если вы зайдете и отредактируете эту исходную страницу, затем перейдите по любой из этих ссылок (указателей), и вы получите эту новую обновленную страницу.
Причина, по которой указатели, похоже, смущают многих людей, заключается в том, что они в основном не имеют никакого опыта в компьютерной архитектуре. Поскольку многие, похоже, не имеют представления о том, как на самом деле реализованы компьютеры (машины), работа в C/C++ кажется чуждой.
Задача состоит в том, чтобы попросить их реализовать простую виртуальную машину на основе байт-кода (на любом языке, который они выбрали, для этого прекрасно работает python) с набором инструкций, ориентированных на операции с указателями (загрузка, сохранение, прямая / косвенная адресация). Затем попросите их написать простые программы для этого набора инструкций.
Все, что требует немного большего, чем простое добавление, будет включать указатели, и они обязательно получат это.
Почему указатели являются таким лидирующим фактором путаницы для многих новых и даже старых студентов уровня колледжа на языке C/C++?
Концепция заполнителя для значения - переменных - отображает то, чему нас учат в школе - алгебру. Не существует существующей параллели, которую вы можете провести, не понимая, как физически распределена память в компьютере, и никто не думает об этом, пока они не имеют дело с вещами низкого уровня - на уровне связи C/C++/byte.,
Существуют ли какие-либо инструменты или мыслительные процессы, которые помогли вам понять, как работают указатели на уровне переменной, функции и за ее пределами?
Адреса ящиков. Я помню, когда я учился программировать 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
Возможно, это объясняет некоторые из основ на примере?
Причина, по которой я с трудом разбираюсь в указателях, заключается в том, что во многих объяснениях содержится много дерьма о передаче по ссылке. Все это путает проблему. Когда вы используете параметр указателя, вы все равно передаете по значению; но значение оказывается адресом, а не, скажем, int.
Кто-то уже связался с этим учебником, но я могу выделить момент, когда я начал понимать указатели:
Учебник по указателям и массивам в C: Глава 3 - Указатели и строки
int puts(const char *s);
На данный момент игнорируйте
const.
Параметр переданputs()
является указателем, то есть значением указателя (поскольку все параметры в C передаются по значению), а значением указателя является адрес, на который он указывает, или, просто, адрес. Таким образом, когда мы пишемputs(strA);
как мы уже видели, мы передаем адрес strA[0].
В тот момент, когда я прочитал эти слова, облака разошлись, и луч солнца окутал меня пониманием указателя.
Даже если вы являетесь разработчиком VB .NET или C# (как и я) и никогда не используете небезопасный код, все равно стоит понять, как работают указатели, иначе вы не поймете, как работают ссылки на объекты. Тогда у вас будет распространенное, но ошибочное мнение, что передача ссылки на объект в метод копирует объект.
"Учебник Теда Дженсена по указателям и массивам в Си" я нашел отличным ресурсом для изучения указателей. Он разделен на 10 уроков, начиная с объяснения того, что такое указатели (и для чего они нужны) и заканчивая указателями на функции. http://home.netcom.com/~tjensen/ptr/cpoint.htm
Далее Beej's Guide по сетевому программированию обучает API сокетов Unix, из которого вы можете начать делать действительно забавные вещи. http://beej.us/guide/bgnet/
Сложности указателей выходят за рамки того, что мы можем легко научить. Заставить учеников указывать друг на друга и использовать листы бумаги с домашними адресами - это оба отличные инструменты обучения Они делают большую работу по представлению основных понятий. Действительно, изучение основных понятий жизненно важно для успешного использования указателей. Однако в производственном коде часто встречаются гораздо более сложные сценарии, чем эти простые демонстрации.
Я был связан с системами, где у нас были структуры, указывающие на другие структуры, указывающие на другие структуры. Некоторые из этих структур также содержали встроенные структуры (а не указатели на дополнительные структуры). Это где указатели становятся действительно запутанными. Если у вас есть несколько уровней косвенности, и вы начинаете в конечном итоге с кодом, подобным этому:
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| |
| o-o | o-o | o-o |
Все эти машины одного типа (машины), поэтому один из способов думать об этом состоит в том, что наши машины представляют собой некие данные (скажем, int
) но они имеют разные значения (blue
, red
, green
; это может быть цвет enum
)
Введите указатель
Теперь, если я отвезу вас на эту парковку и попрослю вас найти мне синюю машину, вы протяните один палец и при помощи него сможете указать на синюю машину в месте 1. Это все равно, что взять указатель и присвоить его адресу памяти. (int *finger = parking_lot
)
Ваш палец (указатель) не является ответом на мой вопрос. Взгляд на ваш палец мне ничего не говорит, но если я посмотрю, куда указывает ваш палец (разыменовывает указатель), я могу найти автомобиль (данные), который искал.
Переназначение указателя
Теперь я могу попросить вас найти красную машину, а вы можете перенаправить палец на новую машину. Теперь ваш указатель (такой же, как и раньше) показывает мне новые данные (место для парковки, где находится красная машина) того же типа (машина).
Указатель не изменился физически, это все еще ваш палец, изменились только данные, которые он мне показывал. (адрес "места для парковки")
Двойные указатели (или указатель на указатель)
Это работает с более чем одним указателем. Я могу спросить, где находится указатель, указывающий на красную машину, и вы можете использовать другую руку и указать пальцем на первый палец. (это как int **finger_two = &finger
)
Теперь, если я хочу знать, где находится синяя машина, я могу следовать по направлению первого пальца ко второму пальцу, к машине (данные).
Свисающий указатель
Теперь предположим, что вы очень похожи на статую и хотите, чтобы ваша рука указывала на красную машину бесконечно. Что если эта красная машина уедет?
1 2 3
-------------------
| o=o | | o=o |
| |B| | | |G| |
| o-o | | o-o |
Ваш указатель все еще указывает на то, где находилась красная машина, но ее больше нет. Скажем, новая машина останавливается там... оранжевая машина. Теперь, если я снова спрошу вас, "где красная машина", вы все еще указываете туда, но теперь вы ошибаетесь. Это не красная машина, это оранжевая.
Арифметика указателей
Итак, вы все еще указываете на второе место для парковки (сейчас занято автомобилем Orange)
1 2 3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |
Ну, теперь у меня новый вопрос... Я хочу знать цвет машины на следующем месте парковки. Вы можете видеть, что вы указываете на точку 2, поэтому вы просто добавляете 1, и вы указываете на следующую точку. (finger+1
), теперь, так как я хотел знать, какие данные были там, вы должны проверить это место (а не только палец), чтобы вы могли отложить указатель (*(finger+1)
) чтобы увидеть там зеленый автомобиль (данные в этом месте)
Пример учебника с хорошим набором диаграмм очень помогает в понимании указателей.
Джоэл Спольски делает несколько хороших замечаний о понимании указателей в своей статье " Партизанское руководство по интервью":
По некоторым причинам большинство людей, кажется, рождаются без той части мозга, которая понимает указатели. Это вещь способностей, а не навыков - она требует сложной формы вдвойне направленного мышления, что некоторые люди просто не могут сделать.
Проблема с указателями не в концепции. Это исполнение и язык вовлечены. Дополнительная путаница приводит к тому, что учителя предполагают, что это КОНЦЕПЦИЯ указателей труднее, а не жаргон или запутанный беспорядок C и C++ делает концепцию. Так много усилий затрачивается на объяснение концепции (как в принятом ответе на этот вопрос), и это в значительной степени просто напрасно тратится на кого-то вроде меня, потому что я уже все это понимаю. Это просто объясняет не ту часть проблемы.
Чтобы дать вам представление о том, откуда я родом, я тот, кто прекрасно понимает указатели, и я могу грамотно использовать их на языке ассемблера. Потому что на ассемблере они не упоминаются как указатели. Они называются адресами. Когда дело доходит до программирования и использования указателей в C, я допускаю много ошибок и очень запутываюсь. Я до сих пор не разобрался в этом. Позвольте привести пример.
Когда API говорит:
int doIt(char *buffer )
//*buffer is a pointer to the buffer
чего он хочет?
это может хотеть:
число, представляющее адрес буфера
(Чтобы дать это, я говорю doIt(mybuffer)
, или же doIt(*myBuffer)
?)
число, представляющее адрес к адресу в буфере
(в том, что doIt(&mybuffer)
или же doIt(mybuffer)
или же doIt(*mybuffer)
?)
число, представляющее адрес по адресу к адресу в буфере
(может быть, это doIt(&mybuffer)
, либо это doIt(&&mybuffer)
? или даже doIt(&&&mybuffer)
)
и так далее, и используемый язык не делает это настолько ясным, потому что он включает в себя слова "указатель" и "ссылка", которые не имеют такого большого значения и ясности для меня, как "x содержит адрес для y" и " эта функция требует адреса к y". Ответ также зависит от того, с какого черта "mybuffer" должен начинаться, и что он намерен делать с ним. Язык не поддерживает уровни вложенности, которые встречаются на практике. Например, когда мне нужно передать "указатель" на функцию, которая создает новый буфер, и он изменяет указатель так, чтобы он указывал на новое местоположение буфера. Действительно ли он хочет указатель или указатель на указатель, чтобы он знал, куда идти, чтобы изменить содержимое указателя. Большую часть времени мне просто нужно угадать, что подразумевается под "указателем", и большую часть времени я ошибаюсь, независимо от того, какой опыт я получаю при угадывании.
"Указатель" просто перегружен. Указатель является адресом для значения? или это переменная, которая содержит адрес значения. Когда функция хочет указатель, она хочет адрес, который содержит переменная указателя, или она хочет адрес переменной указателя? Я не совсем понимаю.
Я думаю, что основным препятствием для понимания указателей являются плохие учителя.
Практически всех учат лжи об указателях: они являются не чем иным, как адресами памяти или позволяют указывать на произвольные места.
И конечно, что они трудны для понимания, опасны и полумагичны.
Ничего из этого не является правдой. Указатели - это на самом деле довольно простые понятия, если вы придерживаетесь того, что говорит о них язык C++, и не наделяете их атрибутами, которые "обычно" работают на практике, но тем не менее не гарантируются языком. и так не являются частью реальной концепции указателя.
Я попытался написать объяснение этому несколько месяцев назад в этом посте - надеюсь, это кому-нибудь поможет.
(Обратите внимание, что до того, как кто-нибудь станет со мной педантичен, да, стандарт C++ говорит, что указатели представляют адреса памяти. Но он не говорит, что "указатели являются адресами памяти и ничем иным, как адресами памяти, и могут использоваться или считаться взаимозаменяемыми с памятью". адреса ". Различие важно)
Я думаю, что сделать указатели сложнее в изучении, так это то, что до указателей вас устраивает мысль о том, что "в этой ячейке памяти находится набор битов, представляющих целое число, двойное число, символ, что угодно".
Когда вы впервые видите указатель, вы на самом деле не получаете то, что находится в этом месте памяти. "Что ты имеешь в виду, он содержит адрес?"
Я не согласен с понятием, что "вы либо получаете их, либо нет".
Их становится легче понять, когда вы начинаете находить для них реальное применение (например, не передавать большие структуры в функции).
Причина, по которой это так трудно понять, не в том, что это сложная концепция, а в том, что синтаксис противоречив.
int *mypointer;
Сначала вы узнали, что самая левая часть создания переменной определяет тип переменной. Объявление указателя не работает так в C и C++. Вместо этого они говорят, что переменная указывает на тип слева. В этом случае: *
mypointer указывает на int.
Я не полностью понял указатели, пока не попробую использовать их в C# (с небезопасным), они работают точно так же, но с логическим и последовательным синтаксисом. Указатель сам по себе является типом. Здесь mypointer является указателем на int.
int* mypointer;
Даже не заводите меня на указатели функций...
Я мог работать с указателями, когда знал только C++. Я как бы знал, что делать в некоторых случаях, а что не делать методом проб / ошибок. Но то, что дало мне полное понимание, это язык ассемблера. Если вы выполняете серьезную отладку на уровне инструкций с помощью написанной вами программы на языке ассемблера, вы должны понимать многие вещи.
Я думаю, что главная причина, по которой у людей возникают проблемы, заключается в том, что, как правило, этому не учат интересно и увлекательно. Я хотел бы, чтобы лектор собрал 10 волонтеров из толпы и дал им по 1 метру каждого, чтобы они стояли в определенной конфигурации и использовали линейки, чтобы указывать друг на друга. Затем покажите арифметику указателей, перемещая людей (и куда они указывают своих правителей). Это был бы простой, но эффективный (и прежде всего запоминающийся) способ показать концепции, не слишком увязая в механике.
Как только вы попадаете на C и C++, некоторым людям становится все труднее. Я не уверен, что это потому, что они, наконец, выдвигают теорию о том, что они не понимают должным образом на практике, или потому что манипулирование указателями по сути сложнее в этих языках. Я не очень хорошо помню свой собственный переход, но я знал указатели на Паскале, а затем перешел на Си и совершенно потерян.
Мне нравится аналогия с домашним адресом, но я всегда думал, что адрес относится к самому почтовому ящику. Таким образом, вы можете визуализировать концепцию разыменования указателя (открытия почтового ящика).
Например, следуя по связанному списку: 1) начните со своей бумаги с адресом 2) перейдите по адресу на бумаге 3) откройте почтовый ящик, чтобы найти новый лист бумаги со следующим адресом на нем
В линейном связанном списке в последнем почтовом ящике ничего нет (конец списка). В круговом связанном списке последний почтовый ящик содержит адрес первого почтового ящика.
Обратите внимание, что на шаге 3 происходит разыменование, и вы можете потерпеть крах или ошибиться, если адрес недействителен. Предполагая, что вы можете подойти к почтовому ящику с неверным адресом, представьте, что там есть черная дыра или что-то еще, что выворачивает мир наизнанку:)
Я не думаю, что сами указатели сбивают с толку. Большинство людей могут понять концепцию. Теперь, сколько указателей вы можете думать или сколько уровней косвенности вам удобно. Не нужно слишком много, чтобы поставить людей на край. Тот факт, что они могут быть случайно изменены из-за ошибок в вашей программе, также может сильно затруднить их отладку, когда в вашем коде что-то не так.
Я думаю, что это может быть проблема синтаксиса. Синтаксис C/C++ для указателей кажется непоследовательным и более сложным, чем необходимо.
По иронии судьбы, то, что на самом деле помогло мне понять указатели, было столкновение с концепцией итератора в стандартной библиотеке шаблонов C++. Это иронично, потому что я могу только предположить, что итераторы были задуманы как обобщение указателя.
Иногда вы просто не можете видеть лес, пока не научитесь игнорировать деревья.
Путаница возникает из-за нескольких уровней абстракции, смешанных вместе в концепции "указатель". Программистов не смущают обычные ссылки в Java/Python, но указатели отличаются тем, что они раскрывают характеристики базовой архитектуры памяти.
Хорошим принципом является чистое разделение слоев абстракции, а указатели этого не делают.
Мне нравилось объяснять это с точки зрения массивов и индексов - люди могут быть не знакомы с указателями, но они обычно знают, что такое индекс.
Итак, я говорю, представьте, что ОЗУ - это массив (а у вас всего 10 байтов ОЗУ):
unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };
Тогда указатель на переменную на самом деле является просто индексом (первого байта) этой переменной в ОЗУ.
Так что если у вас есть указатель / индекс unsigned char index = 2
тогда значение, очевидно, является третьим элементом или числом 4. Указатель на указатель - это то место, где вы берете это число и используете его как сам индекс, как RAM[RAM[index]]
,
Я бы нарисовал массив на листе бумаги и просто использовал его, чтобы показать такие вещи, как множество указателей, указывающих на одну и ту же память, арифметику указателей, указатель на указатель и так далее.
В некоторых ответах выше утверждалось, что "указатели не очень сложны", но они не обращались непосредственно к тому месту, где "указатель сложен!" происходит от. Несколько лет назад я обучал студентов-первокурсников CS (всего один год, так как я явно сосал их), и мне было ясно, что идея указателя не сложна. Трудно понять, почему и когда вам нужен указатель.
Я не думаю, что вы можете отделить этот вопрос - почему и когда использовать указатель - от объяснения более широких проблем разработки программного обеспечения. Почему каждая переменная не должна быть глобальной переменной и почему нужно выделять похожий код в функции (чтобы получить это, используйте указатели, чтобы специализировать их поведение на своем сайте вызова).
Я не вижу, что так смущает указатели. Они указывают на место в памяти, то есть оно хранит адрес памяти. В C/C++ вы можете указать тип, на который указывает указатель. Например:
int* my_int_pointer;
Говорит, что my_int_pointer содержит адрес для местоположения, которое содержит int.
Проблема с указателями заключается в том, что они указывают на место в памяти, поэтому легко отследить место, в котором вы не должны находиться. В качестве доказательства посмотрите на многочисленные дыры в безопасности приложений C/C++ от переполнения буфера (увеличение указателя) мимо выделенной границы).
Неплохой способ понять это с помощью итераторов... но продолжайте искать, вы увидите, как Александреску начнет жаловаться на них.
Многие бывшие разработчики C-C++ (которые никогда не понимали, что итераторы являются современным указателем до создания дампов языка) переходят на C# и все еще считают, что у них есть достойные итераторы.
Хм, проблема в том, что все, что делают итераторы, в полной мере не согласны с тем, чего стремятся достичь платформы времени исполнения (Java/CLR): новое, простое использование "все - как разработчик". Что может быть хорошо, но они сказали это однажды в фиолетовой книге, и они сказали это даже до и до C:
Косвенность.
Очень мощная концепция, но никогда, если вы делаете это все время. Итераторы полезны, поскольку они помогают с абстракцией алгоритмов, еще один пример. А время компиляции - это место для алгоритма, очень простого. Вы знаете код + данные или на другом языке C#:
IEnumerable + LINQ + Massive Framework = 300 МБ штрафных санкций за время выполнения при паршивом перетаскивании приложений через кучи экземпляров ссылочных типов.
"Le Pointer дешев".
Номер почтового ящика.
Это часть информации, которая позволяет вам получить доступ к чему-то еще.
(И если вы выполняете арифметику с номерами почтовых ящиков, у вас могут возникнуть проблемы, потому что письмо идет не в тот ящик. А если кто-то переходит в другое состояние - без адреса переадресации - тогда у вас есть свисающий указатель. с другой стороны - если почтовое отделение пересылает почту, то у вас есть указатель на указатель.)
У каждого новичка в C/C++ одна и та же проблема, и эта проблема возникает не потому, что "указатели трудно выучить", а "кто и как это объясняет". Некоторые учащиеся собирают это в устной форме, а некоторые визуально, и лучший способ объяснить это - использовать пример "тренировка" (подходит для словесного и визуального примера).
Где "локомотив" - это указатель, который ничего не может удержать, а "вагон" - это то, что "локомотив" пытается тянуть (или указывать). После этого вы можете классифицировать сам "вагон", может ли он содержать животных, растения или людей (или их комбинацию).
Просто чтобы немного запутать вещи, иногда приходится работать с ручками, а не с указателями. Дескрипторы - это указатели на указатели, так что серверная часть может перемещать объекты в памяти для дефрагментации кучи. Если указатель изменяется в середине процедуры, результаты непредсказуемы, поэтому сначала нужно заблокировать дескриптор, чтобы убедиться, что ничего никуда не денется.
http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html говорит об этом немного более связно, чем я.:-)