Указатели - это концепция, которая поначалу многих может сбить с толку, особенно когда речь идет о копировании значений указателей и сохранении ссылок на один и тот же блок памяти.
Я обнаружил, что лучшая аналогия - рассматривать указатель как лист бумаги с адресом дома и блоком памяти, на который он ссылается как на настоящий дом. Таким образом, все виды операций можно легко объяснить.
Я добавил немного кода 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), и ваш адрес идет в середину дома. Любые попытки трактовать эту часть большого трехадресного дома как один маленький дом также могут оказаться ужасными.
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 (= 28)
ст
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
1234Дом 0028 5678Кабина 0000
| ^ |
+ -------- + * (без ссылки)
Обычно адрес, который нигде не указывает, является нулевым адресом.
В общих чертах, что такое указатель?
Указатель - это просто переменная, содержащая адрес памяти. Обычно вы можете попросить язык программирования дать вам его номер, но большинство языков программирования и сред выполнения стараются скрыть тот факт, что число находится под ним, просто потому, что само число на самом деле не имеет никакого значения для вас. Лучше всего рассматривать указатель как черный ящик, т.е. вы на самом деле не знаете или не заботитесь о том, как это на самом деле реализовано, до тех пор, пока оно работает.