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

StackOverflow https://stackoverflow.com/questions/5727

  •  08-06-2019
  •  | 
  •  

Вопрос

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

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

Это было полезно?

Решение

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

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

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

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

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


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

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             <--+

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


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

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

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

Иногда вы можете даже обнаружить, что по соседнему адресу расположен довольно большой дом, занимающий три адреса (Главная улица 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]
                        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):

  • ч1 = 4
  • ч2 = 23

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

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

Обычно адрес, который «никуда не указывает», сохраняется как нулевой адрес.


В общих чертах, что такое указатель?

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

Другие советы

На моем первом уроке информатики мы выполнили следующее упражнение.Конечно, это был лекционный зал, в котором обучалось примерно 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

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

Причина, по которой мне поначалу было трудно понять указатели, заключалась в том, что многие объяснения содержат много чуши о передаче по ссылке.Все это лишь запутывает проблему.Когда вы используете параметр указателя, вы все еще передача по значению;но значение оказывается адресом, а не, скажем, 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/

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

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

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

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

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

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

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


Подготовьте почву:

Рассмотрим парковку на 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)), чтобы увидеть, что там есть зеленая машина (данные в этом месте)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Я не согласен с утверждением, что «вы либо получаете их, либо нет».

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

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

   int *mypointer;

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

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

  int* mypointer;

Даже не заставляйте меня говорить об указателях на функции...

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

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

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

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

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

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

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

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

Я думаю, что это может быть проблема с синтаксисом.Синтаксис указателей 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]].

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

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

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

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

Неплохой способ понять это с помощью итераторов..но продолжайте искать и увидите, как Александреску начнет на них жаловаться.

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

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

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

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

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

«Le Pointer дешев».

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

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

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

int* my_int_pointer;

Сообщает, что my_int_pointer содержит адрес места, содержащего целое число.

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top