Większość programistów postrzega pamięć jako „czarną skrzynkę”. Wierzą, że zmienne to po prostu nazwy przypisane wartościom. Działa to, dopóki nie trzeba napisać kodu, który manipuluje danymi bezpośrednio na poziomie maszyny. Następnie napotykasz wskazówki.
Zmienna regularna jest prosta. Jest to fragment pamięci o nazwie, której używasz w swoim kodzie. Zadeklaruj zmienną całkowitą i, a kompilator zarezerwuje cztery bajty. Widzisz i. Maszyna widzi adres. Wartość znajduje się w tych czterech bajtach.
Wskaźniki łamią ten prosty model.
Wskaźnik nie przechowuje wartości. Zapisuje adres. Wskazuje na inną zmienną. Dokładniej, przechowuje lokalizację tej zmiennej w pamięci. To rozróżnienie jest niezwykle ważne. Jeśli je pomieszasz, program ulegnie awarii. Twardy.
Konstrukcja jest dwupoziomowa. Po pierwsze, istnieje sama zmienna wskaźnikowa. Zawiera adres numeryczny. Po drugie, istnieje zmienna docelowa. Adres w indeksie prowadzi do prawdziwych danych. Kiedy zrozumiesz ten podział, otworzysz przed sobą znaczące możliwości w programowaniu systemów.
Rozważ ten standardowy przykład w C:
Pierwsza linia deklaruje dwie zwykłe zmienne całkowite, i i j. Następna linia, int *p, deklaruje wskaźnik. Gwiazdka jest kluczowa. Mówi kompilatorowi, że p nie jest liczbą całkowitą. To jest wskaźnik na liczbę całkowitą. To samo możesz zrobić z liczbami zmiennoprzecinkowymi, strukturami, symbolami i czymkolwiek innym. Składnia pozostaje taka sama.
Następnie następuje część, która często dezorientuje początkujących: p = &i;.
W C ampersand & jest operatorem adresu. &i nie zwraca wartości i. Zwraca adres pamięci, w którym znajduje się i. Przypisanie p = &i kopiuje ten adres do wskaźnika p. Teraz p wskazuje na i.
Jeśli pominiesz ten krok, p będzie zawierać śmieci. Będzie zawierał losowy, nieokreślony adres. Późniejsze wyłuskiwanie referencji prawdopodobnie spowoduje błąd segmentacji. Program ulegnie awarii, ponieważ nakazałeś mu uzyskać dostęp do pamięci, której nie posiada.
Wizualizacja pomaga. Pomyśl o pamięci jak o siatce.
Przed inicjalizacją i, j i p istnieją, ale zawierają nieznane wartości. Liczby całkowite to pola ze znakami zapytania. Mogą zawierać wszystko. Wskaźnik p to okrąg. Zawiera strzałki wskazujące losowe kierunki. Jeszcze nie wie, dokąd iść.
Po wykonaniu p = &i strzałki się zmieniają. Wszystkie wskazują bezpośrednio na i. Teraz p wie dokładnie, gdzie jest i.
Związek jest oczywisty. p lokalizacja sklepów. i przechowuje dane. Używasz p, aby dostać się do i.
Mechanizm ten pozwala C efektywnie zarządzać pamięcią. Umożliwia funkcjom modyfikowanie zmiennych przekazywanych przez referencję. Zapewnia dynamiczną alokację pamięci. Umożliwia tworzenie złożonych struktur danych, takich jak połączone listy i drzewa.
Ale wprowadza także ryzyko. Jeden zły adres i czytasz lub zapisujesz dane w przestrzeń.
Gwiazdka w *p = 5 robi coś innego niż gwiazdka w int *p. Druga gwiazdka była częścią definicji typu. Zadeklarował zmienną jako wskaźnik. Pierwsza gwiazdka to operator dereferencji. Oznacza to „przejdź do adresu zapisanego w p i zmień tam wartość”.
Zatem *p = 5 umieszcza liczbę 5 w miejscu pamięci wskazanym przez p. Ponieważ p wskazuje na i, i staje się 5. Zmiennej j przypisuje się wówczas wartość i. Na ekranie pojawią się liczby 5, 5 i 5. Wszystkie trzy wartości odzwierciedlają tę samą zasadniczą zmianę.
Zrozumienie działania wskaźników w C wymaga zmiany sposobu myślenia. Przestań myśleć o zmiennych jak o pojemnikach. Zacznij myśleć o nich jak o lokalizacjach. Wskaźnik jest mapą. Zmienna jest miejscem docelowym. Jeśli mapa jest błędna, jesteś zgubiony. A w programowaniu niskiego poziomu utrata orientacji jest kosztowna.
Wyjaśnienie operatora pośredniego
Kiedy przypisujesz adres zmiennej „i” do wskaźnika „p”, w rzeczywistości nadajesz temu samemu blokowi pamięci dwie różne nazwy. Zmienna „i” nadal zapewnia bezpośredni dostęp do niej, ale „p” służy teraz jako uchwyt, punkt odniesienia przy dostępie do tych samych danych.
W C ta dwoistość definiuje sposób działania zmiennych wskaźnikowych. Sam wskaźnik p jest po prostu kontenerem przechowującym adres. Nie zawiera znaczenia, które Cię interesuje. Zamiast tego przechowuje lokalizację, w której znajduje się ta wartość. Tutaj właśnie wchodzi w grę operator dostępu pośredniego *.
Użycie *p umożliwia dostęp do danych pod adresem zapisanym w p. Można to nazwać dereferencją. Jeśli napiszesz *p = 5, nie zmienisz adresu zapisanego w p. Idziesz pod adres wskazany przez p i nadpisujesz tam wartość cyfrą 5.
To rozróżnienie ma kluczowe znaczenie dla zrozumienia zarządzania pamięcią. „p” wskazuje gdzie szukać. *p wskazuje co chcesz obejrzeć. Zamieszanie między nimi prowadzi do błędów, które są trudne do wyśledzenia, ponieważ możesz przypadkowo zmienić adres zamiast danych lub spróbować zapisać dane w losowym miejscu pamięci z powodu nieprawidłowej inicjalizacji p.
Gdy opanujesz ten model mentalny, wskaźniki przestaną być abstrakcyjną składnią i staną się bezpośrednim kanałem dostępu do pamięci. Możesz zmieniać zmienne w całym zakresie, przekazywać duże struktury przez referencje bez ich kopiowania i tworzyć dynamiczne struktury danych, które rosną i zmniejszają się w miarę potrzeb. To potężne narzędzie, jeśli po prostu przestaniesz walczyć ze składnią.
Poniższy fragment kodu ilustruje dokładnie to zachowanie, pokazując, jak wartość „i” zmienia się tylko dlatego, że w celu uzyskania dostępu do niej użyto „p”.
Wartość „i” jest aktualizowana natychmiast. Adres się nie zmienił. Zmieniła się tylko treść.
Jak wskaźniki przechowują wartości i adresy
Wydaje się, że wszystko jest proste, ale mechanika pracy może Cię zdezorientować. Ponieważ zmiennej i przypisano wartość 5, a wskaźnikowi p przechowuje adres i, dereferencja p (*p ) zwraca 5. Kiedy wykonasz j = i;, skopiujesz tę wartość. Zatem „j” staje się 5.
Wykonaj teraz instrukcję printf. Dostaniesz „5 5 5”.
Wynika to z podwójnej natury wskaźników. Wyobraź sobie, że pełnią dwie różne role. Po pierwsze, sama zmienna wskaźnikowa przechowuje adres w pamięci. Po drugie, wskazuje na konkretny typ danych znajdujący się pod tym adresem.
W naszym przykładzie:
– p jest wskaźnikiem. Zapisuje lokalizację w pamięci.
– *p to wartość. To są rzeczywiste dane przechowywane pod tym adresem.
Mieszanie tych pojęć prowadzi do błędów. Jeśli potraktujesz adres jako dane, otrzymasz śmieci. Jeśli potraktujesz dane jako adres, program ulegnie awarii. Zrozumienie, z czym masz do czynienia, zmienia wszystko.
Dlaczego to rozróżnienie jest ważne w codziennym programowaniu? Ponieważ zrozumienie różnicy pomiędzy miejscem przechowywania danych a tym, czym są dane, pozwala na bezpośrednie manipulowanie pamięcią. Jest to przydatne w przypadku konstrukcji dynamicznych, ale niebezpieczne, jeśli popełnisz błąd. Jeden zły ruch, a system spróbuje odczytać dane z nieistniejącej lokalizacji.
Zaczynasz rozumieć, dlaczego C pozostaje tak potężny, jak niebezpieczny.




















