Покажчики - це концепція, яка для багатьох може спочатку збивати з пантелику, зокрема, якщо мова йде про копіювання значень вказівника навколо та все ще посилання на той самий блок пам'яті.
Я виявив, що найкращою аналогією є розгляд вказівника як аркуша паперу з домашньою адресою, а блок пам'яті називає його фактичним будинком. Таким чином, всі види операцій можна легко пояснити.
Я додав код 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;
Тут я спочатку будую будинок і отримую його адресу. Потім я щось роблю додому (використовую його, ... код, залишений як вправа для читача), а потім звільняю його. Нарешті я очищаю адресу зі своєї змінної.
Макет пам'яті:
h <- +
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
може спрацювати, але це лише чиста удача. Швидше за все, це не вдасться в місці покупців в середині критичної операції.
h <- +
v + - перед безкоштовною
--- [ttttNNNNNNNNNN] --- |
1234 мій будинок <- +
h <- +
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] --- [ttttNNNNNNNNN] --- [ttttNNNNNNNNN]
1234 мій будинок 5678 мій будинок
Більш поширений спосіб отримати цей метод - просто забути звільнити щось, замість того, щоб перезаписати його, як зазначено вище. У термінах Delphi це відбудеться за допомогою наступного методу:
procedure OpenTheFrontDoorOfANewHouse;
var
h: THouse;
begin
h := THouse.Create('My house');
h.OpenFrontDoor;
// uh-oh, no .Free here, where does the address go?
end;
Після виконання цього методу в наших змінних немає місця, що адреса до будинку існує, але будинок все ще там.
Макет пам'яті:
h <- +
v + - до втрати покажчика
--- [ttttNNNNNNNNNN] --- |
1234 мій будинок <- +
h (зараз вказує нікуди) <- +
+ - після втрати покажчика
--- [ttttNNNNNNNNNN] --- |
1234 мій будинок <- +
Як бачите, старі дані залишаються недоторканими в пам'яті і не використовуються розподільником пам'яті. Алокатор відслідковує, які області пам’яті були використані, і не використовуватиме їх повторно, якщо ви не звільните її.
Звільнення пам'яті, але збереження (тепер недійсної) посилання
Знесіть будинок, видаліть один з аркушів паперу, але у вас також є інший аркуш паперу зі старою адресою, коли ви підете на адресу, ви не знайдете будинку, але ви можете знайти щось, що нагадує руїни одного.
Можливо, ви навіть знайдете будинок, але це не той будинок, до якого ви спочатку отримали адресу, і тому будь-які спроби використовувати його так, ніби він належить вам, можуть жахливо зазнати невдачі.
Іноді ви навіть можете виявити, що на сусідній адресі встановлено досить великий будинок, який займає три адреси (Головна вулиця 1-3), а ваша адреса йде в середину будинку. Будь-які спроби трактувати цю частину великого 3-адресного будинку як єдиний маленький будинок також можуть жахливо зазнати краху.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1.Free;
h1 := nil;
h2.OpenFrontDoor; // uh-oh, what happened to our house?
Тут будинок був зруйнований через посилання в h1
, і, хоча також h1
був очищений, h2
досі має стару застарілу адресу. Доступ до будинку, який вже не стоїть, може не працювати.
Це зміна звисаючого вище вказівника. Дивіться його макет пам'яті.
Перекриття буфера
Ви переїжджаєте в будинок більше, ніж можливо, помістившись в будинок або двір сусідів. Коли власник того сусіднього будинку пізніше повернеться додому, він знайде всілякі речі, які він вважатиме своїми.
З цієї причини я вибрав масив фіксованого розміру. Щоб встановити сцену, припустимо, що другий будинок, який ми виділимо, чомусь буде розміщений перед першим у пам’яті. Іншими словами, другий будинок матиме нижчу адресу, ніж перший. Крім того, вони виділяються прямо поруч.
Таким чином, цей код:
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := THouse.Create('My other house somewhere');
^-----------------------^
longer than 10 characters
0123456789 <-- 10 characters
Макет пам'яті після першого розподілу:
h1
v
----------------------- [ttttNNNNNNNNN]
5678Мий будинок
Макет пам'яті після другого розподілу:
h2 h1
vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNN]
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
vv
--- [ttttNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNLLLL]
1234Дома + 5678Кабін +
| ^ |
+ -------- + * (немає посилання)
В основному, що таке пам'ять?
Адреса пам'яті - це в основному просто число. Якщо ви вважаєте пам'ять як великий масив байтів, перший байт має адресу 0, наступний - адресу 1 і так далі вгору. Це спрощено, але досить добре.
Отже, цей макет пам'яті:
h1 h2
vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNN] --- [ttttNNNNNNNNN]
1234 мій будинок 5678 мій будинок
Можливо, є ці дві адреси (крайній лівий - адреса 0):
Що означає, що наш зв'язаний список вище може виглядати так:
h1 (= 4) h2 (= 28)
vv
--- [ttttNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNLLLL]
1234Дома 0028 5678Кабін 0000
| ^ |
+ -------- + * (немає посилання)
Типово зберігати адресу, яка "не вказує нікуди", як нульову адресу.
В основному, що таке покажчик?
Вказівник - це лише змінна, що містить адресу пам'яті. Зазвичай ви можете попросити мову програмування вказати вам її номер, але більшість мов програмування та час виконання намагається приховати той факт, що під ним є число, лише тому, що саме число насправді не має для вас ніякого значення. Найкраще думати про вказівник як про чорну скриньку, тобто. ви насправді не знаєте і не піклуєтесь про те, як це реально реалізовано, доки воно працює.