Wskaźniki to koncepcja, która dla wielu może na początku być myląca, szczególnie jeśli chodzi o kopiowanie wartości wskaźników i wciąż odwoływanie się do tego samego bloku pamięci.
Odkryłem, że najlepszą analogią jest rozważenie wskaźnika jako kawałka papieru z adresem domu, a blok pamięci, który określa jako rzeczywisty dom. W ten sposób można łatwo wyjaśnić wszystkie rodzaje operacji.
Poniżej dodałem trochę kodu Delphi i, w stosownych przypadkach, komentarze. Wybrałem Delphi, ponieważ mój inny główny język programowania, C #, nie wykazuje w ten sam sposób wycieków pamięci.
Jeśli chcesz tylko nauczyć się koncepcji wskaźników na wysokim poziomie, powinieneś zignorować części oznaczone jako „Układ pamięci” w objaśnieniu poniżej. Mają one dawać przykłady tego, jak pamięć może wyglądać po operacjach, ale mają bardziej niski poziom. Jednak, aby dokładnie wyjaśnić, jak naprawdę działają przepełnienia bufora, ważne było, aby dodać te diagramy.
Oświadczenie: Dla wszystkich celów i celów to objaśnienie i przykładowe układy pamięci są znacznie uproszczone. Jest więcej narzutu i dużo więcej szczegółów, które musisz znać, jeśli chcesz radzić sobie z pamięcią na niskim poziomie. Jednak w celu wyjaśnienia pamięci i wskaźników jest ona wystarczająco dokładna.
Załóżmy, że klasa THouse zastosowana poniżej wygląda następująco:
type
THouse = class
private
FName : array[0..9] of Char;
public
constructor Create(name: PChar);
end;
Po zainicjowaniu obiektu house nazwa nadana konstruktorowi jest kopiowana do prywatnego pola FName. Istnieje powód, dla którego jest zdefiniowany jako tablica o stałym rozmiarze.
W pamięci pojawi się narzut związany z przydziałem domu, zilustruję to poniżej w następujący sposób:
--- [ttttNNNNNNNNNN] ---
^ ^
| |
| + - tablica FName
|
+ - narzut
Obszar „tttt” jest narzutem, zwykle będzie go więcej dla różnych typów środowisk uruchomieniowych i języków, takich jak 8 lub 12 bajtów. Konieczne jest, aby wszelkie wartości przechowywane w tym obszarze nigdy nie były zmieniane przez nic innego niż przydział pamięci lub podstawowe procedury systemowe, w przeciwnym razie istnieje ryzyko awarii programu.
Przydziel pamięć
Poproś przedsiębiorcę, aby zbudował Twój dom i podał adres do domu. W przeciwieństwie do realnego świata, alokacji pamięci nie można powiedzieć, gdzie alokować, ale znajdzie odpowiednie miejsce z wystarczającą ilością miejsca i zgłosi adres do przydzielonej pamięci.
Innymi słowy, przedsiębiorca wybierze miejsce.
THouse.Create('My house');
Układ pamięci:
--- [ttttNNNNNNNNNN] ---
1234 Mój dom
Zachowaj zmienną z adresem
Zapisz adres swojego nowego domu na kartce papieru. Ten artykuł posłuży jako odniesienie do twojego domu. Bez tego kawałka papieru zgubiłeś się i nie możesz znaleźć domu, chyba że już w nim jesteś.
var
h: THouse;
begin
h := THouse.Create('My house');
...
Układ pamięci:
h
v
--- [ttttNNNNNNNNNN] ---
1234 Mój dom
Skopiuj wartość wskaźnika
Po prostu napisz adres na nowej kartce papieru. Masz teraz dwa kawałki papieru, które zabiorą cię do tego samego domu, a nie do dwóch oddzielnych domów. Wszelkie próby podążania za adresem z jednego papieru i zmiany układu mebli w tym domu sprawią, że drugi dom zostanie zmodyfikowany w ten sam sposób, chyba że można jednoznacznie wykryć, że w rzeczywistości jest to tylko jeden dom.
Uwaga Jest to zazwyczaj koncepcja, którą mam największy problem z wyjaśnieniem ludziom, dwa wskaźniki nie oznaczają dwóch obiektów lub bloków pamięci.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1
v
--- [ttttNNNNNNNNNN] ---
1234 Mój dom
^
h2
Uwolnienie pamięci
Zburz dom. Możesz później ponownie użyć papieru na nowy adres, jeśli chcesz, lub wyczyścić go, aby zapomnieć adres do domu, który już nie istnieje.
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
h := nil;
Tutaj najpierw buduję dom i zdobywam jego adres. Potem robię coś do domu (użyj go, ... kodu, pozostawionego jako ćwiczenie dla czytelnika), a potem go uwalniam. Na koniec usuwam adres z mojej zmiennej.
Układ pamięci:
h <- +
v + - przed darmowym
--- [ttttNNNNNNNNNN] --- |
1234 Mój dom <- +
h (teraz nigdzie nie wskazuje) <- +
+ - po darmowym
---------------------- | (Uwaga, pamięć może nadal występować
xx34 Mój dom <- + zawiera pewne dane)
Zwisające wskaźniki
Mówisz swojemu przedsiębiorcy, żeby zniszczył dom, ale zapominasz usunąć adres z kartki papieru. Kiedy później spojrzysz na kartkę papieru, zapomniałeś, że domu już tam nie ma, i udasz się do niego z nieudanymi wynikami (patrz także część o nieprawidłowym odnośniku poniżej).
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
... // forgot to clear h here
h.OpenFrontDoor; // will most likely fail
Używanie h
po wezwaniu .Free
może zadziałać, ale to po prostu szczęście. Najprawdopodobniej zakończy się niepowodzeniem u klienta w trakcie krytycznej operacji.
h <- +
v + - przed darmowym
--- [ttttNNNNNNNNNN] --- |
1234 Mój dom <- +
h <- +
v + - po darmowym
---------------------- |
xx34 Mój dom <- +
Jak widać, h nadal wskazuje na resztki danych w pamięci, ale ponieważ mogą one nie być kompletne, użycie ich jak poprzednio może się nie powieść.
Wyciek pamięci
Gubisz kawałek papieru i nie możesz znaleźć domu. Dom wciąż gdzieś stoi, a kiedy później chcesz zbudować nowy dom, nie możesz ponownie użyć tego miejsca.
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;
Tutaj nadpisaliśmy zawartość h
zmiennej adresem nowego domu, ale stary wciąż stoi ... gdzieś. Po tym kodzie nie ma sposobu, aby dotrzeć do tego domu, a on pozostanie stojący. Innymi słowy, przydzielona pamięć pozostanie przydzielona do momentu zamknięcia aplikacji, w którym to momencie system operacyjny ją rozerwa.
Układ pamięci po pierwszym przydziale:
h
v
--- [ttttNNNNNNNNNN] ---
1234 Mój dom
Układ pamięci po drugim przydziale:
h
v
--- [ttttNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234 Mój dom 5678 Mój dom
Bardziej powszechnym sposobem uzyskania tej metody jest po prostu zapomnienie o zwolnieniu czegoś, zamiast zastąpienia go jak wyżej. W ujęciu Delphi nastąpi to za pomocą następującej metody:
procedure OpenTheFrontDoorOfANewHouse;
var
h: THouse;
begin
h := THouse.Create('My house');
h.OpenFrontDoor;
// uh-oh, no .Free here, where does the address go?
end;
Po wykonaniu tej metody w naszych zmiennych nie ma miejsca na adres do domu, ale dom wciąż tam jest.
Układ pamięci:
h <- +
v + - przed utratą wskaźnika
--- [ttttNNNNNNNNNN] --- |
1234 Mój dom <- +
h (teraz nigdzie nie wskazuje) <- +
+ - po utracie wskaźnika
--- [ttttNNNNNNNNNN] --- |
1234 Mój dom <- +
Jak widać, stare dane pozostają nietknięte w pamięci i nie zostaną ponownie wykorzystane przez alokator pamięci. Program przydzielający śledzi, które obszary pamięci zostały wykorzystane i nie będzie ich ponownie używał, dopóki go nie zwolnisz.
Zwolnienie pamięci, ale zachowanie (teraz niepoprawnego) odwołania
Zburz dom, usuń jeden z kawałków papieru, ale masz też inny kawałek papieru ze starym adresem na nim, kiedy idziesz pod ten adres, nie znajdziesz domu, ale możesz znaleźć coś, co przypomina ruiny z jednego.
Być może nawet znajdziesz dom, ale nie jest to dom, do którego pierwotnie nadano ci adres, a zatem wszelkie próby użycia go tak, jakby należał do ciebie, mogą okropnie zawieść.
Czasami może się okazać, że na sąsiednim adresie jest ustawiony dość duży dom, który zajmuje trzy adresy (Main Street 1-3), a twój adres znajduje się na środku domu. Wszelkie próby potraktowania tej części dużego 3-adresowego domu jako pojedynczego małego domu również mogą zakończyć się niepowodzeniem.
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?
Tutaj dom został zburzony przez odniesienie do h1
, a chociaż h1
został wyczyszczony, h2
nadal ma stary, nieaktualny adres. Dostęp do domu, który już nie stoi, może działać lub nie.
Jest to odmiana wiszącego wskaźnika powyżej. Zobacz jego układ pamięci.
Przepełnienie bufora
Przenosisz więcej rzeczy do domu, niż możesz zmieścić, rozlewając się do domu sąsiadów lub podwórka. Kiedy właściciel sąsiedniego domu później wróci do domu, znajdzie wszystkie rzeczy, które uzna za własne.
Dlatego wybrałem tablicę o stałym rozmiarze. Aby ustawić scenę, załóż, że drugi dom, który przydzielimy, z jakiegoś powodu zostanie umieszczony przed pierwszym w pamięci. Innymi słowy, drugi dom będzie miał niższy adres niż pierwszy. Są one również przydzielane obok siebie.
Zatem ten kod:
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := THouse.Create('My other house somewhere');
^-----------------------^
longer than 10 characters
0123456789 <-- 10 characters
Układ pamięci po pierwszym przydziale:
h1
v
----------------------- [ttttNNNNNNNNNN]
5678 Mój dom
Układ pamięci po drugim przydziale:
h2 h1
vv
--- [ttttNNNNNNNNN] ---- [ttttNNNNNNNNNN]
1234 Mój drugi dom gdzieś
^ --- + - ^
|
+ - zastąpione
Część, która najczęściej powoduje awarię, polega na zastąpieniu ważnych części przechowywanych danych, które tak naprawdę nie powinny być losowo zmieniane. Na przykład może nie być problemem, że części nazwy h1-house zostały zmienione pod względem awarii programu, ale nadpisanie narzutu obiektu najprawdopodobniej ulegnie awarii podczas próby użycia uszkodzonego obiektu, podobnie jak zastępowanie łączy przechowywanych w innych obiektach w obiekcie.
Połączone listy
Gdy podążasz za adresem na kartce papieru, docierasz do domu, w którym znajduje się kolejna kartka papieru z nowym adresem, na następny dom w łańcuchu i tak dalej.
var
h1, h2: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
Tutaj tworzymy link z naszego domu do naszej kabiny. Możemy podążać za łańcuchem, dopóki dom nie będzie miał NextHouse
odniesienia, co oznacza, że jest ostatni. Aby odwiedzić wszystkie nasze domy, możemy użyć następującego kodu:
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;
Układ pamięci (dodano NextHouse jako łącze w obiekcie, zaznaczone czterema LLLL na poniższym diagramie):
h1 h2
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
1234 Strona główna + 5678 Kabina +
| ^ |
+ -------- + * (brak linku)
W skrócie, jaki jest adres pamięci?
Adres pamięci to w zasadzie tylko liczba. Jeśli myślisz o pamięci jako dużej tablicy bajtów, pierwszy bajt ma adres 0, następny adres 1 i tak dalej. Jest to uproszczone, ale wystarczająco dobre.
Więc ten układ pamięci:
h1 h2
vv
--- [ttttNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234 Mój dom 5678 Mój dom
Może mieć te dwa adresy (skrajnie lewy - to adres 0):
Co oznacza, że nasza powyższa lista linków może wyglądać tak:
h1 (= 4) h2 (= 28)
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
1234 Strona główna 0028 5678 Kabina 0000
| ^ |
+ -------- + * (brak linku)
Typowe jest przechowywanie adresu, który „nigdzie nie wskazuje” jako adresu zerowego.
W skrócie, czym jest wskaźnik?
Wskaźnik to po prostu zmienna zawierająca adres pamięci. Zwykle możesz poprosić język programowania o podanie jego numeru, ale większość języków programowania i środowisk uruchomieniowych stara się ukryć fakt, że pod nim znajduje się liczba, tylko dlatego, że sam numer nie ma dla ciebie żadnego znaczenia. Najlepiej jest myśleć o wskaźniku jak o czarnej skrzynce, tj. tak naprawdę nie wiesz ani nie obchodzi Cię, w jaki sposób jest on faktycznie wdrażany, dopóki działa.