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

465

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

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

  • 0
    stackoverflow.com/questions/1432963/...
  • 20
    Тезис этого вопроса заключается в том, что указатели трудно понять. Этот вопрос не дает никаких доказательств того, что указатели сложнее понять, чем что-либо еще.
Показать ещё 5 комментариев
Теги:
pointers

28 ответов

762
Лучший ответ

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

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

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

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

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


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

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

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

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

---[ttttNNNNNNNNNN]---
     ^   ^
     |   |
     |   +- the FName array
     |
     +- overhead

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


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

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

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

THouse.Create('My house');

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

---[ttttNNNNNNNNNN]---
    1234My house

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

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

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

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

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

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

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

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

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

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

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

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

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

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

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after free
----------------------          | (note, memory might still
    xx34My house             <--+  contain some data)

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

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

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                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h                        <--+
    v                           +- after free
----------------------          |
    xx34My house             <--+

Как вы можете видеть, 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 с адресом нового дома, но старый все еще стоит... где-то. После этого кода нет возможности добраться до этого дома, и он останется стоять. Другими словами, выделенная память останется выделенной до тех пор, пока приложение не закроется, и в этот момент операционная система оторвется от нее.

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

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

Размещение памяти после второго выделения:

                       h
                       v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

Более распространенный способ получить этот метод - это просто забыть что-то освободить, а не переписывать его, как описано выше. В терминах 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                           +- before losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

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


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

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

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

Иногда вы даже можете обнаружить, что на соседнем адресе есть довольно большой дом, который занимает три адреса (Main Street 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]
                        5678My house

Размещение памяти после второго выделения:

    h2                  h1
    v                   v
---[ttttNNNNNNNNNN]----[ttttNNNNNNNNNN]
    1234My other house somewhereouse
                        ^---+--^
                            |
                            +- overwritten

Часть, которая чаще всего вызывает сбой, - это когда вы перезаписываете важные части данных, которые вы не могли случайно изменить. Например может быть, не проблема, что части имени 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
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home       +        5678Cabin      +
                   |        ^              |
                   +--------+              * (no link)

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

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

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

    h1                 h2
    v                  v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

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

  • h1 = 4
  • h2 = 23

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

    h1 (=4)                 h2 (=28)
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home      0028      5678Cabin     0000
                   |        ^              |
                   +--------+              * (no link)

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


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

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

  • 56
    Переполнение буфера смешно. «Сосед приходит домой, раскрывает свой череп, скользит по твоему барахлу и судится с тобой в забвении».
  • 10
    Это хорошее объяснение концепции, конечно. Концепция не является той вещью, которую я нахожу запутывающей относительно указателей, таким образом, это целое эссе было немного потрачено впустую.
Показать ещё 25 комментариев
146

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

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

Джон встает

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

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

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

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

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

Сэм встает

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

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

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

  • 5
    Я не думаю, что я неправильно понял. Мое намерение состояло в том, чтобы изменить значение указываемой переменной с Джона на Сэма. Немного сложнее представлять с людьми, потому что похоже, что вы меняете значение обоих указателей.
  • 24
    Но причина, по которой это сбивает с толку, заключается в том, что Джон не встал со своего места, а Сэм сел, как мы могли себе представить. Это больше похоже на то, как Сэм подошел, сунул руку в Джона и клонировал программирование Сэма в тело Джона, как переплетение Гюго в перезагруженной матрице.
Показать ещё 7 комментариев
121

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

  • 15
    Мне действительно это нравится. Нетрудно видеть, что двойная запись гиперссылки не приводит к появлению двух веб-сайтов (точно так же, как int *a = b не создает двух копий *b ).
  • 4
    Это на самом деле очень интуитивно понятно, и каждый должен иметь к этому отношение. Хотя есть много сценариев, где эта аналогия разваливается. Отлично подходит для быстрого ознакомления. +1
Показать ещё 4 комментария
50

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

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

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

  • 2
    Интересно. Не знаю, как начать идти об этом, хотя. Любые ресурсы, чтобы поделиться?
  • 1
    Согласен. Например, я научился программировать на ассемблере до C, и, зная, как работают регистры, учить указатели было легко. На самом деле, не было много обучения, все это было очень естественно.
Показать ещё 4 комментария
27

Почему указатели представляют собой ведущий фактор путаницы для многих новых, и даже старых, студентов уровня колледжа на языке 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

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

  • 0
    +1 за "без понимания того, как физически заложена память". Я пришел к Си из языка ассемблера, и концепция указателей была очень естественной и простой; и я видел людей с языковым прошлым более высокого уровня, которые пытаются это понять. Что еще хуже, синтаксис вводит в заблуждение (указатели на функции!), Поэтому одновременное изучение концепции и синтаксиса является причиной неприятностей.
  • 4
    Я знаю это старый пост, но было бы здорово, если бы к сообщению был добавлен вывод предоставленного кода.
Показать ещё 1 комментарий
23

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

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

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

int puts(const char *s);

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

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

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

  • 0
    Мне интересно, какой смысл иметь указатель. В большинстве блоков кода, с которыми я сталкиваюсь, указатель виден только в его объявлении.
  • 0
    @ Wolfpack'08 ... что ?? Какой код вы смотрите ??
Показать ещё 1 комментарий
19

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

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

  • 1
    Я второй учебник Теда Дженсена. Он разбивает указатели на уровень детализации, это не слишком подробно, ни одна книга, которую я читал, не делает. Очень полезно! :)
12

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

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

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

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

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

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

10

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


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

Рассмотрим парковку с 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 |

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


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

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

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

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

  • 0
    Только не используйте слово «двойной указатель». Указатели могут указывать на что угодно, поэтому очевидно, что у вас могут быть указатели, указывающие на другие указатели. Они не являются двойными указателями.
  • 0
    Я думаю, что здесь не хватает того, что сами «пальцы», продолжая вашу аналогию, каждый «занимают место для парковки». Я не уверен, что людям трудно понять указатели на высоком уровне абстракции вашей аналогии, это понимание того, что указатели - это изменчивые вещи, которые занимают области памяти, и насколько это полезно, что, кажется, уклоняется от людей.
Показать ещё 3 комментария
10

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

Джоэл Спольский делает несколько замечаний о понимании указателей в своем Guerrilla Guide to Interviewing:

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

10

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

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

8

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

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

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

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

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

(Обратите внимание: прежде чем кто-либо станет педантичным для меня, да, стандарт C++ говорит, что указатели представляют адреса памяти. Но он не говорит, что "указатели - это адреса памяти и только адреса памяти и могут использоваться или считаться взаимозаменяемыми с адресами памяти ". Различие важно)

  • 0
    В конце концов, нулевой указатель не указывает на нулевой адрес в памяти, даже если его «значение» C равно нулю. Это совершенно отдельная концепция, и если вы справитесь с ней неправильно, вы можете в конечном итоге обратиться (и разыменовать) к чему-то, чего вы не ожидали. В некоторых случаях это может быть даже нулевой адрес в памяти (особенно теперь, когда адресное пространство обычно плоское), но в других это может быть опущено оптимизирующим компилятором как неопределенное поведение, или доступ к некоторой другой части памяти, которая связана с "нулем" для данного типа указателя. Веселье наступает.
  • 0
    Не обязательно. Вы должны быть в состоянии смоделировать компьютер в вашей голове, чтобы указатели имели смысл (а также для отладки других программ). Не каждый может сделать это.
7

Проблема с указателями - это не концепция. Это исполнение и язык. Дополнительная путаница приводит к тому, что учителя предполагают, что концепция CONCEPT указателей, которые сложны, а не жаргон, или запутанный беспорядок 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", и что он намерен делать с ним. Язык не поддерживает уровни гнездования, которые встречаются на практике. Например, когда мне нужно передать "указатель" на функцию, которая создает новый буфер, и изменяет указатель на новое местоположение буфера. Он действительно хочет, чтобы указатель или указатель на указатель, поэтому он знает, куда идти, чтобы изменить содержимое указателя. Большую часть времени я просто должен угадать, что подразумевается под "указателем", и большую часть времени я ошибаюсь, независимо от того, сколько опыта я угадываю.

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

  • 0
    Я видел, как это объясняется так: если вы видите объявление указателя типа double *(*(*fn)(int))(char) , то результат вычисления *(*(*fn)(42))('x') будет double . Вы можете снять уровни оценки, чтобы понять, какими должны быть промежуточные типы.
  • 0
    @BerndJendrissek Не уверен, что я следую. Каков результат оценки (*(*fn)(42))('x') тогда?
Показать ещё 4 комментария
5

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

5

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

   int *mypointer;

Сначала вы узнали, что самая левая часть создания переменной определяет тип переменной. Объявление указателя не работает так, как в C и С++. Вместо этого они говорят, что переменная указывает на тип слева. В этом случае: * mypointer указывает на int.

Я не полностью понял указатели, пока не попытался использовать их в С# (с небезопасными), они работают точно так же, но с логическим и последовательным синтаксисом. Указатель - это сам тип. Здесь mypointer есть указатель на int.

  int* mypointer;

Не заставляйте меня начинать с указателей функций...

  • 2
    На самом деле, оба ваших фрагмента являются действительными C. В течение многих лет стиль C является наиболее распространенным. Например, второе немного более распространено в C ++.
  • 1
    Второй фрагмент не очень хорошо работает с более сложными объявлениями. И синтаксис не будет таким «противоречивым», как только вы поймете, что правая часть объявления указателя показывает вам, что вы должны сделать с указателем, чтобы получить что-то, чей тип является атомарным спецификатором типа слева.
Показать ещё 12 комментариев
5

Я думаю, что то, что затрудняет изучение указателей, заключается в том, что до тех пор, пока вам не станет удобнее думать, что "в этой ячейке памяти есть набор битов, которые представляют собой int, double, character, whatever".

Когда вы впервые видите указатель, вы действительно не получаете то, что находится в этом месте памяти. "Что значит, у него есть адрес?"

Я не согласен с понятием, что "вы либо получаете их, либо нет".

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

4

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

Например, после связанного списка: 1) начните с вашей бумаги с адресом 2) Перейдите к адресу на бумаге 3) Откройте почтовый ящик, чтобы найти новый лист бумаги со следующим адресом на нем

В линейном связанном списке последний почтовый ящик не имеет в нем ничего (конец списка). В круговом связанном списке последний почтовый ящик имеет адрес первого почтового ящика.

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

  • 0
    Противоречие с аналогией номера почтового ящика состоит в том, что, хотя язык, изобретенный Деннисом Ритчи, определяет поведение в терминах адресов байтов и значений, хранимых в этих байтах, язык, определенный стандартом C, предлагает «оптимизировать» реализации для использования поведенческого поведения. модель, которая является более сложной, но определяет различные аспекты модели способами, которые неоднозначны, противоречивы и неполны.
4

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

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

2

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

Итак, я говорю, представьте, что ОЗУ - это массив (и у вас всего 10 байт ОЗУ):

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

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

Итак, если у вас есть указатель/индекс unsigned char index = 2, то это, очевидно, третий элемент или число 4. Указатель на указатель - это место, где вы берете это число и используете его как сам индекс, например RAM[RAM[index]].

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

2

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

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

  • 1
    Интересно то, что указатели C на самом деле не демонстрируют какой-либо особенности базовой архитектуры памяти. Единственные различия между ссылками на Java и указателями на Си состоят в том, что у вас могут быть сложные типы, включающие указатели (например, int *** или char * ( ) (void * )), существует арифметика указателей для массивов и указателей на структуру членов, наличие пустоты * и двойственность массива / указателя. Кроме того, они работают точно так же.
  • 0
    Хорошая точка зрения. Это арифметика указателя и возможность переполнения буфера - вырваться из абстракции, вырваться из актуальной области памяти - это делает это.
Показать ещё 1 комментарий
2

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

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

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

  • 0
    Проблема в основном в синтаксисе объявления C. Но использование указателя было бы проще, если бы (*p) было бы (p->) , и, таким образом, мы бы имели p->->x вместо неоднозначного *p->x
  • 0
    @MSalters Боже мой, ты шутишь, верно? Там нет никаких несоответствий там. a->b просто означает (*a).b .
Показать ещё 2 комментария
2

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

1

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

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

1

Неплохой способ понять это, через итераторы.. но продолжайте искать, вы увидите, как Alexandrescu начинает жаловаться на них.

Многие ex-С++-разработчики (которые никогда не понимали, что итераторы являются современным указателем перед тем, как сбрасывать язык) переходят на С# и все еще верят, что у них есть достойные итераторы.

Хм, проблема в том, что все эти итераторы находятся в полном противоречии с тем, что пытаются реализовать на платформах времени выполнения (Java/CLR): новое, простое, все-есть-а-dev. Что может быть хорошо, но они сказали это однажды в пурпурной книге, и они сказали это еще до и до C:

Косвенность.

Очень мощная концепция, но никогда не так, если вы все это делаете. Итераторы полезны, поскольку они помогают с абстракцией алгоритмов, еще одним примером. А время компиляции - это место для алгоритма, очень простое. Вы знаете код + данные или на этом другом языке С#:

IEnumerable + LINQ + Massive Framework = 300 Мбайт времени исполнения, отпугивание, отталкивание приложений через кучи экземпляров ссылочных типов.

"Le Pointer дешев".

  • 4
    Какое это имеет отношение к чему-либо?
  • 0
    ... что вы пытаетесь сказать, кроме того, что "статическое связывание - лучшая вещь когда-либо" и "я не понимаю, как работает что-то отличное от того, что я узнал ранее"?
Показать ещё 1 комментарий
1

Номер почтового ящика.

Это часть информации, которая позволяет вам получить доступ к чему-то еще.

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

1

Я не вижу, что так запутанно в отношении указателей. Они указывают на местоположение в памяти, то есть хранят адрес памяти. В C/С++ вы можете указать тип, на который указывает указатель. Например:

int* my_int_pointer;

Говорит, что my_int_pointer содержит адрес в местоположении, содержащем int.

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

0

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

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

0

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

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 говорит об этом чуть более согласованно, чем я.: -)

  • 0
    -1: дескрипторы не являются указателями на указатели; они не являются указателями в любом смысле. Не путай их.
  • 0
    «Они не являются указателями в любом смысле», - умоляю я отличаться.
Показать ещё 2 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню